修 id 解析
This commit is contained in:
@@ -240,3 +240,85 @@ CommandSet 引用仍能解析到 LogicCommandSet 定义(无回归)
|
||||
- AttachTest 实机:三个原始场景全部按预期(Locomotor 命中 LocomotorTemplate、_SKN 命中 W3DContainer、cannon 双候选均可精确定位)。
|
||||
|
||||
`.vsix` 已重新打包(13:33)。D 盘恢复后照旧可补跑 GenEvoTest / Corona 回归。
|
||||
|
||||
---
|
||||
|
||||
## 九、问题分析(第四轮,2026-08-01):模块 `id` 被误报为未解析引用
|
||||
|
||||
### 问题:`id="ModuleTag_Draw"` 误报 `Unresolved reference`
|
||||
|
||||
**现象**:AttachTest `Allied Vehicle\Guardian Tank\GameObject.xml` 第 45 行
|
||||
|
||||
```xml
|
||||
<TruckDraw
|
||||
id="ModuleTag_Draw"
|
||||
...>
|
||||
```
|
||||
|
||||
报 `Unresolved reference "ModuleTag_Draw" (not found in the current index)`,
|
||||
hover 同时显示 `No matching definition of the expected declared type...`。
|
||||
但该 id 是 TruckDraw(GameObject 模块)自身的标识,只在所属 `<GameObject />` 内部有效,
|
||||
此处就是定义处,全局资产索引中不存在(也不应存在)它的定义。
|
||||
|
||||
**根因(两层叠加)**:
|
||||
|
||||
1. **模型生成器丢失属性级 `xas:refType`**:`ModuleData@id` 在 XSD 中声明为
|
||||
|
||||
```xml
|
||||
<xs:complexType name="ModuleData" xas:isPolymorphic="true">
|
||||
<xs:attribute name="id" type="Poid" xas:refType="ModuleData" />
|
||||
</xs:complexType>
|
||||
```
|
||||
|
||||
refType 写在 `<xs:attribute>` 节点上,而 `tools/xsd-to-model.mjs` 只从 simple type
|
||||
描述符读取 refType,属性级声明被丢弃。`Poid` 本身带 `xas:isWeakRef="true"`(“管线对象
|
||||
ID”),于是该 id 在模型里变成 `{ type: "Poid", refType: null, isRef: true }`——一个
|
||||
“无类型引用”,导致**所有继承自 ModuleData 的模块类型(以及 ObjectFilter、MapObject、
|
||||
GameScript、AIStateTactic 等共 430 个类型)的 `id` 都被当作全局引用检查**。
|
||||
|
||||
2. **`id` 的语义是“定义点”而非“引用”**:即便 refType 正确,嵌套元素(模块、nugget、
|
||||
地图对象)的 `id` 也只是其局部标识,检查全局 unresolved 必然误报。反过来,XSD 里确有一类
|
||||
真正引用其他资产类型的 `id`(如 `RoadObject@id` 为 `AssetReference` + `xas:refType="Road"`),
|
||||
这类检查必须保留。
|
||||
|
||||
**验证**:
|
||||
|
||||
- 用未修改的生成器对当前 SDK XSD 重新生成模型,与仓库内模型逐字段一致(0 差异),
|
||||
证明修复后重新生成的 diff 只落在属性级 refType 上,无无关噪音。
|
||||
- 修复后 `W3DTruckDrawModuleData@id` → `refType: ModuleData`;
|
||||
`isReferenceAttributeOfType(...) === false`;空索引下 `TruckDraw@id` 不再产生诊断;
|
||||
全文件扫描 0 个 `id` 误报,61 处真实引用属性(`inheritFrom`、`CommandSet`、`Side`、
|
||||
`Locomotor`、`TrackMarks` 等)行为不变。
|
||||
|
||||
**修复**:
|
||||
|
||||
1. **生成器**(`tools/xsd-to-model.mjs`):`collectAttributes` 改为
|
||||
`attr["@_refType"] ?? desc?.refType`(属性级优先、simple type 兜底),重新生成
|
||||
`schema-model.json`——共恢复 444 处 refType(含继承传播),`isRef` 与其他字段零变化。
|
||||
附带收益:`Locomotor → LocomotorTemplate`、`Armor → ArmorTemplate`、
|
||||
`ThingTemplate → GameObject` 等此前被当作“无类型引用”的属性恢复真实类型
|
||||
(第三轮中 Locomotor“无 refType”的结论实为该生成器 bug 的误判)。
|
||||
2. **局部引用规则**(`src/indexer/refs.ts` 新增 `isLocalReferenceAttribute`):
|
||||
- `id`:无 refType,或 refType 与元素自身类型兼容(`isAssignableTo`,如
|
||||
`W3DTruckDrawModuleData → ModuleData`)→ 定义点,不做全局引用检查;
|
||||
refType 指向不同类型(`RoadObject@id → Road`)→ 保留真实引用检查;
|
||||
- 非 `id` 且类型为 `Poid` 的属性(`ModuleId`、`AutoResolveBody`、`SoundRef`、
|
||||
`AttachModuleId`…)→ 管线局部引用,全局索引无法判定,不检查。
|
||||
`isReferenceAttributeOfType` 与 `resolveReferenceTargetsForType` 同步使用该守卫,
|
||||
诊断 / hover / 跳转 / 补全行为一致。
|
||||
3. **补全**(`src/features/completion.ts`):`id` 与 Poid 属性不再按 refType 提供
|
||||
全局资产补全(模块 id 是局部的,全局资产列表是错误建议)。
|
||||
|
||||
**测试(31 → 37,全部通过)**:
|
||||
|
||||
- `refs.test.mjs`:TruckDraw 实景结构(GameObject → Draws → TruckDraw)下 `id` 不再是
|
||||
引用且不解析;`RoadObject@id` 仍是引用并能解析到 Road;`AttachModuleId` 等 Poid 属性
|
||||
不误报;Locomotor 改为严格类型引用(同名 GameObject 不再匹配);
|
||||
- `schemaModel.test.mjs`:`ModuleData@id` / `MapObject@id` / `ThingTemplate` /
|
||||
`RoadObject@id` / `AttachModuleId` 的属性级 refType 断言;
|
||||
- 回归:原有 31 个用例全部保持通过。
|
||||
|
||||
> **后续可做**:GameObject 内模块 id 的“局部作用域”解析——`AttachModuleId`、
|
||||
> `ModuleId` 等模块引用指向同一 GameObject 内的兄弟模块,但部分引用(如武器上的
|
||||
> `AttachModuleId`)目标 GameObject 跨文件无法静态确定,本轮先统一不检查;待局部
|
||||
> 作用域建模落地后再启用这些引用的解析与诊断。
|
||||
|
||||
+6
-5
@@ -1,6 +1,6 @@
|
||||
# 调研结论与实施计划(已按最新代码同步更新)
|
||||
|
||||
> 说明:本文档随实现演进持续同步。最近一次同步(2026-08-01)对齐了实现过程中新增的模块与设计变更:BAB 精确搜索路径、manifest 类型/ID 推导、上下文感知元素类型、无类型引用、精确跳转范围、嵌套 `xi:include`、注入式语法高亮等。
|
||||
> 说明:本文档随实现演进持续同步。最近一次同步(2026-08-01)对齐了实现过程中新增的模块与设计变更:BAB 精确搜索路径、manifest 类型/ID 推导、上下文感知元素类型、属性级 refType / Poid 局部引用(`id` 定义点)、精确跳转范围、嵌套 `xi:include`、注入式语法高亮等。
|
||||
|
||||
## 一、调研结论(带证据)
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
- 根元素 `AssetDeclaration`:`Tags` / `Includes` / `Defines` + **295 个顶层资产元素**(含内联声明)。
|
||||
- 每个元素名对应一个 `complexType`,子元素用 `xs:sequence` / `xs:choice` 定义,属性用 `xs:attribute` 定义;复杂类型通过 `xs:extension` 继承(如 `BaseInheritableAsset` 提供 `inheritFrom`)。
|
||||
- `Includes/Ref.xsd` 定义了大量带 `xas:refType="<资产类型>"` 的引用类型(如 `CommandSet` 引用 `LogicCommandSet`)→ 补全/导航按引用类型过滤的依据。
|
||||
- `XmlEdit:Default` 提供默认值;`xs:enumeration` 提供枚举值;`xas:isRef="true"` 无 `refType` 时为**无类型引用**(匹配任意已声明资产)。
|
||||
- `XmlEdit:Default` 提供默认值;`xs:enumeration` 提供枚举值。`xas:refType` 可声明在 simple type 上,也可声明在 `<xs:attribute>` 节点上(模型生成器两者都读、属性级优先)。
|
||||
- `Poid`("Pipeline Object Id",`xas:isWeakRef="true"`)表示**管线局部标识**:`id` 属性定义元素自身(如 `ModuleData@id` → refType `ModuleData`);`ModuleId`、`AutoResolveBody`、`SoundRef` 等 Poid 属性引用同一资产/子树内的模块、子对象、材质——它们都不对全局资产索引做 resolved 判定。
|
||||
- **同名元素在不同父节点下类型不同**(如 `<Weapon>` 在武器槽下是 `WeaponSlot_WeaponData`,在别处可能是 `WeaponRef`)→ 需要上下文感知解析。
|
||||
|
||||
### 4. 领域特有约定
|
||||
@@ -107,7 +108,7 @@ test/
|
||||
2. **索引范围与默认值**:索引“项目 Data + additionalmaps + 沿 include 可达的 SageXml 原版源码”;SDK 路径默认 `C:\Apps\RA3-MODSDK-X`(可配置)。`reference` include 解析为 `builtmods` 下对应 manifest(惰性解析、按文件缓存),manifest 缺失/无效时回退到占位 XML。
|
||||
3. **manifest 资产建模**:类型优先用哈希表,未知时从名称前缀推导;可引用 ID 取最后冒号段;类型名统一走大小写规范化(`W3dContainer` ↔ `W3DContainer`),类型匹配严格遵循 XSD 继承链。
|
||||
4. **上下文感知元素类型**:同名元素按父元素类型解析(`resolveElementType` 沿解析树逐层 `childTypeOf`,失败回退全局映射),保证 `<Weapon>` 等元素的属性/引用判定正确。
|
||||
5. **引用判定与解析**:`refType` 或 `isRef` 均视为引用;带 `refType` 时严格按类型过滤(同名 ID 不串类型);无类型引用匹配任意声明 ID;`inheritFrom` 按可继承类型过滤。
|
||||
5. **引用判定与解析**:`refType` 或 `isRef` 均视为引用;带 `refType` 时严格按类型过滤(同名 ID 不串类型);`inheritFrom` 按可继承类型过滤。**局部作用域例外**(`isLocalReferenceAttribute`):`id` 是元素自身的定义点——无 refType 或 refType 与自身类型兼容时不检查、不解析(`RoadObject@id→Road` 这类跨类型 id 引用保留检查);Poid 类型属性是管线局部引用,全局索引无法判定,不检查、不解析。
|
||||
6. **重复 ID 诊断**:与 `check_duplicate_ids.py` 一致——SageXml 不参与冲突判定,mod 覆盖原版视为正常。
|
||||
7. **未解析引用诊断**:按设置严重级别报告(默认 warning);类型不匹配时给出明确文案("有同名 ID 但类型不匹配")。`definitionMode` 设置控制跳转候选:`all`(mod + 原版全部列出,mod 优先)或 `project-only`。
|
||||
8. **跳转精度**:XML 定义跳转到 `id` 属性值的精确 Range;manifest 定义映射到源码文件(如 SageXml)时也在文件内精确定位;找不到再回退到记录行。
|
||||
@@ -122,7 +123,7 @@ test/
|
||||
4. [x] 纯 TS 核心:include 解析、manifest 解析、XML 解析封装、索引器、引用解析。
|
||||
5. [x] 功能层:补全、hover、导航、诊断、大纲、高亮 grammar。
|
||||
6. [x] 单测(fixture Mod,31 个用例全绿)+ 编译 + `vsce package` 打包(ra3-mod-xml-0.1.0.vsix,约 259KB)。
|
||||
7. [x] 在 AttachTest / GenEvoTest / Corona 上做冒烟验证,并按真实项目反馈修复问题(详见 `docs/analysis-issues.md` 三轮分析)。
|
||||
7. [x] 在 AttachTest / GenEvoTest / Corona 上做冒烟验证,并按真实项目反馈修复问题(详见 `docs/analysis-issues.md` 四轮分析)。
|
||||
|
||||
## 四、验证结果(实测)
|
||||
|
||||
@@ -132,7 +133,7 @@ test/
|
||||
| GenEvoTest | 24 文件 | ~1.8s | 35,392(manifest 35,322) | 项目 ID(alliedmcv 等)正确收录 |
|
||||
| Corona | 3,448 文件(+ 非 XML 资产路径) | ~54.5s | 55,305(manifest 35,322) | 3 个流、183 个 Define、0 诊断 |
|
||||
|
||||
单元测试覆盖:XML 解析(自闭合/容错/偏移)、include 解析(BAB 顺序、SDK 根优先于 SageXml)、manifest 二进制解析(合成 v5 样本、类型/ID 推导)、索引器(资产/Define/流/缺失 include/嵌套 xi:include)、XSD 模型(上下文类型、`childTypeOf`、大小写规范化)、引用过滤(`Weapon="X"` 只跳 `WeaponTemplate`、无类型引用、`Side="Allies"` 命中 manifest 的 `PlayerTemplate`)。
|
||||
单元测试覆盖:XML 解析(自闭合/容错/偏移)、include 解析(BAB 顺序、SDK 根优先于 SageXml)、manifest 二进制解析(合成 v5 样本、类型/ID 推导)、索引器(资产/Define/流/缺失 include/嵌套 xi:include)、XSD 模型(上下文类型、`childTypeOf`、大小写规范化、属性级 refType)、引用过滤(`Weapon="X"` 只跳 `WeaponTemplate`、模块 `id` 定义点、Poid 局部引用、`Side="Allies"` 命中 manifest 的 `PlayerTemplate`)。
|
||||
|
||||
> 注:`D:\Mods\CoronaMod` 位于移动硬盘,当前未连接;GenEvoTest / Corona 的回归需在 D: 盘可用时补跑(用户会另行通知)。
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { parseXml, type XmlElement } from "../language/xmlParser";
|
||||
import { analyzeContext, type CompletionContext } from "../language/context";
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import * as model from "../model/schemaModel";
|
||||
import { isLocalReferenceAttribute } from "../indexer/refs";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
import type { ModIndex, AssetDef } from "../indexer/types";
|
||||
|
||||
@@ -227,6 +228,12 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
return this.assetIdItems(idx, el.name, null, prefix, make);
|
||||
}
|
||||
|
||||
// `id` attributes are definitions and Poid attributes are pipeline-local
|
||||
// references; offering global asset ids for them would be wrong.
|
||||
if (attrInfo && isLocalReferenceAttribute(elType, attr.name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (attrInfo?.refType) {
|
||||
return this.assetIdItems(idx, null, attrInfo.refType, prefix, make);
|
||||
}
|
||||
|
||||
+39
-1
@@ -10,6 +10,37 @@ export interface ReferenceTarget {
|
||||
score: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when an attribute is a "pipeline-local" reference that the global
|
||||
* asset index cannot judge:
|
||||
*
|
||||
* - `id` attributes declare the element's own identity. When the attribute
|
||||
* has no refType (plain Poid / pipeline ids) or its refType is compatible
|
||||
* with the element's own type (e.g. ModuleData@id -> ModuleData), the
|
||||
* element itself is the definition site, so no global definition is
|
||||
* required. An `id` whose refType points at a *different* asset type
|
||||
* (e.g. RoadObject@id -> Road) is a real cross-asset reference and keeps
|
||||
* its reference semantics.
|
||||
* - Poid-typed attributes (ModuleId, AutoResolveBody, SoundRef, ...) are
|
||||
* "pipeline object id" references that are resolved within the same
|
||||
* asset/subtree (modules, sub-objects, pivots, shader materials...),
|
||||
* never against the global asset index.
|
||||
*/
|
||||
export function isLocalReferenceAttribute(
|
||||
typeName: string | null,
|
||||
attrName: string,
|
||||
): boolean {
|
||||
const attr = attributesOfType(typeName).find((a) => a.name === attrName);
|
||||
if (!attr) return false;
|
||||
const isId = attrName.toLowerCase() === "id";
|
||||
if (isId) {
|
||||
if (!attr.refType) return true;
|
||||
if (typeName && isAssignableTo(typeName, attr.refType)) return true;
|
||||
return false;
|
||||
}
|
||||
return attr.type === "Poid";
|
||||
}
|
||||
|
||||
/**
|
||||
* True when an attribute is a typed reference: either the instance
|
||||
* inheritance attribute `inheritFrom`, or an XSD attribute whose simple type
|
||||
@@ -27,7 +58,11 @@ export function isReferenceAttributeOfType(
|
||||
): boolean {
|
||||
if (attrName.toLowerCase() === "inheritfrom") return true;
|
||||
const attr = attributesOfType(typeName).find((a) => a.name === attrName);
|
||||
return attr != null && (attr.refType != null || attr.isRef);
|
||||
if (attr == null || !(attr.refType != null || attr.isRef)) return false;
|
||||
// Definitions (id) and pipeline-local references (Poid) are not references
|
||||
// to global assets, so they never need a definition in the global index.
|
||||
if (isLocalReferenceAttribute(typeName, attrName)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +108,9 @@ export function resolveReferenceTargetsForType(
|
||||
} else {
|
||||
const attr = attributesOfType(typeName).find((a) => a.name === attrName);
|
||||
if (!attr || (!attr.refType && !attr.isRef)) return [];
|
||||
// id definitions and Poid pipeline-local references are never resolved
|
||||
// against the global asset index.
|
||||
if (isLocalReferenceAttribute(typeName, attrName)) return [];
|
||||
refType = attr.refType;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+121
-5
@@ -4,7 +4,15 @@ import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { ModIndexer } from "../out/indexer/indexer.js";
|
||||
import { CachedDirectoryWalker } from "../out/indexer/fileScanner.js";
|
||||
import { isReferenceAttribute, resolveReferenceTargets } from "../out/indexer/refs.js";
|
||||
import {
|
||||
isLocalReferenceAttribute,
|
||||
isReferenceAttribute,
|
||||
isReferenceAttributeOfType,
|
||||
resolveReferenceTargets,
|
||||
resolveReferenceTargetsForType,
|
||||
} from "../out/indexer/refs.js";
|
||||
import { parseXml } from "../out/language/xmlParser.js";
|
||||
import { resolveElementType } from "../out/language/typeContext.js";
|
||||
import * as model from "../out/model/schemaModel.js";
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
@@ -63,10 +71,21 @@ test("isReferenceAttribute distinguishes references from enums/paths", () => {
|
||||
assert.equal(isReferenceAttribute("FireWeaponNugget", "WeaponName"), true);
|
||||
});
|
||||
|
||||
test("untyped references (isRef without refType) resolve to any declared id", () => {
|
||||
// LocomotorSet/@Locomotor has type AssetReference: xas:isRef="true" with
|
||||
// no refType. It must be treated as a reference and match the id regardless
|
||||
// of the target asset type.
|
||||
test("attribute-level xas:refType is preserved in the model", () => {
|
||||
// <xs:attribute name="Locomotor" type="AssetReference"
|
||||
// xas:refType="LocomotorTemplate" /> — the refType lives on
|
||||
// the attribute node, not on the AssetReference simple type.
|
||||
assert.equal(
|
||||
model.attributesOfElement("LocomotorSet").find((a) => a.name === "Locomotor")?.refType,
|
||||
"LocomotorTemplate",
|
||||
);
|
||||
assert.equal(
|
||||
model.attributesOfElement("ArmorSet").find((a) => a.name === "Armor")?.refType,
|
||||
"ArmorTemplate",
|
||||
);
|
||||
});
|
||||
|
||||
test("typed Locomotor references resolve only to LocomotorTemplate defs", () => {
|
||||
const idx = {
|
||||
assetsById: new Map([
|
||||
[
|
||||
@@ -79,6 +98,14 @@ test("untyped references (isRef without refType) resolve to any declared id", ()
|
||||
line: 5,
|
||||
origin: "project",
|
||||
},
|
||||
{
|
||||
// Same id under a different type must NOT satisfy the reference.
|
||||
type: "GameObject",
|
||||
id: "AlliedAntiVehicleVehicleTech1Locomotor",
|
||||
file: "GameObject.xml",
|
||||
line: 1,
|
||||
origin: "project",
|
||||
},
|
||||
],
|
||||
],
|
||||
]),
|
||||
@@ -100,3 +127,92 @@ test("untyped references (isRef without refType) resolve to any declared id", ()
|
||||
// A non-reference attribute still returns nothing.
|
||||
assert.equal(resolveReferenceTargets(idx, "LocomotorSet", "EditorName", "X").length, 0);
|
||||
});
|
||||
|
||||
test("module ids declare the element itself and are never global references", () => {
|
||||
// The exact reported scenario: <TruckDraw id="ModuleTag_Draw"/> inside a
|
||||
// GameObject. ModuleData@id carries xas:refType="ModuleData", which is the
|
||||
// element's own base type -> a definition site, not a reference.
|
||||
const xml = `
|
||||
<AssetDeclaration>
|
||||
<GameObject id="GuardianTank">
|
||||
<Draws>
|
||||
<TruckDraw id="ModuleTag_Draw" />
|
||||
</Draws>
|
||||
</GameObject>
|
||||
</AssetDeclaration>`;
|
||||
const doc = parseXml(xml);
|
||||
const truckDraw = doc.elements.find((e) => e.name === "TruckDraw");
|
||||
assert.ok(truckDraw);
|
||||
const elType = resolveElementType(truckDraw);
|
||||
assert.equal(elType, "W3DTruckDrawModuleData");
|
||||
assert.equal(isLocalReferenceAttribute(elType, "id"), true);
|
||||
assert.equal(isReferenceAttributeOfType(elType, "id"), false);
|
||||
const idx = { assetsById: new Map(), assets: new Map(), defines: new Map() };
|
||||
assert.equal(
|
||||
resolveReferenceTargetsForType(idx, elType, "id", "ModuleTag_Draw").length,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test("Poid attributes reference pipeline-local objects, not global assets", () => {
|
||||
// AttachModuleId / ModuleId / AutoResolveBody are Poid-typed references to
|
||||
// modules inside the same GameObject; the global index cannot judge them.
|
||||
assert.equal(isReferenceAttributeOfType("AttachNugget", "AttachModuleId"), false);
|
||||
assert.equal(isLocalReferenceAttribute("AttachNugget", "AttachModuleId"), true);
|
||||
const idx = { assetsById: new Map(), assets: new Map(), defines: new Map() };
|
||||
assert.equal(
|
||||
resolveReferenceTargetsForType(idx, "AttachNugget", "AttachModuleId", "ModuleTag_X").length,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test("cross-type id references (RoadObject@id -> Road) stay real references", () => {
|
||||
// RoadObject declares its id with xas:refType="Road" (an AssetReference,
|
||||
// NOT the element's own type), so it must keep reference semantics.
|
||||
assert.equal(
|
||||
model.attributesOfType("RoadObject").find((a) => a.name === "id")?.refType,
|
||||
"Road",
|
||||
);
|
||||
assert.equal(isReferenceAttributeOfType("RoadObject", "id"), true);
|
||||
const idx = {
|
||||
assetsById: new Map([
|
||||
[
|
||||
"road01",
|
||||
[
|
||||
{
|
||||
type: "Road",
|
||||
id: "Road01",
|
||||
file: "Roads.xml",
|
||||
line: 3,
|
||||
origin: "project",
|
||||
},
|
||||
],
|
||||
],
|
||||
]),
|
||||
assets: new Map(),
|
||||
projectDir: ".",
|
||||
sdkDir: ".",
|
||||
defines: new Map(),
|
||||
files: new Map(),
|
||||
streams: [],
|
||||
manifests: new Map(),
|
||||
sourceCandidates: [],
|
||||
diagnostics: [],
|
||||
stats: {},
|
||||
};
|
||||
const targets = resolveReferenceTargetsForType(idx, "RoadObject", "id", "Road01");
|
||||
assert.equal(targets.length, 1);
|
||||
assert.equal(targets[0].def.type, "Road");
|
||||
});
|
||||
|
||||
test("top-level asset ids with plain pipeline ids are not references", () => {
|
||||
// LocomotorSet@id / ArmorSet@id are Poid pipeline ids without a refType:
|
||||
// they identify the element and never need a global definition.
|
||||
assert.equal(isReferenceAttributeOfType("LocomotorSet", "id"), false);
|
||||
assert.equal(isReferenceAttributeOfType("ArmorTemplateSet", "id"), false);
|
||||
const idx = { assetsById: new Map(), assets: new Map(), defines: new Map() };
|
||||
assert.equal(
|
||||
resolveReferenceTargetsForType(idx, "LocomotorSet", "id", "AnyLocalId").length,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -17,6 +17,36 @@ test("GameObject has expected attributes", () => {
|
||||
assert.ok(attrs.some((a) => a.name === "inheritFrom"));
|
||||
});
|
||||
|
||||
test("attribute-level xas:refType is captured (module ids, map objects)", () => {
|
||||
// ModuleData@id is declared as <xs:attribute name="id" type="Poid"
|
||||
// xas:refType="ModuleData" />; the refType must reach every module subtype.
|
||||
const moduleId = model.attributesOfType("ModuleData").find((a) => a.name === "id");
|
||||
assert.equal(moduleId?.refType, "ModuleData");
|
||||
assert.equal(
|
||||
model.attributesOfType("W3DTruckDrawModuleData").find((a) => a.name === "id")?.refType,
|
||||
"ModuleData",
|
||||
);
|
||||
// MapObject@id declares itself; ThingTemplate references a GameObject.
|
||||
assert.equal(
|
||||
model.attributesOfType("MapObject").find((a) => a.name === "id")?.refType,
|
||||
"MapObject",
|
||||
);
|
||||
assert.equal(
|
||||
model.attributesOfType("MapObject").find((a) => a.name === "ThingTemplate")?.refType,
|
||||
"GameObject",
|
||||
);
|
||||
// RoadObject@id references a different global asset type (Road).
|
||||
assert.equal(
|
||||
model.attributesOfType("RoadObject").find((a) => a.name === "id")?.refType,
|
||||
"Road",
|
||||
);
|
||||
// Poid pipeline-local references keep their typed targets.
|
||||
assert.equal(
|
||||
model.attributesOfType("AttachNugget").find((a) => a.name === "AttachModuleId")?.refType,
|
||||
"ModuleData",
|
||||
);
|
||||
});
|
||||
|
||||
test("Include has reference/instance/all enum", () => {
|
||||
const attrs = model.attributesOfElement("Include");
|
||||
const type = attrs.find((a) => a.name === "type");
|
||||
|
||||
@@ -300,7 +300,13 @@ function collectAttributes(node, out = []) {
|
||||
doc: docOf(attr),
|
||||
kind: desc?.kind ?? "unknown",
|
||||
type: desc?.name ?? null,
|
||||
refType: normalizeTypeName(desc?.refType) ?? null,
|
||||
// xas:refType may be declared on the attribute itself (e.g.
|
||||
// <xs:attribute name="id" type="Poid" xas:refType="ModuleData" />) or
|
||||
// on its simple type. The attribute-level declaration wins.
|
||||
refType:
|
||||
normalizeTypeName(attr["@_refType"]) ??
|
||||
normalizeTypeName(desc?.refType) ??
|
||||
null,
|
||||
enumValues: desc?.enumValues ?? inlineEnum ?? [],
|
||||
allowsDefine: desc?.allowsDefine ?? false,
|
||||
isRef: desc?.isRef ?? false,
|
||||
|
||||
Reference in New Issue
Block a user