This commit is contained in:
2026-08-11 16:23:58 +02:00
parent 21c2275c00
commit b5e055216c
10 changed files with 292 additions and 27 deletions
+86
View File
@@ -452,3 +452,89 @@ test("diagnostics report unresolved typed content references only", async () =>
"untyped WeakReference content is not diagnosed as a global ref",
);
});
test("fragment diagnostics skip document-level checks but keep subtree validation", async () => {
const text =
`<CreateObjectDie xmlns="uri:ea.com:eala:asset" id="ModuleTag_X" CreationList="OCL_X">\n` +
` <DieMuxData DeathTypo="SUICIDED"/>\n` +
`</CreateObjectDie>`;
const scope = await makeScope(text, makeIdx([]));
const collection = new FakeDiagnosticCollection();
const provider = new Ra3Diagnostics({
isRa3Workspace: () => true,
getScope: async () => scope,
settings: {
diagnoseUnknownElements: true,
reportUnresolvedReferences: "warning",
},
});
provider["collection"] = collection;
await provider.update(makeDocument(text));
const codes = collection.last.diags.map((d) => d.code);
assert.ok(
!codes.includes("missing-id"),
"fragment children are not treated as top-level assets",
);
assert.ok(
!codes.some((c) => c.startsWith("unresolved-reference")),
"fragment references are deferred to the includer context",
);
assert.ok(
codes.includes("unknown-attribute"),
"a known fragment root still validates its subtree attributes",
);
});
test("fragment diagnostics ignore unknown wrapper roots and still report missing xi:include", async () => {
const text =
`<CommonArmorDraws xmlns="uri:ea.com:eala:asset" xmlns:xi="http://www.w3.org/2001/XInclude">\n` +
` <ScriptedModelDraw id="M" Bogus="x"/>\n` +
` <xi:include href="MissingTarget.xml"/>\n` +
`</CommonArmorDraws>`;
const scope = await makeScope(text, makeIdx([]));
const collection = new FakeDiagnosticCollection();
const provider = new Ra3Diagnostics({
isRa3Workspace: () => true,
getScope: async () => scope,
settings: {
diagnoseUnknownElements: true,
reportUnresolvedReferences: "warning",
},
});
provider["collection"] = collection;
await provider.update(makeDocument(text));
const codes = collection.last.diags.map((d) => d.code);
assert.ok(
!codes.includes("unknown-element"),
"wrapper roots are not validated as standalone documents",
);
assert.ok(
!codes.includes("unknown-attribute"),
"unknown wrapper roots do not trigger subtree attribute guessing",
);
assert.ok(
codes.includes("include-not-found"),
"missing xi:include targets inside fragments are still reported",
);
});
test("full documents still require ids on top-level assets", async () => {
const text = `<AssetDeclaration>\n <GameObject/>\n</AssetDeclaration>`;
const scope = await makeScope(text, makeIdx([]));
const collection = new FakeDiagnosticCollection();
const provider = new Ra3Diagnostics({
isRa3Workspace: () => true,
getScope: async () => scope,
settings: {
diagnoseUnknownElements: true,
reportUnresolvedReferences: "warning",
},
});
provider["collection"] = collection;
await provider.update(makeDocument(text));
const codes = collection.last.diags.map((d) => d.code);
assert.ok(
codes.includes("missing-id"),
"AssetDeclaration documents keep top-level id checks",
);
});
+48
View File
@@ -99,6 +99,54 @@ test("logical xi:include expansion gives included modules their Draws context",
assert.ok(localIds.includes("ModuleTag_Headlight"));
});
test("xi:include without xpointer splices the target root element itself", async (t) => {
const tmp = await mkdtemp(join(tmpdir(), "ra3-local-noxpointer-"));
t.after(() => rm(tmp, { recursive: true, force: true }));
const dataDir = join(tmp, "Data");
const includesDir = join(dataDir, "Includes");
await mkdir(includesDir, { recursive: true });
const mainPath = join(dataDir, "Main.xml");
const fragmentPath = join(includesDir, "Fragment.xml");
await writeFile(
fragmentPath,
'<CreateObjectDie xmlns="uri:ea.com:eala:asset" id="ModuleTag_X" CreationList="OCL_X"><DieMuxData DeathTypes="SUICIDED"/></CreateObjectDie>',
"utf8",
);
await writeFile(
mainPath,
'<AssetDeclaration xmlns="uri:ea.com:eala:asset" xmlns:xi="http://www.w3.org/2001/XInclude">' +
'<GameObject id="G"><Behaviors><xi:include href="DATA:Includes/Fragment.xml"/></Behaviors></GameObject>' +
"</AssetDeclaration>",
"utf8",
);
const searchPaths = buildSearchPaths(sdk, tmp);
const text = await readFile(mainPath, "utf8");
const scope = await buildDocumentScope(mainPath, text, 1, {
projectDir: tmp,
sdkDir: sdk,
searchPaths,
readRecords: readParsed,
readDom: readParsed,
});
const behaviors = scope.expanded.elements.find((e) => e.name === "Behaviors");
assert.ok(behaviors, "Behaviors exists");
assert.equal(behaviors.children.length, 1);
const module = behaviors.children[0];
assert.equal(module.name, "CreateObjectDie");
assert.equal(
module.attrs.find((a) => a.name === "id")?.value,
"ModuleTag_X",
);
assert.match(module.sourceFile, /Fragment\.xml$/i);
const dieMux = scope.expanded.elements.find((e) => e.name === "DieMuxData");
assert.ok(dieMux, "DieMuxData is expanded");
assert.equal(
dieMux.parent,
module,
"DieMuxData stays inside the included CreateObjectDie module",
);
});
test("local overlay wins over a global definition with the same id", async () => {
const scope = await makeScope();
const global = {