mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +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
|
**Status:** In Progress
|
||||||
**Created:** 2025-01-11T03:02:54
|
**Created:** 2025-01-11T03:02:54
|
||||||
**Started:** 2025-01-11T03:11:22
|
**Started:** 2025-01-11T03:11:22
|
||||||
**Agent PID:** 93834
|
**Agent PID:** 15570
|
||||||
|
|
||||||
**CRITICAL:**
|
**CRITICAL:**
|
||||||
- NEVER never check a chceckbox and move on to the next checkbox unless the user has confirmed completion of the current checkbox!
|
- 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
|
- 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
|
- Implement custom writeSkin function that properly handles AttachmentMap::Entries iteration
|
||||||
- [x] Create regenerate-all.sh script in tests/ that runs all generators in sequence
|
- [x] Create regenerate-all.sh script in tests/ that runs all generators in sequence
|
||||||
- [ ] Fix C++ JsonWriter output formatting issues (array formatting broken)
|
- [x] ~~Fix C++ JsonWriter output formatting issues (array formatting broken)~~
|
||||||
- [ ] Test with sample skeleton files
|
- [x] ~~Test with sample skeleton files~~
|
||||||
- [ ] TypeScript (spine-ts):
|
- [x] ~~TypeScript (spine-ts):~~
|
||||||
- [ ] Follow what we did for spine-cpp wrt to JsonWriter, SkeletonSerializer and the generator
|
- [x] ~~Follow what we did for spine-cpp wrt to JsonWriter, SkeletonSerializer and the generator~~
|
||||||
- [ ] C#
|
- [x] ~~C#~~
|
||||||
- [ ] Figure out how we can add the HeadlessTest and run it without adding it to the assembly itself
|
- [x] ~~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
|
- [x] ~~Follow what we did for spine-cpp wrt~~
|
||||||
- [ ] C (spine-c):
|
- [x] ~~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)
|
- [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)~~
|
||||||
- [ ] Update tests/README.md to describe the new setup
|
- [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!)
|
### Misc (added by user while Claude worked, need to be refined!)
|
||||||
- [ ] HeadlessTest should probably
|
- [ ] 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.
|
- 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 fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
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));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
function loadExclusions(): { types: Set<string>, methods: Map<string, Set<string>>, fields: Map<string, Set<string>> } {
|
function generatePropertyCode(property: Property, indent: string): string[] {
|
||||||
const exclusionsPath = path.resolve(__dirname, 'java-exclusions.txt');
|
const lines: string[] = [];
|
||||||
const types = new Set<string>();
|
const accessor = `obj.${property.getter}`;
|
||||||
const methods = new Map<string, Set<string>>();
|
|
||||||
const fields = new Map<string, Set<string>>();
|
|
||||||
|
|
||||||
if (!fs.existsSync(exclusionsPath)) {
|
switch (property.kind) {
|
||||||
return { types, methods, fields };
|
case "primitive":
|
||||||
}
|
lines.push(`${indent}json.writeValue(${accessor});`);
|
||||||
|
break;
|
||||||
const content = fs.readFileSync(exclusionsPath, 'utf-8');
|
|
||||||
const lines = content.split('\n');
|
case "object":
|
||||||
|
if (property.isNullable) {
|
||||||
for (const line of lines) {
|
lines.push(`${indent}if (${accessor} == null) {`);
|
||||||
const trimmed = line.trim();
|
lines.push(`${indent} json.writeNull();`);
|
||||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
lines.push(`${indent}} else {`);
|
||||||
|
lines.push(`${indent} ${property.writeMethodCall}(${accessor});`);
|
||||||
const parts = trimmed.split(/\s+/);
|
lines.push(`${indent}}`);
|
||||||
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} }`);
|
|
||||||
} else {
|
} else {
|
||||||
output.push(`${indent} for (${elemType} item : ${expression}) {`);
|
lines.push(`${indent}${property.writeMethodCall}(${accessor});`);
|
||||||
generateWriteValue(output, 'item', elemType, indent + ' ', abstractTypes, classMap);
|
|
||||||
output.push(`${indent} }`);
|
|
||||||
}
|
}
|
||||||
output.push(`${indent} json.writeArrayEnd();`);
|
break;
|
||||||
output.push(`${indent}}`);
|
|
||||||
} else {
|
case "enum":
|
||||||
output.push(`${indent}json.writeArrayStart();`);
|
if (property.isNullable) {
|
||||||
// Handle nested arrays (like float[][])
|
lines.push(`${indent}if (${accessor} == null) {`);
|
||||||
if (elemType.endsWith('[]')) {
|
lines.push(`${indent} json.writeNull();`);
|
||||||
const nestedType = elemType.slice(0, -2);
|
lines.push(`${indent}} else {`);
|
||||||
output.push(`${indent}for (${elemType} nestedArray : ${expression}) {`);
|
lines.push(`${indent} json.writeValue(${accessor}.name());`);
|
||||||
output.push(`${indent} json.writeArrayStart();`);
|
lines.push(`${indent}}`);
|
||||||
output.push(`${indent} for (${nestedType} elem : nestedArray) {`);
|
|
||||||
output.push(`${indent} json.writeValue(elem);`);
|
|
||||||
output.push(`${indent} }`);
|
|
||||||
output.push(`${indent} json.writeArrayEnd();`);
|
|
||||||
output.push(`${indent}}`);
|
|
||||||
} else {
|
} else {
|
||||||
output.push(`${indent}for (${elemType} item : ${expression}) {`);
|
lines.push(`${indent}json.writeValue(${accessor}.name());`);
|
||||||
generateWriteValue(output, 'item', elemType, indent + ' ', abstractTypes, classMap);
|
|
||||||
output.push(`${indent}}`);
|
|
||||||
}
|
}
|
||||||
output.push(`${indent}json.writeArrayEnd();`);
|
break;
|
||||||
}
|
|
||||||
return;
|
case "array":
|
||||||
}
|
if (property.isNullable) {
|
||||||
|
lines.push(`${indent}if (${accessor} == null) {`);
|
||||||
// Special cases for libGDX types
|
lines.push(`${indent} json.writeNull();`);
|
||||||
if (type === 'Color') {
|
lines.push(`${indent}} else {`);
|
||||||
output.push(`${indent}writeColor(${expression});`);
|
lines.push(`${indent} json.writeArrayStart();`);
|
||||||
return;
|
lines.push(`${indent} for (${property.elementType} item : ${accessor}) {`);
|
||||||
}
|
if (property.elementKind === "primitive") {
|
||||||
|
lines.push(`${indent} json.writeValue(item);`);
|
||||||
if (type === 'TextureRegion') {
|
} else {
|
||||||
output.push(`${indent}writeTextureRegion(${expression});`);
|
lines.push(`${indent} ${property.writeMethodCall}(item);`);
|
||||||
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;
|
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();`);
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
}
|
|
||||||
|
case "nestedArray":
|
||||||
if (isNullable) {
|
if (property.isNullable) {
|
||||||
output.push(`${indent}if (${expression} == null) {`);
|
lines.push(`${indent}if (${accessor} == null) {`);
|
||||||
output.push(`${indent} json.writeNull();`);
|
lines.push(`${indent} json.writeNull();`);
|
||||||
output.push(`${indent}} else {`);
|
lines.push(`${indent}} else {`);
|
||||||
output.push(`${indent} write${shortType}(${expression});`);
|
lines.push(`${indent} json.writeArrayStart();`);
|
||||||
output.push(`${indent}}`);
|
lines.push(`${indent} for (${property.elementType}[] nestedArray : ${accessor}) {`);
|
||||||
} else {
|
lines.push(`${indent} if (nestedArray == null) {`);
|
||||||
output.push(`${indent}write${shortType}(${expression});`);
|
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[] = [];
|
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
|
// Generate Java file header
|
||||||
javaOutput.push('package com.esotericsoftware.spine.utils;');
|
javaOutput.push('package com.esotericsoftware.spine.utils;');
|
||||||
javaOutput.push('');
|
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.IntArray;');
|
||||||
javaOutput.push('import com.badlogic.gdx.utils.FloatArray;');
|
javaOutput.push('import com.badlogic.gdx.utils.FloatArray;');
|
||||||
javaOutput.push('');
|
javaOutput.push('');
|
||||||
|
|
||||||
javaOutput.push('import java.util.Locale;');
|
javaOutput.push('import java.util.Locale;');
|
||||||
javaOutput.push('import java.util.Set;');
|
javaOutput.push('import java.util.Set;');
|
||||||
javaOutput.push('import java.util.HashSet;');
|
javaOutput.push('import java.util.HashSet;');
|
||||||
@ -328,90 +133,49 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
|||||||
javaOutput.push(' private JsonWriter json;');
|
javaOutput.push(' private JsonWriter json;');
|
||||||
javaOutput.push('');
|
javaOutput.push('');
|
||||||
|
|
||||||
// Generate main entry methods
|
// Generate public methods
|
||||||
javaOutput.push(' public String serializeSkeletonData(SkeletonData data) {');
|
for (const method of ir.publicMethods) {
|
||||||
javaOutput.push(' visitedObjects.clear();');
|
javaOutput.push(` public String ${method.name}(${method.paramType} ${method.paramName}) {`);
|
||||||
javaOutput.push(' json = new JsonWriter();');
|
javaOutput.push(' visitedObjects.clear();');
|
||||||
javaOutput.push(' writeSkeletonData(data);');
|
javaOutput.push(' json = new JsonWriter();');
|
||||||
javaOutput.push(' json.close();');
|
javaOutput.push(` ${method.writeMethodCall}(${method.paramName});`);
|
||||||
javaOutput.push(' return json.getString();');
|
javaOutput.push(' json.close();');
|
||||||
javaOutput.push(' }');
|
javaOutput.push(' return json.getString();');
|
||||||
javaOutput.push('');
|
javaOutput.push(' }');
|
||||||
javaOutput.push(' public String serializeSkeleton(Skeleton skeleton) {');
|
javaOutput.push('');
|
||||||
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 write methods for all types
|
// Generate write methods
|
||||||
const generatedMethods = new Set<string>();
|
for (const method of ir.writeMethods) {
|
||||||
|
const shortName = method.paramType.split('.').pop()!;
|
||||||
for (const typeName of Array.from(typesNeedingMethods).sort()) {
|
const className = method.paramType.includes('.') ? method.paramType : shortName;
|
||||||
const classInfo = classMap.get(typeName);
|
|
||||||
if (!classInfo) continue;
|
|
||||||
|
|
||||||
// Skip enums - they are handled inline with .name() calls
|
javaOutput.push(` private void ${method.name}(${className} obj) {`);
|
||||||
if (classInfo.isEnum) continue;
|
|
||||||
|
|
||||||
const shortName = typeName.split('.').pop()!;
|
if (method.isAbstractType) {
|
||||||
|
|
||||||
// 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) {
|
|
||||||
// Handle abstract types with instanceof chain
|
// Handle abstract types with instanceof chain
|
||||||
const implementations = classInfo.concreteImplementations || [];
|
if (method.subtypeChecks && method.subtypeChecks.length > 0) {
|
||||||
|
|
||||||
// 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 {
|
|
||||||
let first = true;
|
let first = true;
|
||||||
for (const impl of filteredImplementations) {
|
for (const subtype of method.subtypeChecks) {
|
||||||
const implShortName = impl.split('.').pop()!;
|
const subtypeShortName = subtype.typeName.split('.').pop()!;
|
||||||
const implClassName = impl.includes('.') ? impl : implShortName;
|
const subtypeClassName = subtype.typeName.includes('.') ? subtype.typeName : subtypeShortName;
|
||||||
|
|
||||||
if (first) {
|
if (first) {
|
||||||
javaOutput.push(` if (obj instanceof ${implClassName}) {`);
|
javaOutput.push(` if (obj instanceof ${subtypeClassName}) {`);
|
||||||
first = false;
|
first = false;
|
||||||
} else {
|
} 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(' } else {');
|
||||||
javaOutput.push(` throw new RuntimeException("Unknown ${shortName} type: " + obj.getClass().getName());`);
|
javaOutput.push(` throw new RuntimeException("Unknown ${shortName} type: " + obj.getClass().getName());`);
|
||||||
javaOutput.push(' }');
|
javaOutput.push(' }');
|
||||||
|
} else {
|
||||||
|
javaOutput.push(' json.writeNull(); // No concrete implementations after filtering exclusions');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle concrete types
|
// Handle concrete types
|
||||||
const properties = typeProperties.get(typeName) || [];
|
|
||||||
|
|
||||||
// Add cycle detection
|
// Add cycle detection
|
||||||
javaOutput.push(' if (visitedObjects.contains(obj)) {');
|
javaOutput.push(' if (visitedObjects.contains(obj)) {');
|
||||||
javaOutput.push(' json.writeValue("<circular>");');
|
javaOutput.push(' json.writeValue("<circular>");');
|
||||||
@ -426,22 +190,12 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
|||||||
javaOutput.push(' json.writeName("type");');
|
javaOutput.push(' json.writeName("type");');
|
||||||
javaOutput.push(` json.writeValue("${shortName}");`);
|
javaOutput.push(` json.writeValue("${shortName}");`);
|
||||||
|
|
||||||
// Write properties (skip excluded ones)
|
// Write properties
|
||||||
for (const prop of properties) {
|
for (const property of method.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;
|
|
||||||
|
|
||||||
javaOutput.push('');
|
javaOutput.push('');
|
||||||
javaOutput.push(` json.writeName("${propName}");`);
|
javaOutput.push(` json.writeName("${property.name}");`);
|
||||||
const accessor = prop.isGetter ? `obj.${prop.name}` : `obj.${prop.name}`;
|
const propertyLines = generatePropertyCode(property, ' ');
|
||||||
generateWriteValue(javaOutput, accessor, prop.type, ' ', abstractTypes, classMap);
|
javaOutput.push(...propertyLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
javaOutput.push('');
|
javaOutput.push('');
|
||||||
@ -452,7 +206,7 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
|||||||
javaOutput.push('');
|
javaOutput.push('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add helper methods
|
// Add helper methods for special types
|
||||||
javaOutput.push(' private void writeColor(Color obj) {');
|
javaOutput.push(' private void writeColor(Color obj) {');
|
||||||
javaOutput.push(' if (obj == null) {');
|
javaOutput.push(' if (obj == null) {');
|
||||||
javaOutput.push(' json.writeNull();');
|
javaOutput.push(' json.writeNull();');
|
||||||
@ -491,6 +245,34 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
|||||||
javaOutput.push(' json.writeObjectEnd();');
|
javaOutput.push(' json.writeObjectEnd();');
|
||||||
javaOutput.push(' }');
|
javaOutput.push(' }');
|
||||||
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('}');
|
javaOutput.push('}');
|
||||||
|
|
||||||
return javaOutput.join('\n');
|
return javaOutput.join('\n');
|
||||||
@ -498,17 +280,17 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
// Read analysis result
|
// Read the IR file
|
||||||
const analysisFile = path.resolve(__dirname, '..', 'output', 'analysis-result.json');
|
const irFile = path.resolve(__dirname, 'output', 'serializer-ir.json');
|
||||||
if (!fs.existsSync(analysisFile)) {
|
if (!fs.existsSync(irFile)) {
|
||||||
console.error('Analysis result not found. Run analyze-java-api.ts first.');
|
console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const analysisData: SerializedAnalysisResult = JSON.parse(fs.readFileSync(analysisFile, 'utf8'));
|
const ir: SerializerIR = JSON.parse(fs.readFileSync(irFile, 'utf8'));
|
||||||
|
|
||||||
// Generate Java serializer
|
// Generate Java serializer from IR
|
||||||
const javaCode = generateJavaSerializer(analysisData);
|
const javaCode = generateJavaFromIR(ir);
|
||||||
|
|
||||||
// Write the Java file
|
// Write the Java file
|
||||||
const javaFile = path.resolve(
|
const javaFile = path.resolve(
|
||||||
@ -527,10 +309,13 @@ async function main() {
|
|||||||
fs.mkdirSync(path.dirname(javaFile), { recursive: true });
|
fs.mkdirSync(path.dirname(javaFile), { recursive: true });
|
||||||
fs.writeFileSync(javaFile, javaCode);
|
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) {
|
} catch (error: any) {
|
||||||
console.error('Error:', error.message);
|
console.error('Error:', error.message);
|
||||||
|
console.error('Stack:', error.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -540,4 +325,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|||||||
main();
|
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..."
|
echo "Analyzing Java API..."
|
||||||
npx tsx tests/analyze-java-api.ts
|
npx tsx tests/analyze-java-api.ts
|
||||||
|
|
||||||
|
echo "Generating serializer IR..."
|
||||||
|
npx tsx tests/generate-serializer-ir.ts
|
||||||
|
|
||||||
echo "Generating Java SkeletonSerializer..."
|
echo "Generating Java SkeletonSerializer..."
|
||||||
npx tsx tests/generate-java-serializer.ts
|
npx tsx tests/generate-java-serializer.ts
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user