瀏覽代碼

[c] null-analysis tool.

Mario Zechner 1 月之前
父節點
當前提交
736f5148f1

+ 161 - 39
spine-c/codegen/README.md

@@ -1,6 +1,6 @@
 # Spine C API Code Generator
 # Spine C API Code Generator
 
 
-This TypeScript-based code generator automatically creates a C wrapper API for the Spine C++ runtime. It parses the spine-cpp headers using Clang's AST and generates a complete C API with opaque types, following systematic type conversion rules.
+This TypeScript-based code generator automatically creates a C wrapper API for the Spine C++ runtime. It parses the spine-cpp headers using Clang's AST and generates a complete C API with opaque types, following systematic type conversion rules. The generator also builds inheritance maps and interface information for multi-language binding generation.
 
 
 ## Table of Contents
 ## Table of Contents
 
 
@@ -15,7 +15,8 @@ This TypeScript-based code generator automatically creates a C wrapper API for t
 9. [Array Specializations](#array-specializations)
 9. [Array Specializations](#array-specializations)
 10. [Generated Code Examples](#generated-code-examples)
 10. [Generated Code Examples](#generated-code-examples)
 11. [Implementation Details](#implementation-details)
 11. [Implementation Details](#implementation-details)
-12. [Troubleshooting](#troubleshooting)
+12. [Development Tools](#development-tools)
+13. [Troubleshooting](#troubleshooting)
 
 
 ## Overview
 ## Overview
 
 
@@ -28,6 +29,7 @@ The code generator performs static analysis on the spine-cpp headers to automati
 - Array specializations for different element types
 - Array specializations for different element types
 - Field accessors (getters/setters) for public fields
 - Field accessors (getters/setters) for public fields
 - Automatic validation and conflict detection
 - Automatic validation and conflict detection
+- Inheritance analysis and interface detection for multi-language bindings
 
 
 ## Architecture
 ## Architecture
 
 
@@ -67,7 +69,13 @@ The generator follows a multi-stage pipeline:
    - Writes header files with C function declarations
    - Writes header files with C function declarations
    - Writes implementation files with C++ wrapper code
    - Writes implementation files with C++ wrapper code
    - Generates array specialization files
    - Generates array specialization files
-   - Creates main include files (`types.h`, `spine-c.h`)
+   - Creates main include files (`types.h`)
+
+7. **Inheritance Analysis**
+   - Builds inheritance maps for single-inheritance languages (Dart, Swift, Java)
+   - Identifies pure interfaces vs concrete classes
+   - Detects multiple concrete inheritance (not supported)
+   - Generates inheritance information for language binding generators
 
 
 ## Type System
 ## Type System
 
 
@@ -106,6 +114,7 @@ codegen/
 ├── src/
 ├── src/
 │   ├── index.ts           # Main entry point and orchestration
 │   ├── index.ts           # Main entry point and orchestration
 │   ├── type-extractor.ts  # Clang AST parsing
 │   ├── type-extractor.ts  # Clang AST parsing
+│   ├── cpp-check.ts       # C++ nullability analysis tool
 │   ├── types.ts           # Type definitions and conversion logic
 │   ├── types.ts           # Type definitions and conversion logic
 │   ├── c-types.ts         # C IR type definitions
 │   ├── c-types.ts         # C IR type definitions
 │   ├── array-scanner.ts   # Array specialization detection
 │   ├── array-scanner.ts   # Array specialization detection
@@ -114,18 +123,22 @@ codegen/
 │   ├── ir-generator.ts    # C++ to C IR conversion
 │   ├── ir-generator.ts    # C++ to C IR conversion
 │   ├── c-writer.ts        # File generation
 │   ├── c-writer.ts        # File generation
 │   └── warnings.ts        # Warning collection
 │   └── warnings.ts        # Warning collection
+├── dist/                  # TypeScript compilation output
 ├── exclusions.txt         # Type/method exclusions
 ├── exclusions.txt         # Type/method exclusions
 ├── spine-cpp-types.json   # Extracted type information
 ├── spine-cpp-types.json   # Extracted type information
+├── nullable.md            # C++ nullability analysis results
+├── out.json              # Debug output file
 ├── package.json           # Node.js configuration
 ├── package.json           # Node.js configuration
 ├── tsconfig.json          # TypeScript configuration
 ├── tsconfig.json          # TypeScript configuration
-└── generated/             # Output directory (temporary)
+├── tsfmt.json            # TypeScript formatter configuration
+├── biome.json            # Biome linter configuration
+└── node_modules/         # Dependencies
 ```
 ```
 
 
 Generated files are output to `../src/generated/`:
 Generated files are output to `../src/generated/`:
 - Individual files per type (e.g., `skeleton.h`, `skeleton.cpp`)
 - Individual files per type (e.g., `skeleton.h`, `skeleton.cpp`)
 - `types.h` - Forward declarations for all types
 - `types.h` - Forward declarations for all types
 - `arrays.h/cpp` - Array specializations
 - `arrays.h/cpp` - Array specializations
-- `spine-c.h` - Main include file
 
 
 ## Usage
 ## Usage
 
 
@@ -133,14 +146,36 @@ Generated files are output to `../src/generated/`:
 # Install dependencies
 # Install dependencies
 npm install
 npm install
 
 
-npx -y tsx src/index.ts
+# Run the code generator
+npx tsx src/index.ts
+
+# Or export JSON for debugging
+npx tsx src/index.ts --export-json
+
 # The generated files will be in ../src/generated/
 # The generated files will be in ../src/generated/
 ```
 ```
 
 
+### C++ Nullability Analysis Tool
+
+The codegen includes a tool to analyze spine-cpp for nullability patterns:
+
+```bash
+# Generate nullable.md with clickable links to methods with nullable inputs/outputs
+npm run cpp-check
+```
+
+This tool identifies all methods that either:
+- Return pointer types (nullable return values)
+- Take pointer parameters (nullable inputs)
+
+The output `nullable.md` contains clickable markdown links for easy navigation in VS Code. This is useful for cleaning up the spine-cpp API to use references vs pointers appropriately to signal nullability.
+
 The generator automatically:
 The generator automatically:
 - Detects when spine-cpp headers have changed
 - Detects when spine-cpp headers have changed
 - Regenerates only when necessary
 - Regenerates only when necessary
 - Reports warnings and errors during generation
 - Reports warnings and errors during generation
+- Formats the generated C++ code using the project's formatter
+- Builds inheritance maps for multi-language binding generation
 
 
 ## Type Conversion Rules
 ## Type Conversion Rules
 
 
@@ -320,14 +355,14 @@ Array<PropertyId> → spine_array_property_id
 // Header: skeleton.h
 // Header: skeleton.h
 typedef struct spine_skeleton* spine_skeleton;
 typedef struct spine_skeleton* spine_skeleton;
 
 
-spine_skeleton spine_skeleton_new(spine_skeleton_data data);
+spine_skeleton spine_skeleton_create(spine_skeleton_data data);
 void spine_skeleton_dispose(spine_skeleton self);
 void spine_skeleton_dispose(spine_skeleton self);
 void spine_skeleton_update_cache(spine_skeleton self);
 void spine_skeleton_update_cache(spine_skeleton self);
 float spine_skeleton_get_x(const spine_skeleton self);
 float spine_skeleton_get_x(const spine_skeleton self);
 void spine_skeleton_set_x(spine_skeleton self, float value);
 void spine_skeleton_set_x(spine_skeleton self, float value);
 
 
 // Implementation: skeleton.cpp
 // Implementation: skeleton.cpp
-spine_skeleton spine_skeleton_new(spine_skeleton_data data) {
+spine_skeleton spine_skeleton_create(spine_skeleton_data data) {
     return (spine_skeleton) new (__FILE__, __LINE__) Skeleton((SkeletonData*)data);
     return (spine_skeleton) new (__FILE__, __LINE__) Skeleton((SkeletonData*)data);
 }
 }
 
 
@@ -339,46 +374,83 @@ void spine_skeleton_update_cache(spine_skeleton self) {
 ### Enum Wrapper
 ### Enum Wrapper
 ```c
 ```c
 // Header: blend_mode.h
 // Header: blend_mode.h
+#ifndef SPINE_SPINE_BLEND_MODE_H
+#define SPINE_SPINE_BLEND_MODE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 typedef enum spine_blend_mode {
 typedef enum spine_blend_mode {
     SPINE_BLEND_MODE_NORMAL = 0,
     SPINE_BLEND_MODE_NORMAL = 0,
-    SPINE_BLEND_MODE_ADDITIVE = 1,
-    SPINE_BLEND_MODE_MULTIPLY = 2,
-    SPINE_BLEND_MODE_SCREEN = 3
+    SPINE_BLEND_MODE_ADDITIVE,
+    SPINE_BLEND_MODE_MULTIPLY,
+    SPINE_BLEND_MODE_SCREEN
 } spine_blend_mode;
 } spine_blend_mode;
 
 
-// Implementation: blend_mode.cpp
-spine_blend_mode spine_blend_mode_from_cpp(BlendMode value) {
-    return (spine_blend_mode)value;
+#ifdef __cplusplus
 }
 }
+#endif
 
 
-BlendMode spine_blend_mode_to_cpp(spine_blend_mode value) {
-    return (BlendMode)value;
-}
+#endif /* SPINE_SPINE_BLEND_MODE_H */
 ```
 ```
 
 
 ### Array Specialization
 ### Array Specialization
+Arrays are generated as opaque types with complete CRUD operations. All arrays are consolidated into `arrays.h` and `arrays.cpp`.
+
 ```c
 ```c
-// Header: array_float.h
-typedef struct spine_array_float* spine_array_float;
-
-spine_array_float spine_array_float_new(int32_t capacity);
-void spine_array_float_dispose(spine_array_float self);
-int32_t spine_array_float_get_size(const spine_array_float self);
-float spine_array_float_get(const spine_array_float self, int32_t index);
-void spine_array_float_set(spine_array_float self, int32_t index, float value);
-
-// Implementation: array_float.cpp
-struct spine_array_float {
-    Array<float> data;
-};
+// Header: arrays.h
+SPINE_OPAQUE_TYPE(spine_array_float)
+
+// Creation functions
+spine_array_float spine_array_float_create(void);
+spine_array_float spine_array_float_create_with_capacity(size_t initialCapacity);
+
+// Memory management
+void spine_array_float_dispose(spine_array_float array);
+void spine_array_float_clear(spine_array_float array);
+
+// Size and capacity operations
+size_t spine_array_float_get_capacity(spine_array_float array);
+size_t spine_array_float_size(spine_array_float array);
+spine_array_float spine_array_float_set_size(spine_array_float array, size_t newSize, float defaultValue);
+void spine_array_float_ensure_capacity(spine_array_float array, size_t newCapacity);
+
+// Element operations
+void spine_array_float_add(spine_array_float array, float inValue);
+void spine_array_float_add_all(spine_array_float array, spine_array_float inValue);
+void spine_array_float_clear_and_add_all(spine_array_float array, spine_array_float inValue);
+void spine_array_float_remove_at(spine_array_float array, size_t inIndex);
+
+// Search operations
+bool spine_array_float_contains(spine_array_float array, float inValue);
+int spine_array_float_index_of(spine_array_float array, float inValue);
+
+// Direct buffer access
+float *spine_array_float_buffer(spine_array_float array);
+
+// Implementation: arrays.cpp
+spine_array_float spine_array_float_create(void) {
+    return (spine_array_float) new (__FILE__, __LINE__) Array<float>();
+}
 
 
-spine_array_float spine_array_float_new(int32_t capacity) {
-    auto* arr = new (__FILE__, __LINE__) spine_array_float();
-    arr->data.setCapacity(capacity);
-    return arr;
+void spine_array_float_dispose(spine_array_float array) {
+    delete (Array<float> *) array;
+}
+
+void spine_array_float_add(spine_array_float array, float inValue) {
+    Array<float> *_array = (Array<float> *) array;
+    _array->add(inValue);
+}
+
+float *spine_array_float_buffer(spine_array_float array) {
+    Array<float> *_array = (Array<float> *) array;
+    return _array->buffer();
 }
 }
 ```
 ```
 
 
+Arrays are generated for all basic types (`float`, `int`, `unsigned_short`, `property_id`) and all object types used in collections throughout the API. The implementation directly casts the opaque handle to the underlying `Array<T>*` type.
+
 ## Implementation Details
 ## Implementation Details
 
 
 ### Memory Management
 ### Memory Management
@@ -391,7 +463,7 @@ spine_array_float spine_array_float_new(int32_t capacity) {
 - Only generates constructors for non-abstract classes
 - Only generates constructors for non-abstract classes
 - Only generates constructors for classes inheriting from `SpineObject`
 - Only generates constructors for classes inheriting from `SpineObject`
 - Requires at least one public constructor or explicit exclusion
 - Requires at least one public constructor or explicit exclusion
-- Constructor overloads are numbered: `_new`, `_new2`, `_new3`
+- Constructor overloads are numbered: `_create`, `_create2`, `_create3`
 
 
 ### Field Accessor Generation
 ### Field Accessor Generation
 - Generates getters for all non-static public fields
 - Generates getters for all non-static public fields
@@ -400,8 +472,9 @@ spine_array_float spine_array_float_new(int32_t capacity) {
 - Handles nested field access (e.g., `obj.field.x`)
 - Handles nested field access (e.g., `obj.field.x`)
 
 
 ### Method Overloading
 ### Method Overloading
-- Constructor overloads are numbered: `_new`, `_new2`, `_new3`
-- Other overloads must be excluded (C doesn't support overloading)
+- Constructor overloads are numbered: `_create`, `_create2`, `_create3`, etc.
+- Method overloads are numbered with suffixes: `_1`, `_2`, `_3`, etc.
+- Methods named "create" get `_method` suffix to avoid constructor conflicts
 - Const/non-const conflicts are detected and reported
 - Const/non-const conflicts are detected and reported
 
 
 ### RTTI Handling
 ### RTTI Handling
@@ -410,10 +483,21 @@ spine_array_float spine_array_float_new(int32_t capacity) {
 - RTTI checks are performed in generated code where needed
 - RTTI checks are performed in generated code where needed
 
 
 ### Warning System
 ### Warning System
-- Collects non-fatal issues during generation
+- Collects non-fatal issues during generation using `WarningsCollector`
 - Reports abstract classes, missing constructors, etc.
 - Reports abstract classes, missing constructors, etc.
+- Groups warnings by pattern to avoid repetition
 - Warnings don't stop generation but are reported at the end
 - Warnings don't stop generation but are reported at the end
 
 
+### Interface Detection
+- Automatically identifies pure interfaces (classes with only pure virtual methods)
+- Distinguishes between concrete classes and interfaces for inheritance mapping
+- Used to determine extends vs implements relationships for target languages
+
+### Multiple Inheritance Handling
+- Detects multiple concrete inheritance scenarios
+- Fails generation with clear error messages when unsupported patterns are found
+- Provides guidance on converting concrete classes to interfaces
+
 ## Troubleshooting
 ## Troubleshooting
 
 
 ### Common Errors
 ### Common Errors
@@ -438,6 +522,11 @@ spine_array_float spine_array_float_new(int32_t capacity) {
    - Generated function name collides with a type name
    - Generated function name collides with a type name
    - Solution: Rename method or exclude
    - Solution: Rename method or exclude
 
 
+6. **"Multiple concrete inheritance detected"**
+   - A class inherits from multiple concrete (non-interface) classes
+   - Solution: Convert one of the parent classes to a pure interface
+   - Check the error message for specific guidance on which classes to modify
+
 ### Debugging Tips
 ### Debugging Tips
 
 
 1. Check `spine-cpp-types.json` for extracted type information
 1. Check `spine-cpp-types.json` for extracted type information
@@ -445,6 +534,9 @@ spine_array_float spine_array_float_new(int32_t capacity) {
 3. Verify inheritance with "inherits from SpineObject" messages
 3. Verify inheritance with "inherits from SpineObject" messages
 4. Array specializations are listed with element type mapping
 4. Array specializations are listed with element type mapping
 5. Check warnings at the end of generation for issues
 5. Check warnings at the end of generation for issues
+6. Use `--export-json` flag to export inheritance and type information as JSON
+7. Check `out.json` for debug output when troubleshooting
+8. Review console output for inheritance mapping information (extends/mixins)
 
 
 ### Adding New Types
 ### Adding New Types
 
 
@@ -460,4 +552,34 @@ spine_array_float spine_array_float_new(int32_t capacity) {
 - File generation is parallelized where possible
 - File generation is parallelized where possible
 - Array scanning happens after type filtering for efficiency
 - Array scanning happens after type filtering for efficiency
 - Validation checks run before generation to fail fast
 - Validation checks run before generation to fail fast
-- Incremental generation avoids regenerating unchanged files
+- Incremental generation avoids regenerating unchanged files
+
+## Development Tools
+
+The codegen project includes several development tools and configurations:
+
+### Biome Configuration (`biome.json`)
+- Linting enabled with recommended rules
+- Formatting disabled (uses external formatter)
+- Helps maintain code quality during development
+
+### TypeScript Formatter (`tsfmt.json`)
+- Comprehensive formatting rules for TypeScript code
+- Configures indentation, spacing, and code style
+- Used for consistent code formatting across the project
+
+### Build Output (`dist/`)
+- Contains compiled TypeScript files
+- Generated JavaScript and declaration files
+- Source maps for debugging
+
+### Debug Output (`out.json`)
+- Contains debug information from the generation process
+- Useful for troubleshooting and understanding the generated data structure
+
+### Dependencies
+The project uses minimal dependencies for maximum compatibility:
+- `@types/node` - Node.js type definitions
+- `tsx` - TypeScript execution engine
+- `typescript-formatter` - Code formatting
+- `@biomejs/biome` - Fast linter for code quality

+ 4 - 0
spine-c/codegen/package.json

@@ -1,6 +1,10 @@
 {
 {
   "name": "spine-c-codegen",
   "name": "spine-c-codegen",
   "type": "module",
   "type": "module",
+  "scripts": {
+    "generate": "tsx src/index.ts",
+    "null-analysis": "tsx src/null-analysis.ts"
+  },
   "devDependencies": {
   "devDependencies": {
     "@types/node": "^20.0.0",
     "@types/node": "^20.0.0",
     "tsx": "^4.0.0",
     "tsx": "^4.0.0",

+ 205 - 0
spine-c/codegen/src/null-analysis.ts

@@ -0,0 +1,205 @@
+#!/usr/bin/env node
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { extractTypes } from './type-extractor';
+import type { ClassOrStruct, Method, Type } from './types';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+/**
+ * Checks if a type string represents a pointer to a class instance
+ */
+function isPointerToClass(typeStr: string, allTypes: Type[]): boolean {
+    // Remove const, references, and whitespace
+    const cleanType = typeStr.replace(/\bconst\b/g, '').replace(/&/g, '').trim();
+    
+    // Check if it ends with * (pointer)
+    if (!cleanType.endsWith('*')) {
+        return false;
+    }
+    
+    // Extract the base type (remove the *)
+    const baseType = cleanType.replace(/\*+$/, '').trim();
+    
+    // Check if the base type is a class/struct in our type list
+    return allTypes.some(type => 
+        type.kind !== 'enum' && type.name === baseType
+    );
+}
+
+/**
+ * Checks if a type string represents a class instance (for return types)
+ */
+function isClassType(typeStr: string, allTypes: Type[]): boolean {
+    // Remove const, references, and whitespace  
+    const cleanType = typeStr.replace(/\bconst\b/g, '').replace(/&/g, '').replace(/\*/g, '').trim();
+    
+    // Check if the base type is a class/struct in our type list
+    return allTypes.some(type => 
+        type.kind !== 'enum' && type.name === cleanType
+    );
+}
+
+/**
+ * Analyzes all methods to find those with nullable inputs or outputs
+ */
+function analyzeNullableMethods(): void {
+    console.log('Extracting type information...');
+    const allTypes = extractTypes();
+    
+    const nullableMethods: Array<{
+        filename: string;
+        line: number;
+        signature: string;
+        reason: string;
+    }> = [];
+    
+    // Process each type
+    for (const type of allTypes) {
+        if (type.kind === 'enum') continue;
+        
+        const classType = type as ClassOrStruct;
+        if (!classType.members) continue;
+        
+        // Get the source file name relative to the nullable file location
+        const filename = `../../spine-cpp/include/spine/${classType.name}.h`;
+        
+        // Process each method
+        for (const member of classType.members) {
+            if (member.kind !== 'method') continue;
+            
+            const method = member as Method;
+            const signature = buildMethodSignature(classType.name, method);
+            
+            // Check return type - if it returns a pointer to a class
+            if (method.returnType) {
+                const cleanReturnType = method.returnType.replace(/\bconst\b/g, '').trim();
+                if (isPointerToClass(cleanReturnType, allTypes)) {
+                    nullableMethods.push({
+                        filename,
+                        line: method.loc.line,
+                        signature,
+                        reason: `returns nullable pointer: ${method.returnType}`
+                    });
+                }
+            }
+            
+            // Check parameters - if any parameter is a pointer to a class
+            if (method.parameters) {
+                for (const param of method.parameters) {
+                    if (isPointerToClass(param.type, allTypes)) {
+                        nullableMethods.push({
+                            filename,
+                            line: method.loc.line,
+                            signature,
+                            reason: `takes nullable parameter '${param.name}': ${param.type}`
+                        });
+                        break; // Only report once per method
+                    }
+                }
+            }
+        }
+    }
+    
+    // Sort by filename and line
+    nullableMethods.sort((a, b) => {
+        if (a.filename !== b.filename) {
+            return a.filename.localeCompare(b.filename);
+        }
+        return a.line - b.line;
+    });
+    
+    // Write results to nullable.md file
+    const outputPath = path.join(__dirname, '../nullable.md');
+    
+    const instructions = `# Spine C++ Nullability Cleanup
+
+## Instructions
+
+**Phase 1: Enrich nullable.md (if implementations not yet inlined)**
+If checkboxes don't contain concrete implementations:
+1. Use parallel Task agents to find implementations (agents do NOT write to file)
+2. Each agent researches 10-15 methods and returns structured data:
+   \`\`\`
+   METHOD: [method signature]
+   CPP_HEADER: [file:line] [declaration]
+   CPP_IMPL: [file:line] [implementation code]
+   JAVA_IMPL: [file:line] [java method code]
+   ---
+   \`\`\`
+3. Collect all agent results and do ONE MultiEdit to update nullable.md
+4. Inline implementations BELOW each existing checkbox (keep original checkbox text):
+   \`\`\`
+   - [ ] [keep original checkbox line exactly as is]
+     **C++ Implementation:**
+     \`\`\`cpp
+     // Header: [file:line]
+     [declaration]
+     // Implementation: [file:line] 
+     [implementation body]
+     \`\`\`
+     **Java Implementation:**
+     \`\`\`java
+     // [file:line]
+     [java method body]
+     \`\`\`
+   \`\`\`
+
+**Phase 2: Review and Update**
+For each unchecked checkbox (now with implementations inlined):
+1. **Present both implementations** from the checkbox
+2. **Ask if we need to change the C++ signature** based on Java nullability patterns (y/n)
+3. **Make changes if needed**
+   - Change the signature in the header file
+   - Update the implementation in the corresponding .cpp file
+   - Run \`../../spine-cpp/build.sh\` to confirm the changes compile successfully
+4. **Confirm changes**
+   - Summarize what was changed
+   - Ask for confirmation that the changes are correct (y/n)
+   - If yes, check the checkbox and move to the next unchecked item
+
+## Methods to Review
+
+`;
+
+    const methodsList = nullableMethods.map(m => 
+        `- [ ] [${m.filename}:${m.line}](${m.filename}#L${m.line}) ${m.signature} // ${m.reason}`
+    ).join('\n');
+    
+    fs.writeFileSync(outputPath, instructions + methodsList + '\n');
+    
+    console.log(`Found ${nullableMethods.length} methods with nullable inputs/outputs`);
+    console.log(`Results written to: ${outputPath}`);
+    
+    // Print summary statistics
+    const byReason = new Map<string, number>();
+    for (const method of nullableMethods) {
+        const reasonType = method.reason.startsWith('returns') ? 'nullable return' : 'nullable parameter';
+        byReason.set(reasonType, (byReason.get(reasonType) || 0) + 1);
+    }
+    
+    console.log('\nSummary:');
+    for (const [reason, count] of byReason) {
+        console.log(`  ${reason}: ${count} methods`);
+    }
+}
+
+/**
+ * Builds a method signature string
+ */
+function buildMethodSignature(className: string, method: Method): string {
+    const params = method.parameters?.map(p => `${p.type} ${p.name}`).join(', ') || '';
+    const constStr = method.isConst ? ' const' : '';
+    return `${method.returnType || 'void'} ${className}::${method.name}(${params})${constStr}`;
+}
+
+// Main execution
+if (import.meta.url === `file://${process.argv[1]}`) {
+    try {
+        analyzeNullableMethods();
+    } catch (error) {
+        console.error('Error during analysis:', error);
+        process.exit(1);
+    }
+}

+ 32 - 4
spine-c/codegen/src/type-extractor.ts

@@ -122,12 +122,19 @@ function extractMember(inner: any, parent: any): Member & { access?: 'public' |
 
 
     switch (inner.kind) {
     switch (inner.kind) {
         case 'FieldDecl': {
         case 'FieldDecl': {
+            if (!inner.loc) {
+                throw new Error(`Failed to extract location for field '${inner.name || 'unknown'}' in ${parent.name || 'unknown'}`);
+            }
             const field: Field & { access?: 'public' | 'protected' } = {
             const field: Field & { access?: 'public' | 'protected' } = {
                 kind: 'field',
                 kind: 'field',
                 name: inner.name || '',
                 name: inner.name || '',
                 type: inner.type?.qualType || '',
                 type: inner.type?.qualType || '',
                 isStatic: inner.storageClass === 'static',
                 isStatic: inner.storageClass === 'static',
-                access: 'public' // Will be set correctly later
+                access: 'public', // Will be set correctly later
+                loc: {
+                    line: inner.loc.line || 0,
+                    col: inner.loc.col || 0
+                }
             };
             };
             return field;
             return field;
         }
         }
@@ -136,6 +143,9 @@ function extractMember(inner: any, parent: any): Member & { access?: 'public' |
             // Skip operators - not needed for C wrapper generation
             // Skip operators - not needed for C wrapper generation
             if (inner.name.startsWith('operator')) return null;
             if (inner.name.startsWith('operator')) return null;
 
 
+            if (!inner.loc) {
+                throw new Error(`Failed to extract location for method '${inner.name}' in ${parent.name || 'unknown'}`);
+            }
             const method: Method & { access?: 'public' | 'protected' } = {
             const method: Method & { access?: 'public' | 'protected' } = {
                 kind: 'method',
                 kind: 'method',
                 name: inner.name,
                 name: inner.name,
@@ -145,27 +155,45 @@ function extractMember(inner: any, parent: any): Member & { access?: 'public' |
                 isVirtual: inner.virtual || false,
                 isVirtual: inner.virtual || false,
                 isPure: inner.pure || false,
                 isPure: inner.pure || false,
                 isConst: inner.constQualifier || false,
                 isConst: inner.constQualifier || false,
-                access: 'public' // Will be set correctly later
+                access: 'public', // Will be set correctly later
+                loc: {
+                    line: inner.loc.line || 0,
+                    col: inner.loc.col || 0
+                }
             };
             };
             return method;
             return method;
         }
         }
         case 'CXXConstructorDecl': {
         case 'CXXConstructorDecl': {
+            if (!inner.loc) {
+                throw new Error(`Failed to extract location for constructor '${inner.name || parent.name || 'unknown'}' in ${parent.name || 'unknown'}`);
+            }
             const constr: Constructor & { access?: 'public' | 'protected' } = {
             const constr: Constructor & { access?: 'public' | 'protected' } = {
                 kind: 'constructor',
                 kind: 'constructor',
                 name: inner.name || parent.name || '',
                 name: inner.name || parent.name || '',
                 parameters: extractParameters(inner),
                 parameters: extractParameters(inner),
-                access: 'public' // Will be set correctly later
+                access: 'public', // Will be set correctly later
+                loc: {
+                    line: inner.loc.line || 0,
+                    col: inner.loc.col || 0
+                }
             };
             };
             return constr;
             return constr;
         }
         }
         case 'CXXDestructorDecl': {
         case 'CXXDestructorDecl': {
             // Include destructors for completeness
             // Include destructors for completeness
+            if (!inner.loc) {
+                throw new Error(`Failed to extract location for destructor '${inner.name || `~${parent.name}`}' in ${parent.name || 'unknown'}`);
+            }
             const destructor: Destructor & { access?: 'public' | 'protected' } = {
             const destructor: Destructor & { access?: 'public' | 'protected' } = {
                 kind: 'destructor',
                 kind: 'destructor',
                 name: inner.name || `~${parent.name}`,
                 name: inner.name || `~${parent.name}`,
                 isVirtual: inner.virtual || false,
                 isVirtual: inner.virtual || false,
                 isPure: inner.pure || false,
                 isPure: inner.pure || false,
-                access: 'public' // Will be set correctly later
+                access: 'public', // Will be set correctly later
+                loc: {
+                    line: inner.loc.line || 0,
+                    col: inner.loc.col || 0
+                }
             };
             };
             return destructor;
             return destructor;
         }
         }

+ 16 - 0
spine-c/codegen/src/types.ts

@@ -9,6 +9,10 @@ export type Field = {
     type: string;
     type: string;
     isStatic?: boolean;
     isStatic?: boolean;
     fromSupertype?: string;
     fromSupertype?: string;
+    loc: {
+        line: number;
+        col: number;
+    };
 }
 }
 
 
 export type Method = {
 export type Method = {
@@ -21,6 +25,10 @@ export type Method = {
     isPure?: boolean;
     isPure?: boolean;
     isConst?: boolean;
     isConst?: boolean;
     fromSupertype?: string;
     fromSupertype?: string;
+    loc: {
+        line: number;
+        col: number;
+    };
 }
 }
 
 
 export type Constructor = {
 export type Constructor = {
@@ -28,6 +36,10 @@ export type Constructor = {
     name: string;
     name: string;
     parameters?: Parameter[];
     parameters?: Parameter[];
     fromSupertype?: string;
     fromSupertype?: string;
+    loc: {
+        line: number;
+        col: number;
+    };
 }
 }
 
 
 export type Destructor = {
 export type Destructor = {
@@ -36,6 +48,10 @@ export type Destructor = {
     isVirtual?: boolean;
     isVirtual?: boolean;
     isPure?: boolean;
     isPure?: boolean;
     fromSupertype?: string;
     fromSupertype?: string;
+    loc: {
+        line: number;
+        col: number;
+    };
 };
 };
 
 
 export type Member =
 export type Member =

+ 4 - 54
spine-flutter/codegen/src/index.ts

@@ -9,58 +9,7 @@ import { DartWriter } from './dart-writer.js';
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 
 async function generateFFIBindings(spineCDir: string): Promise<void> {
 async function generateFFIBindings(spineCDir: string): Promise<void> {
-    console.log('Finding all header files...');
-    const generatedDir = path.join(spineCDir, 'src/generated');
-    const headerFiles = fs.readdirSync(generatedDir)
-        .filter(f => f.endsWith('.h'))
-        .map(f => path.join('src/spine-c/src/generated', f))
-        .sort();
-
-    console.log(`Found ${headerFiles.length} header files`);
-
-    // Generate ffigen.yaml configuration
-    console.log('Generating ffigen.yaml configuration...');
-    const ffigenConfig = `# Run with \`dart run ffigen --config ffigen.yaml\`.
-name: SpineDartBindings
-description: |
-  Bindings for Spine C headers.
-
-  Regenerate bindings with \`dart run ffigen --config ffigen.yaml\`.
-output: 'lib/generated/spine_dart_bindings_generated.dart'
-llvm-path:
-    - '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/'
-headers:
-  entry-points:
-    - 'src/spine-c/include/spine-c.h'
-compiler-opts:
-  - '-Isrc/spine-c/include'
-  - '-Isrc/spine-c/src'
-  - '-Isrc/spine-c/src/generated'
-  - '-xc'
-  - '-std=c99'
-functions:
-  include:
-    - 'spine_.*'
-structs:
-  include:
-    - 'spine_.*'
-enums:
-  include:
-    - 'spine_.*'
-typedefs:
-  include:
-    - 'spine_.*'
-preamble: |
-  // ignore_for_file: always_specify_types, constant_identifier_names
-  // ignore_for_file: camel_case_types
-  // ignore_for_file: non_constant_identifier_names
-comments:
-  style: any
-  length: full
-`;
-
-    const ffigenPath = path.join(__dirname, '../../ffigen.yaml');
-    fs.writeFileSync(ffigenPath, ffigenConfig);
+    const ffigenPath = await generateFFigenYaml(spineCDir);
 
 
     // Run ffigen to generate bindings
     // Run ffigen to generate bindings
     console.log('Running ffigen...');
     console.log('Running ffigen...');
@@ -93,7 +42,7 @@ comments:
     console.log('✅ FFI bindings generated successfully!');
     console.log('✅ FFI bindings generated successfully!');
 }
 }
 
 
-async function generateFFigenYamlOnly(spineCDir: string): Promise<void> {
+async function generateFFigenYaml(spineCDir: string): Promise<string> {
     console.log('Finding all header files...');
     console.log('Finding all header files...');
     const generatedDir = path.join(spineCDir, 'src/generated');
     const generatedDir = path.join(spineCDir, 'src/generated');
     const headerFiles = fs.readdirSync(generatedDir)
     const headerFiles = fs.readdirSync(generatedDir)
@@ -147,6 +96,7 @@ comments:
     const ffigenPath = path.join(__dirname, '../../ffigen.yaml');
     const ffigenPath = path.join(__dirname, '../../ffigen.yaml');
     fs.writeFileSync(ffigenPath, ffigenConfig);
     fs.writeFileSync(ffigenPath, ffigenConfig);
     console.log(`FFigen config written to: ${ffigenPath}`);
     console.log(`FFigen config written to: ${ffigenPath}`);
+    return ffigenPath;
 }
 }
 
 
 async function main() {
 async function main() {
@@ -158,7 +108,7 @@ async function main() {
 
 
         // Generate FFI bindings YAML config only
         // Generate FFI bindings YAML config only
         const spineCDir = path.join(__dirname, '../../src/spine-c');
         const spineCDir = path.join(__dirname, '../../src/spine-c');
-        await generateFFigenYamlOnly(spineCDir);
+        await generateFFigenYaml(spineCDir);
         console.log('✅ ffigen.yaml generated successfully!');
         console.log('✅ ffigen.yaml generated successfully!');
         return;
         return;
     }
     }