[c] More robust enum extraction, assign self to local vars for easier debugging.

This commit is contained in:
Mario Zechner 2025-07-15 22:17:48 +02:00
parent fc238bc9b7
commit 2c83fe0252
7 changed files with 379 additions and 37040 deletions

3
.gitignore vendored
View File

@ -246,6 +246,3 @@ docs/spine-runtimes-types.md
spine-c/codegen/dist
tests/output
spine-c/.cache
spine-libgdx/.classpath
spine-libgdx/.factorypath
spine-libgdx/.project

View File

@ -1,20 +1,25 @@
# Generated from CLion C/C++ Code Style settings
BasedOnStyle: LLVM
AccessModifierOffset: -4
FixNamespaceComments: false
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: None
AlignOperands: Align
AllowAllArgumentsOnNextLine: false
AlignOperands: DontAlign
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: Always
AllowShortCaseLabelsOnASingleLine: false
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Always
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterReturnType: None
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: true
BinPackParameters: true
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: false
@ -31,11 +36,19 @@ BraceWrapping:
SplitEmptyRecord: true
BreakBeforeBinaryOperators: None
BreakBeforeTernaryOperators: true
BreakAfterJavaFieldAnnotations: false
PenaltyBreakBeforeFirstCallParameter: 200
PenaltyBreakAssignment: 1000
PenaltyBreakFirstLessLess: 120
PenaltyExcessCharacter: 1000000
PenaltyBreakString: 1000
PenaltyReturnTypeOnItsOwnLine: 1000000
BreakConstructorInitializers: BeforeColon
BreakInheritanceList: BeforeColon
ColumnLimit: 0
ColumnLimit: 120
CompactNamespaces: false
ContinuationIndentWidth: 8
ContinuationIndentWidth: 4
DerivePointerAlignment: false
IndentCaseLabels: true
IndentPPDirectives: None
IndentWidth: 4

View File

