Эх сурвалжийг харах

[c] Add check to codegen for setter/getter pairs with inconsistens nullability (one nullable, the other not)

Mario Zechner 1 сар өмнө
parent
commit
65b138411c

+ 100 - 0
spine-c/codegen/src/checks.ts

@@ -1,5 +1,6 @@
 import { isMethodExcluded } from "./exclusions";
 import { type ClassOrStruct, type Exclusion, type Field, isPrimitive, type Method, type Type, toSnakeCase } from "./types";
+import type { CClassOrStruct, CMethod } from "./c-types";
 
 /**
  * Checks for methods that have both const and non-const versions with different return types.
@@ -341,6 +342,105 @@ export function checkMethodTypeNameConflicts(classes: ClassOrStruct[], allTypes:
     }
 }
 
+/**
+ * Checks for getter/setter pairs where the return type nullability of the getter
+ * doesn't match the parameter nullability of the setter. This creates inconsistent
+ * APIs in target languages (e.g., Dart) where such mismatches are forbidden.
+ * 
+ * @param cTypes - Array of generated C types with their methods
+ */
+export function checkGetterSetterNullabilityMismatch(cTypes: CClassOrStruct[]): void {
+    const mismatches: Array<{ 
+        typeName: string, 
+        fieldName: string, 
+        getterNullable: boolean, 
+        setterNullable: boolean 
+    }> = [];
+
+    for (const cType of cTypes) {
+        if (!cType.methods) continue;
+
+        // Group methods by field name (extract from method names like spine_type_get_field, spine_type_set_field)
+        const fieldAccessors = new Map<string, { getter?: CMethod, setter?: CMethod }>();
+
+        for (const method of cType.methods) {
+            // Check if this is a getter method (ends with _get_<field_name> AND has exactly 1 parameter)
+            const getterMatch = method.name.match(/^(.+)_get_(.+)$/);
+            if (getterMatch && method.parameters?.length === 1) {
+                const fieldName = getterMatch[2];
+                if (!fieldAccessors.has(fieldName)) {
+                    fieldAccessors.set(fieldName, {});
+                }
+                fieldAccessors.get(fieldName)!.getter = method;
+                continue;
+            }
+
+            // Check if this is a setter method (ends with _set_<field_name> AND has exactly 2 parameters)
+            const setterMatch = method.name.match(/^(.+)_set_(.+)$/);
+            if (setterMatch && method.parameters?.length === 2) {
+                const fieldName = setterMatch[2];
+                if (!fieldAccessors.has(fieldName)) {
+                    fieldAccessors.set(fieldName, {});
+                }
+                fieldAccessors.get(fieldName)!.setter = method;
+            }
+        }
+
+        // Check each getter/setter pair for nullability mismatches
+        for (const [fieldName, accessors] of fieldAccessors) {
+            const { getter, setter } = accessors;
+
+            // Skip if we don't have both getter and setter
+            if (!getter || !setter) continue;
+
+            // Extract nullability information
+            const getterNullable = getter.returnTypeNullable || false;
+            
+            // For setters, find the parameter that's not 'self' (should be the value parameter)
+            const valueParam = setter.parameters?.find(p => p.name !== 'self');
+            if (!valueParam) continue;
+            
+            const setterNullable = valueParam.isNullable || false;
+
+            // Check for mismatch
+            if (getterNullable !== setterNullable) {
+                mismatches.push({
+                    typeName: cType.name,
+                    fieldName,
+                    getterNullable,
+                    setterNullable
+                });
+            }
+        }
+    }
+
+    // Report mismatches
+    if (mismatches.length > 0) {
+        console.error("\n" + "=".repeat(80));
+        console.error("GETTER/SETTER NULLABILITY MISMATCHES");
+        console.error("=".repeat(80));
+        console.error(`\nFound ${mismatches.length} getter/setter pairs with mismatched nullability:\n`);
+
+        for (const mismatch of mismatches) {
+            const getterType = mismatch.getterNullable ? "nullable" : "non-nullable";
+            const setterType = mismatch.setterNullable ? "nullable" : "non-nullable";
+            console.error(`  - ${mismatch.typeName}::${mismatch.fieldName}`);
+            console.error(`    Getter returns: ${getterType}`);  
+            console.error(`    Setter expects: ${setterType}`);
+        }
+
+        console.error("\nThese nullability mismatches cause compilation errors in some target");
+        console.error("languages (e.g., Dart). The getter and setter must have consistent nullability.");
+        console.error("You should either:");
+        console.error("  1. Ensure the C++ field type has consistent nullability semantics");
+        console.error("  2. Exclude problematic field getters or setters in exclusions.txt");
+        console.error("  3. Override nullability analysis for specific field types");
+        console.error("=".repeat(80) + "\n");
+
+        process.exit(1);
+    }
+}
+
 /**
  * Checks for methods that return non-primitive types by value.
  * These cannot be wrapped in C without heap allocation.

+ 4 - 1
spine-c/codegen/src/index.ts

@@ -2,7 +2,7 @@
 import * as path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import { CWriter } from './c-writer';
-import { checkConstNonConstConflicts, checkFieldAccessorConflicts, checkMethodTypeNameConflicts, checkMultiLevelPointers, checkValueReturns } from './checks';
+import { checkConstNonConstConflicts, checkFieldAccessorConflicts, checkGetterSetterNullabilityMismatch, checkMethodTypeNameConflicts, checkMultiLevelPointers, checkValueReturns } from './checks';
 import { isTypeExcluded, loadExclusions } from './exclusions';
 import { generateArrays, generateTypes } from './ir-generator';
 import { extractTypes } from './type-extractor';
@@ -71,6 +71,9 @@ export async function generate() {
     const { cTypes, cEnums } = await generateTypes(types, exclusions, allExtractedTypes);
     const cArrayTypes = await generateArrays(types, arrayType, exclusions);
 
+    // Check for getter/setter nullability mismatches
+    checkGetterSetterNullabilityMismatch(cTypes);
+
     // Build interface/pure type information first
     const isInterface = buildInterfaceMap(allExtractedTypes.filter(t => t.kind !== 'enum') as ClassOrStruct[]);