Jelajahi Sumber

[ts] Use correct typescript-formatter package

Mario Zechner 1 bulan lalu
induk
melakukan
46e38c0356

+ 6 - 6
formatters/format-ts.sh

@@ -1,18 +1,18 @@
 #!/bin/bash
 set -e
 
-# Format TypeScript files with Biome
+# Format TypeScript files with tsfmt
 echo "Formatting TypeScript files..."
 
 dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
 
-# 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"
+# Check if tsfmt.json files match
+if ! cmp -s ../spine-ts/tsfmt.json ../tests/tsfmt.json; then
+    echo -e "\033[1;31mERROR: spine-ts/tsfmt.json and tests/tsfmt.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
+cd ../spine-ts && npm run format && cd ../formatters
+cd ../tests && npm run format -r && cd ../formatters

+ 24 - 24
spine-ts/scripts/format.ts

@@ -2,37 +2,37 @@ import { execSync } from 'node:child_process';
 import * as fs from 'node:fs';
 import * as path from 'node:path';
 
-function findTypeScriptFiles(dir: string, files: string[] = []): string[] {
-    if (!fs.existsSync(dir)) return files;
-    
-    fs.readdirSync(dir).forEach(name => {
-        const filePath = path.join(dir, name);
-        const stat = fs.statSync(filePath);
-        
-        if (stat.isDirectory()) {
-            // Skip node_modules and dist directories
-            if (name !== 'node_modules' && name !== 'dist') {
-                findTypeScriptFiles(filePath, files);
-            }
-        } else if (name.endsWith('.ts') && !name.endsWith('.d.ts')) {
-            files.push(filePath);
-        }
-    });
-    
-    return files;
+function findTypeScriptFiles (dir: string, files: string[] = []): string[] {
+	if (!fs.existsSync(dir)) return files;
+
+	fs.readdirSync(dir).forEach(name => {
+		const filePath = path.join(dir, name);
+		const stat = fs.statSync(filePath);
+
+		if (stat.isDirectory()) {
+			// Skip node_modules and dist directories
+			if (name !== 'node_modules' && name !== 'dist') {
+				findTypeScriptFiles(filePath, files);
+			}
+		} else if (name.endsWith('.ts') && !name.endsWith('.d.ts')) {
+			files.push(filePath);
+		}
+	});
+
+	return files;
 }
 
 // Find all TypeScript files in spine-* directories
 const allFiles: string[] = [];
 fs.readdirSync('.').forEach(name => {
-    if (name.startsWith('spine-') && fs.statSync(name).isDirectory()) {
-        findTypeScriptFiles(name, allFiles);
-    }
+	if (name.startsWith('spine-') && fs.statSync(name).isDirectory()) {
+		findTypeScriptFiles(name, allFiles);
+	}
 });
 
 if (allFiles.length > 0) {
-    console.log(`Formatting ${allFiles.length} TypeScript files...`);
-    execSync(`npx tsfmt -r ${allFiles.join(' ')}`, { stdio: 'inherit' });
+	console.log(`Formatting ${allFiles.length} TypeScript files...`);
+	execSync(`npx -y typescript-formatter -r ${allFiles.join(' ')}`, { stdio: 'inherit' });
 } else {
-    console.log('No TypeScript files found to format.');
+	console.log('No TypeScript files found to format.');
 }

+ 1 - 1
tests/package.json

@@ -4,7 +4,7 @@
   "private": true,
   "scripts": {
     "compare": "tsx compare-with-reference-impl.ts",
-    "format": "npx tsfmt -r ./**/*.ts",
+    "format": "npx -y typescript-formatter -r ./**/*.ts",
     "lint": "npx biome lint ."
   },
   "devDependencies": {

+ 703 - 703
tests/src/analyze-java-api.ts

@@ -9,731 +9,731 @@ import type { LspCliResult, SymbolInfo } from '@mariozechner/lsp-cli';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
-function ensureOutputDir(): string {
-    const outputDir = path.resolve(__dirname, '../output');
-    if (!fs.existsSync(outputDir)) {
-        fs.mkdirSync(outputDir, { recursive: true });
-    }
-    return outputDir;
+function ensureOutputDir (): string {
+	const outputDir = path.resolve(__dirname, '../output');
+	if (!fs.existsSync(outputDir)) {
+		fs.mkdirSync(outputDir, { recursive: true });
+	}
+	return outputDir;
 }
 
-function generateLspData(outputDir: string): string {
-    const outputFile = path.join(outputDir, 'spine-libgdx-symbols.json');
-    const projectDir = path.resolve(__dirname, '../../spine-libgdx');
-    const srcDir = path.join(projectDir, 'spine-libgdx/src');
-
-    // Check if we need to regenerate
-    let needsRegeneration = true;
-    if (fs.existsSync(outputFile)) {
-        const outputStats = fs.statSync(outputFile);
-        const outputTime = outputStats.mtime.getTime();
-
-        // Find the newest source file
-        const newestSourceTime = execSync(
-            `find "${srcDir}" -name "*.java" -type f ! -name "SkeletonSerializer.java" -exec stat -f "%m" {} \\; | sort -nr | head -1`,
-            { encoding: 'utf8' }
-        ).trim();
-
-        if (newestSourceTime) {
-            const sourceTime = parseInt(newestSourceTime) * 1000; // Convert to milliseconds
-            needsRegeneration = sourceTime > outputTime;
-        }
-    }
-
-    if (needsRegeneration) {
-        console.error('Generating LSP data for spine-libgdx...');
-        try {
-            execSync(`node ${path.join(__dirname, '../node_modules/@mariozechner/lsp-cli/dist/index.js')} "${projectDir}" java "${outputFile}"`, {
-                stdio: 'inherit' // Show all output
-            });
-            console.error('LSP data generated successfully');
-        } catch (error: any) {
-            console.error('Error generating LSP data:', error.message);
-            throw error;
-        }
-    } else {
-        console.error('Using existing LSP data (up to date)');
-    }
-
-    return outputFile;
+function generateLspData (outputDir: string): string {
+	const outputFile = path.join(outputDir, 'spine-libgdx-symbols.json');
+	const projectDir = path.resolve(__dirname, '../../spine-libgdx');
+	const srcDir = path.join(projectDir, 'spine-libgdx/src');
+
+	// Check if we need to regenerate
+	let needsRegeneration = true;
+	if (fs.existsSync(outputFile)) {
+		const outputStats = fs.statSync(outputFile);
+		const outputTime = outputStats.mtime.getTime();
+
+		// Find the newest source file
+		const newestSourceTime = execSync(
+			`find "${srcDir}" -name "*.java" -type f ! -name "SkeletonSerializer.java" -exec stat -f "%m" {} \\; | sort -nr | head -1`,
+			{ encoding: 'utf8' }
+		).trim();
+
+		if (newestSourceTime) {
+			const sourceTime = parseInt(newestSourceTime) * 1000; // Convert to milliseconds
+			needsRegeneration = sourceTime > outputTime;
+		}
+	}
+
+	if (needsRegeneration) {
+		console.error('Generating LSP data for spine-libgdx...');
+		try {
+			execSync(`node ${path.join(__dirname, '../node_modules/@mariozechner/lsp-cli/dist/index.js')} "${projectDir}" java "${outputFile}"`, {
+				stdio: 'inherit' // Show all output
+			});
+			console.error('LSP data generated successfully');
+		} catch (error: any) {
+			console.error('Error generating LSP data:', error.message);
+			throw error;
+		}
+	} else {
+		console.error('Using existing LSP data (up to date)');
+	}
+
+	return outputFile;
 }
 
-function analyzeClasses(symbols: SymbolInfo[]): Map<string, ClassInfo> {
-    const classMap = new Map<string, ClassInfo>();
-    const srcPath = path.resolve(__dirname, '../../spine-libgdx/spine-libgdx/src/');
-
-    function processSymbol(symbol: SymbolInfo, parentName?: string) {
-        if (symbol.kind !== 'class' && symbol.kind !== 'enum' && symbol.kind !== 'interface') return;
-
-        // Filter: only process symbols in spine-libgdx/src, excluding SkeletonSerializer
-        if (!symbol.file.startsWith(srcPath)) return;
-        if (symbol.file.endsWith('SkeletonSerializer.java')) return;
-
-        const className = parentName ? `${parentName}.${symbol.name}` : symbol.name;
-
-        const classInfo: ClassInfo = {
-            className: className,
-            superTypes: (symbol.supertypes || []).map(st => st.name.replace('$', '.')),
-            superTypeDetails: symbol.supertypes,
-            file: symbol.file,
-            getters: [],
-            fields: [],
-            isAbstract: false,
-            isInterface: symbol.kind === 'interface',
-            isEnum: symbol.kind === 'enum',
-            typeParameters: symbol.typeParameters || []
-        };
-
-        // Check if abstract class
-        if (symbol.preview && symbol.preview.includes('abstract ')) {
-            classInfo.isAbstract = true;
-        }
-
-        // Find all getter methods, public fields, inner classes, and enum values
-        if (symbol.children) {
-            for (const child of symbol.children) {
-                if (child.kind === 'class' || child.kind === 'enum' || child.kind === 'interface') {
-                    // Process inner class
-                    processSymbol(child, className);
-                } else if (child.kind === 'enumMember') {
-                    // Collect enum values
-                    if (!classInfo.enumValues) {
-                        classInfo.enumValues = [];
-                    }
-                    classInfo.enumValues.push(child.name);
-                } else if (child.kind === 'field' && child.preview) {
-                    // Check if it's a public field
-                    if (child.preview.includes('public ')) {
-                        // Extract field type from preview
-                        // Examples: "public float offset;", "public final Array<ToProperty> to = ..."
-                        const fieldMatch = child.preview.match(/public\s+(final\s+)?(.+?)\s+(\w+)\s*[;=]/);
-                        if (fieldMatch) {
-                            const isFinal = !!fieldMatch[1];
-                            const fieldType = fieldMatch[2].trim();
-                            const fieldName = fieldMatch[3];
-                            classInfo.fields.push({ fieldName, fieldType, isFinal });
-                        }
-                    }
-                } else if (child.kind === 'method' &&
-                    child.name.startsWith('get') &&
-                    child.name !== 'getClass()' &&
-                    child.name.endsWith('()')) { // Only parameterless getters
-
-                    const methodName = child.name.slice(0, -2); // Remove ()
-
-                    if (methodName.length > 3 && methodName[3] === methodName[3].toUpperCase()) {
-                        // Extract return type from preview
-                        let returnType = 'unknown';
-                        if (child.preview) {
-                            const returnMatch = child.preview.match(/(?:public|protected|private)?\s*(.+?)\s+\w+\s*\(\s*\)/);
-                            if (returnMatch) {
-                                returnType = returnMatch[1].trim();
-                            }
-                        }
-
-                        classInfo.getters.push({ methodName, returnType });
-                    }
-                }
-            }
-        }
-
-        classMap.set(className, classInfo);
-    }
-
-    for (const symbol of symbols) {
-        processSymbol(symbol);
-    }
-
-    return classMap;
+function analyzeClasses (symbols: SymbolInfo[]): Map<string, ClassInfo> {
+	const classMap = new Map<string, ClassInfo>();
+	const srcPath = path.resolve(__dirname, '../../spine-libgdx/spine-libgdx/src/');
+
+	function processSymbol (symbol: SymbolInfo, parentName?: string) {
+		if (symbol.kind !== 'class' && symbol.kind !== 'enum' && symbol.kind !== 'interface') return;
+
+		// Filter: only process symbols in spine-libgdx/src, excluding SkeletonSerializer
+		if (!symbol.file.startsWith(srcPath)) return;
+		if (symbol.file.endsWith('SkeletonSerializer.java')) return;
+
+		const className = parentName ? `${parentName}.${symbol.name}` : symbol.name;
+
+		const classInfo: ClassInfo = {
+			className: className,
+			superTypes: (symbol.supertypes || []).map(st => st.name.replace('$', '.')),
+			superTypeDetails: symbol.supertypes,
+			file: symbol.file,
+			getters: [],
+			fields: [],
+			isAbstract: false,
+			isInterface: symbol.kind === 'interface',
+			isEnum: symbol.kind === 'enum',
+			typeParameters: symbol.typeParameters || []
+		};
+
+		// Check if abstract class
+		if (symbol.preview && symbol.preview.includes('abstract ')) {
+			classInfo.isAbstract = true;
+		}
+
+		// Find all getter methods, public fields, inner classes, and enum values
+		if (symbol.children) {
+			for (const child of symbol.children) {
+				if (child.kind === 'class' || child.kind === 'enum' || child.kind === 'interface') {
+					// Process inner class
+					processSymbol(child, className);
+				} else if (child.kind === 'enumMember') {
+					// Collect enum values
+					if (!classInfo.enumValues) {
+						classInfo.enumValues = [];
+					}
+					classInfo.enumValues.push(child.name);
+				} else if (child.kind === 'field' && child.preview) {
+					// Check if it's a public field
+					if (child.preview.includes('public ')) {
+						// Extract field type from preview
+						// Examples: "public float offset;", "public final Array<ToProperty> to = ..."
+						const fieldMatch = child.preview.match(/public\s+(final\s+)?(.+?)\s+(\w+)\s*[;=]/);
+						if (fieldMatch) {
+							const isFinal = !!fieldMatch[1];
+							const fieldType = fieldMatch[2].trim();
+							const fieldName = fieldMatch[3];
+							classInfo.fields.push({ fieldName, fieldType, isFinal });
+						}
+					}
+				} else if (child.kind === 'method' &&
+					child.name.startsWith('get') &&
+					child.name !== 'getClass()' &&
+					child.name.endsWith('()')) { // Only parameterless getters
+
+					const methodName = child.name.slice(0, -2); // Remove ()
+
+					if (methodName.length > 3 && methodName[3] === methodName[3].toUpperCase()) {
+						// Extract return type from preview
+						let returnType = 'unknown';
+						if (child.preview) {
+							const returnMatch = child.preview.match(/(?:public|protected|private)?\s*(.+?)\s+\w+\s*\(\s*\)/);
+							if (returnMatch) {
+								returnType = returnMatch[1].trim();
+							}
+						}
+
+						classInfo.getters.push({ methodName, returnType });
+					}
+				}
+			}
+		}
+
+		classMap.set(className, classInfo);
+	}
+
+	for (const symbol of symbols) {
+		processSymbol(symbol);
+	}
+
+	return classMap;
 }
 
-function findAccessibleTypes(
-    classMap: Map<string, ClassInfo>,
-    startingTypes: string[]
+function findAccessibleTypes (
+	classMap: Map<string, ClassInfo>,
+	startingTypes: string[]
 ): Set<string> {
-    const accessible = new Set<string>();
-    const toVisit = [...startingTypes];
-    const visited = new Set<string>();
-
-    // Helper to find all concrete subclasses of a type
-    function findConcreteSubclasses(typeName: string, addToQueue: boolean = true): string[] {
-        const concreteClasses: string[] = [];
-
-        if (!classMap.has(typeName)) return concreteClasses;
-
-        const classInfo = classMap.get(typeName)!;
-
-        // Add the type itself if it's concrete
-        if (!classInfo.isAbstract && !classInfo.isInterface && !classInfo.isEnum) {
-            concreteClasses.push(typeName);
-        }
-
-        // Find all subclasses recursively
-        for (const [className, info] of classMap) {
-            // Check if this class extends our target (handle both qualified and unqualified names)
-            const extendsTarget = info.superTypes.some(st =>
-                st === typeName ||
-                st === typeName.split('.').pop() ||
-                (typeName.includes('.') && className.startsWith(typeName.split('.')[0] + '.') && st === typeName.split('.').pop())
-            );
-
-            if (extendsTarget) {
-                // Recursively find concrete subclasses
-                const subclasses = findConcreteSubclasses(className, false);
-                concreteClasses.push(...subclasses);
-
-                if (addToQueue && !visited.has(className)) {
-                    toVisit.push(className);
-                }
-            }
-        }
-
-        return concreteClasses;
-    }
-
-    while (toVisit.length > 0) {
-        const typeName = toVisit.pop()!;
-
-        if (visited.has(typeName)) continue;
-        visited.add(typeName);
-
-        if (!classMap.has(typeName)) {
-            console.error(`Type ${typeName} not found in classMap`);
-            continue;
-        }
-
-        const classInfo = classMap.get(typeName)!;
-
-        // Add the type itself if it's concrete
-        if (!classInfo.isAbstract && !classInfo.isInterface && !classInfo.isEnum) {
-            accessible.add(typeName);
-            console.error(`Added concrete type: ${typeName}`);
-        }
-
-        // Find all concrete subclasses of this type
-        const concreteClasses = findConcreteSubclasses(typeName);
-        concreteClasses.forEach(c => accessible.add(c));
-
-        // Add types from getter return types and field types
-        const allTypes = [
-            ...classInfo.getters.map(g => g.returnType),
-            ...classInfo.fields.map(f => f.fieldType)
-        ];
-
-        for (const type of allTypes) {
-            const returnType = type
-                .replace(/@Null\s+/g, '') // Remove @Null annotations
-                .replace(/\s+/g, ' ');     // Normalize whitespace
-
-            // Extract types from Array<Type>, IntArray, FloatArray, etc.
-            const arrayMatch = returnType.match(/Array<(.+?)>/);
-            if (arrayMatch) {
-                const innerType = arrayMatch[1].trim();
-                // Handle inner classes like AnimationState.TrackEntry
-                if (innerType.includes('.')) {
-                    if (classMap.has(innerType) && !visited.has(innerType)) {
-                        toVisit.push(innerType);
-                    }
-                } else {
-                    // Try both plain type and as inner class of current type
-                    if (classMap.has(innerType) && !visited.has(innerType)) {
-                        toVisit.push(innerType);
-                    }
-                    // Also try as inner class of the declaring type
-                    const parts = typeName.split('.');
-                    for (let i = parts.length; i >= 1; i--) {
-                        const parentPath = parts.slice(0, i).join('.');
-                        const innerClassPath = `${parentPath}.${innerType}`;
-                        if (classMap.has(innerClassPath) && !visited.has(innerClassPath)) {
-                            toVisit.push(innerClassPath);
-                            break;
-                        }
-                    }
-                }
-            }
-
-            // Extract all capitalized type names
-            const typeMatches = returnType.match(/\b([A-Z]\w+(?:\.[A-Z]\w+)*)\b/g);
-            if (typeMatches) {
-                for (const match of typeMatches) {
-                    if (classMap.has(match) && !visited.has(match)) {
-                        toVisit.push(match);
-                    }
-                    // For non-qualified names, also try as inner class
-                    if (!match.includes('.')) {
-                        // Try as inner class of current type and its parents
-                        const parts = typeName.split('.');
-                        for (let i = parts.length; i >= 1; i--) {
-                            const parentPath = parts.slice(0, i).join('.');
-                            const innerClassPath = `${parentPath}.${match}`;
-                            if (classMap.has(innerClassPath) && !visited.has(innerClassPath)) {
-                                toVisit.push(innerClassPath);
-                                break;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    console.error(`Found ${accessible.size} accessible types`);
-    return accessible;
+	const accessible = new Set<string>();
+	const toVisit = [...startingTypes];
+	const visited = new Set<string>();
+
+	// Helper to find all concrete subclasses of a type
+	function findConcreteSubclasses (typeName: string, addToQueue: boolean = true): string[] {
+		const concreteClasses: string[] = [];
+
+		if (!classMap.has(typeName)) return concreteClasses;
+
+		const classInfo = classMap.get(typeName)!;
+
+		// Add the type itself if it's concrete
+		if (!classInfo.isAbstract && !classInfo.isInterface && !classInfo.isEnum) {
+			concreteClasses.push(typeName);
+		}
+
+		// Find all subclasses recursively
+		for (const [className, info] of classMap) {
+			// Check if this class extends our target (handle both qualified and unqualified names)
+			const extendsTarget = info.superTypes.some(st =>
+				st === typeName ||
+				st === typeName.split('.').pop() ||
+				(typeName.includes('.') && className.startsWith(typeName.split('.')[0] + '.') && st === typeName.split('.').pop())
+			);
+
+			if (extendsTarget) {
+				// Recursively find concrete subclasses
+				const subclasses = findConcreteSubclasses(className, false);
+				concreteClasses.push(...subclasses);
+
+				if (addToQueue && !visited.has(className)) {
+					toVisit.push(className);
+				}
+			}
+		}
+
+		return concreteClasses;
+	}
+
+	while (toVisit.length > 0) {
+		const typeName = toVisit.pop()!;
+
+		if (visited.has(typeName)) continue;
+		visited.add(typeName);
+
+		if (!classMap.has(typeName)) {
+			console.error(`Type ${typeName} not found in classMap`);
+			continue;
+		}
+
+		const classInfo = classMap.get(typeName)!;
+
+		// Add the type itself if it's concrete
+		if (!classInfo.isAbstract && !classInfo.isInterface && !classInfo.isEnum) {
+			accessible.add(typeName);
+			console.error(`Added concrete type: ${typeName}`);
+		}
+
+		// Find all concrete subclasses of this type
+		const concreteClasses = findConcreteSubclasses(typeName);
+		concreteClasses.forEach(c => accessible.add(c));
+
+		// Add types from getter return types and field types
+		const allTypes = [
+			...classInfo.getters.map(g => g.returnType),
+			...classInfo.fields.map(f => f.fieldType)
+		];
+
+		for (const type of allTypes) {
+			const returnType = type
+				.replace(/@Null\s+/g, '') // Remove @Null annotations
+				.replace(/\s+/g, ' ');     // Normalize whitespace
+
+			// Extract types from Array<Type>, IntArray, FloatArray, etc.
+			const arrayMatch = returnType.match(/Array<(.+?)>/);
+			if (arrayMatch) {
+				const innerType = arrayMatch[1].trim();
+				// Handle inner classes like AnimationState.TrackEntry
+				if (innerType.includes('.')) {
+					if (classMap.has(innerType) && !visited.has(innerType)) {
+						toVisit.push(innerType);
+					}
+				} else {
+					// Try both plain type and as inner class of current type
+					if (classMap.has(innerType) && !visited.has(innerType)) {
+						toVisit.push(innerType);
+					}
+					// Also try as inner class of the declaring type
+					const parts = typeName.split('.');
+					for (let i = parts.length; i >= 1; i--) {
+						const parentPath = parts.slice(0, i).join('.');
+						const innerClassPath = `${parentPath}.${innerType}`;
+						if (classMap.has(innerClassPath) && !visited.has(innerClassPath)) {
+							toVisit.push(innerClassPath);
+							break;
+						}
+					}
+				}
+			}
+
+			// Extract all capitalized type names
+			const typeMatches = returnType.match(/\b([A-Z]\w+(?:\.[A-Z]\w+)*)\b/g);
+			if (typeMatches) {
+				for (const match of typeMatches) {
+					if (classMap.has(match) && !visited.has(match)) {
+						toVisit.push(match);
+					}
+					// For non-qualified names, also try as inner class
+					if (!match.includes('.')) {
+						// Try as inner class of current type and its parents
+						const parts = typeName.split('.');
+						for (let i = parts.length; i >= 1; i--) {
+							const parentPath = parts.slice(0, i).join('.');
+							const innerClassPath = `${parentPath}.${match}`;
+							if (classMap.has(innerClassPath) && !visited.has(innerClassPath)) {
+								toVisit.push(innerClassPath);
+								break;
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	console.error(`Found ${accessible.size} accessible types`);
+	return accessible;
 }
 
-function loadExclusions(): { types: Set<string>, methods: Map<string, Set<string>>, fields: Map<string, Set<string>> } {
-    const exclusionsPath = path.resolve(__dirname, '../java-exclusions.txt');
-    const types = new Set<string>();
-    const methods = new Map<string, Set<string>>();
-    const fields = new Map<string, Set<string>>();
-
-    if (!fs.existsSync(exclusionsPath)) {
-        return { types, methods, fields };
-    }
-
-    const content = fs.readFileSync(exclusionsPath, 'utf-8');
-    const lines = content.split('\n');
-
-    for (const line of lines) {
-        const trimmed = line.trim();
-        if (!trimmed || trimmed.startsWith('#')) continue;
-
-        const parts = trimmed.split(/\s+/);
-        if (parts.length < 2) continue;
-
-        const [type, className, property] = parts;
-
-        switch (type) {
-            case 'type':
-                types.add(className);
-                break;
-            case 'method':
-                if (property) {
-                    if (!methods.has(className)) {
-                        methods.set(className, new Set());
-                    }
-                    methods.get(className)!.add(property);
-                }
-                break;
-            case 'field':
-                if (property) {
-                    if (!fields.has(className)) {
-                        fields.set(className, new Set());
-                    }
-                    fields.get(className)!.add(property);
-                }
-                break;
-        }
-    }
-
-    return { types, methods, fields };
+function loadExclusions (): { types: Set<string>, methods: Map<string, Set<string>>, fields: Map<string, Set<string>> } {
+	const exclusionsPath = path.resolve(__dirname, '../java-exclusions.txt');
+	const types = new Set<string>();
+	const methods = new Map<string, Set<string>>();
+	const fields = new Map<string, Set<string>>();
+
+	if (!fs.existsSync(exclusionsPath)) {
+		return { types, methods, fields };
+	}
+
+	const content = fs.readFileSync(exclusionsPath, 'utf-8');
+	const lines = content.split('\n');
+
+	for (const line of lines) {
+		const trimmed = line.trim();
+		if (!trimmed || trimmed.startsWith('#')) continue;
+
+		const parts = trimmed.split(/\s+/);
+		if (parts.length < 2) continue;
+
+		const [type, className, property] = parts;
+
+		switch (type) {
+			case 'type':
+				types.add(className);
+				break;
+			case 'method':
+				if (property) {
+					if (!methods.has(className)) {
+						methods.set(className, new Set());
+					}
+					methods.get(className)!.add(property);
+				}
+				break;
+			case 'field':
+				if (property) {
+					if (!fields.has(className)) {
+						fields.set(className, new Set());
+					}
+					fields.get(className)!.add(property);
+				}
+				break;
+		}
+	}
+
+	return { types, methods, fields };
 }
 
-function isTypeExcluded(typeName: string, exclusions: ReturnType<typeof loadExclusions>): boolean {
-    return exclusions.types.has(typeName);
+function isTypeExcluded (typeName: string, exclusions: ReturnType<typeof loadExclusions>): boolean {
+	return exclusions.types.has(typeName);
 }
 
-function isPropertyExcluded(className: string, propertyName: string, isGetter: boolean, exclusions: ReturnType<typeof loadExclusions>): boolean {
-    if (isGetter) {
-        return exclusions.methods.get(className)?.has(propertyName) || false;
-    } else {
-        return exclusions.fields.get(className)?.has(propertyName) || false;
-    }
+function isPropertyExcluded (className: string, propertyName: string, isGetter: boolean, exclusions: ReturnType<typeof loadExclusions>): boolean {
+	if (isGetter) {
+		return exclusions.methods.get(className)?.has(propertyName) || false;
+	} else {
+		return exclusions.fields.get(className)?.has(propertyName) || false;
+	}
 }
 
-function getAllProperties(classMap: Map<string, ClassInfo>, className: string, symbolsFile: string, exclusions: ReturnType<typeof loadExclusions>): PropertyInfo[] {
-    const allProperties: PropertyInfo[] = [];
-    const visited = new Set<string>();
-    const classInfo = classMap.get(className);
-    if (!classInfo) return [];
-
-    // Build type parameter mapping based on supertype details
-    const typeParamMap = new Map<string, string>();
-
-    // Helper to build parameter mappings for a specific supertype
-    function buildTypeParamMapping(currentClass: string, targetSupertype: string): Map<string, string> {
-        const mapping = new Map<string, string>();
-        const currentInfo = classMap.get(currentClass);
-        if (!currentInfo || !currentInfo.superTypeDetails) return mapping;
-
-        // Find the matching supertype
-        for (const supertype of currentInfo.superTypeDetails) {
-            if (supertype.name === targetSupertype && supertype.typeArguments) {
-                // Get the supertype's class info to know its type parameters
-                const supertypeInfo = classMap.get(targetSupertype);
-                if (supertypeInfo && supertypeInfo.typeParameters) {
-                    // Map type parameters to arguments
-                    for (let i = 0; i < Math.min(supertypeInfo.typeParameters.length, supertype.typeArguments.length); i++) {
-                        mapping.set(supertypeInfo.typeParameters[i], supertype.typeArguments[i]);
-                    }
-                }
-                break;
-            }
-        }
-        return mapping;
-    }
-
-    function resolveType(type: string, typeMap: Map<string, string> = new Map()): string {
-        // Resolve generic type parameters
-        if (typeMap.has(type)) {
-            return typeMap.get(type)!;
-        }
-        // TODO: Handle complex types like Array<T>, Map<K, V>, etc.
-        return type;
-    }
-
-    // Collect properties in inheritance order (most specific first)
-    function collectProperties(currentClass: string, inheritanceLevel: number = 0, currentTypeMap: Map<string, string> = new Map()) {
-        if (visited.has(currentClass)) return;
-        visited.add(currentClass);
-
-        const classInfo = classMap.get(currentClass);
-        if (!classInfo) return;
-
-        // Add this class's getters with resolved types
-        for (const getter of classInfo.getters) {
-            const propertyName = getter.methodName + '()';
-            allProperties.push({
-                name: propertyName,
-                type: resolveType(getter.returnType, currentTypeMap),
-                isGetter: true,
-                inheritedFrom: inheritanceLevel === 0 ? undefined : currentClass,
-                excluded: isPropertyExcluded(currentClass, propertyName, true, exclusions)
-            });
-        }
-
-        // Add this class's public fields
-        for (const field of classInfo.fields) {
-            allProperties.push({
-                name: field.fieldName,
-                type: resolveType(field.fieldType, currentTypeMap),
-                isGetter: false,
-                inheritedFrom: inheritanceLevel === 0 ? undefined : currentClass,
-                excluded: isPropertyExcluded(currentClass, field.fieldName, false, exclusions)
-            });
-        }
-
-        // Recursively collect from supertypes
-        for (const superType of classInfo.superTypes) {
-            // Build type parameter mapping for this supertype
-            const supertypeMapping = buildTypeParamMapping(currentClass, superType);
-
-            // Compose mappings - resolve type arguments through current mapping
-            const composedMapping = new Map<string, string>();
-            for (const [param, arg] of supertypeMapping) {
-                composedMapping.set(param, resolveType(arg, currentTypeMap));
-            }
-
-            // Try to find the supertype - it might be unqualified
-            let superClassInfo = classMap.get(superType);
-
-            // If not found and it's unqualified, try to find it as an inner class
-            if (!superClassInfo && !superType.includes('.')) {
-                // Try as inner class of the same parent
-                if (currentClass.includes('.')) {
-                    const parentPrefix = currentClass.substring(0, currentClass.lastIndexOf('.'));
-                    const qualifiedSuper = `${parentPrefix}.${superType}`;
-                    superClassInfo = classMap.get(qualifiedSuper);
-                    if (superClassInfo) {
-                        collectProperties(qualifiedSuper, inheritanceLevel + 1, composedMapping);
-                        continue;
-                    }
-                }
-
-                // Try as top-level class
-                for (const [name, info] of classMap) {
-                    if (name === superType || name.endsWith(`.${superType}`)) {
-                        collectProperties(name, inheritanceLevel + 1, composedMapping);
-                        break;
-                    }
-                }
-            } else if (superClassInfo) {
-                collectProperties(superType, inheritanceLevel + 1, composedMapping);
-            }
-        }
-    }
-
-    collectProperties(className);
-
-    // Remove duplicates (overridden methods/shadowed fields), keeping the most specific one
-    const seen = new Map<string, PropertyInfo>();
-    for (const prop of allProperties) {
-        const key = prop.isGetter ? prop.name : `field:${prop.name}`;
-        if (!seen.has(key)) {
-            seen.set(key, prop);
-        }
-    }
-
-    return Array.from(seen.values());
+function getAllProperties (classMap: Map<string, ClassInfo>, className: string, symbolsFile: string, exclusions: ReturnType<typeof loadExclusions>): PropertyInfo[] {
+	const allProperties: PropertyInfo[] = [];
+	const visited = new Set<string>();
+	const classInfo = classMap.get(className);
+	if (!classInfo) return [];
+
+	// Build type parameter mapping based on supertype details
+	const typeParamMap = new Map<string, string>();
+
+	// Helper to build parameter mappings for a specific supertype
+	function buildTypeParamMapping (currentClass: string, targetSupertype: string): Map<string, string> {
+		const mapping = new Map<string, string>();
+		const currentInfo = classMap.get(currentClass);
+		if (!currentInfo || !currentInfo.superTypeDetails) return mapping;
+
+		// Find the matching supertype
+		for (const supertype of currentInfo.superTypeDetails) {
+			if (supertype.name === targetSupertype && supertype.typeArguments) {
+				// Get the supertype's class info to know its type parameters
+				const supertypeInfo = classMap.get(targetSupertype);
+				if (supertypeInfo && supertypeInfo.typeParameters) {
+					// Map type parameters to arguments
+					for (let i = 0; i < Math.min(supertypeInfo.typeParameters.length, supertype.typeArguments.length); i++) {
+						mapping.set(supertypeInfo.typeParameters[i], supertype.typeArguments[i]);
+					}
+				}
+				break;
+			}
+		}
+		return mapping;
+	}
+
+	function resolveType (type: string, typeMap: Map<string, string> = new Map()): string {
+		// Resolve generic type parameters
+		if (typeMap.has(type)) {
+			return typeMap.get(type)!;
+		}
+		// TODO: Handle complex types like Array<T>, Map<K, V>, etc.
+		return type;
+	}
+
+	// Collect properties in inheritance order (most specific first)
+	function collectProperties (currentClass: string, inheritanceLevel: number = 0, currentTypeMap: Map<string, string> = new Map()) {
+		if (visited.has(currentClass)) return;
+		visited.add(currentClass);
+
+		const classInfo = classMap.get(currentClass);
+		if (!classInfo) return;
+
+		// Add this class's getters with resolved types
+		for (const getter of classInfo.getters) {
+			const propertyName = getter.methodName + '()';
+			allProperties.push({
+				name: propertyName,
+				type: resolveType(getter.returnType, currentTypeMap),
+				isGetter: true,
+				inheritedFrom: inheritanceLevel === 0 ? undefined : currentClass,
+				excluded: isPropertyExcluded(currentClass, propertyName, true, exclusions)
+			});
+		}
+
+		// Add this class's public fields
+		for (const field of classInfo.fields) {
+			allProperties.push({
+				name: field.fieldName,
+				type: resolveType(field.fieldType, currentTypeMap),
+				isGetter: false,
+				inheritedFrom: inheritanceLevel === 0 ? undefined : currentClass,
+				excluded: isPropertyExcluded(currentClass, field.fieldName, false, exclusions)
+			});
+		}
+
+		// Recursively collect from supertypes
+		for (const superType of classInfo.superTypes) {
+			// Build type parameter mapping for this supertype
+			const supertypeMapping = buildTypeParamMapping(currentClass, superType);
+
+			// Compose mappings - resolve type arguments through current mapping
+			const composedMapping = new Map<string, string>();
+			for (const [param, arg] of supertypeMapping) {
+				composedMapping.set(param, resolveType(arg, currentTypeMap));
+			}
+
+			// Try to find the supertype - it might be unqualified
+			let superClassInfo = classMap.get(superType);
+
+			// If not found and it's unqualified, try to find it as an inner class
+			if (!superClassInfo && !superType.includes('.')) {
+				// Try as inner class of the same parent
+				if (currentClass.includes('.')) {
+					const parentPrefix = currentClass.substring(0, currentClass.lastIndexOf('.'));
+					const qualifiedSuper = `${parentPrefix}.${superType}`;
+					superClassInfo = classMap.get(qualifiedSuper);
+					if (superClassInfo) {
+						collectProperties(qualifiedSuper, inheritanceLevel + 1, composedMapping);
+						continue;
+					}
+				}
+
+				// Try as top-level class
+				for (const [name, info] of classMap) {
+					if (name === superType || name.endsWith(`.${superType}`)) {
+						collectProperties(name, inheritanceLevel + 1, composedMapping);
+						break;
+					}
+				}
+			} else if (superClassInfo) {
+				collectProperties(superType, inheritanceLevel + 1, composedMapping);
+			}
+		}
+	}
+
+	collectProperties(className);
+
+	// Remove duplicates (overridden methods/shadowed fields), keeping the most specific one
+	const seen = new Map<string, PropertyInfo>();
+	for (const prop of allProperties) {
+		const key = prop.isGetter ? prop.name : `field:${prop.name}`;
+		if (!seen.has(key)) {
+			seen.set(key, prop);
+		}
+	}
+
+	return Array.from(seen.values());
 }
 
 // Helper to find all implementations of a type (both concrete and abstract)
-function findAllImplementations(classMap: Map<string, ClassInfo>, typeName: string, concreteOnly: boolean = false): string[] {
-    const implementations: string[] = [];
-    const visited = new Set<string>();
-
-    function findImplementations(currentType: string) {
-        if (visited.has(currentType)) return;
-        visited.add(currentType);
-
-        // Get the short name for comparison
-        const currentShortName = currentType.split('.').pop()!;
-        const currentPrefix = currentType.includes('.') ? currentType.split('.')[0] : '';
-
-        for (const [className, classInfo] of classMap) {
-            // Check if this class extends/implements the current type
-            let extendsType = false;
-
-            // For inner classes, we need to check if they're in the same outer class
-            if (currentPrefix && className.startsWith(currentPrefix + '.')) {
-                // Both are inner classes of the same outer class
-                extendsType = classInfo.superTypes.some(st =>
-                    st === currentShortName || st === currentType
-                );
-            } else {
-                // Standard inheritance check
-                extendsType = classInfo.superTypes.some(st =>
-                    st === currentType || st === currentShortName
-                );
-            }
-
-            if (extendsType) {
-                if (!classInfo.isAbstract && !classInfo.isInterface && !classInfo.isEnum) {
-                    // This is a concrete implementation
-                    implementations.push(className);
-                } else {
-                    // This is abstract/interface
-                    if (!concreteOnly) {
-                        // Include abstract types when getting all implementations
-                        implementations.push(className);
-                    }
-                    // Always recurse to find further implementations
-                    findImplementations(className);
-                }
-            }
-        }
-    }
-
-    findImplementations(typeName);
-    return [...new Set(implementations)].sort(); // Remove duplicates and sort
+function findAllImplementations (classMap: Map<string, ClassInfo>, typeName: string, concreteOnly: boolean = false): string[] {
+	const implementations: string[] = [];
+	const visited = new Set<string>();
+
+	function findImplementations (currentType: string) {
+		if (visited.has(currentType)) return;
+		visited.add(currentType);
+
+		// Get the short name for comparison
+		const currentShortName = currentType.split('.').pop()!;
+		const currentPrefix = currentType.includes('.') ? currentType.split('.')[0] : '';
+
+		for (const [className, classInfo] of classMap) {
+			// Check if this class extends/implements the current type
+			let extendsType = false;
+
+			// For inner classes, we need to check if they're in the same outer class
+			if (currentPrefix && className.startsWith(currentPrefix + '.')) {
+				// Both are inner classes of the same outer class
+				extendsType = classInfo.superTypes.some(st =>
+					st === currentShortName || st === currentType
+				);
+			} else {
+				// Standard inheritance check
+				extendsType = classInfo.superTypes.some(st =>
+					st === currentType || st === currentShortName
+				);
+			}
+
+			if (extendsType) {
+				if (!classInfo.isAbstract && !classInfo.isInterface && !classInfo.isEnum) {
+					// This is a concrete implementation
+					implementations.push(className);
+				} else {
+					// This is abstract/interface
+					if (!concreteOnly) {
+						// Include abstract types when getting all implementations
+						implementations.push(className);
+					}
+					// Always recurse to find further implementations
+					findImplementations(className);
+				}
+			}
+		}
+	}
+
+	findImplementations(typeName);
+	return [...new Set(implementations)].sort(); // Remove duplicates and sort
 }
 
-function analyzeForSerialization(classMap: Map<string, ClassInfo>, symbolsFile: string): AnalysisResult {
-    const startingTypes = ['SkeletonData', 'Skeleton', 'AnimationState'];
-    const accessibleTypes = findAccessibleTypes(classMap, startingTypes);
-
-    // First pass: populate implementations for all abstract types
-    for (const [className, classInfo] of classMap) {
-        if (classInfo.isAbstract || classInfo.isInterface) {
-            // Get only concrete implementations
-            const concreteImplementations = findAllImplementations(classMap, className, true);
-            classInfo.concreteImplementations = concreteImplementations;
-
-            // Get all implementations (including intermediate abstract types)
-            const allImplementations = findAllImplementations(classMap, className, false);
-            classInfo.allImplementations = allImplementations;
-        }
-    }
-
-    // Collect abstract types and their implementations
-    const abstractTypes = new Map<string, string[]>();
-    const allTypesToGenerate = new Set<string>(accessibleTypes);
-
-    // Find all abstract types referenced by accessible types
-    for (const typeName of accessibleTypes) {
-        const classInfo = classMap.get(typeName);
-        if (!classInfo) continue;
-
-        // Check return types and field types for abstract classes
-        const allTypes = [
-            ...classInfo.getters.map(g => g.returnType),
-            ...classInfo.fields.map(f => f.fieldType)
-        ];
-
-        for (const type of allTypes) {
-            const returnType = type
-                .replace(/@Null\s+/g, '')
-                .replace(/\s+/g, ' ');
-
-            // Extract types from Array<Type>
-            let checkTypes: string[] = [];
-            const arrayMatch = returnType.match(/Array<(.+?)>/);
-            if (arrayMatch) {
-                checkTypes.push(arrayMatch[1].trim());
-            } else if (returnType.match(/^[A-Z]\w+$/)) {
-                checkTypes.push(returnType);
-            }
-
-            // Also check for type names that might be inner classes
-            const typeMatches = returnType.match(/\b([A-Z]\w+)\b/g);
-            if (typeMatches) {
-                for (const match of typeMatches) {
-                    // Try as inner class of current type
-                    const parts = typeName.split('.');
-                    for (let i = parts.length; i >= 1; i--) {
-                        const parentPath = parts.slice(0, i).join('.');
-                        const innerClassPath = `${parentPath}.${match}`;
-                        if (classMap.has(innerClassPath)) {
-                            checkTypes.push(innerClassPath);
-                            break;
-                        }
-                    }
-                }
-            }
-
-            for (const checkType of checkTypes) {
-                if (checkType && classMap.has(checkType)) {
-                    const typeInfo = classMap.get(checkType)!;
-                    if (typeInfo.isAbstract || typeInfo.isInterface) {
-                        // Use the already populated concreteImplementations
-                        const implementations = typeInfo.concreteImplementations || [];
-                        abstractTypes.set(checkType, implementations);
-
-                        // Add all concrete implementations to types to generate
-                        implementations.forEach(impl => allTypesToGenerate.add(impl));
-                    }
-                }
-            }
-        }
-    }
-
-    // Load exclusions
-    const exclusions = loadExclusions();
-
-    // Filter out excluded types from allTypesToGenerate
-    const filteredTypesToGenerate = new Set<string>();
-    for (const typeName of allTypesToGenerate) {
-        if (!isTypeExcluded(typeName, exclusions)) {
-            filteredTypesToGenerate.add(typeName);
-        } else {
-            console.error(`Excluding type: ${typeName}`);
-        }
-    }
-
-
-    // Update allTypesToGenerate to the filtered set
-    allTypesToGenerate.clear();
-    filteredTypesToGenerate.forEach(type => allTypesToGenerate.add(type));
-
-    // Collect all properties for each type (including inherited ones)
-    const typeProperties = new Map<string, PropertyInfo[]>();
-    for (const typeName of allTypesToGenerate) {
-        const props = getAllProperties(classMap, typeName, symbolsFile, exclusions);
-        typeProperties.set(typeName, props);
-    }
-
-    // Also collect properties for abstract types (so we know what properties their implementations should have)
-    for (const abstractType of abstractTypes.keys()) {
-        if (!typeProperties.has(abstractType) && !isTypeExcluded(abstractType, exclusions)) {
-            const props = getAllProperties(classMap, abstractType, symbolsFile, exclusions);
-            typeProperties.set(abstractType, props);
-        }
-    }
-
-    // Second pass: find additional concrete types referenced in properties
-    const additionalTypes = new Set<string>();
-    for (const [typeName, props] of typeProperties) {
-        for (const prop of props) {
-            const propType = prop.type.replace(/@Null\s+/g, '').trim();
-
-            // Check if it's a simple type name
-            const typeMatch = propType.match(/^([A-Z]\w+)$/);
-            if (typeMatch) {
-                const type = typeMatch[1];
-                if (classMap.has(type)) {
-                    const typeInfo = classMap.get(type)!;
-                    if (!typeInfo.isAbstract && !typeInfo.isInterface && !typeInfo.isEnum) {
-                        if (!allTypesToGenerate.has(type)) {
-                            additionalTypes.add(type);
-                            console.error(`Found additional type ${type} from property ${prop.name} of ${typeName}`);
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    // Add the additional types (filtered)
-    additionalTypes.forEach(type => {
-        if (!isTypeExcluded(type, exclusions)) {
-            allTypesToGenerate.add(type);
-        } else {
-            console.error(`Excluding additional type: ${type}`);
-        }
-    });
-
-    // Get properties for the additional types too
-    for (const typeName of additionalTypes) {
-        if (!isTypeExcluded(typeName, exclusions)) {
-            const props = getAllProperties(classMap, typeName, symbolsFile, exclusions);
-            typeProperties.set(typeName, props);
-        } else {
-            console.error(`Excluding additional type: ${typeName}`);
-        }
-    }
-
-    return {
-        classMap,
-        accessibleTypes,
-        abstractTypes,
-        allTypesToGenerate,
-        typeProperties
-    };
+function analyzeForSerialization (classMap: Map<string, ClassInfo>, symbolsFile: string): AnalysisResult {
+	const startingTypes = ['SkeletonData', 'Skeleton', 'AnimationState'];
+	const accessibleTypes = findAccessibleTypes(classMap, startingTypes);
+
+	// First pass: populate implementations for all abstract types
+	for (const [className, classInfo] of classMap) {
+		if (classInfo.isAbstract || classInfo.isInterface) {
+			// Get only concrete implementations
+			const concreteImplementations = findAllImplementations(classMap, className, true);
+			classInfo.concreteImplementations = concreteImplementations;
+
+			// Get all implementations (including intermediate abstract types)
+			const allImplementations = findAllImplementations(classMap, className, false);
+			classInfo.allImplementations = allImplementations;
+		}
+	}
+
+	// Collect abstract types and their implementations
+	const abstractTypes = new Map<string, string[]>();
+	const allTypesToGenerate = new Set<string>(accessibleTypes);
+
+	// Find all abstract types referenced by accessible types
+	for (const typeName of accessibleTypes) {
+		const classInfo = classMap.get(typeName);
+		if (!classInfo) continue;
+
+		// Check return types and field types for abstract classes
+		const allTypes = [
+			...classInfo.getters.map(g => g.returnType),
+			...classInfo.fields.map(f => f.fieldType)
+		];
+
+		for (const type of allTypes) {
+			const returnType = type
+				.replace(/@Null\s+/g, '')
+				.replace(/\s+/g, ' ');
+
+			// Extract types from Array<Type>
+			let checkTypes: string[] = [];
+			const arrayMatch = returnType.match(/Array<(.+?)>/);
+			if (arrayMatch) {
+				checkTypes.push(arrayMatch[1].trim());
+			} else if (returnType.match(/^[A-Z]\w+$/)) {
+				checkTypes.push(returnType);
+			}
+
+			// Also check for type names that might be inner classes
+			const typeMatches = returnType.match(/\b([A-Z]\w+)\b/g);
+			if (typeMatches) {
+				for (const match of typeMatches) {
+					// Try as inner class of current type
+					const parts = typeName.split('.');
+					for (let i = parts.length; i >= 1; i--) {
+						const parentPath = parts.slice(0, i).join('.');
+						const innerClassPath = `${parentPath}.${match}`;
+						if (classMap.has(innerClassPath)) {
+							checkTypes.push(innerClassPath);
+							break;
+						}
+					}
+				}
+			}
+
+			for (const checkType of checkTypes) {
+				if (checkType && classMap.has(checkType)) {
+					const typeInfo = classMap.get(checkType)!;
+					if (typeInfo.isAbstract || typeInfo.isInterface) {
+						// Use the already populated concreteImplementations
+						const implementations = typeInfo.concreteImplementations || [];
+						abstractTypes.set(checkType, implementations);
+
+						// Add all concrete implementations to types to generate
+						implementations.forEach(impl => allTypesToGenerate.add(impl));
+					}
+				}
+			}
+		}
+	}
+
+	// Load exclusions
+	const exclusions = loadExclusions();
+
+	// Filter out excluded types from allTypesToGenerate
+	const filteredTypesToGenerate = new Set<string>();
+	for (const typeName of allTypesToGenerate) {
+		if (!isTypeExcluded(typeName, exclusions)) {
+			filteredTypesToGenerate.add(typeName);
+		} else {
+			console.error(`Excluding type: ${typeName}`);
+		}
+	}
+
+
+	// Update allTypesToGenerate to the filtered set
+	allTypesToGenerate.clear();
+	filteredTypesToGenerate.forEach(type => allTypesToGenerate.add(type));
+
+	// Collect all properties for each type (including inherited ones)
+	const typeProperties = new Map<string, PropertyInfo[]>();
+	for (const typeName of allTypesToGenerate) {
+		const props = getAllProperties(classMap, typeName, symbolsFile, exclusions);
+		typeProperties.set(typeName, props);
+	}
+
+	// Also collect properties for abstract types (so we know what properties their implementations should have)
+	for (const abstractType of abstractTypes.keys()) {
+		if (!typeProperties.has(abstractType) && !isTypeExcluded(abstractType, exclusions)) {
+			const props = getAllProperties(classMap, abstractType, symbolsFile, exclusions);
+			typeProperties.set(abstractType, props);
+		}
+	}
+
+	// Second pass: find additional concrete types referenced in properties
+	const additionalTypes = new Set<string>();
+	for (const [typeName, props] of typeProperties) {
+		for (const prop of props) {
+			const propType = prop.type.replace(/@Null\s+/g, '').trim();
+
+			// Check if it's a simple type name
+			const typeMatch = propType.match(/^([A-Z]\w+)$/);
+			if (typeMatch) {
+				const type = typeMatch[1];
+				if (classMap.has(type)) {
+					const typeInfo = classMap.get(type)!;
+					if (!typeInfo.isAbstract && !typeInfo.isInterface && !typeInfo.isEnum) {
+						if (!allTypesToGenerate.has(type)) {
+							additionalTypes.add(type);
+							console.error(`Found additional type ${type} from property ${prop.name} of ${typeName}`);
+						}
+					}
+				}
+			}
+		}
+	}
+
+	// Add the additional types (filtered)
+	additionalTypes.forEach(type => {
+		if (!isTypeExcluded(type, exclusions)) {
+			allTypesToGenerate.add(type);
+		} else {
+			console.error(`Excluding additional type: ${type}`);
+		}
+	});
+
+	// Get properties for the additional types too
+	for (const typeName of additionalTypes) {
+		if (!isTypeExcluded(typeName, exclusions)) {
+			const props = getAllProperties(classMap, typeName, symbolsFile, exclusions);
+			typeProperties.set(typeName, props);
+		} else {
+			console.error(`Excluding additional type: ${typeName}`);
+		}
+	}
+
+	return {
+		classMap,
+		accessibleTypes,
+		abstractTypes,
+		allTypesToGenerate,
+		typeProperties
+	};
 }
 
-async function main() {
-    try {
-        // Ensure output directory exists
-        const outputDir = ensureOutputDir();
-
-        // Generate LSP data
-        const jsonFile = generateLspData(outputDir);
-
-        // Read and parse the JSON
-        const jsonContent = fs.readFileSync(jsonFile, 'utf8');
-        const lspData: LspCliResult = JSON.parse(jsonContent);
-
-        console.error(`Analyzing ${lspData.symbols.length} symbols...`);
-
-        // Analyze all classes
-        const classMap = analyzeClasses(lspData.symbols);
-        console.error(`Found ${classMap.size} classes`);
-
-        // Perform serialization analysis
-        const analysisResult = analyzeForSerialization(classMap, jsonFile);
-        console.error(`Found ${analysisResult.accessibleTypes.size} accessible types`);
-        console.error(`Found ${analysisResult.allTypesToGenerate.size} types to generate`);
-
-        // Save analysis result to file
-        const analysisFile = path.join(outputDir, 'analysis-result.json');
-
-        // Convert Maps to arrays and handle nested Maps in ClassInfo
-        const classMapArray: [string, any][] = [];
-        for (const [name, info] of analysisResult.classMap) {
-            const serializedInfo = {
-                ...info,
-                typeParameters: info.typeParameters ? Array.from(info.typeParameters.entries()) : undefined
-            };
-            classMapArray.push([name, serializedInfo]);
-        }
-
-        const resultToSave = {
-            ...analysisResult,
-            // Convert Maps and Sets to arrays for JSON serialization
-            classMap: classMapArray,
-            accessibleTypes: Array.from(analysisResult.accessibleTypes),
-            abstractTypes: Array.from(analysisResult.abstractTypes.entries()),
-            allTypesToGenerate: Array.from(analysisResult.allTypesToGenerate),
-            typeProperties: Array.from(analysisResult.typeProperties.entries())
-        };
-
-        fs.writeFileSync(analysisFile, JSON.stringify(resultToSave, null, 2));
-        console.log(`Analysis result written to: ${analysisFile}`);
-
-    } catch (error: any) {
-        console.error('Error:', error.message);
-        process.exit(1);
-    }
+async function main () {
+	try {
+		// Ensure output directory exists
+		const outputDir = ensureOutputDir();
+
+		// Generate LSP data
+		const jsonFile = generateLspData(outputDir);
+
+		// Read and parse the JSON
+		const jsonContent = fs.readFileSync(jsonFile, 'utf8');
+		const lspData: LspCliResult = JSON.parse(jsonContent);
+
+		console.error(`Analyzing ${lspData.symbols.length} symbols...`);
+
+		// Analyze all classes
+		const classMap = analyzeClasses(lspData.symbols);
+		console.error(`Found ${classMap.size} classes`);
+
+		// Perform serialization analysis
+		const analysisResult = analyzeForSerialization(classMap, jsonFile);
+		console.error(`Found ${analysisResult.accessibleTypes.size} accessible types`);
+		console.error(`Found ${analysisResult.allTypesToGenerate.size} types to generate`);
+
+		// Save analysis result to file
+		const analysisFile = path.join(outputDir, 'analysis-result.json');
+
+		// Convert Maps to arrays and handle nested Maps in ClassInfo
+		const classMapArray: [string, any][] = [];
+		for (const [name, info] of analysisResult.classMap) {
+			const serializedInfo = {
+				...info,
+				typeParameters: info.typeParameters ? Array.from(info.typeParameters.entries()) : undefined
+			};
+			classMapArray.push([name, serializedInfo]);
+		}
+
+		const resultToSave = {
+			...analysisResult,
+			// Convert Maps and Sets to arrays for JSON serialization
+			classMap: classMapArray,
+			accessibleTypes: Array.from(analysisResult.accessibleTypes),
+			abstractTypes: Array.from(analysisResult.abstractTypes.entries()),
+			allTypesToGenerate: Array.from(analysisResult.allTypesToGenerate),
+			typeProperties: Array.from(analysisResult.typeProperties.entries())
+		};
+
+		fs.writeFileSync(analysisFile, JSON.stringify(resultToSave, null, 2));
+		console.log(`Analysis result written to: ${analysisFile}`);
+
+	} catch (error: any) {
+		console.error('Error:', error.message);
+		process.exit(1);
+	}
 }
 
 main();

+ 438 - 438
tests/src/generate-cpp-serializer.ts

@@ -7,454 +7,454 @@ import type { Property, SerializerIR } from './generate-serializer-ir';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
-function transformType(javaType: string): string {
-    // Remove package prefixes
-    const simpleName = javaType.includes('.') ? javaType.split('.').pop()! : javaType;
-
-    // Handle primitive types
-    if (simpleName === 'String') return 'const String&';
-    if (simpleName === 'int') return 'int';
-    if (simpleName === 'float') return 'float';
-    if (simpleName === 'boolean') return 'bool';
-    if (simpleName === 'short') return 'short';
-
-    // Handle arrays
-    if (simpleName.endsWith('[]')) {
-        const baseType = simpleName.slice(0, -2);
-        return `Array<${transformType(baseType)}>`;
-    }
-
-    // Object types become pointers
-    return simpleName;
+function transformType (javaType: string): string {
+	// Remove package prefixes
+	const simpleName = javaType.includes('.') ? javaType.split('.').pop()! : javaType;
+
+	// Handle primitive types
+	if (simpleName === 'String') return 'const String&';
+	if (simpleName === 'int') return 'int';
+	if (simpleName === 'float') return 'float';
+	if (simpleName === 'boolean') return 'bool';
+	if (simpleName === 'short') return 'short';
+
+	// Handle arrays
+	if (simpleName.endsWith('[]')) {
+		const baseType = simpleName.slice(0, -2);
+		return `Array<${transformType(baseType)}>`;
+	}
+
+	// Object types become pointers
+	return simpleName;
 }
 
-function generatePropertyCode(property: Property, indent: string, enumMappings: { [enumName: string]: { [javaValue: string]: string } }): string[] {
-    const lines: string[] = [];
-
-    // Transform field access for C++: add _ prefix except for Color fields
-    let accessor = `obj->${property.getter}`;
-    if (!property.getter.includes('()')) {
-        // This is a field access, not a method call
-        const fieldName = property.getter;
-        // Color fields (r, g, b, a) don't get _ prefix, all others do
-        const isColorField = ['r', 'g', 'b', 'a'].includes(fieldName);
-        if (!isColorField) {
-            accessor = `obj->_${fieldName}`;
-        } else {
-            accessor = `obj->${fieldName}`;
-        }
-    }
-
-    // C++-specific: darkColor specifically has hasDarkColor() method
-    const isDarkColor = property.kind === "object" &&
-        property.valueType === "Color" &&
-        property.getter === "getDarkColor()";
-
-    if (isDarkColor) {
-        const colorAccessor = `&${accessor}`;
-
-        lines.push(`${indent}if (obj->hasDarkColor()) {`);
-        lines.push(`${indent}    ${property.writeMethodCall}(${colorAccessor});`);
-        lines.push(`${indent}} else {`);
-        lines.push(`${indent}    _json.writeNull();`);
-        lines.push(`${indent}}`);
-        return lines;
-    }
-
-    switch (property.kind) {
-        case "primitive":
-            lines.push(`${indent}_json.writeValue(${accessor});`);
-            break;
-
-        case "object":
-            if (property.isNullable) {
-                lines.push(`${indent}if (${accessor} == nullptr) {`);
-                lines.push(`${indent}    _json.writeNull();`);
-                lines.push(`${indent}} else {`);
-                lines.push(`${indent}    ${property.writeMethodCall}(${accessor});`);
-                lines.push(`${indent}}`);
-            } else {
-                lines.push(`${indent}${property.writeMethodCall}(${accessor});`);
-            }
-            break;
-
-        case "enum":
-            const enumName = property.enumName;
-            const enumMap = enumMappings[enumName];
-
-            if (enumMap && Object.keys(enumMap).length > 0) {
-                // Generate switch statement for enum
-                lines.push(`${indent}_json.writeValue([&]() -> String {`);
-                lines.push(`${indent}    switch(${accessor}) {`);
-
-                for (const [javaValue, cppValue] of Object.entries(enumMap)) {
-                    lines.push(`${indent}        case ${cppValue}: return "${javaValue}";`);
-                }
-
-                lines.push(`${indent}        default: return "unknown";`);
-                lines.push(`${indent}    }`);
-                lines.push(`${indent}}());`);
-            } else {
-                // Fallback if no enum mapping
-                lines.push(`${indent}_json.writeValue(String::valueOf((int)${accessor}));`);
-            }
-            break;
-
-        case "array":
-            // In C++, arrays are never null - empty arrays (size() == 0) are equivalent to Java null
-            lines.push(`${indent}_json.writeArrayStart();`);
-            lines.push(`${indent}for (size_t i = 0; i < ${accessor}.size(); i++) {`);
-            const elementAccess = `${accessor}[i]`;
-            if (property.elementKind === "primitive") {
-                lines.push(`${indent}    _json.writeValue(${elementAccess});`);
-            } else {
-                lines.push(`${indent}    ${property.writeMethodCall}(${elementAccess});`);
-            }
-            lines.push(`${indent}}`);
-            lines.push(`${indent}_json.writeArrayEnd();`);
-            break;
-
-        case "nestedArray":
-            // Nested arrays are always considered non-null in both Java and C++
-            lines.push(`${indent}_json.writeArrayStart();`);
-            lines.push(`${indent}for (size_t i = 0; i < ${accessor}.size(); i++) {`);
-            lines.push(`${indent}    Array<${property.elementType}>& nestedArray = ${accessor}[i];`);
-            lines.push(`${indent}    _json.writeArrayStart();`);
-            lines.push(`${indent}    for (size_t j = 0; j < nestedArray.size(); j++) {`);
-            lines.push(`${indent}        _json.writeValue(nestedArray[j]);`);
-            lines.push(`${indent}    }`);
-            lines.push(`${indent}    _json.writeArrayEnd();`);
-            lines.push(`${indent}}`);
-            lines.push(`${indent}_json.writeArrayEnd();`);
-            break;
-    }
-
-    return lines;
+function generatePropertyCode (property: Property, indent: string, enumMappings: { [enumName: string]: { [javaValue: string]: string } }): string[] {
+	const lines: string[] = [];
+
+	// Transform field access for C++: add _ prefix except for Color fields
+	let accessor = `obj->${property.getter}`;
+	if (!property.getter.includes('()')) {
+		// This is a field access, not a method call
+		const fieldName = property.getter;
+		// Color fields (r, g, b, a) don't get _ prefix, all others do
+		const isColorField = ['r', 'g', 'b', 'a'].includes(fieldName);
+		if (!isColorField) {
+			accessor = `obj->_${fieldName}`;
+		} else {
+			accessor = `obj->${fieldName}`;
+		}
+	}
+
+	// C++-specific: darkColor specifically has hasDarkColor() method
+	const isDarkColor = property.kind === "object" &&
+		property.valueType === "Color" &&
+		property.getter === "getDarkColor()";
+
+	if (isDarkColor) {
+		const colorAccessor = `&${accessor}`;
+
+		lines.push(`${indent}if (obj->hasDarkColor()) {`);
+		lines.push(`${indent}    ${property.writeMethodCall}(${colorAccessor});`);
+		lines.push(`${indent}} else {`);
+		lines.push(`${indent}    _json.writeNull();`);
+		lines.push(`${indent}}`);
+		return lines;
+	}
+
+	switch (property.kind) {
+		case "primitive":
+			lines.push(`${indent}_json.writeValue(${accessor});`);
+			break;
+
+		case "object":
+			if (property.isNullable) {
+				lines.push(`${indent}if (${accessor} == nullptr) {`);
+				lines.push(`${indent}    _json.writeNull();`);
+				lines.push(`${indent}} else {`);
+				lines.push(`${indent}    ${property.writeMethodCall}(${accessor});`);
+				lines.push(`${indent}}`);
+			} else {
+				lines.push(`${indent}${property.writeMethodCall}(${accessor});`);
+			}
+			break;
+
+		case "enum":
+			const enumName = property.enumName;
+			const enumMap = enumMappings[enumName];
+
+			if (enumMap && Object.keys(enumMap).length > 0) {
+				// Generate switch statement for enum
+				lines.push(`${indent}_json.writeValue([&]() -> String {`);
+				lines.push(`${indent}    switch(${accessor}) {`);
+
+				for (const [javaValue, cppValue] of Object.entries(enumMap)) {
+					lines.push(`${indent}        case ${cppValue}: return "${javaValue}";`);
+				}
+
+				lines.push(`${indent}        default: return "unknown";`);
+				lines.push(`${indent}    }`);
+				lines.push(`${indent}}());`);
+			} else {
+				// Fallback if no enum mapping
+				lines.push(`${indent}_json.writeValue(String::valueOf((int)${accessor}));`);
+			}
+			break;
+
+		case "array":
+			// In C++, arrays are never null - empty arrays (size() == 0) are equivalent to Java null
+			lines.push(`${indent}_json.writeArrayStart();`);
+			lines.push(`${indent}for (size_t i = 0; i < ${accessor}.size(); i++) {`);
+			const elementAccess = `${accessor}[i]`;
+			if (property.elementKind === "primitive") {
+				lines.push(`${indent}    _json.writeValue(${elementAccess});`);
+			} else {
+				lines.push(`${indent}    ${property.writeMethodCall}(${elementAccess});`);
+			}
+			lines.push(`${indent}}`);
+			lines.push(`${indent}_json.writeArrayEnd();`);
+			break;
+
+		case "nestedArray":
+			// Nested arrays are always considered non-null in both Java and C++
+			lines.push(`${indent}_json.writeArrayStart();`);
+			lines.push(`${indent}for (size_t i = 0; i < ${accessor}.size(); i++) {`);
+			lines.push(`${indent}    Array<${property.elementType}>& nestedArray = ${accessor}[i];`);
+			lines.push(`${indent}    _json.writeArrayStart();`);
+			lines.push(`${indent}    for (size_t j = 0; j < nestedArray.size(); j++) {`);
+			lines.push(`${indent}        _json.writeValue(nestedArray[j]);`);
+			lines.push(`${indent}    }`);
+			lines.push(`${indent}    _json.writeArrayEnd();`);
+			lines.push(`${indent}}`);
+			lines.push(`${indent}_json.writeArrayEnd();`);
+			break;
+	}
+
+	return lines;
 }
 
-function generateCppFromIR(ir: SerializerIR): string {
-    const cppOutput: string[] = [];
-
-    // Generate C++ file header
-    cppOutput.push('#ifndef Spine_SkeletonSerializer_h');
-    cppOutput.push('#define Spine_SkeletonSerializer_h');
-    cppOutput.push('');
-    cppOutput.push('#include <spine/spine.h>');
-    cppOutput.push('#include "JsonWriter.h"');
-    cppOutput.push('#include <stdio.h>');
-    cppOutput.push('#include <stdlib.h>');
-    cppOutput.push('');
-    cppOutput.push('namespace spine {');
-    cppOutput.push('');
-    cppOutput.push('class SkeletonSerializer {');
-    cppOutput.push('private:');
-    cppOutput.push('    HashMap<void*, bool> _visitedObjects;');
-    cppOutput.push('    JsonWriter _json;');
-    cppOutput.push('');
-    cppOutput.push('public:');
-    cppOutput.push('    SkeletonSerializer() {}');
-    cppOutput.push('    ~SkeletonSerializer() {}');
-    cppOutput.push('');
-
-    // Generate public methods
-    for (const method of ir.publicMethods) {
-        const cppParamType = transformType(method.paramType);
-        cppOutput.push(`    String ${method.name}(${cppParamType}* ${method.paramName}) {`);
-        cppOutput.push('        _visitedObjects.clear();');
-        cppOutput.push('        _json = JsonWriter();');
-        cppOutput.push(`        ${method.writeMethodCall}(${method.paramName});`);
-        cppOutput.push('        return _json.getString();');
-        cppOutput.push('    }');
-        cppOutput.push('');
-    }
-
-    cppOutput.push('private:');
-
-    // Generate write methods
-    for (const method of ir.writeMethods) {
-        const shortName = method.paramType.split('.').pop()!;
-        const cppType = transformType(method.paramType);
-
-        // Custom writeSkin and writeSkinEntry implementations
-        if (method.name === 'writeSkin') {
-            cppOutput.push('    void writeSkin(Skin* obj) {');
-            cppOutput.push('        if (_visitedObjects.containsKey(obj)) {');
-            cppOutput.push('            _json.writeValue("<circular>");');
-            cppOutput.push('            return;');
-            cppOutput.push('        }');
-            cppOutput.push('        _visitedObjects.put(obj, true);');
-            cppOutput.push('');
-            cppOutput.push('        _json.writeObjectStart();');
-            cppOutput.push('        _json.writeName("type");');
-            cppOutput.push('        _json.writeValue("Skin");');
-            cppOutput.push('');
-            cppOutput.push('        _json.writeName("attachments");');
-            cppOutput.push('        _json.writeArrayStart();');
-            cppOutput.push('        Skin::AttachmentMap::Entries entries = obj->getAttachments();');
-            cppOutput.push('        while (entries.hasNext()) {');
-            cppOutput.push('            Skin::AttachmentMap::Entry& entry = entries.next();');
-            cppOutput.push('            writeSkinEntry(&entry);');
-            cppOutput.push('        }');
-            cppOutput.push('        _json.writeArrayEnd();');
-            cppOutput.push('');
-            cppOutput.push('        _json.writeName("bones");');
-            cppOutput.push('        _json.writeArrayStart();');
-            cppOutput.push('        for (size_t i = 0; i < obj->getBones().size(); i++) {');
-            cppOutput.push('            BoneData* item = obj->getBones()[i];');
-            cppOutput.push('            writeBoneData(item);');
-            cppOutput.push('        }');
-            cppOutput.push('        _json.writeArrayEnd();');
-            cppOutput.push('');
-            cppOutput.push('        _json.writeName("constraints");');
-            cppOutput.push('        _json.writeArrayStart();');
-            cppOutput.push('        for (size_t i = 0; i < obj->getConstraints().size(); i++) {');
-            cppOutput.push('            ConstraintData* item = obj->getConstraints()[i];');
-            cppOutput.push('            writeConstraintData(item);');
-            cppOutput.push('        }');
-            cppOutput.push('        _json.writeArrayEnd();');
-            cppOutput.push('');
-            cppOutput.push('        _json.writeName("name");');
-            cppOutput.push('        _json.writeValue(obj->getName());');
-            cppOutput.push('');
-            cppOutput.push('        _json.writeName("color");');
-            cppOutput.push('        writeColor(&obj->getColor());');
-            cppOutput.push('');
-            cppOutput.push('        _json.writeObjectEnd();');
-            cppOutput.push('    }');
-            cppOutput.push('');
-            continue;
-        }
-
-        // Custom writeSkinEntry
-        if (method.name === 'writeSkinEntry') {
-            // Custom writeSkinEntry implementation
-            cppOutput.push('    void writeSkinEntry(Skin::AttachmentMap::Entry* obj) {');
-            cppOutput.push('        _json.writeObjectStart();');
-            cppOutput.push('        _json.writeName("type");');
-            cppOutput.push('        _json.writeValue("SkinEntry");');
-            cppOutput.push('        _json.writeName("slotIndex");');
-            cppOutput.push('        _json.writeValue((int)obj->_slotIndex);');
-            cppOutput.push('        _json.writeName("name");');
-            cppOutput.push('        _json.writeValue(obj->_name);');
-            cppOutput.push('        _json.writeName("attachment");');
-            cppOutput.push('        writeAttachment(obj->_attachment);');
-            cppOutput.push('        _json.writeObjectEnd();');
-            cppOutput.push('    }');
-            cppOutput.push('');
-            continue;
-        }
-
-        cppOutput.push(`    void ${method.name}(${cppType}* obj) {`);
-
-        if (method.isAbstractType) {
-            // Handle abstract types with instanceof chain
-            if (method.subtypeChecks && method.subtypeChecks.length > 0) {
-                let first = true;
-                for (const subtype of method.subtypeChecks) {
-                    const subtypeShortName = subtype.typeName.split('.').pop()!;
-
-                    if (first) {
-                        cppOutput.push(`        if (obj->getRTTI().instanceOf(${subtypeShortName}::rtti)) {`);
-                        first = false;
-                    } else {
-                        cppOutput.push(`        } else if (obj->getRTTI().instanceOf(${subtypeShortName}::rtti)) {`);
-                    }
-                    cppOutput.push(`            ${subtype.writeMethodCall}((${subtypeShortName}*)obj);`);
-                }
-                cppOutput.push('        } else {');
-                cppOutput.push(`            fprintf(stderr, "Error: Unknown ${shortName} type\\n"); exit(1);`);
-                cppOutput.push('        }');
-            } else {
-                cppOutput.push('        _json.writeNull(); // No concrete implementations after filtering exclusions');
-            }
-        } else {
-            // Handle concrete types
-            // Add cycle detection
-            cppOutput.push('        if (_visitedObjects.containsKey(obj)) {');
-            cppOutput.push('            _json.writeValue("<circular>");');
-            cppOutput.push('            return;');
-            cppOutput.push('        }');
-            cppOutput.push('        _visitedObjects.put(obj, true);');
-            cppOutput.push('');
-
-            cppOutput.push('        _json.writeObjectStart();');
-
-            // Write type field
-            cppOutput.push('        _json.writeName("type");');
-            cppOutput.push(`        _json.writeValue("${shortName}");`);
-
-            // Write properties
-            for (const property of method.properties) {
-                cppOutput.push('');
-                cppOutput.push(`        _json.writeName("${property.name}");`);
-                const propertyLines = generatePropertyCode(property, '        ', ir.enumMappings);
-                cppOutput.push(...propertyLines);
-            }
-
-            cppOutput.push('');
-            cppOutput.push('        _json.writeObjectEnd();');
-        }
-
-        cppOutput.push('    }');
-        cppOutput.push('');
-    }
-
-    // Add custom helper methods for special types
-    cppOutput.push('    // Custom helper methods');
-    cppOutput.push('    void writeColor(Color* obj) {');
-    cppOutput.push('        if (obj == nullptr) {');
-    cppOutput.push('            _json.writeNull();');
-    cppOutput.push('        } else {');
-    cppOutput.push('            _json.writeObjectStart();');
-    cppOutput.push('            _json.writeName("r");');
-    cppOutput.push('            _json.writeValue(obj->r);');
-    cppOutput.push('            _json.writeName("g");');
-    cppOutput.push('            _json.writeValue(obj->g);');
-    cppOutput.push('            _json.writeName("b");');
-    cppOutput.push('            _json.writeValue(obj->b);');
-    cppOutput.push('            _json.writeName("a");');
-    cppOutput.push('            _json.writeValue(obj->a);');
-    cppOutput.push('            _json.writeObjectEnd();');
-    cppOutput.push('        }');
-    cppOutput.push('    }');
-    cppOutput.push('');
-
-    cppOutput.push('    void writeColor(const Color& obj) {');
-    cppOutput.push('        _json.writeObjectStart();');
-    cppOutput.push('        _json.writeName("r");');
-    cppOutput.push('        _json.writeValue(obj.r);');
-    cppOutput.push('        _json.writeName("g");');
-    cppOutput.push('        _json.writeValue(obj.g);');
-    cppOutput.push('        _json.writeName("b");');
-    cppOutput.push('        _json.writeValue(obj.b);');
-    cppOutput.push('        _json.writeName("a");');
-    cppOutput.push('        _json.writeValue(obj.a);');
-    cppOutput.push('        _json.writeObjectEnd();');
-    cppOutput.push('    }');
-    cppOutput.push('');
-
-    cppOutput.push('    void writeTextureRegion(TextureRegion* obj) {');
-    cppOutput.push('        if (obj == nullptr) {');
-    cppOutput.push('            _json.writeNull();');
-    cppOutput.push('        } else {');
-    cppOutput.push('            _json.writeObjectStart();');
-    cppOutput.push('            _json.writeName("u");');
-    cppOutput.push('            _json.writeValue(obj->getU());');
-    cppOutput.push('            _json.writeName("v");');
-    cppOutput.push('            _json.writeValue(obj->getV());');
-    cppOutput.push('            _json.writeName("u2");');
-    cppOutput.push('            _json.writeValue(obj->getU2());');
-    cppOutput.push('            _json.writeName("v2");');
-    cppOutput.push('            _json.writeValue(obj->getV2());');
-    cppOutput.push('            _json.writeName("width");');
-    cppOutput.push('            _json.writeValue(obj->getRegionWidth());');
-    cppOutput.push('            _json.writeName("height");');
-    cppOutput.push('            _json.writeValue(obj->getRegionHeight());');
-    cppOutput.push('            _json.writeObjectEnd();');
-    cppOutput.push('        }');
-    cppOutput.push('    }');
-    cppOutput.push('');
-
-    cppOutput.push('    void writeTextureRegion(const TextureRegion& obj) {');
-    cppOutput.push('        _json.writeObjectStart();');
-    cppOutput.push('        _json.writeName("u");');
-    cppOutput.push('        _json.writeValue(obj.getU());');
-    cppOutput.push('        _json.writeName("v");');
-    cppOutput.push('        _json.writeValue(obj.getV());');
-    cppOutput.push('        _json.writeName("u2");');
-    cppOutput.push('        _json.writeValue(obj.getU2());');
-    cppOutput.push('        _json.writeName("v2");');
-    cppOutput.push('        _json.writeValue(obj.getV2());');
-    cppOutput.push('        _json.writeName("width");');
-    cppOutput.push('        _json.writeValue(obj.getRegionWidth());');
-    cppOutput.push('        _json.writeName("height");');
-    cppOutput.push('        _json.writeValue(obj.getRegionHeight());');
-    cppOutput.push('        _json.writeObjectEnd();');
-    cppOutput.push('    }');
-    cppOutput.push('');
-
-    cppOutput.push('    void writeIntArray(const Array<int>& obj) {');
-    cppOutput.push('        _json.writeArrayStart();');
-    cppOutput.push('        for (size_t i = 0; i < obj.size(); i++) {');
-    cppOutput.push('            _json.writeValue(obj[i]);');
-    cppOutput.push('        }');
-    cppOutput.push('        _json.writeArrayEnd();');
-    cppOutput.push('    }');
-    cppOutput.push('');
-
-    cppOutput.push('    void writeFloatArray(const Array<float>& obj) {');
-    cppOutput.push('        _json.writeArrayStart();');
-    cppOutput.push('        for (size_t i = 0; i < obj.size(); i++) {');
-    cppOutput.push('            _json.writeValue(obj[i]);');
-    cppOutput.push('        }');
-    cppOutput.push('        _json.writeArrayEnd();');
-    cppOutput.push('    }');
-    cppOutput.push('');
-
-    // Add reference versions for write methods (excluding custom implementations)
-    cppOutput.push('    // Reference versions of write methods');
-    const writeMethods = ir.writeMethods.filter(m =>
-        !m.isAbstractType &&
-        m.name !== 'writeSkin' &&
-        m.name !== 'writeSkinEntry'
-    );
-    for (const method of writeMethods) {
-        const cppType = transformType(method.paramType);
-        cppOutput.push(`    void ${method.name}(const ${cppType}& obj) {`);
-        cppOutput.push(`        ${method.name}(const_cast<${cppType}*>(&obj));`);
-        cppOutput.push('    }');
-        cppOutput.push('');
-    }
-
-
-    // C++ footer
-    cppOutput.push('};');
-    cppOutput.push('');
-    cppOutput.push('} // namespace spine');
-    cppOutput.push('');
-    cppOutput.push('#endif');
-
-    return cppOutput.join('\n');
+function generateCppFromIR (ir: SerializerIR): string {
+	const cppOutput: string[] = [];
+
+	// Generate C++ file header
+	cppOutput.push('#ifndef Spine_SkeletonSerializer_h');
+	cppOutput.push('#define Spine_SkeletonSerializer_h');
+	cppOutput.push('');
+	cppOutput.push('#include <spine/spine.h>');
+	cppOutput.push('#include "JsonWriter.h"');
+	cppOutput.push('#include <stdio.h>');
+	cppOutput.push('#include <stdlib.h>');
+	cppOutput.push('');
+	cppOutput.push('namespace spine {');
+	cppOutput.push('');
+	cppOutput.push('class SkeletonSerializer {');
+	cppOutput.push('private:');
+	cppOutput.push('    HashMap<void*, bool> _visitedObjects;');
+	cppOutput.push('    JsonWriter _json;');
+	cppOutput.push('');
+	cppOutput.push('public:');
+	cppOutput.push('    SkeletonSerializer() {}');
+	cppOutput.push('    ~SkeletonSerializer() {}');
+	cppOutput.push('');
+
+	// Generate public methods
+	for (const method of ir.publicMethods) {
+		const cppParamType = transformType(method.paramType);
+		cppOutput.push(`    String ${method.name}(${cppParamType}* ${method.paramName}) {`);
+		cppOutput.push('        _visitedObjects.clear();');
+		cppOutput.push('        _json = JsonWriter();');
+		cppOutput.push(`        ${method.writeMethodCall}(${method.paramName});`);
+		cppOutput.push('        return _json.getString();');
+		cppOutput.push('    }');
+		cppOutput.push('');
+	}
+
+	cppOutput.push('private:');
+
+	// Generate write methods
+	for (const method of ir.writeMethods) {
+		const shortName = method.paramType.split('.').pop()!;
+		const cppType = transformType(method.paramType);
+
+		// Custom writeSkin and writeSkinEntry implementations
+		if (method.name === 'writeSkin') {
+			cppOutput.push('    void writeSkin(Skin* obj) {');
+			cppOutput.push('        if (_visitedObjects.containsKey(obj)) {');
+			cppOutput.push('            _json.writeValue("<circular>");');
+			cppOutput.push('            return;');
+			cppOutput.push('        }');
+			cppOutput.push('        _visitedObjects.put(obj, true);');
+			cppOutput.push('');
+			cppOutput.push('        _json.writeObjectStart();');
+			cppOutput.push('        _json.writeName("type");');
+			cppOutput.push('        _json.writeValue("Skin");');
+			cppOutput.push('');
+			cppOutput.push('        _json.writeName("attachments");');
+			cppOutput.push('        _json.writeArrayStart();');
+			cppOutput.push('        Skin::AttachmentMap::Entries entries = obj->getAttachments();');
+			cppOutput.push('        while (entries.hasNext()) {');
+			cppOutput.push('            Skin::AttachmentMap::Entry& entry = entries.next();');
+			cppOutput.push('            writeSkinEntry(&entry);');
+			cppOutput.push('        }');
+			cppOutput.push('        _json.writeArrayEnd();');
+			cppOutput.push('');
+			cppOutput.push('        _json.writeName("bones");');
+			cppOutput.push('        _json.writeArrayStart();');
+			cppOutput.push('        for (size_t i = 0; i < obj->getBones().size(); i++) {');
+			cppOutput.push('            BoneData* item = obj->getBones()[i];');
+			cppOutput.push('            writeBoneData(item);');
+			cppOutput.push('        }');
+			cppOutput.push('        _json.writeArrayEnd();');
+			cppOutput.push('');
+			cppOutput.push('        _json.writeName("constraints");');
+			cppOutput.push('        _json.writeArrayStart();');
+			cppOutput.push('        for (size_t i = 0; i < obj->getConstraints().size(); i++) {');
+			cppOutput.push('            ConstraintData* item = obj->getConstraints()[i];');
+			cppOutput.push('            writeConstraintData(item);');
+			cppOutput.push('        }');
+			cppOutput.push('        _json.writeArrayEnd();');
+			cppOutput.push('');
+			cppOutput.push('        _json.writeName("name");');
+			cppOutput.push('        _json.writeValue(obj->getName());');
+			cppOutput.push('');
+			cppOutput.push('        _json.writeName("color");');
+			cppOutput.push('        writeColor(&obj->getColor());');
+			cppOutput.push('');
+			cppOutput.push('        _json.writeObjectEnd();');
+			cppOutput.push('    }');
+			cppOutput.push('');
+			continue;
+		}
+
+		// Custom writeSkinEntry
+		if (method.name === 'writeSkinEntry') {
+			// Custom writeSkinEntry implementation
+			cppOutput.push('    void writeSkinEntry(Skin::AttachmentMap::Entry* obj) {');
+			cppOutput.push('        _json.writeObjectStart();');
+			cppOutput.push('        _json.writeName("type");');
+			cppOutput.push('        _json.writeValue("SkinEntry");');
+			cppOutput.push('        _json.writeName("slotIndex");');
+			cppOutput.push('        _json.writeValue((int)obj->_slotIndex);');
+			cppOutput.push('        _json.writeName("name");');
+			cppOutput.push('        _json.writeValue(obj->_name);');
+			cppOutput.push('        _json.writeName("attachment");');
+			cppOutput.push('        writeAttachment(obj->_attachment);');
+			cppOutput.push('        _json.writeObjectEnd();');
+			cppOutput.push('    }');
+			cppOutput.push('');
+			continue;
+		}
+
+		cppOutput.push(`    void ${method.name}(${cppType}* obj) {`);
+
+		if (method.isAbstractType) {
+			// Handle abstract types with instanceof chain
+			if (method.subtypeChecks && method.subtypeChecks.length > 0) {
+				let first = true;
+				for (const subtype of method.subtypeChecks) {
+					const subtypeShortName = subtype.typeName.split('.').pop()!;
+
+					if (first) {
+						cppOutput.push(`        if (obj->getRTTI().instanceOf(${subtypeShortName}::rtti)) {`);
+						first = false;
+					} else {
+						cppOutput.push(`        } else if (obj->getRTTI().instanceOf(${subtypeShortName}::rtti)) {`);
+					}
+					cppOutput.push(`            ${subtype.writeMethodCall}((${subtypeShortName}*)obj);`);
+				}
+				cppOutput.push('        } else {');
+				cppOutput.push(`            fprintf(stderr, "Error: Unknown ${shortName} type\\n"); exit(1);`);
+				cppOutput.push('        }');
+			} else {
+				cppOutput.push('        _json.writeNull(); // No concrete implementations after filtering exclusions');
+			}
+		} else {
+			// Handle concrete types
+			// Add cycle detection
+			cppOutput.push('        if (_visitedObjects.containsKey(obj)) {');
+			cppOutput.push('            _json.writeValue("<circular>");');
+			cppOutput.push('            return;');
+			cppOutput.push('        }');
+			cppOutput.push('        _visitedObjects.put(obj, true);');
+			cppOutput.push('');
+
+			cppOutput.push('        _json.writeObjectStart();');
+
+			// Write type field
+			cppOutput.push('        _json.writeName("type");');
+			cppOutput.push(`        _json.writeValue("${shortName}");`);
+
+			// Write properties
+			for (const property of method.properties) {
+				cppOutput.push('');
+				cppOutput.push(`        _json.writeName("${property.name}");`);
+				const propertyLines = generatePropertyCode(property, '        ', ir.enumMappings);
+				cppOutput.push(...propertyLines);
+			}
+
+			cppOutput.push('');
+			cppOutput.push('        _json.writeObjectEnd();');
+		}
+
+		cppOutput.push('    }');
+		cppOutput.push('');
+	}
+
+	// Add custom helper methods for special types
+	cppOutput.push('    // Custom helper methods');
+	cppOutput.push('    void writeColor(Color* obj) {');
+	cppOutput.push('        if (obj == nullptr) {');
+	cppOutput.push('            _json.writeNull();');
+	cppOutput.push('        } else {');
+	cppOutput.push('            _json.writeObjectStart();');
+	cppOutput.push('            _json.writeName("r");');
+	cppOutput.push('            _json.writeValue(obj->r);');
+	cppOutput.push('            _json.writeName("g");');
+	cppOutput.push('            _json.writeValue(obj->g);');
+	cppOutput.push('            _json.writeName("b");');
+	cppOutput.push('            _json.writeValue(obj->b);');
+	cppOutput.push('            _json.writeName("a");');
+	cppOutput.push('            _json.writeValue(obj->a);');
+	cppOutput.push('            _json.writeObjectEnd();');
+	cppOutput.push('        }');
+	cppOutput.push('    }');
+	cppOutput.push('');
+
+	cppOutput.push('    void writeColor(const Color& obj) {');
+	cppOutput.push('        _json.writeObjectStart();');
+	cppOutput.push('        _json.writeName("r");');
+	cppOutput.push('        _json.writeValue(obj.r);');
+	cppOutput.push('        _json.writeName("g");');
+	cppOutput.push('        _json.writeValue(obj.g);');
+	cppOutput.push('        _json.writeName("b");');
+	cppOutput.push('        _json.writeValue(obj.b);');
+	cppOutput.push('        _json.writeName("a");');
+	cppOutput.push('        _json.writeValue(obj.a);');
+	cppOutput.push('        _json.writeObjectEnd();');
+	cppOutput.push('    }');
+	cppOutput.push('');
+
+	cppOutput.push('    void writeTextureRegion(TextureRegion* obj) {');
+	cppOutput.push('        if (obj == nullptr) {');
+	cppOutput.push('            _json.writeNull();');
+	cppOutput.push('        } else {');
+	cppOutput.push('            _json.writeObjectStart();');
+	cppOutput.push('            _json.writeName("u");');
+	cppOutput.push('            _json.writeValue(obj->getU());');
+	cppOutput.push('            _json.writeName("v");');
+	cppOutput.push('            _json.writeValue(obj->getV());');
+	cppOutput.push('            _json.writeName("u2");');
+	cppOutput.push('            _json.writeValue(obj->getU2());');
+	cppOutput.push('            _json.writeName("v2");');
+	cppOutput.push('            _json.writeValue(obj->getV2());');
+	cppOutput.push('            _json.writeName("width");');
+	cppOutput.push('            _json.writeValue(obj->getRegionWidth());');
+	cppOutput.push('            _json.writeName("height");');
+	cppOutput.push('            _json.writeValue(obj->getRegionHeight());');
+	cppOutput.push('            _json.writeObjectEnd();');
+	cppOutput.push('        }');
+	cppOutput.push('    }');
+	cppOutput.push('');
+
+	cppOutput.push('    void writeTextureRegion(const TextureRegion& obj) {');
+	cppOutput.push('        _json.writeObjectStart();');
+	cppOutput.push('        _json.writeName("u");');
+	cppOutput.push('        _json.writeValue(obj.getU());');
+	cppOutput.push('        _json.writeName("v");');
+	cppOutput.push('        _json.writeValue(obj.getV());');
+	cppOutput.push('        _json.writeName("u2");');
+	cppOutput.push('        _json.writeValue(obj.getU2());');
+	cppOutput.push('        _json.writeName("v2");');
+	cppOutput.push('        _json.writeValue(obj.getV2());');
+	cppOutput.push('        _json.writeName("width");');
+	cppOutput.push('        _json.writeValue(obj.getRegionWidth());');
+	cppOutput.push('        _json.writeName("height");');
+	cppOutput.push('        _json.writeValue(obj.getRegionHeight());');
+	cppOutput.push('        _json.writeObjectEnd();');
+	cppOutput.push('    }');
+	cppOutput.push('');
+
+	cppOutput.push('    void writeIntArray(const Array<int>& obj) {');
+	cppOutput.push('        _json.writeArrayStart();');
+	cppOutput.push('        for (size_t i = 0; i < obj.size(); i++) {');
+	cppOutput.push('            _json.writeValue(obj[i]);');
+	cppOutput.push('        }');
+	cppOutput.push('        _json.writeArrayEnd();');
+	cppOutput.push('    }');
+	cppOutput.push('');
+
+	cppOutput.push('    void writeFloatArray(const Array<float>& obj) {');
+	cppOutput.push('        _json.writeArrayStart();');
+	cppOutput.push('        for (size_t i = 0; i < obj.size(); i++) {');
+	cppOutput.push('            _json.writeValue(obj[i]);');
+	cppOutput.push('        }');
+	cppOutput.push('        _json.writeArrayEnd();');
+	cppOutput.push('    }');
+	cppOutput.push('');
+
+	// Add reference versions for write methods (excluding custom implementations)
+	cppOutput.push('    // Reference versions of write methods');
+	const writeMethods = ir.writeMethods.filter(m =>
+		!m.isAbstractType &&
+		m.name !== 'writeSkin' &&
+		m.name !== 'writeSkinEntry'
+	);
+	for (const method of writeMethods) {
+		const cppType = transformType(method.paramType);
+		cppOutput.push(`    void ${method.name}(const ${cppType}& obj) {`);
+		cppOutput.push(`        ${method.name}(const_cast<${cppType}*>(&obj));`);
+		cppOutput.push('    }');
+		cppOutput.push('');
+	}
+
+
+	// C++ footer
+	cppOutput.push('};');
+	cppOutput.push('');
+	cppOutput.push('} // namespace spine');
+	cppOutput.push('');
+	cppOutput.push('#endif');
+
+	return cppOutput.join('\n');
 }
 
-async function main() {
-    try {
-        // Read the IR file
-        const irFile = path.resolve(__dirname, '../output/serializer-ir.json');
-        if (!fs.existsSync(irFile)) {
-            console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
-            process.exit(1);
-        }
-
-        const ir: SerializerIR = JSON.parse(fs.readFileSync(irFile, 'utf8'));
-
-        // Generate C++ serializer from IR
-        const cppCode = generateCppFromIR(ir);
-
-        // Write the C++ file
-        const cppFile = path.resolve(
-            __dirname,
-            '../../spine-cpp/tests/SkeletonSerializer.h'
-        );
-
-        fs.mkdirSync(path.dirname(cppFile), { recursive: true });
-        fs.writeFileSync(cppFile, cppCode);
-
-        console.log(`Generated C++ serializer from IR: ${cppFile}`);
-        console.log(`- ${ir.publicMethods.length} public methods`);
-        console.log(`- ${ir.writeMethods.length} write methods`);
-        console.log(`- ${Object.keys(ir.enumMappings).length} enum mappings`);
-
-    } catch (error: any) {
-        console.error('Error:', error.message);
-        console.error('Stack:', error.stack);
-        process.exit(1);
-    }
+async function main () {
+	try {
+		// Read the IR file
+		const irFile = path.resolve(__dirname, '../output/serializer-ir.json');
+		if (!fs.existsSync(irFile)) {
+			console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
+			process.exit(1);
+		}
+
+		const ir: SerializerIR = JSON.parse(fs.readFileSync(irFile, 'utf8'));
+
+		// Generate C++ serializer from IR
+		const cppCode = generateCppFromIR(ir);
+
+		// Write the C++ file
+		const cppFile = path.resolve(
+			__dirname,
+			'../../spine-cpp/tests/SkeletonSerializer.h'
+		);
+
+		fs.mkdirSync(path.dirname(cppFile), { recursive: true });
+		fs.writeFileSync(cppFile, cppCode);
+
+		console.log(`Generated C++ serializer from IR: ${cppFile}`);
+		console.log(`- ${ir.publicMethods.length} public methods`);
+		console.log(`- ${ir.writeMethods.length} write methods`);
+		console.log(`- ${Object.keys(ir.enumMappings).length} enum mappings`);
+
+	} catch (error: any) {
+		console.error('Error:', error.message);
+		console.error('Stack:', error.stack);
+		process.exit(1);
+	}
 }
 
 // Allow running as a script or importing the function
 if (import.meta.url === `file://${process.argv[1]}`) {
-    main();
+	main();
 }
 
 export { generateCppFromIR };

+ 309 - 309
tests/src/generate-java-serializer.ts

@@ -7,323 +7,323 @@ import type { Property, SerializerIR, WriteMethod } from './generate-serializer-
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
-function generatePropertyCode(property: Property, indent: string, method?: WriteMethod): string[] {
-    const lines: string[] = [];
-    const accessor = `obj.${property.getter}`;
-
-    switch (property.kind) {
-        case "primitive":
-            lines.push(`${indent}json.writeValue(${accessor});`);
-            break;
-
-        case "object":
-            if (property.isNullable) {
-                lines.push(`${indent}if (${accessor} == null) {`);
-                lines.push(`${indent}    json.writeNull();`);
-                lines.push(`${indent}} else {`);
-                lines.push(`${indent}    ${property.writeMethodCall}(${accessor});`);
-                lines.push(`${indent}}`);
-            } else {
-                lines.push(`${indent}${property.writeMethodCall}(${accessor});`);
-            }
-            break;
-
-        case "enum":
-            if (property.isNullable) {
-                lines.push(`${indent}if (${accessor} == null) {`);
-                lines.push(`${indent}    json.writeNull();`);
-                lines.push(`${indent}} else {`);
-                lines.push(`${indent}    json.writeValue(${accessor}.name());`);
-                lines.push(`${indent}}`);
-            } else {
-                lines.push(`${indent}json.writeValue(${accessor}.name());`);
-            }
-            break;
-
-        case "array":
-            // Special handling for Skin attachments - sort by slot index
-            const isSkinAttachments = method?.paramType === 'Skin' && property.name === 'attachments' && property.elementType === 'SkinEntry';
-            const sortedAccessor = isSkinAttachments ? 'sortedAttachments' : accessor;
-
-            if (isSkinAttachments) {
-                lines.push(`${indent}Array<${property.elementType}> sortedAttachments = new Array<>(${accessor});`);
-                lines.push(`${indent}sortedAttachments.sort((a, b) -> Integer.compare(a.getSlotIndex(), b.getSlotIndex()));`);
-            }
-
-            if (property.isNullable) {
-                lines.push(`${indent}if (${accessor} == null) {`);
-                lines.push(`${indent}    json.writeNull();`);
-                lines.push(`${indent}} else {`);
-                lines.push(`${indent}    json.writeArrayStart();`);
-                lines.push(`${indent}    for (${property.elementType} item : ${sortedAccessor}) {`);
-                if (property.elementKind === "primitive") {
-                    lines.push(`${indent}        json.writeValue(item);`);
-                } else {
-                    lines.push(`${indent}        ${property.writeMethodCall}(item);`);
-                }
-                lines.push(`${indent}    }`);
-                lines.push(`${indent}    json.writeArrayEnd();`);
-                lines.push(`${indent}}`);
-            } else {
-                lines.push(`${indent}json.writeArrayStart();`);
-                lines.push(`${indent}for (${property.elementType} item : ${sortedAccessor}) {`);
-                if (property.elementKind === "primitive") {
-                    lines.push(`${indent}    json.writeValue(item);`);
-                } else {
-                    lines.push(`${indent}    ${property.writeMethodCall}(item);`);
-                }
-                lines.push(`${indent}}`);
-                lines.push(`${indent}json.writeArrayEnd();`);
-            }
-            break;
-
-        case "nestedArray":
-            if (property.isNullable) {
-                lines.push(`${indent}if (${accessor} == null) {`);
-                lines.push(`${indent}    json.writeNull();`);
-                lines.push(`${indent}} else {`);
-                lines.push(`${indent}    json.writeArrayStart();`);
-                lines.push(`${indent}    for (${property.elementType}[] nestedArray : ${accessor}) {`);
-                lines.push(`${indent}        if (nestedArray == null) {`);
-                lines.push(`${indent}            json.writeNull();`);
-                lines.push(`${indent}        } else {`);
-                lines.push(`${indent}            json.writeArrayStart();`);
-                lines.push(`${indent}            for (${property.elementType} elem : nestedArray) {`);
-                lines.push(`${indent}                json.writeValue(elem);`);
-                lines.push(`${indent}            }`);
-                lines.push(`${indent}            json.writeArrayEnd();`);
-                lines.push(`${indent}        }`);
-                lines.push(`${indent}    }`);
-                lines.push(`${indent}    json.writeArrayEnd();`);
-                lines.push(`${indent}}`);
-            } else {
-                lines.push(`${indent}json.writeArrayStart();`);
-                lines.push(`${indent}for (${property.elementType}[] nestedArray : ${accessor}) {`);
-                lines.push(`${indent}    json.writeArrayStart();`);
-                lines.push(`${indent}    for (${property.elementType} elem : nestedArray) {`);
-                lines.push(`${indent}        json.writeValue(elem);`);
-                lines.push(`${indent}    }`);
-                lines.push(`${indent}    json.writeArrayEnd();`);
-                lines.push(`${indent}}`);
-                lines.push(`${indent}json.writeArrayEnd();`);
-            }
-            break;
-    }
-
-    return lines;
+function generatePropertyCode (property: Property, indent: string, method?: WriteMethod): string[] {
+	const lines: string[] = [];
+	const accessor = `obj.${property.getter}`;
+
+	switch (property.kind) {
+		case "primitive":
+			lines.push(`${indent}json.writeValue(${accessor});`);
+			break;
+
+		case "object":
+			if (property.isNullable) {
+				lines.push(`${indent}if (${accessor} == null) {`);
+				lines.push(`${indent}    json.writeNull();`);
+				lines.push(`${indent}} else {`);
+				lines.push(`${indent}    ${property.writeMethodCall}(${accessor});`);
+				lines.push(`${indent}}`);
+			} else {
+				lines.push(`${indent}${property.writeMethodCall}(${accessor});`);
+			}
+			break;
+
+		case "enum":
+			if (property.isNullable) {
+				lines.push(`${indent}if (${accessor} == null) {`);
+				lines.push(`${indent}    json.writeNull();`);
+				lines.push(`${indent}} else {`);
+				lines.push(`${indent}    json.writeValue(${accessor}.name());`);
+				lines.push(`${indent}}`);
+			} else {
+				lines.push(`${indent}json.writeValue(${accessor}.name());`);
+			}
+			break;
+
+		case "array":
+			// Special handling for Skin attachments - sort by slot index
+			const isSkinAttachments = method?.paramType === 'Skin' && property.name === 'attachments' && property.elementType === 'SkinEntry';
+			const sortedAccessor = isSkinAttachments ? 'sortedAttachments' : accessor;
+
+			if (isSkinAttachments) {
+				lines.push(`${indent}Array<${property.elementType}> sortedAttachments = new Array<>(${accessor});`);
+				lines.push(`${indent}sortedAttachments.sort((a, b) -> Integer.compare(a.getSlotIndex(), b.getSlotIndex()));`);
+			}
+
+			if (property.isNullable) {
+				lines.push(`${indent}if (${accessor} == null) {`);
+				lines.push(`${indent}    json.writeNull();`);
+				lines.push(`${indent}} else {`);
+				lines.push(`${indent}    json.writeArrayStart();`);
+				lines.push(`${indent}    for (${property.elementType} item : ${sortedAccessor}) {`);
+				if (property.elementKind === "primitive") {
+					lines.push(`${indent}        json.writeValue(item);`);
+				} else {
+					lines.push(`${indent}        ${property.writeMethodCall}(item);`);
+				}
+				lines.push(`${indent}    }`);
+				lines.push(`${indent}    json.writeArrayEnd();`);
+				lines.push(`${indent}}`);
+			} else {
+				lines.push(`${indent}json.writeArrayStart();`);
+				lines.push(`${indent}for (${property.elementType} item : ${sortedAccessor}) {`);
+				if (property.elementKind === "primitive") {
+					lines.push(`${indent}    json.writeValue(item);`);
+				} else {
+					lines.push(`${indent}    ${property.writeMethodCall}(item);`);
+				}
+				lines.push(`${indent}}`);
+				lines.push(`${indent}json.writeArrayEnd();`);
+			}
+			break;
+
+		case "nestedArray":
+			if (property.isNullable) {
+				lines.push(`${indent}if (${accessor} == null) {`);
+				lines.push(`${indent}    json.writeNull();`);
+				lines.push(`${indent}} else {`);
+				lines.push(`${indent}    json.writeArrayStart();`);
+				lines.push(`${indent}    for (${property.elementType}[] nestedArray : ${accessor}) {`);
+				lines.push(`${indent}        if (nestedArray == null) {`);
+				lines.push(`${indent}            json.writeNull();`);
+				lines.push(`${indent}        } else {`);
+				lines.push(`${indent}            json.writeArrayStart();`);
+				lines.push(`${indent}            for (${property.elementType} elem : nestedArray) {`);
+				lines.push(`${indent}                json.writeValue(elem);`);
+				lines.push(`${indent}            }`);
+				lines.push(`${indent}            json.writeArrayEnd();`);
+				lines.push(`${indent}        }`);
+				lines.push(`${indent}    }`);
+				lines.push(`${indent}    json.writeArrayEnd();`);
+				lines.push(`${indent}}`);
+			} else {
+				lines.push(`${indent}json.writeArrayStart();`);
+				lines.push(`${indent}for (${property.elementType}[] nestedArray : ${accessor}) {`);
+				lines.push(`${indent}    json.writeArrayStart();`);
+				lines.push(`${indent}    for (${property.elementType} elem : nestedArray) {`);
+				lines.push(`${indent}        json.writeValue(elem);`);
+				lines.push(`${indent}    }`);
+				lines.push(`${indent}    json.writeArrayEnd();`);
+				lines.push(`${indent}}`);
+				lines.push(`${indent}json.writeArrayEnd();`);
+			}
+			break;
+	}
+
+	return lines;
 }
 
-function generateJavaFromIR(ir: SerializerIR): string {
-    const javaOutput: string[] = [];
-
-    // Generate Java file header
-    javaOutput.push('package com.esotericsoftware.spine.utils;');
-    javaOutput.push('');
-    javaOutput.push('import com.esotericsoftware.spine.*;');
-    javaOutput.push('import com.esotericsoftware.spine.Animation.*;');
-    javaOutput.push('import com.esotericsoftware.spine.AnimationState.*;');
-    javaOutput.push('import com.esotericsoftware.spine.BoneData.Inherit;');
-    javaOutput.push('import com.esotericsoftware.spine.Skin.SkinEntry;');
-    javaOutput.push('import com.esotericsoftware.spine.PathConstraintData.*;');
-    javaOutput.push('import com.esotericsoftware.spine.TransformConstraintData.*;');
-    javaOutput.push('import com.esotericsoftware.spine.attachments.*;');
-    javaOutput.push('import com.badlogic.gdx.graphics.Color;');
-    javaOutput.push('import com.badlogic.gdx.graphics.g2d.TextureRegion;');
-    javaOutput.push('import com.badlogic.gdx.utils.Array;');
-    javaOutput.push('import com.badlogic.gdx.utils.IntArray;');
-    javaOutput.push('import com.badlogic.gdx.utils.FloatArray;');
-    javaOutput.push('');
-    javaOutput.push('import java.util.Locale;');
-    javaOutput.push('import java.util.Set;');
-    javaOutput.push('import java.util.HashSet;');
-    javaOutput.push('');
-    javaOutput.push('public class SkeletonSerializer {');
-    javaOutput.push('    private final Set<Object> visitedObjects = new HashSet<>();');
-    javaOutput.push('    private JsonWriter json;');
-    javaOutput.push('');
-
-    // Generate public methods
-    for (const method of ir.publicMethods) {
-        javaOutput.push(`    public String ${method.name}(${method.paramType} ${method.paramName}) {`);
-        javaOutput.push('        visitedObjects.clear();');
-        javaOutput.push('        json = new JsonWriter();');
-        javaOutput.push(`        ${method.writeMethodCall}(${method.paramName});`);
-        javaOutput.push('        json.close();');
-        javaOutput.push('        return json.getString();');
-        javaOutput.push('    }');
-        javaOutput.push('');
-    }
-
-    // Generate write methods
-    for (const method of ir.writeMethods) {
-        const shortName = method.paramType.split('.').pop()!;
-        const className = method.paramType.includes('.') ? method.paramType : shortName;
-
-        javaOutput.push(`    private void ${method.name}(${className} obj) {`);
-
-        if (method.isAbstractType) {
-            // Handle abstract types with instanceof chain
-            if (method.subtypeChecks && method.subtypeChecks.length > 0) {
-                let first = true;
-                for (const subtype of method.subtypeChecks) {
-                    const subtypeShortName = subtype.typeName.split('.').pop()!;
-                    const subtypeClassName = subtype.typeName.includes('.') ? subtype.typeName : subtypeShortName;
-
-                    if (first) {
-                        javaOutput.push(`        if (obj instanceof ${subtypeClassName}) {`);
-                        first = false;
-                    } else {
-                        javaOutput.push(`        } else if (obj instanceof ${subtypeClassName}) {`);
-                    }
-                    javaOutput.push(`            ${subtype.writeMethodCall}((${subtypeClassName}) obj);`);
-                }
-                javaOutput.push('        } else {');
-                javaOutput.push(`            throw new RuntimeException("Unknown ${shortName} type: " + obj.getClass().getName());`);
-                javaOutput.push('        }');
-            } else {
-                javaOutput.push('        json.writeNull(); // No concrete implementations after filtering exclusions');
-            }
-        } else {
-            // Handle concrete types
-            // Add cycle detection
-            javaOutput.push('        if (visitedObjects.contains(obj)) {');
-            javaOutput.push('            json.writeValue("<circular>");');
-            javaOutput.push('            return;');
-            javaOutput.push('        }');
-            javaOutput.push('        visitedObjects.add(obj);');
-            javaOutput.push('');
-
-            javaOutput.push('        json.writeObjectStart();');
-
-            // Write type field
-            javaOutput.push('        json.writeName("type");');
-            javaOutput.push(`        json.writeValue("${shortName}");`);
-
-            // Write properties
-            for (const property of method.properties) {
-                javaOutput.push('');
-                javaOutput.push(`        json.writeName("${property.name}");`);
-                const propertyLines = generatePropertyCode(property, '        ', method);
-                javaOutput.push(...propertyLines);
-            }
-
-            javaOutput.push('');
-            javaOutput.push('        json.writeObjectEnd();');
-        }
-
-        javaOutput.push('    }');
-        javaOutput.push('');
-    }
-
-    // Add helper methods for special types
-    javaOutput.push('    private void writeColor(Color obj) {');
-    javaOutput.push('        if (obj == null) {');
-    javaOutput.push('            json.writeNull();');
-    javaOutput.push('        } else {');
-    javaOutput.push('            json.writeObjectStart();');
-    javaOutput.push('            json.writeName("r");');
-    javaOutput.push('            json.writeValue(obj.r);');
-    javaOutput.push('            json.writeName("g");');
-    javaOutput.push('            json.writeValue(obj.g);');
-    javaOutput.push('            json.writeName("b");');
-    javaOutput.push('            json.writeValue(obj.b);');
-    javaOutput.push('            json.writeName("a");');
-    javaOutput.push('            json.writeValue(obj.a);');
-    javaOutput.push('            json.writeObjectEnd();');
-    javaOutput.push('        }');
-    javaOutput.push('    }');
-    javaOutput.push('');
-
-    javaOutput.push('    private void writeTextureRegion(TextureRegion obj) {');
-    javaOutput.push('        if (obj == null) {');
-    javaOutput.push('            json.writeNull();');
-    javaOutput.push('        } else {');
-    javaOutput.push('            json.writeObjectStart();');
-    javaOutput.push('            json.writeName("u");');
-    javaOutput.push('            json.writeValue(obj.getU());');
-    javaOutput.push('            json.writeName("v");');
-    javaOutput.push('            json.writeValue(obj.getV());');
-    javaOutput.push('            json.writeName("u2");');
-    javaOutput.push('            json.writeValue(obj.getU2());');
-    javaOutput.push('            json.writeName("v2");');
-    javaOutput.push('            json.writeValue(obj.getV2());');
-    javaOutput.push('            json.writeName("width");');
-    javaOutput.push('            json.writeValue(obj.getRegionWidth());');
-    javaOutput.push('            json.writeName("height");');
-    javaOutput.push('            json.writeValue(obj.getRegionHeight());');
-    javaOutput.push('            json.writeObjectEnd();');
-    javaOutput.push('        }');
-    javaOutput.push('    }');
-
-    // Add IntArray and FloatArray helper methods
-    javaOutput.push('');
-    javaOutput.push('    private void writeIntArray(IntArray obj) {');
-    javaOutput.push('        if (obj == null) {');
-    javaOutput.push('            json.writeNull();');
-    javaOutput.push('        } else {');
-    javaOutput.push('            json.writeArrayStart();');
-    javaOutput.push('            for (int i = 0; i < obj.size; i++) {');
-    javaOutput.push('                json.writeValue(obj.get(i));');
-    javaOutput.push('            }');
-    javaOutput.push('            json.writeArrayEnd();');
-    javaOutput.push('        }');
-    javaOutput.push('    }');
-    javaOutput.push('');
-
-    javaOutput.push('    private void writeFloatArray(FloatArray obj) {');
-    javaOutput.push('        if (obj == null) {');
-    javaOutput.push('            json.writeNull();');
-    javaOutput.push('        } else {');
-    javaOutput.push('            json.writeArrayStart();');
-    javaOutput.push('            for (int i = 0; i < obj.size; i++) {');
-    javaOutput.push('                json.writeValue(obj.get(i));');
-    javaOutput.push('            }');
-    javaOutput.push('            json.writeArrayEnd();');
-    javaOutput.push('        }');
-    javaOutput.push('    }');
-
-    javaOutput.push('}');
-
-    return javaOutput.join('\n');
+function generateJavaFromIR (ir: SerializerIR): string {
+	const javaOutput: string[] = [];
+
+	// Generate Java file header
+	javaOutput.push('package com.esotericsoftware.spine.utils;');
+	javaOutput.push('');
+	javaOutput.push('import com.esotericsoftware.spine.*;');
+	javaOutput.push('import com.esotericsoftware.spine.Animation.*;');
+	javaOutput.push('import com.esotericsoftware.spine.AnimationState.*;');
+	javaOutput.push('import com.esotericsoftware.spine.BoneData.Inherit;');
+	javaOutput.push('import com.esotericsoftware.spine.Skin.SkinEntry;');
+	javaOutput.push('import com.esotericsoftware.spine.PathConstraintData.*;');
+	javaOutput.push('import com.esotericsoftware.spine.TransformConstraintData.*;');
+	javaOutput.push('import com.esotericsoftware.spine.attachments.*;');
+	javaOutput.push('import com.badlogic.gdx.graphics.Color;');
+	javaOutput.push('import com.badlogic.gdx.graphics.g2d.TextureRegion;');
+	javaOutput.push('import com.badlogic.gdx.utils.Array;');
+	javaOutput.push('import com.badlogic.gdx.utils.IntArray;');
+	javaOutput.push('import com.badlogic.gdx.utils.FloatArray;');
+	javaOutput.push('');
+	javaOutput.push('import java.util.Locale;');
+	javaOutput.push('import java.util.Set;');
+	javaOutput.push('import java.util.HashSet;');
+	javaOutput.push('');
+	javaOutput.push('public class SkeletonSerializer {');
+	javaOutput.push('    private final Set<Object> visitedObjects = new HashSet<>();');
+	javaOutput.push('    private JsonWriter json;');
+	javaOutput.push('');
+
+	// Generate public methods
+	for (const method of ir.publicMethods) {
+		javaOutput.push(`    public String ${method.name}(${method.paramType} ${method.paramName}) {`);
+		javaOutput.push('        visitedObjects.clear();');
+		javaOutput.push('        json = new JsonWriter();');
+		javaOutput.push(`        ${method.writeMethodCall}(${method.paramName});`);
+		javaOutput.push('        json.close();');
+		javaOutput.push('        return json.getString();');
+		javaOutput.push('    }');
+		javaOutput.push('');
+	}
+
+	// Generate write methods
+	for (const method of ir.writeMethods) {
+		const shortName = method.paramType.split('.').pop()!;
+		const className = method.paramType.includes('.') ? method.paramType : shortName;
+
+		javaOutput.push(`    private void ${method.name}(${className} obj) {`);
+
+		if (method.isAbstractType) {
+			// Handle abstract types with instanceof chain
+			if (method.subtypeChecks && method.subtypeChecks.length > 0) {
+				let first = true;
+				for (const subtype of method.subtypeChecks) {
+					const subtypeShortName = subtype.typeName.split('.').pop()!;
+					const subtypeClassName = subtype.typeName.includes('.') ? subtype.typeName : subtypeShortName;
+
+					if (first) {
+						javaOutput.push(`        if (obj instanceof ${subtypeClassName}) {`);
+						first = false;
+					} else {
+						javaOutput.push(`        } else if (obj instanceof ${subtypeClassName}) {`);
+					}
+					javaOutput.push(`            ${subtype.writeMethodCall}((${subtypeClassName}) obj);`);
+				}
+				javaOutput.push('        } else {');
+				javaOutput.push(`            throw new RuntimeException("Unknown ${shortName} type: " + obj.getClass().getName());`);
+				javaOutput.push('        }');
+			} else {
+				javaOutput.push('        json.writeNull(); // No concrete implementations after filtering exclusions');
+			}
+		} else {
+			// Handle concrete types
+			// Add cycle detection
+			javaOutput.push('        if (visitedObjects.contains(obj)) {');
+			javaOutput.push('            json.writeValue("<circular>");');
+			javaOutput.push('            return;');
+			javaOutput.push('        }');
+			javaOutput.push('        visitedObjects.add(obj);');
+			javaOutput.push('');
+
+			javaOutput.push('        json.writeObjectStart();');
+
+			// Write type field
+			javaOutput.push('        json.writeName("type");');
+			javaOutput.push(`        json.writeValue("${shortName}");`);
+
+			// Write properties
+			for (const property of method.properties) {
+				javaOutput.push('');
+				javaOutput.push(`        json.writeName("${property.name}");`);
+				const propertyLines = generatePropertyCode(property, '        ', method);
+				javaOutput.push(...propertyLines);
+			}
+
+			javaOutput.push('');
+			javaOutput.push('        json.writeObjectEnd();');
+		}
+
+		javaOutput.push('    }');
+		javaOutput.push('');
+	}
+
+	// Add helper methods for special types
+	javaOutput.push('    private void writeColor(Color obj) {');
+	javaOutput.push('        if (obj == null) {');
+	javaOutput.push('            json.writeNull();');
+	javaOutput.push('        } else {');
+	javaOutput.push('            json.writeObjectStart();');
+	javaOutput.push('            json.writeName("r");');
+	javaOutput.push('            json.writeValue(obj.r);');
+	javaOutput.push('            json.writeName("g");');
+	javaOutput.push('            json.writeValue(obj.g);');
+	javaOutput.push('            json.writeName("b");');
+	javaOutput.push('            json.writeValue(obj.b);');
+	javaOutput.push('            json.writeName("a");');
+	javaOutput.push('            json.writeValue(obj.a);');
+	javaOutput.push('            json.writeObjectEnd();');
+	javaOutput.push('        }');
+	javaOutput.push('    }');
+	javaOutput.push('');
+
+	javaOutput.push('    private void writeTextureRegion(TextureRegion obj) {');
+	javaOutput.push('        if (obj == null) {');
+	javaOutput.push('            json.writeNull();');
+	javaOutput.push('        } else {');
+	javaOutput.push('            json.writeObjectStart();');
+	javaOutput.push('            json.writeName("u");');
+	javaOutput.push('            json.writeValue(obj.getU());');
+	javaOutput.push('            json.writeName("v");');
+	javaOutput.push('            json.writeValue(obj.getV());');
+	javaOutput.push('            json.writeName("u2");');
+	javaOutput.push('            json.writeValue(obj.getU2());');
+	javaOutput.push('            json.writeName("v2");');
+	javaOutput.push('            json.writeValue(obj.getV2());');
+	javaOutput.push('            json.writeName("width");');
+	javaOutput.push('            json.writeValue(obj.getRegionWidth());');
+	javaOutput.push('            json.writeName("height");');
+	javaOutput.push('            json.writeValue(obj.getRegionHeight());');
+	javaOutput.push('            json.writeObjectEnd();');
+	javaOutput.push('        }');
+	javaOutput.push('    }');
+
+	// Add IntArray and FloatArray helper methods
+	javaOutput.push('');
+	javaOutput.push('    private void writeIntArray(IntArray obj) {');
+	javaOutput.push('        if (obj == null) {');
+	javaOutput.push('            json.writeNull();');
+	javaOutput.push('        } else {');
+	javaOutput.push('            json.writeArrayStart();');
+	javaOutput.push('            for (int i = 0; i < obj.size; i++) {');
+	javaOutput.push('                json.writeValue(obj.get(i));');
+	javaOutput.push('            }');
+	javaOutput.push('            json.writeArrayEnd();');
+	javaOutput.push('        }');
+	javaOutput.push('    }');
+	javaOutput.push('');
+
+	javaOutput.push('    private void writeFloatArray(FloatArray obj) {');
+	javaOutput.push('        if (obj == null) {');
+	javaOutput.push('            json.writeNull();');
+	javaOutput.push('        } else {');
+	javaOutput.push('            json.writeArrayStart();');
+	javaOutput.push('            for (int i = 0; i < obj.size; i++) {');
+	javaOutput.push('                json.writeValue(obj.get(i));');
+	javaOutput.push('            }');
+	javaOutput.push('            json.writeArrayEnd();');
+	javaOutput.push('        }');
+	javaOutput.push('    }');
+
+	javaOutput.push('}');
+
+	return javaOutput.join('\n');
 }
 
-async function main() {
-    try {
-        // Read the IR file
-        const irFile = path.resolve(__dirname, '../output/serializer-ir.json');
-        if (!fs.existsSync(irFile)) {
-            console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
-            process.exit(1);
-        }
-
-        const ir: SerializerIR = JSON.parse(fs.readFileSync(irFile, 'utf8'));
-
-        // Generate Java serializer from IR
-        const javaCode = generateJavaFromIR(ir);
-
-        // Write the Java file
-        const javaFile = path.resolve(
-            __dirname,
-            '../../spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java'
-        );
-
-        fs.mkdirSync(path.dirname(javaFile), { recursive: true });
-        fs.writeFileSync(javaFile, javaCode);
-
-        console.log(`Generated Java serializer from IR: ${javaFile}`);
-        console.log(`- ${ir.publicMethods.length} public methods`);
-        console.log(`- ${ir.writeMethods.length} write methods`);
-
-    } catch (error: any) {
-        console.error('Error:', error.message);
-        console.error('Stack:', error.stack);
-        process.exit(1);
-    }
+async function main () {
+	try {
+		// Read the IR file
+		const irFile = path.resolve(__dirname, '../output/serializer-ir.json');
+		if (!fs.existsSync(irFile)) {
+			console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
+			process.exit(1);
+		}
+
+		const ir: SerializerIR = JSON.parse(fs.readFileSync(irFile, 'utf8'));
+
+		// Generate Java serializer from IR
+		const javaCode = generateJavaFromIR(ir);
+
+		// Write the Java file
+		const javaFile = path.resolve(
+			__dirname,
+			'../../spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java'
+		);
+
+		fs.mkdirSync(path.dirname(javaFile), { recursive: true });
+		fs.writeFileSync(javaFile, javaCode);
+
+		console.log(`Generated Java serializer from IR: ${javaFile}`);
+		console.log(`- ${ir.publicMethods.length} public methods`);
+		console.log(`- ${ir.writeMethods.length} write methods`);
+
+	} catch (error: any) {
+		console.error('Error:', error.message);
+		console.error('Stack:', error.stack);
+		process.exit(1);
+	}
 }
 
 // Allow running as a script or importing the function
 if (import.meta.url === `file://${process.argv[1]}`) {
-    main();
+	main();
 }
 
 export { generateJavaFromIR };

+ 518 - 518
tests/src/generate-serializer-ir.ts

@@ -9,567 +9,567 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 // IR Type Definitions
 interface SerializerIR {
-  publicMethods: PublicMethod[];
-  writeMethods: WriteMethod[];
-  enumMappings: { [enumName: string]: { [javaValue: string]: string } };
+	publicMethods: PublicMethod[];
+	writeMethods: WriteMethod[];
+	enumMappings: { [enumName: string]: { [javaValue: string]: string } };
 }
 
 interface PublicMethod {
-  name: string;
-  paramType: string;
-  paramName: string;
-  writeMethodCall: string;
+	name: string;
+	paramType: string;
+	paramName: string;
+	writeMethodCall: string;
 }
 
 interface WriteMethod {
-  name: string;
-  paramType: string;
-  properties: Property[];
-  isAbstractType: boolean;
-  subtypeChecks?: SubtypeCheck[];
+	name: string;
+	paramType: string;
+	properties: Property[];
+	isAbstractType: boolean;
+	subtypeChecks?: SubtypeCheck[];
 }
 
 interface SubtypeCheck {
-  typeName: string;
-  writeMethodCall: string;
+	typeName: string;
+	writeMethodCall: string;
 }
 
 type Property = Primitive | Object | Enum | Array | NestedArray;
 
 interface Primitive {
-  kind: "primitive";
-  name: string;
-  getter: string;
-  valueType: string;
-  isNullable: boolean;
+	kind: "primitive";
+	name: string;
+	getter: string;
+	valueType: string;
+	isNullable: boolean;
 }
 
 interface Object {
-  kind: "object";
-  name: string;
-  getter: string;
-  valueType: string;
-  writeMethodCall: string;
-  isNullable: boolean;
+	kind: "object";
+	name: string;
+	getter: string;
+	valueType: string;
+	writeMethodCall: string;
+	isNullable: boolean;
 }
 
 interface Enum {
-  kind: "enum";
-  name: string;
-  getter: string;
-  enumName: string;
-  isNullable: boolean;
+	kind: "enum";
+	name: string;
+	getter: string;
+	enumName: string;
+	isNullable: boolean;
 }
 
 interface Array {
-  kind: "array";
-  name: string;
-  getter: string;
-  elementType: string;
-  elementKind: "primitive" | "object";
-  writeMethodCall?: string;
-  isNullable: boolean;
+	kind: "array";
+	name: string;
+	getter: string;
+	elementType: string;
+	elementKind: "primitive" | "object";
+	writeMethodCall?: string;
+	isNullable: boolean;
 }
 
 interface NestedArray {
-  kind: "nestedArray";
-  name: string;
-  getter: string;
-  elementType: string;
-  isNullable: boolean;
+	kind: "nestedArray";
+	name: string;
+	getter: string;
+	elementType: string;
+	isNullable: boolean;
 }
 
 interface SerializedAnalysisResult {
-    classMap: [string, ClassInfo][];
-    accessibleTypes: string[];
-    abstractTypes: [string, string[]][];
-    allTypesToGenerate: string[];
-    typeProperties: [string, PropertyInfo[]][];
+	classMap: [string, ClassInfo][];
+	accessibleTypes: string[];
+	abstractTypes: [string, string[]][];
+	allTypesToGenerate: string[];
+	typeProperties: [string, PropertyInfo[]][];
 }
 
-function loadExclusions(): { types: Set<string>, methods: Map<string, Set<string>>, fields: Map<string, Set<string>> } {
-    const exclusionsPath = path.resolve(__dirname, '../java-exclusions.txt');
-    const types = new Set<string>();
-    const methods = new Map<string, Set<string>>();
-    const fields = new Map<string, Set<string>>();
-
-    if (!fs.existsSync(exclusionsPath)) {
-        return { types, methods, fields };
-    }
-
-    const content = fs.readFileSync(exclusionsPath, 'utf-8');
-    const lines = content.split('\n');
-
-    for (const line of lines) {
-        const trimmed = line.trim();
-        if (!trimmed || trimmed.startsWith('#')) continue;
-
-        const parts = trimmed.split(/\s+/);
-        if (parts.length < 2) continue;
-
-        const [type, className, property] = parts;
-
-        switch (type) {
-            case 'type':
-                types.add(className);
-                break;
-            case 'method':
-                if (property) {
-                    if (!methods.has(className)) {
-                        methods.set(className, new Set());
-                    }
-                    methods.get(className)!.add(property);
-                }
-                break;
-            case 'field':
-                if (property) {
-                    if (!fields.has(className)) {
-                        fields.set(className, new Set());
-                    }
-                    fields.get(className)!.add(property);
-                }
-                break;
-        }
-    }
-
-    return { types, methods, fields };
+function loadExclusions (): { types: Set<string>, methods: Map<string, Set<string>>, fields: Map<string, Set<string>> } {
+	const exclusionsPath = path.resolve(__dirname, '../java-exclusions.txt');
+	const types = new Set<string>();
+	const methods = new Map<string, Set<string>>();
+	const fields = new Map<string, Set<string>>();
+
+	if (!fs.existsSync(exclusionsPath)) {
+		return { types, methods, fields };
+	}
+
+	const content = fs.readFileSync(exclusionsPath, 'utf-8');
+	const lines = content.split('\n');
+
+	for (const line of lines) {
+		const trimmed = line.trim();
+		if (!trimmed || trimmed.startsWith('#')) continue;
+
+		const parts = trimmed.split(/\s+/);
+		if (parts.length < 2) continue;
+
+		const [type, className, property] = parts;
+
+		switch (type) {
+			case 'type':
+				types.add(className);
+				break;
+			case 'method':
+				if (property) {
+					if (!methods.has(className)) {
+						methods.set(className, new Set());
+					}
+					methods.get(className)!.add(property);
+				}
+				break;
+			case 'field':
+				if (property) {
+					if (!fields.has(className)) {
+						fields.set(className, new Set());
+					}
+					fields.get(className)!.add(property);
+				}
+				break;
+		}
+	}
+
+	return { types, methods, fields };
 }
 
-function analyzePropertyType(propType: string, classMap: Map<string, ClassInfo>): Property {
-    // Handle null annotations
-    const isNullable = propType.includes('@Null');
-    propType = propType.replace(/@Null\s+/g, '').trim();
-
-    // Extract property name and getter from the type analysis
-    // This is a simplified version - in practice we'd get this from PropertyInfo
-    const name = "propertyName"; // placeholder
-    const getter = "getPropertyName"; // placeholder
-
-    // Primitive types
-    if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(propType)) {
-        return {
-            kind: "primitive",
-            name,
-            getter,
-            valueType: propType,
-            isNullable
-        };
-    }
-
-    // Check if it's an enum
-    let classInfo = classMap.get(propType);
-    if (!classInfo && !propType.includes('.')) {
-        // Try to find by short name
-        for (const [fullName, info] of classMap) {
-            if (fullName.split('.').pop() === propType) {
-                classInfo = info;
-                break;
-            }
-        }
-    }
-
-    if (classInfo?.isEnum) {
-        return {
-            kind: "enum",
-            name,
-            getter,
-            enumName: propType.split('.').pop()!,
-            isNullable
-        };
-    }
-
-    // Arrays
-    if (propType.startsWith('Array<')) {
-        const innerType = propType.match(/Array<(.+?)>/)![1].trim();
-        const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(innerType) ? "primitive" : "object";
-
-        return {
-            kind: "array",
-            name,
-            getter,
-            elementType: innerType,
-            elementKind,
-            writeMethodCall: elementKind === "object" ? `write${innerType}` : undefined,
-            isNullable
-        };
-    }
-
-    // Handle nested arrays (like float[][])
-    if (propType.endsWith('[]')) {
-        const elemType = propType.slice(0, -2);
-        if (elemType.endsWith('[]')) {
-            const nestedType = elemType.slice(0, -2);
-            return {
-                kind: "nestedArray",
-                name,
-                getter,
-                elementType: nestedType,
-                isNullable
-            };
-        } else {
-            const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(elemType) ? "primitive" : "object";
-            return {
-                kind: "array",
-                name,
-                getter,
-                elementType: elemType,
-                elementKind,
-                writeMethodCall: elementKind === "object" ? `write${elemType}` : undefined,
-                isNullable
-            };
-        }
-    }
-
-    // Special libGDX types that get custom handling
-    if (['Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) {
-        return {
-            kind: "object",
-            name,
-            getter,
-            valueType: propType,
-            writeMethodCall: `write${propType}`,
-            isNullable
-        };
-    }
-
-    // Object types
-    const shortType = propType.split('.').pop()!;
-    return {
-        kind: "object",
-        name,
-        getter,
-        valueType: propType,
-        writeMethodCall: `write${shortType}`,
-        isNullable
-    };
+function analyzePropertyType (propType: string, classMap: Map<string, ClassInfo>): Property {
+	// Handle null annotations
+	const isNullable = propType.includes('@Null');
+	propType = propType.replace(/@Null\s+/g, '').trim();
+
+	// Extract property name and getter from the type analysis
+	// This is a simplified version - in practice we'd get this from PropertyInfo
+	const name = "propertyName"; // placeholder
+	const getter = "getPropertyName"; // placeholder
+
+	// Primitive types
+	if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(propType)) {
+		return {
+			kind: "primitive",
+			name,
+			getter,
+			valueType: propType,
+			isNullable
+		};
+	}
+
+	// Check if it's an enum
+	let classInfo = classMap.get(propType);
+	if (!classInfo && !propType.includes('.')) {
+		// Try to find by short name
+		for (const [fullName, info] of classMap) {
+			if (fullName.split('.').pop() === propType) {
+				classInfo = info;
+				break;
+			}
+		}
+	}
+
+	if (classInfo?.isEnum) {
+		return {
+			kind: "enum",
+			name,
+			getter,
+			enumName: propType.split('.').pop()!,
+			isNullable
+		};
+	}
+
+	// Arrays
+	if (propType.startsWith('Array<')) {
+		const innerType = propType.match(/Array<(.+?)>/)![1].trim();
+		const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(innerType) ? "primitive" : "object";
+
+		return {
+			kind: "array",
+			name,
+			getter,
+			elementType: innerType,
+			elementKind,
+			writeMethodCall: elementKind === "object" ? `write${innerType}` : undefined,
+			isNullable
+		};
+	}
+
+	// Handle nested arrays (like float[][])
+	if (propType.endsWith('[]')) {
+		const elemType = propType.slice(0, -2);
+		if (elemType.endsWith('[]')) {
+			const nestedType = elemType.slice(0, -2);
+			return {
+				kind: "nestedArray",
+				name,
+				getter,
+				elementType: nestedType,
+				isNullable
+			};
+		} else {
+			const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(elemType) ? "primitive" : "object";
+			return {
+				kind: "array",
+				name,
+				getter,
+				elementType: elemType,
+				elementKind,
+				writeMethodCall: elementKind === "object" ? `write${elemType}` : undefined,
+				isNullable
+			};
+		}
+	}
+
+	// Special libGDX types that get custom handling
+	if (['Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) {
+		return {
+			kind: "object",
+			name,
+			getter,
+			valueType: propType,
+			writeMethodCall: `write${propType}`,
+			isNullable
+		};
+	}
+
+	// Object types
+	const shortType = propType.split('.').pop()!;
+	return {
+		kind: "object",
+		name,
+		getter,
+		valueType: propType,
+		writeMethodCall: `write${shortType}`,
+		isNullable
+	};
 }
 
-function generateSerializerIR(analysisData: SerializedAnalysisResult): SerializerIR {
-    // Convert arrays back to Maps
-    const classMap = new Map(analysisData.classMap);
-    const abstractTypes = new Map(analysisData.abstractTypes);
-    const typeProperties = new Map(analysisData.typeProperties);
-    const exclusions = loadExclusions();
-
-    // Generate enum mappings
-    const enumMappings: { [enumName: string]: { [javaValue: string]: string } } = {};
-    for (const [className, classInfo] of classMap) {
-        if (classInfo.isEnum && classInfo.enumValues) {
-            const shortName = className.split('.').pop()!;
-            const valueMap: { [javaValue: string]: string } = {};
-
-            for (const javaValue of classInfo.enumValues) {
-                // Convert Java enum value to C++ enum value
-                // e.g. "setup" -> "MixBlend_Setup", "first" -> "MixBlend_First"
-                const cppValue = `${shortName}_${javaValue.charAt(0).toUpperCase() + javaValue.slice(1)}`;
-                valueMap[javaValue] = cppValue;
-            }
-
-            enumMappings[shortName] = valueMap;
-        }
-    }
-
-    // Generate public methods
-    const publicMethods: PublicMethod[] = [
-        {
-            name: "serializeSkeletonData",
-            paramType: "SkeletonData",
-            paramName: "data",
-            writeMethodCall: "writeSkeletonData"
-        },
-        {
-            name: "serializeSkeleton",
-            paramType: "Skeleton",
-            paramName: "skeleton",
-            writeMethodCall: "writeSkeleton"
-        },
-        {
-            name: "serializeAnimationState",
-            paramType: "AnimationState",
-            paramName: "state",
-            writeMethodCall: "writeAnimationState"
-        }
-    ];
-
-    // Collect all types that need write methods
-    const typesNeedingMethods = new Set<string>();
-
-    // Add all types from allTypesToGenerate
-    for (const type of analysisData.allTypesToGenerate) {
-        typesNeedingMethods.add(type);
-    }
-
-    // Add all abstract types that are referenced (but not excluded)
-    for (const [abstractType] of abstractTypes) {
-        if (!exclusions.types.has(abstractType)) {
-            typesNeedingMethods.add(abstractType);
-        }
-    }
-
-    // Add types referenced in properties
-    for (const [typeName, props] of typeProperties) {
-        if (!typesNeedingMethods.has(typeName)) continue;
-
-        for (const prop of props) {
-            let propType = prop.type.replace(/@Null\s+/g, '').trim();
-
-            // Extract type from Array<Type>
-            const arrayMatch = propType.match(/Array<(.+?)>/);
-            if (arrayMatch) {
-                propType = arrayMatch[1].trim();
-            }
-
-            // Extract type from Type[]
-            if (propType.endsWith('[]')) {
-                propType = propType.slice(0, -2);
-            }
-
-            // Skip primitives and special types
-            if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long',
-                 'Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) {
-                continue;
-            }
-
-            // Add the type if it's a class (but not excluded)
-            if (propType.match(/^[A-Z]/)) {
-                if (!exclusions.types.has(propType)) {
-                    typesNeedingMethods.add(propType);
-                }
-
-                // Also check if it's an abstract type in classMap
-                for (const [fullName, info] of classMap) {
-                    if (fullName === propType || fullName.split('.').pop() === propType) {
-                        if ((info.isAbstract || info.isInterface) && !exclusions.types.has(fullName)) {
-                            typesNeedingMethods.add(fullName);
-                        }
-                        break;
-                    }
-                }
-            }
-        }
-    }
-
-    // Generate write methods
-    const writeMethods: WriteMethod[] = [];
-    const generatedMethods = new Set<string>();
-
-    for (const typeName of Array.from(typesNeedingMethods).sort()) {
-        const classInfo = classMap.get(typeName);
-        if (!classInfo) continue;
-
-        // Skip enums - they are handled inline with .name() calls
-        if (classInfo.isEnum) continue;
-
-        const shortName = typeName.split('.').pop()!;
-
-        // Skip if already generated (handle name collisions)
-        if (generatedMethods.has(shortName)) continue;
-        generatedMethods.add(shortName);
-
-        const writeMethod: WriteMethod = {
-            name: `write${shortName}`,
-            paramType: typeName,
-            properties: [],
-            isAbstractType: classInfo.isAbstract || classInfo.isInterface || false
-        };
-
-        if (classInfo.isAbstract || classInfo.isInterface) {
-            // Handle abstract types with instanceof chain
-            const implementations = classInfo.concreteImplementations || [];
-
-            // Filter out excluded types from implementations
-            const filteredImplementations = implementations.filter(impl => {
-                return !exclusions.types.has(impl);
-            });
-
-            if (filteredImplementations.length > 0) {
-                writeMethod.subtypeChecks = filteredImplementations.map(impl => {
-                    const implShortName = impl.split('.').pop()!;
-                    return {
-                        typeName: impl,
-                        writeMethodCall: `write${implShortName}`
-                    };
-                });
-            }
-        } else {
-            // Handle concrete types - convert properties
-            const properties = typeProperties.get(typeName) || [];
-
-            for (const prop of properties) {
-                if (prop.excluded) {
-                    continue; // Skip excluded properties
-                }
-
-                const propName = prop.isGetter ?
-                    prop.name.replace('get', '').replace('()', '').charAt(0).toLowerCase() +
-                    prop.name.replace('get', '').replace('()', '').slice(1) :
-                    prop.name;
-
-                // Handle getter vs field access
-                let getter: string;
-                if (prop.isGetter) {
-                    // It's a method call - ensure it has parentheses
-                    getter = prop.name.includes('()') ? prop.name : `${prop.name}()`;
-                } else {
-                    // It's a field access - use the field name directly
-                    getter = prop.name;
-                }
-
-                // Analyze the property type to determine the correct Property variant
-                const irProperty = analyzePropertyWithDetails(prop, propName, getter, classMap);
-                writeMethod.properties.push(irProperty);
-            }
-        }
-
-        writeMethods.push(writeMethod);
-    }
-
-    return {
-        publicMethods,
-        writeMethods,
-        enumMappings
-    };
+function generateSerializerIR (analysisData: SerializedAnalysisResult): SerializerIR {
+	// Convert arrays back to Maps
+	const classMap = new Map(analysisData.classMap);
+	const abstractTypes = new Map(analysisData.abstractTypes);
+	const typeProperties = new Map(analysisData.typeProperties);
+	const exclusions = loadExclusions();
+
+	// Generate enum mappings
+	const enumMappings: { [enumName: string]: { [javaValue: string]: string } } = {};
+	for (const [className, classInfo] of classMap) {
+		if (classInfo.isEnum && classInfo.enumValues) {
+			const shortName = className.split('.').pop()!;
+			const valueMap: { [javaValue: string]: string } = {};
+
+			for (const javaValue of classInfo.enumValues) {
+				// Convert Java enum value to C++ enum value
+				// e.g. "setup" -> "MixBlend_Setup", "first" -> "MixBlend_First"
+				const cppValue = `${shortName}_${javaValue.charAt(0).toUpperCase() + javaValue.slice(1)}`;
+				valueMap[javaValue] = cppValue;
+			}
+
+			enumMappings[shortName] = valueMap;
+		}
+	}
+
+	// Generate public methods
+	const publicMethods: PublicMethod[] = [
+		{
+			name: "serializeSkeletonData",
+			paramType: "SkeletonData",
+			paramName: "data",
+			writeMethodCall: "writeSkeletonData"
+		},
+		{
+			name: "serializeSkeleton",
+			paramType: "Skeleton",
+			paramName: "skeleton",
+			writeMethodCall: "writeSkeleton"
+		},
+		{
+			name: "serializeAnimationState",
+			paramType: "AnimationState",
+			paramName: "state",
+			writeMethodCall: "writeAnimationState"
+		}
+	];
+
+	// Collect all types that need write methods
+	const typesNeedingMethods = new Set<string>();
+
+	// Add all types from allTypesToGenerate
+	for (const type of analysisData.allTypesToGenerate) {
+		typesNeedingMethods.add(type);
+	}
+
+	// Add all abstract types that are referenced (but not excluded)
+	for (const [abstractType] of abstractTypes) {
+		if (!exclusions.types.has(abstractType)) {
+			typesNeedingMethods.add(abstractType);
+		}
+	}
+
+	// Add types referenced in properties
+	for (const [typeName, props] of typeProperties) {
+		if (!typesNeedingMethods.has(typeName)) continue;
+
+		for (const prop of props) {
+			let propType = prop.type.replace(/@Null\s+/g, '').trim();
+
+			// Extract type from Array<Type>
+			const arrayMatch = propType.match(/Array<(.+?)>/);
+			if (arrayMatch) {
+				propType = arrayMatch[1].trim();
+			}
+
+			// Extract type from Type[]
+			if (propType.endsWith('[]')) {
+				propType = propType.slice(0, -2);
+			}
+
+			// Skip primitives and special types
+			if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long',
+				'Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) {
+				continue;
+			}
+
+			// Add the type if it's a class (but not excluded)
+			if (propType.match(/^[A-Z]/)) {
+				if (!exclusions.types.has(propType)) {
+					typesNeedingMethods.add(propType);
+				}
+
+				// Also check if it's an abstract type in classMap
+				for (const [fullName, info] of classMap) {
+					if (fullName === propType || fullName.split('.').pop() === propType) {
+						if ((info.isAbstract || info.isInterface) && !exclusions.types.has(fullName)) {
+							typesNeedingMethods.add(fullName);
+						}
+						break;
+					}
+				}
+			}
+		}
+	}
+
+	// Generate write methods
+	const writeMethods: WriteMethod[] = [];
+	const generatedMethods = new Set<string>();
+
+	for (const typeName of Array.from(typesNeedingMethods).sort()) {
+		const classInfo = classMap.get(typeName);
+		if (!classInfo) continue;
+
+		// Skip enums - they are handled inline with .name() calls
+		if (classInfo.isEnum) continue;
+
+		const shortName = typeName.split('.').pop()!;
+
+		// Skip if already generated (handle name collisions)
+		if (generatedMethods.has(shortName)) continue;
+		generatedMethods.add(shortName);
+
+		const writeMethod: WriteMethod = {
+			name: `write${shortName}`,
+			paramType: typeName,
+			properties: [],
+			isAbstractType: classInfo.isAbstract || classInfo.isInterface || false
+		};
+
+		if (classInfo.isAbstract || classInfo.isInterface) {
+			// Handle abstract types with instanceof chain
+			const implementations = classInfo.concreteImplementations || [];
+
+			// Filter out excluded types from implementations
+			const filteredImplementations = implementations.filter(impl => {
+				return !exclusions.types.has(impl);
+			});
+
+			if (filteredImplementations.length > 0) {
+				writeMethod.subtypeChecks = filteredImplementations.map(impl => {
+					const implShortName = impl.split('.').pop()!;
+					return {
+						typeName: impl,
+						writeMethodCall: `write${implShortName}`
+					};
+				});
+			}
+		} else {
+			// Handle concrete types - convert properties
+			const properties = typeProperties.get(typeName) || [];
+
+			for (const prop of properties) {
+				if (prop.excluded) {
+					continue; // Skip excluded properties
+				}
+
+				const propName = prop.isGetter ?
+					prop.name.replace('get', '').replace('()', '').charAt(0).toLowerCase() +
+					prop.name.replace('get', '').replace('()', '').slice(1) :
+					prop.name;
+
+				// Handle getter vs field access
+				let getter: string;
+				if (prop.isGetter) {
+					// It's a method call - ensure it has parentheses
+					getter = prop.name.includes('()') ? prop.name : `${prop.name}()`;
+				} else {
+					// It's a field access - use the field name directly
+					getter = prop.name;
+				}
+
+				// Analyze the property type to determine the correct Property variant
+				const irProperty = analyzePropertyWithDetails(prop, propName, getter, classMap);
+				writeMethod.properties.push(irProperty);
+			}
+		}
+
+		writeMethods.push(writeMethod);
+	}
+
+	return {
+		publicMethods,
+		writeMethods,
+		enumMappings
+	};
 }
 
-function analyzePropertyWithDetails(prop: PropertyInfo, propName: string, getter: string, classMap: Map<string, ClassInfo>): Property {
-    // Handle null annotations
-    const isNullable = prop.type.includes('@Null');
-    let propType = prop.type.replace(/@Null\s+/g, '').trim();
-
-    // Primitive types
-    if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(propType)) {
-        return {
-            kind: "primitive",
-            name: propName,
-            getter,
-            valueType: propType,
-            isNullable
-        };
-    }
-
-    // Check if it's an enum
-    let classInfo = classMap.get(propType);
-    if (!classInfo && !propType.includes('.')) {
-        // Try to find by short name
-        for (const [fullName, info] of classMap) {
-            if (fullName.split('.').pop() === propType) {
-                classInfo = info;
-                propType = fullName; // Use full name
-                break;
-            }
-        }
-    }
-
-    if (classInfo?.isEnum) {
-        return {
-            kind: "enum",
-            name: propName,
-            getter,
-            enumName: propType.split('.').pop()!,
-            isNullable
-        };
-    }
-
-    // Arrays
-    if (propType.startsWith('Array<')) {
-        const innerType = propType.match(/Array<(.+?)>/)![1].trim();
-        const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(innerType) ? "primitive" : "object";
-
-        return {
-            kind: "array",
-            name: propName,
-            getter,
-            elementType: innerType,
-            elementKind,
-            writeMethodCall: elementKind === "object" ? `write${innerType.split('.').pop()}` : undefined,
-            isNullable
-        };
-    }
-
-    // Handle nested arrays (like float[][])
-    if (propType.endsWith('[]')) {
-        const elemType = propType.slice(0, -2);
-        if (elemType.endsWith('[]')) {
-            const nestedType = elemType.slice(0, -2);
-            return {
-                kind: "nestedArray",
-                name: propName,
-                getter,
-                elementType: nestedType,
-                isNullable
-            };
-        } else {
-            const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(elemType) ? "primitive" : "object";
-            return {
-                kind: "array",
-                name: propName,
-                getter,
-                elementType: elemType,
-                elementKind,
-                writeMethodCall: elementKind === "object" ? `write${elemType.split('.').pop()}` : undefined,
-                isNullable
-            };
-        }
-    }
-
-    // Special libGDX types that get custom handling
-    if (['Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) {
-        return {
-            kind: "object",
-            name: propName,
-            getter,
-            valueType: propType,
-            writeMethodCall: `write${propType}`,
-            isNullable
-        };
-    }
-
-    // Object types
-    const shortType = propType.split('.').pop()!;
-    return {
-        kind: "object",
-        name: propName,
-        getter,
-        valueType: propType,
-        writeMethodCall: `write${shortType}`,
-        isNullable
-    };
+function analyzePropertyWithDetails (prop: PropertyInfo, propName: string, getter: string, classMap: Map<string, ClassInfo>): Property {
+	// Handle null annotations
+	const isNullable = prop.type.includes('@Null');
+	let propType = prop.type.replace(/@Null\s+/g, '').trim();
+
+	// Primitive types
+	if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(propType)) {
+		return {
+			kind: "primitive",
+			name: propName,
+			getter,
+			valueType: propType,
+			isNullable
+		};
+	}
+
+	// Check if it's an enum
+	let classInfo = classMap.get(propType);
+	if (!classInfo && !propType.includes('.')) {
+		// Try to find by short name
+		for (const [fullName, info] of classMap) {
+			if (fullName.split('.').pop() === propType) {
+				classInfo = info;
+				propType = fullName; // Use full name
+				break;
+			}
+		}
+	}
+
+	if (classInfo?.isEnum) {
+		return {
+			kind: "enum",
+			name: propName,
+			getter,
+			enumName: propType.split('.').pop()!,
+			isNullable
+		};
+	}
+
+	// Arrays
+	if (propType.startsWith('Array<')) {
+		const innerType = propType.match(/Array<(.+?)>/)![1].trim();
+		const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(innerType) ? "primitive" : "object";
+
+		return {
+			kind: "array",
+			name: propName,
+			getter,
+			elementType: innerType,
+			elementKind,
+			writeMethodCall: elementKind === "object" ? `write${innerType.split('.').pop()}` : undefined,
+			isNullable
+		};
+	}
+
+	// Handle nested arrays (like float[][])
+	if (propType.endsWith('[]')) {
+		const elemType = propType.slice(0, -2);
+		if (elemType.endsWith('[]')) {
+			const nestedType = elemType.slice(0, -2);
+			return {
+				kind: "nestedArray",
+				name: propName,
+				getter,
+				elementType: nestedType,
+				isNullable
+			};
+		} else {
+			const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(elemType) ? "primitive" : "object";
+			return {
+				kind: "array",
+				name: propName,
+				getter,
+				elementType: elemType,
+				elementKind,
+				writeMethodCall: elementKind === "object" ? `write${elemType.split('.').pop()}` : undefined,
+				isNullable
+			};
+		}
+	}
+
+	// Special libGDX types that get custom handling
+	if (['Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) {
+		return {
+			kind: "object",
+			name: propName,
+			getter,
+			valueType: propType,
+			writeMethodCall: `write${propType}`,
+			isNullable
+		};
+	}
+
+	// Object types
+	const shortType = propType.split('.').pop()!;
+	return {
+		kind: "object",
+		name: propName,
+		getter,
+		valueType: propType,
+		writeMethodCall: `write${shortType}`,
+		isNullable
+	};
 }
 
-async function main() {
-    try {
-        // Read analysis result
-        const analysisFile = path.resolve(__dirname, '../output/analysis-result.json');
-        if (!fs.existsSync(analysisFile)) {
-            console.error('Analysis result not found. Run analyze-java-api.ts first.');
-            process.exit(1);
-        }
-
-        const analysisData: SerializedAnalysisResult = JSON.parse(fs.readFileSync(analysisFile, 'utf8'));
-
-        // Generate IR
-        const ir = generateSerializerIR(analysisData);
-
-        // Write the IR file
-        const irFile = path.resolve(__dirname, '../output/serializer-ir.json');
-        fs.mkdirSync(path.dirname(irFile), { recursive: true });
-        fs.writeFileSync(irFile, JSON.stringify(ir, null, 2));
-
-        console.log(`Generated serializer IR: ${irFile}`);
-        console.log(`- ${ir.publicMethods.length} public methods`);
-        console.log(`- ${ir.writeMethods.length} write methods`);
-        console.log(`- ${Object.keys(ir.enumMappings).length} enum mappings`);
-
-    } catch (error: any) {
-        console.error('Error:', error.message);
-        console.error('Stack:', error.stack);
-        process.exit(1);
-    }
+async function main () {
+	try {
+		// Read analysis result
+		const analysisFile = path.resolve(__dirname, '../output/analysis-result.json');
+		if (!fs.existsSync(analysisFile)) {
+			console.error('Analysis result not found. Run analyze-java-api.ts first.');
+			process.exit(1);
+		}
+
+		const analysisData: SerializedAnalysisResult = JSON.parse(fs.readFileSync(analysisFile, 'utf8'));
+
+		// Generate IR
+		const ir = generateSerializerIR(analysisData);
+
+		// Write the IR file
+		const irFile = path.resolve(__dirname, '../output/serializer-ir.json');
+		fs.mkdirSync(path.dirname(irFile), { recursive: true });
+		fs.writeFileSync(irFile, JSON.stringify(ir, null, 2));
+
+		console.log(`Generated serializer IR: ${irFile}`);
+		console.log(`- ${ir.publicMethods.length} public methods`);
+		console.log(`- ${ir.writeMethods.length} write methods`);
+		console.log(`- ${Object.keys(ir.enumMappings).length} enum mappings`);
+
+	} catch (error: any) {
+		console.error('Error:', error.message);
+		console.error('Stack:', error.stack);
+		process.exit(1);
+	}
 }
 
 // Allow running as a script or importing the function
 if (import.meta.url === `file://${process.argv[1]}`) {
-    main();
+	main();
 }
 
 export { generateSerializerIR, type SerializerIR, type PublicMethod, type WriteMethod, type Property };

+ 28 - 28
tests/src/types.ts

@@ -2,44 +2,44 @@ import { Supertype } from '@mariozechner/lsp-cli';
 
 // Shared types for the Spine serializer generator
 export interface ClassInfo {
-    className: string;
-    superTypes: string[]; // Just the names for backward compatibility
-    superTypeDetails?: Supertype[]; // Full details with type arguments
-    getters: GetterInfo[];
-    fields: FieldInfo[];
-    file: string;
-    isAbstract: boolean;
-    isInterface: boolean;
-    isEnum: boolean;
-    typeParameters?: string[]; // The class's own type parameters
-    enumValues?: string[]; // For enums
-    concreteImplementations?: string[]; // For abstract classes/interfaces - only leaf concrete types
-    allImplementations?: string[]; // For abstract classes/interfaces - includes intermediate abstract types
+	className: string;
+	superTypes: string[]; // Just the names for backward compatibility
+	superTypeDetails?: Supertype[]; // Full details with type arguments
+	getters: GetterInfo[];
+	fields: FieldInfo[];
+	file: string;
+	isAbstract: boolean;
+	isInterface: boolean;
+	isEnum: boolean;
+	typeParameters?: string[]; // The class's own type parameters
+	enumValues?: string[]; // For enums
+	concreteImplementations?: string[]; // For abstract classes/interfaces - only leaf concrete types
+	allImplementations?: string[]; // For abstract classes/interfaces - includes intermediate abstract types
 }
 
 export interface GetterInfo {
-    methodName: string;
-    returnType: string;
+	methodName: string;
+	returnType: string;
 }
 
 export interface FieldInfo {
-    fieldName: string;
-    fieldType: string;
-    isFinal: boolean;
+	fieldName: string;
+	fieldType: string;
+	isFinal: boolean;
 }
 
 export interface PropertyInfo {
-    name: string;
-    type: string;
-    isGetter: boolean;
-    inheritedFrom?: string; // Which class this property was inherited from
-    excluded: boolean; // Whether this property should be excluded from serialization
+	name: string;
+	type: string;
+	isGetter: boolean;
+	inheritedFrom?: string; // Which class this property was inherited from
+	excluded: boolean; // Whether this property should be excluded from serialization
 }
 
 export interface AnalysisResult {
-    classMap: Map<string, ClassInfo>;
-    accessibleTypes: Set<string>;
-    abstractTypes: Map<string, string[]>; // abstract type -> concrete implementations
-    allTypesToGenerate: Set<string>; // all types that need write methods
-    typeProperties: Map<string, PropertyInfo[]>; // type -> all properties (including inherited)
+	classMap: Map<string, ClassInfo>;
+	accessibleTypes: Set<string>;
+	abstractTypes: Map<string, string[]>; // abstract type -> concrete implementations
+	allTypesToGenerate: Set<string>; // all types that need write methods
+	typeProperties: Map<string, PropertyInfo[]>; // type -> all properties (including inherited)
 }