@ -2,38 +2,292 @@
set -e
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
trap "cleanup" ERR
# Default: format all languages
FORMAT_JAVA=true
FORMAT_TS=true
FORMAT_CPP=true
FORMAT_CSHARP=true
FORMAT_HAXE=true
FORMAT_DART=true
FORMAT_SWIFT=true
setup() {
cp $dir/.clang-format $dir/..
cp $dir/build.gradle $dir/..
cp $dir/settings.gradle $dir/..
cp $dir/.editorconfig $dir/../spine-csharp
cp $dir/.editorconfig $dir/../spine-monogame
cp $dir/.editorconfig $dir/../spine-unity
# Parse command line arguments
show_help() {
echo "Spine Runtimes Code Formatter"
echo ""
echo "Usage: ./format.sh [options]"
echo ""
echo "Options:"
echo " --help, -h Show this help message"
echo " java Format only Java files"
echo " ts Format only TypeScript files"
echo " cpp Format only C/C++ files"
echo " csharp Format only C# files"
echo " haxe Format only Haxe files"
echo " dart Format only Dart files"
echo " swift Format only Swift files"
echo ""
echo "If no language flags are specified, all languages will be formatted."
echo "Multiple language flags can be combined, e.g.: ./format.sh java ts"
echo ""
echo "Tools used:"
echo " Java: Spotless with Eclipse formatter"
echo " TypeScript: Biome"
echo " C/C++: clang-format"
echo " C#: dotnet-format"
echo " Haxe: haxe formatter"
echo " Dart: dart format"
echo " Swift: swift-format"
exit 0
}
cleanup() {
rm $dir/../.clang-format
rm $dir/../build.gradle
rm $dir/../settings.gradle
rm $dir/../spine-csharp/.editorconfig
rm $dir/../spine-monogame/.editorconfig
rm $dir/../spine-unity/.editorconfig
}
# copy Gradle, dotnet-format, and clang-format config to root
setup
# Execute spotless and dotnet-format
pushd $dir/..
./formatters/gradlew spotlessApply
if [ "$1" != "skipdotnet" ] ; then
dotnet-format spine-csharp/spine-csharp.sln
dotnet-format -f spine-monogame
dotnet-format -f spine-unity
# If any language flags are specified, disable all by default
if [[ "$*" == *"java"* ]] || [[ "$*" == *"ts"* ]] || [[ "$*" == *"cpp"* ]] || [[ "$*" == *"csharp"* ]] || [[ "$*" == *"haxe"* ]] || [[ "$*" == *"dart"* ]] || [[ "$*" == *"swift"* ]]; then
FORMAT_JAVA=false
FORMAT_TS=false
FORMAT_CPP=false
FORMAT_CSHARP=false
FORMAT_HAXE=false
FORMAT_DART=false
FORMAT_SWIFT=false
fi
popd
# Delete Gradle, dotnet-format, and clang-format config files in root
cleanup
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help|-h)
show_help
;;
java)
FORMAT_JAVA=true
shift
;;
ts)
FORMAT_TS=true
shift
;;
cpp)
FORMAT_CPP=true
shift
;;
csharp)
FORMAT_CSHARP=true
shift
;;
haxe)
FORMAT_HAXE=true
shift
;;
dart)
FORMAT_DART=true
shift
;;
swift)
FORMAT_SWIFT=true
shift
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Stay in formatters directory and use relative paths
# cd $dir/..
# Format C/C++ files with clang-format
if [ "$FORMAT_CPP" = true ]; then
echo "Formatting C/C++ files..."
if [ ! -f "$dir/.clang-format" ]; then
echo "Error: .clang-format not found in formatters directory"
exit 1
fi
# Define C/C++ source directories - be specific to avoid engine sources
cpp_dirs=(
# spine-cpp
"../spine-cpp/include/spine"
"../spine-cpp/src/spine"
"../spine-cpp/spine-cpp-lite"
"../spine-cpp/tests"
# spine-c
"../spine-c/include"
"../spine-c/src"
"../spine-c/src/generated"
"../spine-c/tests"
# spine-godot
"../spine-godot/spine_godot"
# spine-ue
"../spine-ue/Source/SpineUE"
"../spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Public"
"../spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private"
"../spine-ue/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public"
"../spine-ue/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private"
# spine-glfw
"../spine-glfw/src"
"../spine-glfw/example"
# spine-sdl
"../spine-sdl/src"
"../spine-sdl/example"
# spine-sfml
"../spine-sfml/c/src/spine"
"../spine-sfml/c/example"
"../spine-sfml/cpp/src/spine"
"../spine-sfml/cpp/example"
# spine-cocos2dx
"../spine-cocos2dx/spine-cocos2dx/src/spine"
"../spine-cocos2dx/example/Classes"
# spine-ios
"../spine-ios/Sources/SpineCppLite"
"../spine-ios/Sources/SpineCppLite/include"
"../spine-ios/Sources/SpineShadersStructs"
"../spine-ios/Example/Spine iOS Example"
# spine-flutter
"../spine-flutter/ios/Classes"
"../spine-flutter/macos/Classes"
"../spine-flutter/src"
)
# Collect all C/C++ files from specified directories
files=()
for cpp_dir in "${cpp_dirs[@]}"; do
if [ -d "$cpp_dir" ]; then
while IFS= read -r -d '' file; do
files+=("$file")
done < <(find "$cpp_dir" \( -name "*.c" -o -name "*.cpp" -o -name "*.h" \) \
-not -path "*/.*" \
-not -path "*/build/*" \
-not -path "*/cmake-build-*/*" \
-not -path "*/third_party/*" \
-not -path "*/external/*" \
-not -type l \
-print0)
fi
done
echo "Found ${#files[@]} C/C++ files to format"
# Format each file with progress
count=0
errors=0
for file in "${files[@]}"; do
count=$((count + 1))
# Show progress every 10 files or for the last file
if [ $((count % 10)) -eq 0 ] || [ $count -eq ${#files[@]} ]; then
printf "\r[$count/${#files[@]}] Formatting: %-80s" "$(basename "$file")"
fi
# Format the file and capture any errors
if ! clang-format -i -style=file:"$dir/.clang-format" "$file" 2>/dev/null; then
printf "\nError formatting: $file\n"
errors=$((errors + 1))
fi
done
# Clear the progress line and show completion
printf "\r%-100s\r" " "
if [ $errors -gt 0 ]; then
echo "Completed with $errors errors"
fi
echo "C/C++ formatting complete"
fi
# Format Java files with Spotless (keeping this for Eclipse formatter)
if [ "$FORMAT_JAVA" = true ]; then
echo "Formatting Java files..."
./formatters/gradlew -p formatters spotlessJavaApply --quiet
fi
# Format C# files with dotnet-format
if [ "$FORMAT_CSHARP" = true ]; then
echo "Formatting C# files..."
if command -v dotnet-format &> /dev/null; then
# Copy .editorconfig to C# directories
cp .editorconfig ../spine-csharp/ 2>/dev/null || true
cp .editorconfig ../spine-monogame/ 2>/dev/null || true
cp .editorconfig ../spine-unity/ 2>/dev/null || true
dotnet-format ../spine-csharp/spine-csharp.sln || true
dotnet-format -f ../spine-monogame || true
dotnet-format -f ../spine-unity || true
# Clean up .editorconfig files
rm -f ../spine-csharp/.editorconfig
rm -f ../spine-monogame/.editorconfig
rm -f ../spine-unity/.editorconfig
else
echo "Warning: dotnet-format not found. Skipping C# formatting."
fi
fi
# Format TypeScript files with Biome
if [ "$FORMAT_TS" = true ]; then
echo "Formatting TypeScript files..."
# Check if biome.json files match
if ! cmp -s ../spine-ts/biome.json ../tests/biome.json; then
echo -e "\033[1;31mERROR: spine-ts/biome.json and tests/biome.json differ!\033[0m"
echo -e "\033[1;31mPlease sync them to ensure consistent formatting.\033[0m"
exit 1
fi
# Format TypeScript files
cd ../spine-ts && npx biome format --write . && cd ../formatters
cd ../tests && npx biome format --write --config-path ../spine-ts . && cd ../formatters
fi
# Format Dart files
if [ "$FORMAT_DART" = true ]; then
echo "Formatting Dart files..."
if command -v dart &> /dev/null; then
find .. -name "*.dart" \
-not -path "*/.*" \
-not -path "*/node_modules/*" \
-not -path "*/build/*" \
-exec dart format {} +
else
echo "Warning: dart not found. Skipping Dart formatting."
fi
fi
# Format Haxe files
if [ "$FORMAT_HAXE" = true ]; then
echo "Formatting Haxe files..."
if command -v haxelib &> /dev/null && haxelib list formatter &> /dev/null; then
find .. -name "*.hx" \
-not -path "*/.*" \
-not -path "*/node_modules/*" \
-not -path "*/build/*" \
| xargs haxelib run formatter -s
else
echo "Warning: haxe formatter not found. Install with: haxelib install formatter"
fi
fi
# Format Swift files
if [ "$FORMAT_SWIFT" = true ]; then
echo "Formatting Swift files..."
if command -v swift-format &> /dev/null; then
find .. -name "*.swift" \
-not -path "*/.*" \
-not -path "*/build/*" \
-not -path "*/DerivedData/*" \
| xargs swift-format -i
else
echo "Warning: swift-format not found. Install from https://github.com/apple/swift-format"
fi
fi
echo "Formatting complete!"

View File

@ -33,6 +33,9 @@ fi
# Run codegen if requested
if [ "$1" = "codegen" ]; then
npx tsx codegen/src/index.ts
# Format the generated C++ files
echo "Formatting generated C++ files..."
../formatters/format.sh cpp
exit 0
fi

File diff suppressed because it is too large Load Diff

View File

@ -307,11 +307,13 @@ export function generateFieldAccessors(type: ClassOrStruct, knownTypeNames: Set<
}
function generateFieldGetterBody(field: Field, cppTypeName: string, knownTypeNames: Set<string>): string {
const fieldAccess = `((${cppTypeName}*)self)->${field.name}`;
// Use local variable to avoid cast->field line breaks
const setup = `${cppTypeName} *_self = (${cppTypeName} *) self;`;
const fieldAccess = `_self->${field.name}`;
// Handle String fields
if (field.type === 'String' || field.type === 'const String' || field.type === 'const String&') {
return `return ${fieldAccess}.buffer();`;
return `${setup}\n\treturn ${fieldAccess}.buffer();`;
}
// Handle reference types
@ -320,10 +322,10 @@ function generateFieldGetterBody(field: Field, cppTypeName: string, knownTypeNam
const cType = toCTypeName(baseType, knownTypeNames);
if (isPrimitive(baseType)) {
return `return ${fieldAccess};`;
return `${setup}\n\treturn ${fieldAccess};`;
}
return `return (${cType})&${fieldAccess};`;
return `${setup}\n\treturn (${cType})&${fieldAccess};`;
}
// Handle pointer types
@ -331,35 +333,37 @@ function generateFieldGetterBody(field: Field, cppTypeName: string, knownTypeNam
const baseType = field.type.slice(0, -1).trim();
if (isPrimitive(baseType)) {
return `return ${fieldAccess};`;
return `${setup}\n\treturn ${fieldAccess};`;
}
const cType = toCTypeName(field.type, knownTypeNames);
return `return (${cType})${fieldAccess};`;
return `${setup}\n\treturn (${cType})${fieldAccess};`;
}
// Handle enum types
if (knownTypeNames.has(field.type)) {
const cType = toCTypeName(field.type, knownTypeNames);
return `return (${cType})${fieldAccess};`;
return `${setup}\n\treturn (${cType})${fieldAccess};`;
}
// Handle primitive types
if (isPrimitive(field.type)) {
return `return ${fieldAccess};`;
return `${setup}\n\treturn ${fieldAccess};`;
}
// Handle non-primitive value types (need to return address)
const cType = toCTypeName(field.type, knownTypeNames);
return `return (${cType})&${fieldAccess};`;
return `${setup}\n\treturn (${cType})&${fieldAccess};`;
}
function generateFieldSetterBody(field: Field, cppTypeName: string, knownTypeNames: Set<string>): string {
const fieldAccess = `((${cppTypeName}*)self)->${field.name}`;
// Use local variable to avoid cast->field line breaks
const setup = `${cppTypeName} *_self = (${cppTypeName} *) self;`;
const fieldAccess = `_self->${field.name}`;
// Handle String fields
if (field.type === 'String') {
return `${fieldAccess} = String(value);`;
return `${setup}\n\t${fieldAccess} = String(value);`;
}
// Handle Array types
@ -367,23 +371,23 @@ function generateFieldSetterBody(field: Field, cppTypeName: string, knownTypeNam
const arrayMatch = field.type.match(/^Array<(.+?)>$/);
if (arrayMatch) {
const elementType = arrayMatch[1];
return `${fieldAccess} = *((Array<${elementType}>*)value);`;
return `${setup}\n\t${fieldAccess} = *((Array<${elementType}>*)value);`;
}
}
// Handle enum types
if (knownTypeNames.has(field.type)) {
return `${fieldAccess} = (${field.type})value;`;
return `${setup}\n\t${fieldAccess} = (${field.type})value;`;
}
// Handle pointer types (cast back from opaque type)
if (field.type.endsWith('*') && !isPrimitive(field.type)) {
const baseType = field.type.slice(0, -1).trim();
return `${fieldAccess} = (${baseType}*)value;`;
return `${setup}\n\t${fieldAccess} = (${baseType}*)value;`;
}
// Handle everything else
return `${fieldAccess} = value;`;
return `${setup}\n\t${fieldAccess} = value;`;
}
/**
@ -677,16 +681,20 @@ function generateMethod(type: ClassOrStruct, method: Method, cTypeName: string,
// Generate method body
let methodCall: string;
let body: string;
if (method.isStatic) {
methodCall = `${cppTypeName}::${method.name}(${buildCppArgs(method.parameters || [], cParams, knownTypeNames)})`;
} else if (method.fromSupertype) {
// For inherited methods that may be hidden by derived class methods with same name,
// explicitly call the base class version
methodCall = `((${method.fromSupertype}*)(${cppTypeName}*)self)->${method.name}(${buildCppArgs(method.parameters || [], cParams.slice(1), knownTypeNames)})`;
body = generateReturnStatement(method.returnType, methodCall, knownTypeNames);
} else {
methodCall = `((${cppTypeName}*)self)->${method.name}(${buildCppArgs(method.parameters || [], cParams.slice(1), knownTypeNames)})`;
// Use local variable to avoid cast->method line breaks
const instanceVar = method.fromSupertype ?
`${method.fromSupertype} *_self = (${method.fromSupertype} *) (${cppTypeName} *) self;` :
`${cppTypeName} *_self = (${cppTypeName} *) self;`;
methodCall = `_self->${method.name}(${buildCppArgs(method.parameters || [], cParams.slice(1), knownTypeNames)})`;
const returnStatement = generateReturnStatement(method.returnType, methodCall, knownTypeNames);
body = `${instanceVar}\n\t${returnStatement}`;
}
const body = generateReturnStatement(method.returnType, methodCall, knownTypeNames);
return {
name: cMethodName,
@ -843,17 +851,19 @@ function generateArrayMethod(cTypeName: string, method: Method, cppElementType:
}
function generateArrayMethodBody(method: Method, cppElementType: string, cElementType: string, knownTypeNames: Set<string>): string {
const self = `((Array<${cppElementType}>*)array)->`;
// Use local variable to avoid cast->method line breaks
const setup = `Array<${cppElementType}> *_array = (Array<${cppElementType}> *) array;`;
// Build method call arguments using shared function
const cppArgs = method.parameters ?
buildCppArgs(method.parameters, convertParameters(method.parameters, knownTypeNames), knownTypeNames) :
'';
const methodCall = `${self}${method.name}(${cppArgs})`;
const methodCall = `_array->${method.name}(${cppArgs})`;
// Use shared return value handling
return generateReturnStatement(method.returnType, methodCall, knownTypeNames, cppElementType, cElementType);
const returnStatement = generateReturnStatement(method.returnType, methodCall, knownTypeNames, cppElementType, cElementType);
return `${setup}\n\t${returnStatement}`;
}
function isMethodExcluded(typeName: string, method: Method, exclusions: Exclusion[]): boolean {

View File

@ -16,30 +16,47 @@ function extractEnumValueFromSource(
): string | null | undefined {
if (!enumConstNode.loc) return undefined;
const line = sourceLines[enumConstNode.loc.line - 1];
if (!line) return undefined;
// Find enum name and check for '='
const nameMatch = line.match(new RegExp(`\\b${enumConstNode.name}\\b`));
if (!nameMatch) return undefined;
const afterName = line.substring(nameMatch.index! + enumConstNode.name.length);
const equalIndex = afterName.indexOf('=');
if (equalIndex === -1) return null; // No explicit value
// Extract value expression
let valueText = afterName.substring(equalIndex + 1);
// Handle multi-line values
let currentLine = enumConstNode.loc.line;
while (currentLine < sourceLines.length && !valueText.match(/[,}]/)) {
valueText += '\n' + sourceLines[currentLine++];
let startLine = enumConstNode.loc.line - 1;
// Build a multi-line buffer starting from the enum constant
let buffer = '';
let foundName = false;
// Look for the enum constant name across multiple lines
for (let i = startLine; i < sourceLines.length && i < startLine + 5; i++) {
const line = sourceLines[i];
if (!line) continue;
buffer += line + '\n';
// Check if we found the enum constant name
if (!foundName && line.match(new RegExp(`\\b${enumConstNode.name}\\b`))) {
foundName = true;
}
// If we found a comma or closing brace, we have the complete enum entry
if (foundName && (line.includes(',') || line.includes('}'))) {
break;
}
}
// Extract up to comma or brace
const endMatch = valueText.match(/^(.*?)([,}])/s);
if (endMatch) valueText = endMatch[1];
if (!foundName) return undefined;
// Extract the part after the enum name
const nameMatch = buffer.match(new RegExp(`\\b${enumConstNode.name}\\b\\s*([^,}]*)[,}]?`));
if (!nameMatch) return undefined;
const afterName = nameMatch[1];
// Check if there's an assignment
if (!afterName.includes('=')) return null; // No explicit value
// Extract the value after '='
const equalMatch = afterName.match(/=\s*(.+)/);
if (!equalMatch) return null;
let valueText = equalMatch[1];
// Clean up
return valueText
.replace(/\/\/.*$/gm, '') // Remove single-line comments