mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-22 18:26:12 +08:00
Improve spine-c-new codegen array scanner and type handling
- Fix array scanner to properly handle primitive pointer types (e.g., Array<float*>) - Add comprehensive multi-level pointer detection and error reporting - Improve const type checking to prevent false positives - Extract reusable warnings collector for consistent error formatting - Add checks for problematic array types (const elements, template parameters) - Sort array specializations by category (primitives, enums, pointers) - Export isPrimitive function and ArraySpecialization interface - Move array type regex to module level for performance - Fix discriminated union types for Exclusion and Member interfaces
This commit is contained in:
parent
e324cf5cb1
commit
dbd2c2bb37
@ -1,61 +1,59 @@
|
||||
import { Type, SpineTypes } from './types';
|
||||
import { isTypeExcluded } from './exclusions';
|
||||
import { Type, ArraySpecialization, isPrimitive, toSnakeCase, Member } from './types';
|
||||
import { WarningsCollector } from './warnings';
|
||||
|
||||
export interface ArraySpecialization {
|
||||
cppType: string; // e.g. "Array<float>"
|
||||
elementType: string; // e.g. "float"
|
||||
cTypeName: string; // e.g. "spine_array_float"
|
||||
cElementType: string; // e.g. "float" or "spine_animation"
|
||||
isPointer: boolean;
|
||||
isEnum: boolean;
|
||||
isPrimitive: boolean;
|
||||
// Note: This regex won't correctly parse nested arrays like Array<Array<int>>
|
||||
// It will match "Array<Array<int>" instead of the full type.
|
||||
// This is actually OK because we handle nested arrays as unsupported anyway.
|
||||
const ARRAY_REGEX = /Array<([^>]+)>/g;
|
||||
|
||||
/**
|
||||
* Extracts Array<T> types from a type string and adds them to the arrayTypes map
|
||||
*/
|
||||
function extractArrayTypes(
|
||||
typeStr: string | undefined,
|
||||
arrayTypes: Map<string, {type: Type, member: Member}[]>,
|
||||
type: Type,
|
||||
member: Member
|
||||
) {
|
||||
if (!typeStr) return;
|
||||
|
||||
// Reset regex lastIndex to ensure it starts from the beginning
|
||||
ARRAY_REGEX.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = ARRAY_REGEX.exec(typeStr)) !== null) {
|
||||
const arrayType = match[0];
|
||||
const arrayTypeSources = arrayTypes.get(arrayType) || [];
|
||||
arrayTypeSources.push({type, member});
|
||||
arrayTypes.set(arrayType, arrayTypeSources);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans all spine-cpp types to find Array<T> specializations
|
||||
* Only includes arrays from non-excluded types
|
||||
* Scans included spine-cpp types to find Array<T> specializations
|
||||
*/
|
||||
export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[], enumTypes: Set<string>): ArraySpecialization[] {
|
||||
const arrayTypes = new Set<string>();
|
||||
const warnings: string[] = [];
|
||||
export function scanArraySpecializations(includedTypes: Type[]): ArraySpecialization[] {
|
||||
const arrayTypes = new Map<string, {type: Type, member: Member}[]>();
|
||||
const warnings = new WarningsCollector();
|
||||
|
||||
// Extract Array<T> from a type string
|
||||
function extractArrayTypes(typeStr: string | undefined) {
|
||||
if (!typeStr) return;
|
||||
// Process all included types
|
||||
for (const type of includedTypes) {
|
||||
if (!type.members) continue;
|
||||
|
||||
const regex = /Array<([^>]+)>/g;
|
||||
let match;
|
||||
while ((match = regex.exec(typeStr)) !== null) {
|
||||
arrayTypes.add(match[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Process all types
|
||||
for (const header of Object.keys(typesJson)) {
|
||||
for (const type of typesJson[header]) {
|
||||
// Skip excluded types and template types
|
||||
if (isTypeExcluded(type.name, exclusions) || type.isTemplate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!type.members) continue;
|
||||
|
||||
for (const member of type.members) {
|
||||
switch (member.kind) {
|
||||
case 'method':
|
||||
extractArrayTypes(member.returnType);
|
||||
if (member.parameters) {
|
||||
for (const param of member.parameters) {
|
||||
extractArrayTypes(param.type);
|
||||
}
|
||||
for (const member of type.members) {
|
||||
switch (member.kind) {
|
||||
case 'method':
|
||||
extractArrayTypes(member.returnType, arrayTypes, type, member);
|
||||
if (member.parameters) {
|
||||
for (const param of member.parameters) {
|
||||
extractArrayTypes(param.type, arrayTypes, type, member);
|
||||
}
|
||||
break;
|
||||
case 'field':
|
||||
extractArrayTypes(member.type);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'field':
|
||||
extractArrayTypes(member.type, arrayTypes, type, member);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,26 +61,34 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[
|
||||
// Convert to specializations
|
||||
const specializations: ArraySpecialization[] = [];
|
||||
|
||||
for (const arrayType of arrayTypes) {
|
||||
// Get all enum names from included types
|
||||
const enumNames = new Set(includedTypes.filter(t => t.kind === 'enum').map(t => t.name));
|
||||
|
||||
for (const [arrayType, sources] of arrayTypes) {
|
||||
const elementMatch = arrayType.match(/Array<(.+)>$/);
|
||||
if (!elementMatch) continue;
|
||||
|
||||
const elementType = elementMatch[1].trim();
|
||||
|
||||
// Skip template placeholders
|
||||
if (elementType === 'T' || elementType === 'K') {
|
||||
// For template types, check if element type is a template parameter
|
||||
const firstSource = sources[0];
|
||||
const sourceType = firstSource.type;
|
||||
if (sourceType.isTemplate && sourceType.templateParams?.includes(elementType)) {
|
||||
// Warn about template placeholders like T, K
|
||||
warnings.addWarning(arrayType, `Template class uses generic array with template parameter '${elementType}'`, sources);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle nested arrays - emit warning
|
||||
if (elementType.startsWith('Array<')) {
|
||||
warnings.push(`Skipping nested array: ${arrayType} - manual handling required`);
|
||||
// Check for const element types (not allowed in arrays)
|
||||
if (elementType.startsWith('const ') || elementType.includes(' const ')) {
|
||||
warnings.addWarning(arrayType, "Arrays should not have const element types", sources);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle String arrays - emit warning
|
||||
if (elementType === 'String') {
|
||||
warnings.push(`Skipping String array: ${arrayType} - should be fixed in spine-cpp`);
|
||||
// Check for multi-level pointers (unsupported, should be caught by checkMultiLevelPointers in index.ts)
|
||||
const pointerCount = (elementType.match(/\*/g) || []).length;
|
||||
if (pointerCount > 1) {
|
||||
warnings.addWarning(arrayType, "Multi-level pointers are not supported", sources);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -92,27 +98,23 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[
|
||||
|
||||
// Remove "class " or "struct " prefix if present
|
||||
cleanElementType = cleanElementType.replace(/^(?:class|struct)\s+/, '');
|
||||
const isEnum = enumTypes.has(cleanElementType) || cleanElementType === 'PropertyId';
|
||||
const isPrimitive = !isPointer && !isEnum &&
|
||||
['int', 'float', 'double', 'bool', 'char', 'unsigned short', 'size_t'].includes(cleanElementType);
|
||||
const isEnum = enumNames.has(cleanElementType) || cleanElementType === 'PropertyId';
|
||||
const isPrimPointer = isPointer && isPrimitive(cleanElementType);
|
||||
const isPrim = !isPointer && !isEnum && isPrimitive(cleanElementType);
|
||||
|
||||
// Generate C type names
|
||||
let cTypeName: string;
|
||||
let cElementType: string;
|
||||
|
||||
if (isPrimitive) {
|
||||
// Map primitive types
|
||||
const typeMap: { [key: string]: string } = {
|
||||
'int': 'int',
|
||||
'unsigned short': 'unsigned short',
|
||||
'float': 'float',
|
||||
'double': 'double',
|
||||
'bool': 'bool',
|
||||
'char': 'char',
|
||||
'size_t': 'size_t'
|
||||
};
|
||||
cElementType = typeMap[cleanElementType] || cleanElementType;
|
||||
cTypeName = `spine_array_${cElementType.replace(/_t$/, '')}`;
|
||||
if (isPrim) {
|
||||
cElementType = cleanElementType;
|
||||
// Replace whitespace with underscore for multi-word types like "unsigned short"
|
||||
cTypeName = `spine_array_${cleanElementType.replace(/\s+/g, '_')}`;
|
||||
} else if (isPrimPointer) {
|
||||
// Primitive pointer types: keep the pointer in cElementType
|
||||
cElementType = elementType; // e.g., "float*"
|
||||
// Generate unique name with _ptr suffix
|
||||
cTypeName = `spine_array_${cleanElementType.replace(/\s+/g, '_')}_ptr`;
|
||||
} else if (isEnum) {
|
||||
// Handle enums
|
||||
if (cleanElementType === 'PropertyId') {
|
||||
@ -120,19 +122,31 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[
|
||||
cTypeName = 'spine_array_property_id';
|
||||
} else {
|
||||
// Convert enum name to snake_case
|
||||
const snakeCase = cleanElementType.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
|
||||
const snakeCase = toSnakeCase(cleanElementType);
|
||||
cElementType = `spine_${snakeCase}`;
|
||||
cTypeName = `spine_array_${snakeCase}`;
|
||||
}
|
||||
} else if (isPointer) {
|
||||
// Handle pointer types
|
||||
const snakeCase = cleanElementType.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
|
||||
// Handle non-primitive pointer types (e.g., Bone*)
|
||||
const snakeCase = toSnakeCase(cleanElementType);
|
||||
cElementType = `spine_${snakeCase}`;
|
||||
cTypeName = `spine_array_${snakeCase}`;
|
||||
} else {
|
||||
// Unknown type - skip
|
||||
warnings.push(`Unknown array element type: ${elementType}`);
|
||||
continue;
|
||||
// Check for problematic types
|
||||
if (elementType.startsWith('Array<')) {
|
||||
// C doesn't support nested templates, would need manual Array<Array<T>> implementation
|
||||
warnings.addWarning(arrayType, "C doesn't support nested templates", sources);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (elementType === 'String') {
|
||||
// String arrays should use const char** instead
|
||||
warnings.addWarning(arrayType, "String arrays should use const char** in C API", sources);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unknown type - throw!
|
||||
throw new Error(`Unsupported array element type: ${elementType} in ${arrayType} at ${firstSource.type.name}::${firstSource.member.name}`);
|
||||
}
|
||||
|
||||
specializations.push({
|
||||
@ -142,21 +156,45 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[
|
||||
cElementType: cElementType,
|
||||
isPointer: isPointer,
|
||||
isEnum: isEnum,
|
||||
isPrimitive: isPrimitive
|
||||
isPrimitive: isPrim,
|
||||
sourceMember: firstSource.member // Use first occurrence for debugging
|
||||
});
|
||||
}
|
||||
|
||||
// Print warnings
|
||||
if (warnings.length > 0) {
|
||||
console.log('\nArray Generation Warnings:');
|
||||
for (const warning of warnings) {
|
||||
console.log(` - ${warning}`);
|
||||
warnings.printWarnings('Array Generation Warnings:');
|
||||
|
||||
// Sort specializations: primitives first, then enums, then pointers
|
||||
specializations.sort((a, b) => {
|
||||
// Sort by type category first
|
||||
const getCategory = (spec: ArraySpecialization) => {
|
||||
if (spec.isPrimitive) return 0;
|
||||
if (spec.isEnum) return 1;
|
||||
if (spec.isPointer) return 2;
|
||||
// This should never happen - every specialization must be one of the above
|
||||
throw new Error(`Invalid ArraySpecialization state for ${spec.cppType}: ` +
|
||||
`isPrimitive=${spec.isPrimitive}, isEnum=${spec.isEnum}, isPointer=${spec.isPointer}`);
|
||||
};
|
||||
|
||||
const categoryA = getCategory(a);
|
||||
const categoryB = getCategory(b);
|
||||
|
||||
if (categoryA !== categoryB) {
|
||||
return categoryA - categoryB;
|
||||
}
|
||||
|
||||
// Within same category, sort by name
|
||||
return a.cTypeName.localeCompare(b.cTypeName);
|
||||
});
|
||||
|
||||
// Log found specializations for debugging
|
||||
if (specializations.length > 0) {
|
||||
console.log('Found array specializations:');
|
||||
for (const spec of specializations) {
|
||||
console.log(` - ${spec.cppType} → ${spec.cTypeName} (element: ${spec.cElementType})`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Sort by C type name for consistent output
|
||||
specializations.sort((a, b) => a.cTypeName.localeCompare(b.cTypeName));
|
||||
|
||||
return specializations;
|
||||
}
|
||||
@ -86,6 +86,115 @@ function checkConstNonConstConflicts(classes: Type[], exclusions: Exclusion[]):
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for multi-level pointers in method signatures and errors if found
|
||||
*/
|
||||
function checkMultiLevelPointers(types: Type[]) {
|
||||
const errors: {type: string, member: string, signature: string}[] = [];
|
||||
|
||||
// Helper to check if a type has multi-level pointers
|
||||
function hasMultiLevelPointers(typeStr: string): boolean {
|
||||
// First check the outer type (after removing template content)
|
||||
let outerType = typeStr;
|
||||
|
||||
// Extract all template contents for separate checking
|
||||
const templateContents: string[] = [];
|
||||
let depth = 0;
|
||||
let templateStart = -1;
|
||||
|
||||
for (let i = 0; i < typeStr.length; i++) {
|
||||
if (typeStr[i] === '<') {
|
||||
if (depth === 0) {
|
||||
templateStart = i + 1;
|
||||
}
|
||||
depth++;
|
||||
} else if (typeStr[i] === '>') {
|
||||
depth--;
|
||||
if (depth === 0 && templateStart !== -1) {
|
||||
templateContents.push(typeStr.substring(templateStart, i));
|
||||
templateStart = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all template content from outer type
|
||||
outerType = outerType.replace(/<[^>]+>/g, '<>');
|
||||
|
||||
// Check outer type for consecutive pointers
|
||||
if (/\*\s*\*/.test(outerType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recursively check template contents
|
||||
for (const content of templateContents) {
|
||||
if (hasMultiLevelPointers(content)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const type of types) {
|
||||
if (!type.members) continue;
|
||||
|
||||
for (const member of type.members) {
|
||||
if (member.kind === 'method') {
|
||||
// Check return type
|
||||
if (hasMultiLevelPointers(member.returnType)) {
|
||||
errors.push({
|
||||
type: type.name,
|
||||
member: member.name,
|
||||
signature: `return type: ${member.returnType}`
|
||||
});
|
||||
}
|
||||
|
||||
// Check parameters
|
||||
if (member.parameters) {
|
||||
for (const param of member.parameters) {
|
||||
if (hasMultiLevelPointers(param.type)) {
|
||||
errors.push({
|
||||
type: type.name,
|
||||
member: member.name,
|
||||
signature: `parameter '${param.name}': ${param.type}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (member.kind === 'field') {
|
||||
// Check field type
|
||||
if (hasMultiLevelPointers(member.type)) {
|
||||
errors.push({
|
||||
type: type.name,
|
||||
member: member.name,
|
||||
signature: `field type: ${member.type}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we found multi-level pointers, report them and exit
|
||||
if (errors.length > 0) {
|
||||
console.error("\n" + "=".repeat(80));
|
||||
console.error("MULTI-LEVEL POINTER ERROR");
|
||||
console.error("=".repeat(80));
|
||||
console.error(`\nFound ${errors.length} multi-level pointer usage(s) which are not supported:\n`);
|
||||
|
||||
for (const error of errors) {
|
||||
console.error(` - ${error.type}::${error.member} - ${error.signature}`);
|
||||
}
|
||||
|
||||
console.error("\nMulti-level pointers (e.g., char**, void***) cannot be represented in the C API.");
|
||||
console.error("You need to either:");
|
||||
console.error(" 1. Refactor the C++ code to avoid multi-level pointers");
|
||||
console.error(" 2. Exclude these types/methods in exclusions.txt");
|
||||
console.error("=".repeat(80) + "\n");
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Extract types if needed
|
||||
extractTypes();
|
||||
@ -134,6 +243,9 @@ async function main() {
|
||||
// Check for const/non-const conflicts
|
||||
checkConstNonConstConflicts(classes, exclusions);
|
||||
|
||||
// Check for multi-level pointers
|
||||
checkMultiLevelPointers(includedTypes);
|
||||
|
||||
// Create a set of valid type names for type checking
|
||||
const validTypes = new Set<string>(includedTypes.map(t => t.name));
|
||||
|
||||
@ -187,8 +299,7 @@ async function main() {
|
||||
|
||||
// Generate Array specializations
|
||||
console.log('\nScanning for Array specializations...');
|
||||
const enumNames = new Set(enums.map(e => e.name));
|
||||
const arraySpecs = scanArraySpecializations(typesJson, exclusions, enumNames);
|
||||
const arraySpecs = scanArraySpecializations(includedTypes);
|
||||
console.log(`Found ${arraySpecs.length} array specializations to generate`);
|
||||
|
||||
if (arraySpecs.length > 0) {
|
||||
|
||||
@ -80,6 +80,17 @@ export type Exclusion =
|
||||
isConst?: boolean; // Whether the method is const (e.g., void foo() const), NOT whether return type is const
|
||||
};
|
||||
|
||||
export interface ArraySpecialization {
|
||||
cppType: string; // e.g. "Array<float>"
|
||||
elementType: string; // e.g. "float"
|
||||
cTypeName: string; // e.g. "spine_array_float"
|
||||
cElementType: string; // e.g. "float" or "spine_animation"
|
||||
isPointer: boolean;
|
||||
isEnum: boolean;
|
||||
isPrimitive: boolean;
|
||||
sourceMember: Member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a PascalCase or camelCase name to snake_case.
|
||||
*
|
||||
@ -123,7 +134,7 @@ export function toCFunctionName(typeName: string, methodName: string): string {
|
||||
* - "Array<float>" → false (starts uppercase)
|
||||
* - "const Array<float>&" → false ("Array" starts uppercase)
|
||||
*/
|
||||
function isPrimitive(cppType: string): boolean {
|
||||
export function isPrimitive(cppType: string): boolean {
|
||||
const tokens = cppType.split(/\s+/);
|
||||
return tokens.every(token => {
|
||||
// Remove any trailing punctuation like *, &
|
||||
|
||||
67
spine-c-new/codegen/src/warnings.ts
Normal file
67
spine-c-new/codegen/src/warnings.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Type, Member } from './types';
|
||||
|
||||
export interface Warning {
|
||||
reason: string;
|
||||
locations: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages warnings during code generation, grouping them by type
|
||||
*/
|
||||
export class WarningsCollector {
|
||||
private warnings = new Map<string, Warning>();
|
||||
|
||||
/**
|
||||
* Add a warning for a specific type pattern (e.g., "Array<String>")
|
||||
*/
|
||||
addWarning(pattern: string, reason: string, sources: {type: Type, member: Member}[]) {
|
||||
if (!this.warnings.has(pattern)) {
|
||||
this.warnings.set(pattern, { reason, locations: [] });
|
||||
}
|
||||
const warning = this.warnings.get(pattern)!;
|
||||
for (const source of sources) {
|
||||
warning.locations.push(`${source.type.name}::${source.member.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single warning with location string
|
||||
*/
|
||||
addSingleWarning(pattern: string, reason: string, location: string) {
|
||||
if (!this.warnings.has(pattern)) {
|
||||
this.warnings.set(pattern, { reason, locations: [] });
|
||||
}
|
||||
this.warnings.get(pattern)!.locations.push(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any warnings
|
||||
*/
|
||||
hasWarnings(): boolean {
|
||||
return this.warnings.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all warnings
|
||||
*/
|
||||
getWarnings(): Map<string, Warning> {
|
||||
return this.warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print warnings to console in a formatted way
|
||||
*/
|
||||
printWarnings(title: string = 'Warnings:') {
|
||||
if (this.warnings.size > 0) {
|
||||
console.log(`\n${title}`);
|
||||
for (const [pattern, info] of this.warnings) {
|
||||
console.log(` ${pattern}: ${info.reason}`);
|
||||
console.log(' Found in:');
|
||||
for (const location of info.locations) {
|
||||
console.log(` - ${location}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user