spine-runtimes/tests/analyze-java-api.ts
Mario Zechner 429ed9dd3b [tests] Complete C++ SkeletonSerializer auto-generation from Java
- Implement comprehensive C++ serializer generator (tests/generate-cpp-serializer.ts)
- Direct transformation of Java SkeletonSerializer to C++ header-only implementation
- Handle all C++-specific API differences:
  * Field access patterns (obj.field → obj->field, private fields → obj->_field)
  * Null check removal for reference-returning methods (getBones, getEdges)
  * Nested array null check elimination (getVertices, getDrawOrders)
  * Enum serialization via switch statements replacing .name() calls
  * Custom function replacement system for C++-specific implementations
- Add specialized C++ implementations:
  * writeColor: handle public Color fields (r,g,b,a without underscore)
  * writeSkin: iterate AttachmentMap::Entries and call writeSkinEntry
  * writeSkinEntry: handle AttachmentMap::Entry instead of Java SkinEntry
- Auto-generate both pointer and reference versions of all write methods
- Create JsonWriter.h as header-only port of Java JsonWriter
- Update HeadlessTest.cpp to use generated SkeletonSerializer
- Add comprehensive type analysis and enum mapping from analysis-result.json
- Implement exclusion system for filtering unwanted types/methods
- Fix Java generator nested array null checks that were incorrectly hardcoded

Generated C++ serializer produces identical JSON output to Java reference implementation.
2025-07-13 02:06:44 +02:00

759 lines
30 KiB
TypeScript
Executable File

#!/usr/bin/env tsx
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import type { Symbol, LspOutput, ClassInfo, PropertyInfo, AnalysisResult } from './types';
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 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<string, ClassInfo> {
const classMap = new Map<string, ClassInfo>();
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<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[]
): 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 (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 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 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());
}
// 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 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: 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();