#!/usr/bin/env tsx import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import type { Symbol, LspOutput, ClassInfo, PropertyInfo, AnalysisResult } from './types'; function ensureOutputDir(): string { const outputDir = path.join(process.cwd(), '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 = '/Users/badlogic/workspaces/spine-runtimes/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(`npx lsp-cli "${projectDir}" java "${outputFile}"`, { encoding: 'utf8', stdio: ['ignore', 'ignore', 'pipe'] // Hide stdout but show stderr }); 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: Symbol[]): Map { const classMap = new Map(); const srcPath = '/Users/badlogic/workspaces/spine-runtimes/spine-libgdx/spine-libgdx/src/'; function processSymbol(symbol: Symbol, 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 || [] }; // No need to parse superTypes from preview anymore - lsp-cli handles this properly now // Check if abstract class if (symbol.preview && symbol.preview.includes('abstract ')) { classInfo.isAbstract = true; } // Log type parameter information if available if (symbol.typeParameters && symbol.typeParameters.length > 0) { console.error(`Class ${className} has type parameters: ${symbol.typeParameters.join(', ')}`); } if (symbol.supertypes) { for (const supertype of symbol.supertypes) { if (supertype.typeArguments && supertype.typeArguments.length > 0) { console.error(` extends ${supertype.name}<${supertype.typeArguments.join(', ')}>`); } } } // 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 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, startingTypes: string[] ): Set { const accessible = new Set(); const toVisit = [...startingTypes]; const visited = new Set(); // 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, 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 (match === 'BoneLocal') { console.error(`Found BoneLocal in return type of ${typeName}`); } if (classMap.has(match) && !visited.has(match)) { toVisit.push(match); if (match === 'BoneLocal') { console.error(`Added BoneLocal to toVisit`); } } // 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 getAllProperties(classMap: Map, className: string, symbolsFile: string): PropertyInfo[] { const allProperties: PropertyInfo[] = []; const visited = new Set(); const classInfo = classMap.get(className); if (!classInfo) return []; // Build type parameter mapping based on supertype details const typeParamMap = new Map(); // Helper to build parameter mappings for a specific supertype function buildTypeParamMapping(currentClass: string, targetSupertype: string): Map { const mapping = new Map(); 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 = new Map()): string { // Resolve generic type parameters if (typeMap.has(type)) { return typeMap.get(type)!; } // TODO: Handle complex types like Array, Map, etc. return type; } // Collect properties in inheritance order (most specific first) function collectProperties(currentClass: string, inheritanceLevel: number = 0, currentTypeMap: Map = 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) { allProperties.push({ name: getter.methodName + '()', type: resolveType(getter.returnType, currentTypeMap), isGetter: true, inheritedFrom: inheritanceLevel === 0 ? undefined : currentClass }); } // 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 }); } // 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(); 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(); 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, typeName: string, concreteOnly: boolean = false): string[] { const implementations: string[] = []; const visited = new Set(); 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, 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(); const allTypesToGenerate = new Set(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 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)); } } } } } // Collect all properties for each type (including inherited ones) const typeProperties = new Map(); for (const typeName of allTypesToGenerate) { const props = getAllProperties(classMap, typeName, symbolsFile); 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)) { const props = getAllProperties(classMap, abstractType, symbolsFile); typeProperties.set(abstractType, props); } } // Second pass: find additional concrete types referenced in properties const additionalTypes = new Set(); 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 additionalTypes.forEach(type => allTypesToGenerate.add(type)); // Get properties for the additional types too for (const typeName of additionalTypes) { const props = getAllProperties(classMap, typeName, symbolsFile); typeProperties.set(typeName, props); } 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: LspOutput = 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();