feat(config-editor): add AST parser (speed/enumRef/raw) with tests
This commit is contained in:
47
extensions/pixelhero-config-editor/__tests__/parser.test.ts
Normal file
47
extensions/pixelhero-config-editor/__tests__/parser.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as ts from 'typescript';
|
||||
import { parseExpression, findExportObjectLiteral } from '../src/io/parser';
|
||||
|
||||
function parse(text: string): ts.Expression {
|
||||
const src = ts.createSourceFile('x.ts', `const _ = ${text};`, ts.ScriptTarget.Latest, true);
|
||||
const decl = src.statements[0] as ts.VariableStatement;
|
||||
return decl.declarationList.declarations[0].initializer!;
|
||||
}
|
||||
|
||||
test('num / str / bool', () => {
|
||||
assert.deepEqual(parseExpression(parse('400')), { kind: 'num', value: 400 });
|
||||
assert.deepEqual(parseExpression(parse('"小铁卫"')), { kind: 'str', value: '小铁卫' });
|
||||
assert.deepEqual(parseExpression(parse('true')), { kind: 'bool', value: true });
|
||||
});
|
||||
test('enumRef: FacSet.HERO', () => {
|
||||
assert.deepEqual(parseExpression(parse('FacSet.HERO')), { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' });
|
||||
});
|
||||
test('speed: AtkSpeedSet[AtkSpeedLv.Slow3].cd', () => {
|
||||
assert.deepEqual(parseExpression(parse('AtkSpeedSet[AtkSpeedLv.Slow3].cd')), { kind: 'speed', level: 'Slow3' });
|
||||
});
|
||||
test('non-Speed two-segment access falls back to enumRef', () => {
|
||||
assert.deepEqual(parseExpression(parse('Foo.bar')), { kind: 'enumRef', qualifier: 'Foo', member: 'bar' });
|
||||
});
|
||||
test('arr', () => {
|
||||
assert.deepEqual(parseExpression(parse('[1,2,3]')), { kind: 'arr', items: [
|
||||
{ kind: 'num', value: 1 }, { kind: 'num', value: 2 }, { kind: 'num', value: 3 }] });
|
||||
});
|
||||
test('obj with numeric + identifier keys', () => {
|
||||
const v = parseExpression(parse('{6001:{uuid:6001},name:"x"}'));
|
||||
assert.equal(v.kind, 'obj');
|
||||
assert.deepEqual(v.props['6001'], { kind: 'obj', props: { uuid: { kind: 'num', value: 6001 } } });
|
||||
assert.deepEqual(v.props['name'], { kind: 'str', value: 'x' });
|
||||
});
|
||||
test('raw: unsupported expression preserved verbatim', () => {
|
||||
const v = parseExpression(parse('a + b'));
|
||||
assert.equal(v.kind, 'raw');
|
||||
assert.equal((v as any).text, 'a + b');
|
||||
});
|
||||
test('findExportObjectLiteral locates HeroInfo', () => {
|
||||
const src = ts.createSourceFile('x.ts',
|
||||
`export const HeroInfo: Record<number, any> = { 5011:{uuid:5011} };`, ts.ScriptTarget.Latest, true);
|
||||
const node = findExportObjectLiteral(src, 'HeroInfo');
|
||||
assert.ok(node);
|
||||
assert.equal(node!.properties.length, 1);
|
||||
});
|
||||
65
extensions/pixelhero-config-editor/src/io/parser.ts
Normal file
65
extensions/pixelhero-config-editor/src/io/parser.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as ts from 'typescript';
|
||||
import { RecordValue } from './recordValue';
|
||||
|
||||
/** 在 SourceFile 中查找 `export const <name> = {...}`,返回其对象字面量节点。 */
|
||||
export function findExportObjectLiteral(src: ts.SourceFile, name: string): ts.ObjectLiteralExpression | null {
|
||||
let result: ts.ObjectLiteralExpression | null = null;
|
||||
const visit = (node: ts.Node) => {
|
||||
if (result) return;
|
||||
if (ts.isVariableStatement(node)) {
|
||||
for (const d of node.declarationList.declarations) {
|
||||
if (d.name.getText() === name && d.initializer && ts.isObjectLiteralExpression(d.initializer)) {
|
||||
result = d.initializer;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(src);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 识别 `AtkSpeedSet[AtkSpeedLv.<level>].cd`,否则 null。 */
|
||||
function tryParseSpeedExpr(node: ts.PropertyAccessExpression): RecordValue | null {
|
||||
if (node.name.text !== 'cd') return null;
|
||||
const inner = node.expression;
|
||||
if (!ts.isElementAccessExpression(inner)) return null;
|
||||
if (!ts.isIdentifier(inner.expression) || inner.expression.text !== 'AtkSpeedSet') return null;
|
||||
const arg = inner.argumentExpression;
|
||||
if (!ts.isPropertyAccessExpression(arg)) return null;
|
||||
if (!ts.isIdentifier(arg.expression) || arg.expression.text !== 'AtkSpeedLv') return null;
|
||||
return { kind: 'speed', level: arg.name.text };
|
||||
}
|
||||
|
||||
export function parseExpression(node: ts.Expression): RecordValue {
|
||||
if (ts.isNumericLiteral(node)) return { kind: 'num', value: Number(node.text) };
|
||||
if (ts.isStringLiteral(node)) return { kind: 'str', value: node.text };
|
||||
if (node.kind === ts.SyntaxKind.TrueKeyword) return { kind: 'bool', value: true };
|
||||
if (node.kind === ts.SyntaxKind.FalseKeyword) return { kind: 'bool', value: false };
|
||||
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
const speed = tryParseSpeedExpr(node);
|
||||
if (speed) return speed;
|
||||
if (ts.isIdentifier(node.expression)) {
|
||||
return { kind: 'enumRef', qualifier: node.expression.text, member: node.name.text };
|
||||
}
|
||||
return { kind: 'raw', text: node.getText() }; // 多级 a.b.c 等
|
||||
}
|
||||
|
||||
if (ts.isArrayLiteralExpression(node)) {
|
||||
return { kind: 'arr', items: node.elements.map(e => parseExpression(e)) };
|
||||
}
|
||||
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
const props: Record<string, RecordValue> = {};
|
||||
for (const m of node.properties) {
|
||||
if (ts.isPropertyAssignment(m)) {
|
||||
props[m.name.getText()] = parseExpression(m.initializer);
|
||||
}
|
||||
}
|
||||
return { kind: 'obj', props };
|
||||
}
|
||||
|
||||
return { kind: 'raw', text: node.getText() }; // 二元/模板/调用/其他元素访问
|
||||
}
|
||||
Reference in New Issue
Block a user