From 65b138411c5c0b09fd73b1e74a9ff0c7da4db33a Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 25 Jul 2025 22:16:13 +0200 Subject: [PATCH] [c] Add check to codegen for setter/getter pairs with inconsistens nullability (one nullable, the other not) --- spine-c/codegen/src/checks.ts | 100 ++++++++++++++++++++++++++++++++++ spine-c/codegen/src/index.ts | 5 +- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/spine-c/codegen/src/checks.ts b/spine-c/codegen/src/checks.ts index ac2552e88..5d0231213 100644 --- a/spine-c/codegen/src/checks.ts +++ b/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(); + + for (const method of cType.methods) { + // Check if this is a getter method (ends with _get_ 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_ 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. diff --git a/spine-c/codegen/src/index.ts b/spine-c/codegen/src/index.ts index 9135a26db..c3ea39c0d 100644 --- a/spine-c/codegen/src/index.ts +++ b/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[]);