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

This commit is contained in:
Mario Zechner 2025-07-25 22:16:13 +02:00
parent 92022e9c76
commit 65b138411c
2 changed files with 104 additions and 1 deletions

View File

@ -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.

View File

@ -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[]);