mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-26 19:51:47 +08:00
[tests] serializer ir generator, ir-based java serializer generator
This commit is contained in:
parent
102b030db3
commit
4bf777a658
@ -3,7 +3,7 @@
|
||||
**Status:** In Progress
|
||||
**Created:** 2025-01-11T03:02:54
|
||||
**Started:** 2025-01-11T03:11:22
|
||||
**Agent PID:** 93834
|
||||
**Agent PID:** 15570
|
||||
|
||||
**CRITICAL:**
|
||||
- NEVER never check a chceckbox and move on to the next checkbox unless the user has confirmed completion of the current checkbox!
|
||||
@ -237,18 +237,94 @@ The test programs will print both SkeletonData (setup pose/static data) and Skel
|
||||
- Create mechanism in tests/generate-cpp-serializer.ts to replace auto-generated functions with hand-written ones
|
||||
- Implement custom writeSkin function that properly handles AttachmentMap::Entries iteration
|
||||
- [x] Create regenerate-all.sh script in tests/ that runs all generators in sequence
|
||||
- [ ] Fix C++ JsonWriter output formatting issues (array formatting broken)
|
||||
- [ ] Test with sample skeleton files
|
||||
- [ ] TypeScript (spine-ts):
|
||||
- [ ] Follow what we did for spine-cpp wrt to JsonWriter, SkeletonSerializer and the generator
|
||||
- [ ] C#
|
||||
- [ ] Figure out how we can add the HeadlessTest and run it without adding it to the assembly itself
|
||||
- [ ] Follow what we did for spine-cpp wrt
|
||||
- [ ] C (spine-c):
|
||||
- [ ] Follow what we did for spine-cpp wrt to JsonWriter, SkeletonSerializer and the generator (this one will need some ultrathink and discussion with the user before code changs)
|
||||
- [ ] Update tests/README.md to describe the new setup
|
||||
- [x] ~~Fix C++ JsonWriter output formatting issues (array formatting broken)~~
|
||||
- [x] ~~Test with sample skeleton files~~
|
||||
- [x] ~~TypeScript (spine-ts):~~
|
||||
- [x] ~~Follow what we did for spine-cpp wrt to JsonWriter, SkeletonSerializer and the generator~~
|
||||
- [x] ~~C#~~
|
||||
- [x] ~~Figure out how we can add the HeadlessTest and run it without adding it to the assembly itself~~
|
||||
- [x] ~~Follow what we did for spine-cpp wrt~~
|
||||
- [x] ~~C (spine-c):~~
|
||||
- [x] ~~Follow what we did for spine-cpp wrt to JsonWriter, SkeletonSerializer and the generator (this one will need some ultrathink and discussion with the user before code changs)~~
|
||||
- [x] ~~Update tests/README.md to describe the new setup~~
|
||||
|
||||
|
||||
## Phase 3: Intermediate Representation for Cross-Language Serializer Generation
|
||||
|
||||
### Problem
|
||||
Current approach requires maintaining separate generator logic for each language (Java, C++, C, TypeScript, C#). The complex analysis, exclusion handling, and serialization logic is duplicated across generators, making maintenance difficult.
|
||||
|
||||
### Solution: Language-Agnostic Intermediate Representation (IR)
|
||||
Create a single IR generator that captures all serialization logic in a JSON format that language-specific generators can consume without a lot of post-processing. The IR will still
|
||||
contain Java specific types and names. Language specific generators then just have to translate.
|
||||
|
||||
### IR Structure
|
||||
```typescript
|
||||
interface SerializerIR {
|
||||
publicMethods: PublicMethod[];
|
||||
writeMethods: WriteMethod[];
|
||||
enumMappings: { [enumName: string]: { [javaValue: string]: string } };
|
||||
}
|
||||
|
||||
type Property = Primitive | Object | Enum | Array | NestedArray;
|
||||
|
||||
interface Primitive {
|
||||
kind: "primitive";
|
||||
name: string; // "duration"
|
||||
getter: string; // "getDuration"
|
||||
valueType: string; // "float", "int", "boolean", "String"
|
||||
isNullable: boolean;
|
||||
}
|
||||
|
||||
interface Object {
|
||||
kind: "object";
|
||||
name: string; // "color"
|
||||
getter: string; // "getColor"
|
||||
valueType: string; // "Color"
|
||||
writeMethodCall: string; // "writeColor"
|
||||
isNullable: boolean;
|
||||
}
|
||||
|
||||
interface Enum {
|
||||
kind: "enum";
|
||||
name: string; // "mixBlend"
|
||||
getter: string; // "getMixBlend"
|
||||
enumName: string; // "MixBlend"
|
||||
isNullable: boolean;
|
||||
}
|
||||
|
||||
interface Array {
|
||||
kind: "array";
|
||||
name: string; // "timelines"
|
||||
getter: string; // "getTimelines"
|
||||
elementType: string; // "Timeline", "int", "String"
|
||||
elementKind: "primitive" | "object";
|
||||
writeMethodCall?: string; // "writeTimeline" (for objects)
|
||||
isNullable: boolean;
|
||||
}
|
||||
|
||||
interface NestedArray {
|
||||
kind: "nestedArray";
|
||||
name: string; // "vertices"
|
||||
getter: string; // "getVertices"
|
||||
elementType: string; // "float"
|
||||
isNullable: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Plan
|
||||
- [x] Create tests/generate-serializer-ir.ts:
|
||||
- [x] Reuse logic from tests/generate-java-serializer.ts (analysis, exclusions, property detection)
|
||||
- [x] Output SerializerIR as JSON to tests/output/serializer-ir.json
|
||||
- [x] All exclusions and filtering pre-applied - no post-processing needed
|
||||
- [ ] Update language generators to consume IR:
|
||||
- [x] Replace tests/generate-java-serializer.ts with IR-based version
|
||||
- [ ] Modify tests/generate-cpp-serializer.ts to use IR
|
||||
- [ ] Create tests/generate-ts-serializer.ts using IR
|
||||
- [ ] Create tests/generate-cs-serializer.ts using IR
|
||||
- [ ] Language generators focus purely on syntax transformation
|
||||
- [ ] Update tests/regenerate-all.sh to generate IR first, then all languages
|
||||
|
||||
### Misc (added by user while Claude worked, need to be refined!)
|
||||
- [ ] HeadlessTest should probably
|
||||
- Have a mode that does what we currently do: take files and animation name, and output serialized skeleton data and skeleton. Used for ad-hoc testing of files submitted by users in error reports etc.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,304 +3,110 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import type { ClassInfo, PropertyInfo } from './types';
|
||||
import type { SerializerIR, PublicMethod, WriteMethod, Property } from './generate-serializer-ir';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
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>>();
|
||||
function generatePropertyCode(property: Property, indent: string): string[] {
|
||||
const lines: string[] = [];
|
||||
const accessor = `obj.${property.getter}`;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
interface SerializedAnalysisResult {
|
||||
classMap: [string, ClassInfo][];
|
||||
accessibleTypes: string[];
|
||||
abstractTypes: [string, string[]][];
|
||||
allTypesToGenerate: string[];
|
||||
typeProperties: [string, PropertyInfo[]][];
|
||||
}
|
||||
|
||||
function generateWriteValue(output: string[], expression: string, type: string, indent: string, abstractTypes: Map<string, string[]>, classMap: Map<string, ClassInfo>) {
|
||||
// Handle null annotations
|
||||
const isNullable = type.includes('@Null');
|
||||
type = type.replace(/@Null\s+/g, '').trim();
|
||||
|
||||
// Primitive types
|
||||
if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(type)) {
|
||||
output.push(`${indent}json.writeValue(${expression});`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's an enum - need to handle both short and full names
|
||||
let classInfo = classMap.get(type);
|
||||
if (!classInfo && !type.includes('.')) {
|
||||
// Try to find by short name
|
||||
for (const [fullName, info] of classMap) {
|
||||
if (fullName.split('.').pop() === type) {
|
||||
classInfo = info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (classInfo?.isEnum) {
|
||||
if (isNullable) {
|
||||
output.push(`${indent}if (${expression} == null) {`);
|
||||
output.push(`${indent} json.writeNull();`);
|
||||
output.push(`${indent}} else {`);
|
||||
output.push(`${indent} json.writeValue(${expression}.name());`);
|
||||
output.push(`${indent}}`);
|
||||
} else {
|
||||
output.push(`${indent}json.writeValue(${expression}.name());`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrays
|
||||
if (type.startsWith('Array<')) {
|
||||
const innerType = type.match(/Array<(.+?)>/)![1].trim();
|
||||
if (isNullable) {
|
||||
output.push(`${indent}if (${expression} == null) {`);
|
||||
output.push(`${indent} json.writeNull();`);
|
||||
output.push(`${indent}} else {`);
|
||||
output.push(`${indent} json.writeArrayStart();`);
|
||||
output.push(`${indent} for (${innerType} item : ${expression}) {`);
|
||||
generateWriteValue(output, 'item', innerType, indent + ' ', abstractTypes, classMap);
|
||||
output.push(`${indent} }`);
|
||||
output.push(`${indent} json.writeArrayEnd();`);
|
||||
output.push(`${indent}}`);
|
||||
} else {
|
||||
output.push(`${indent}json.writeArrayStart();`);
|
||||
output.push(`${indent}for (${innerType} item : ${expression}) {`);
|
||||
generateWriteValue(output, 'item', innerType, indent + ' ', abstractTypes, classMap);
|
||||
output.push(`${indent}}`);
|
||||
output.push(`${indent}json.writeArrayEnd();`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'IntArray' || type === 'FloatArray') {
|
||||
if (isNullable) {
|
||||
output.push(`${indent}if (${expression} == null) {`);
|
||||
output.push(`${indent} json.writeNull();`);
|
||||
output.push(`${indent}} else {`);
|
||||
output.push(`${indent} json.writeArrayStart();`);
|
||||
output.push(`${indent} for (int i = 0; i < ${expression}.size; i++) {`);
|
||||
output.push(`${indent} json.writeValue(${expression}.get(i));`);
|
||||
output.push(`${indent} }`);
|
||||
output.push(`${indent} json.writeArrayEnd();`);
|
||||
output.push(`${indent}}`);
|
||||
} else {
|
||||
output.push(`${indent}json.writeArrayStart();`);
|
||||
output.push(`${indent}for (int i = 0; i < ${expression}.size; i++) {`);
|
||||
output.push(`${indent} json.writeValue(${expression}.get(i));`);
|
||||
output.push(`${indent}}`);
|
||||
output.push(`${indent}json.writeArrayEnd();`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type.endsWith('[]')) {
|
||||
const elemType = type.slice(0, -2);
|
||||
if (isNullable) {
|
||||
output.push(`${indent}if (${expression} == null) {`);
|
||||
output.push(`${indent} json.writeNull();`);
|
||||
output.push(`${indent}} else {`);
|
||||
output.push(`${indent} json.writeArrayStart();`);
|
||||
// Handle nested arrays (like float[][])
|
||||
if (elemType.endsWith('[]')) {
|
||||
const nestedType = elemType.slice(0, -2);
|
||||
output.push(`${indent} for (${elemType} nestedArray : ${expression}) {`);
|
||||
output.push(`${indent} if (nestedArray == null) {`);
|
||||
output.push(`${indent} json.writeNull();`);
|
||||
output.push(`${indent} } else {`);
|
||||
output.push(`${indent} json.writeArrayStart();`);
|
||||
output.push(`${indent} for (${nestedType} elem : nestedArray) {`);
|
||||
output.push(`${indent} json.writeValue(elem);`);
|
||||
output.push(`${indent} }`);
|
||||
output.push(`${indent} json.writeArrayEnd();`);
|
||||
output.push(`${indent} }`);
|
||||
output.push(`${indent} }`);
|
||||
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 {
|
||||
output.push(`${indent} for (${elemType} item : ${expression}) {`);
|
||||
generateWriteValue(output, 'item', elemType, indent + ' ', abstractTypes, classMap);
|
||||
output.push(`${indent} }`);
|
||||
lines.push(`${indent}${property.writeMethodCall}(${accessor});`);
|
||||
}
|
||||
output.push(`${indent} json.writeArrayEnd();`);
|
||||
output.push(`${indent}}`);
|
||||
} else {
|
||||
output.push(`${indent}json.writeArrayStart();`);
|
||||
// Handle nested arrays (like float[][])
|
||||
if (elemType.endsWith('[]')) {
|
||||
const nestedType = elemType.slice(0, -2);
|
||||
output.push(`${indent}for (${elemType} nestedArray : ${expression}) {`);
|
||||
output.push(`${indent} json.writeArrayStart();`);
|
||||
output.push(`${indent} for (${nestedType} elem : nestedArray) {`);
|
||||
output.push(`${indent} json.writeValue(elem);`);
|
||||
output.push(`${indent} }`);
|
||||
output.push(`${indent} json.writeArrayEnd();`);
|
||||
output.push(`${indent}}`);
|
||||
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 {
|
||||
output.push(`${indent}for (${elemType} item : ${expression}) {`);
|
||||
generateWriteValue(output, 'item', elemType, indent + ' ', abstractTypes, classMap);
|
||||
output.push(`${indent}}`);
|
||||
lines.push(`${indent}json.writeValue(${accessor}.name());`);
|
||||
}
|
||||
output.push(`${indent}json.writeArrayEnd();`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Special cases for libGDX types
|
||||
if (type === 'Color') {
|
||||
output.push(`${indent}writeColor(${expression});`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'TextureRegion') {
|
||||
output.push(`${indent}writeTextureRegion(${expression});`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle objects
|
||||
const shortType = type.split('.').pop()!;
|
||||
|
||||
// Check if this type exists in classMap (for abstract types that might not be in generated methods)
|
||||
let foundInClassMap = classMap.has(type);
|
||||
if (!foundInClassMap && !type.includes('.')) {
|
||||
// Try to find by short name
|
||||
for (const [fullName, info] of classMap) {
|
||||
if (fullName.split('.').pop() === type) {
|
||||
foundInClassMap = true;
|
||||
// If it's abstract/interface, we need the instanceof chain
|
||||
if (info.isAbstract || info.isInterface) {
|
||||
type = fullName; // Use full name for abstract types
|
||||
break;
|
||||
|
||||
case "array":
|
||||
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 : ${accessor}) {`);
|
||||
if (property.elementKind === "primitive") {
|
||||
lines.push(`${indent} json.writeValue(item);`);
|
||||
} else {
|
||||
lines.push(`${indent} ${property.writeMethodCall}(item);`);
|
||||
}
|
||||
break;
|
||||
lines.push(`${indent} }`);
|
||||
lines.push(`${indent} json.writeArrayEnd();`);
|
||||
lines.push(`${indent}}`);
|
||||
} else {
|
||||
lines.push(`${indent}json.writeArrayStart();`);
|
||||
lines.push(`${indent}for (${property.elementType} item : ${accessor}) {`);
|
||||
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();`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isNullable) {
|
||||
output.push(`${indent}if (${expression} == null) {`);
|
||||
output.push(`${indent} json.writeNull();`);
|
||||
output.push(`${indent}} else {`);
|
||||
output.push(`${indent} write${shortType}(${expression});`);
|
||||
output.push(`${indent}}`);
|
||||
} else {
|
||||
output.push(`${indent}write${shortType}(${expression});`);
|
||||
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 generateJavaSerializer(analysisData: SerializedAnalysisResult): string {
|
||||
function generateJavaFromIR(ir: SerializerIR): string {
|
||||
const javaOutput: string[] = [];
|
||||
|
||||
// Convert arrays back to Maps
|
||||
const classMap = new Map(analysisData.classMap);
|
||||
const abstractTypes = new Map(analysisData.abstractTypes);
|
||||
const typeProperties = new Map(analysisData.typeProperties);
|
||||
|
||||
// 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)
|
||||
const exclusions = loadExclusions();
|
||||
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 Java file header
|
||||
javaOutput.push('package com.esotericsoftware.spine.utils;');
|
||||
javaOutput.push('');
|
||||
@ -318,7 +124,6 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
||||
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;');
|
||||
@ -328,90 +133,49 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
||||
javaOutput.push(' private JsonWriter json;');
|
||||
javaOutput.push('');
|
||||
|
||||
// Generate main entry methods
|
||||
javaOutput.push(' public String serializeSkeletonData(SkeletonData data) {');
|
||||
javaOutput.push(' visitedObjects.clear();');
|
||||
javaOutput.push(' json = new JsonWriter();');
|
||||
javaOutput.push(' writeSkeletonData(data);');
|
||||
javaOutput.push(' json.close();');
|
||||
javaOutput.push(' return json.getString();');
|
||||
javaOutput.push(' }');
|
||||
javaOutput.push('');
|
||||
javaOutput.push(' public String serializeSkeleton(Skeleton skeleton) {');
|
||||
javaOutput.push(' visitedObjects.clear();');
|
||||
javaOutput.push(' json = new JsonWriter();');
|
||||
javaOutput.push(' writeSkeleton(skeleton);');
|
||||
javaOutput.push(' json.close();');
|
||||
javaOutput.push(' return json.getString();');
|
||||
javaOutput.push(' }');
|
||||
javaOutput.push('');
|
||||
javaOutput.push(' public String serializeAnimationState(AnimationState state) {');
|
||||
javaOutput.push(' visitedObjects.clear();');
|
||||
javaOutput.push(' json = new JsonWriter();');
|
||||
javaOutput.push(' writeAnimationState(state);');
|
||||
javaOutput.push(' json.close();');
|
||||
javaOutput.push(' return json.getString();');
|
||||
javaOutput.push(' }');
|
||||
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 all types
|
||||
const generatedMethods = new Set<string>();
|
||||
|
||||
for (const typeName of Array.from(typesNeedingMethods).sort()) {
|
||||
const classInfo = classMap.get(typeName);
|
||||
if (!classInfo) continue;
|
||||
// Generate write methods
|
||||
for (const method of ir.writeMethods) {
|
||||
const shortName = method.paramType.split('.').pop()!;
|
||||
const className = method.paramType.includes('.') ? method.paramType : shortName;
|
||||
|
||||
// Skip enums - they are handled inline with .name() calls
|
||||
if (classInfo.isEnum) continue;
|
||||
javaOutput.push(` private void ${method.name}(${className} obj) {`);
|
||||
|
||||
const shortName = typeName.split('.').pop()!;
|
||||
|
||||
// Skip if already generated (handle name collisions)
|
||||
if (generatedMethods.has(shortName)) continue;
|
||||
generatedMethods.add(shortName);
|
||||
|
||||
// Use full class name for inner classes
|
||||
const className = typeName.includes('.') ? typeName : shortName;
|
||||
|
||||
javaOutput.push(` private void write${shortName}(${className} obj) {`);
|
||||
|
||||
if (classInfo.isEnum) {
|
||||
// Handle enums
|
||||
javaOutput.push(' json.writeValue(obj.name());');
|
||||
} else if (classInfo.isAbstract || classInfo.isInterface) {
|
||||
if (method.isAbstractType) {
|
||||
// Handle abstract types with instanceof chain
|
||||
const implementations = classInfo.concreteImplementations || [];
|
||||
|
||||
// Filter out excluded types from implementations
|
||||
const exclusions = loadExclusions();
|
||||
const filteredImplementations = implementations.filter(impl => {
|
||||
return !exclusions.types.has(impl);
|
||||
});
|
||||
|
||||
if (filteredImplementations.length === 0) {
|
||||
javaOutput.push(' json.writeNull(); // No concrete implementations after filtering exclusions');
|
||||
} else {
|
||||
if (method.subtypeChecks && method.subtypeChecks.length > 0) {
|
||||
let first = true;
|
||||
for (const impl of filteredImplementations) {
|
||||
const implShortName = impl.split('.').pop()!;
|
||||
const implClassName = impl.includes('.') ? impl : implShortName;
|
||||
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 ${implClassName}) {`);
|
||||
javaOutput.push(` if (obj instanceof ${subtypeClassName}) {`);
|
||||
first = false;
|
||||
} else {
|
||||
javaOutput.push(` } else if (obj instanceof ${implClassName}) {`);
|
||||
javaOutput.push(` } else if (obj instanceof ${subtypeClassName}) {`);
|
||||
}
|
||||
javaOutput.push(` write${implShortName}((${implClassName}) obj);`);
|
||||
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
|
||||
const properties = typeProperties.get(typeName) || [];
|
||||
|
||||
// Add cycle detection
|
||||
javaOutput.push(' if (visitedObjects.contains(obj)) {');
|
||||
javaOutput.push(' json.writeValue("<circular>");');
|
||||
@ -426,22 +190,12 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
||||
javaOutput.push(' json.writeName("type");');
|
||||
javaOutput.push(` json.writeValue("${shortName}");`);
|
||||
|
||||
// Write properties (skip excluded ones)
|
||||
for (const prop of properties) {
|
||||
if (prop.excluded) {
|
||||
javaOutput.push(` // Skipping excluded property: ${prop.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const propName = prop.isGetter ?
|
||||
prop.name.replace('get', '').replace('()', '').charAt(0).toLowerCase() +
|
||||
prop.name.replace('get', '').replace('()', '').slice(1) :
|
||||
prop.name;
|
||||
|
||||
// Write properties
|
||||
for (const property of method.properties) {
|
||||
javaOutput.push('');
|
||||
javaOutput.push(` json.writeName("${propName}");`);
|
||||
const accessor = prop.isGetter ? `obj.${prop.name}` : `obj.${prop.name}`;
|
||||
generateWriteValue(javaOutput, accessor, prop.type, ' ', abstractTypes, classMap);
|
||||
javaOutput.push(` json.writeName("${property.name}");`);
|
||||
const propertyLines = generatePropertyCode(property, ' ');
|
||||
javaOutput.push(...propertyLines);
|
||||
}
|
||||
|
||||
javaOutput.push('');
|
||||
@ -452,7 +206,7 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
||||
javaOutput.push('');
|
||||
}
|
||||
|
||||
// Add helper methods
|
||||
// Add helper methods for special types
|
||||
javaOutput.push(' private void writeColor(Color obj) {');
|
||||
javaOutput.push(' if (obj == null) {');
|
||||
javaOutput.push(' json.writeNull();');
|
||||
@ -491,6 +245,34 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
||||
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');
|
||||
@ -498,17 +280,17 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
||||
|
||||
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.');
|
||||
// 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 analysisData: SerializedAnalysisResult = JSON.parse(fs.readFileSync(analysisFile, 'utf8'));
|
||||
const ir: SerializerIR = JSON.parse(fs.readFileSync(irFile, 'utf8'));
|
||||
|
||||
// Generate Java serializer
|
||||
const javaCode = generateJavaSerializer(analysisData);
|
||||
// Generate Java serializer from IR
|
||||
const javaCode = generateJavaFromIR(ir);
|
||||
|
||||
// Write the Java file
|
||||
const javaFile = path.resolve(
|
||||
@ -527,10 +309,13 @@ async function main() {
|
||||
fs.mkdirSync(path.dirname(javaFile), { recursive: true });
|
||||
fs.writeFileSync(javaFile, javaCode);
|
||||
|
||||
console.log(`Generated Java serializer: ${javaFile}`);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -540,4 +325,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { generateJavaSerializer };
|
||||
export { generateJavaFromIR };
|
||||
575
tests/generate-serializer-ir.ts
Normal file
575
tests/generate-serializer-ir.ts
Normal file
@ -0,0 +1,575 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import type { ClassInfo, PropertyInfo } from './types';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// IR Type Definitions
|
||||
interface SerializerIR {
|
||||
publicMethods: PublicMethod[];
|
||||
writeMethods: WriteMethod[];
|
||||
enumMappings: { [enumName: string]: { [javaValue: string]: string } };
|
||||
}
|
||||
|
||||
interface PublicMethod {
|
||||
name: string;
|
||||
paramType: string;
|
||||
paramName: string;
|
||||
writeMethodCall: string;
|
||||
}
|
||||
|
||||
interface WriteMethod {
|
||||
name: string;
|
||||
paramType: string;
|
||||
properties: Property[];
|
||||
isAbstractType: boolean;
|
||||
subtypeChecks?: SubtypeCheck[];
|
||||
}
|
||||
|
||||
interface SubtypeCheck {
|
||||
typeName: string;
|
||||
writeMethodCall: string;
|
||||
}
|
||||
|
||||
type Property = Primitive | Object | Enum | Array | NestedArray;
|
||||
|
||||
interface Primitive {
|
||||
kind: "primitive";
|
||||
name: string;
|
||||
getter: string;
|
||||
valueType: string;
|
||||
isNullable: boolean;
|
||||
}
|
||||
|
||||
interface Object {
|
||||
kind: "object";
|
||||
name: string;
|
||||
getter: string;
|
||||
valueType: string;
|
||||
writeMethodCall: string;
|
||||
isNullable: boolean;
|
||||
}
|
||||
|
||||
interface Enum {
|
||||
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;
|
||||
}
|
||||
|
||||
interface NestedArray {
|
||||
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[]][];
|
||||
}
|
||||
|
||||
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 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
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
export { generateSerializerIR, type SerializerIR, type PublicMethod, type WriteMethod, type Property };
|
||||
@ -8,6 +8,9 @@ cd "$SCRIPT_DIR/.."
|
||||
echo "Analyzing Java API..."
|
||||
npx tsx tests/analyze-java-api.ts
|
||||
|
||||
echo "Generating serializer IR..."
|
||||
npx tsx tests/generate-serializer-ir.ts
|
||||
|
||||
echo "Generating Java SkeletonSerializer..."
|
||||
npx tsx tests/generate-java-serializer.ts
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user