2 Commits
Author SHA1 Message Date
lanyi 429ea367f3 xi include part 1 2026-08-01 14:30:14 +02:00
lanyi 656fab349d 修 id 解析 2026-08-01 14:15:13 +02:00
11 changed files with 636 additions and 47 deletions
+214
View File
@@ -240,3 +240,217 @@ 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 跨文件无法静态确定,本轮先统一不检查;待局部
> 作用域建模落地后再启用这些引用的解析与诊断。
---
## 十、问题分析(第五轮,2026-08-01):`xi:include` 的 `href`/`xpointer` 误报未知属性
### 问题
AttachTest `Allied Vehicle\Guardian Tank\GameObject.xml` 第 249 行附近:
```xml
<xi:include
href="DATA:Includes/HeadlightDraw2.xml"
xpointer="xmlns(n=uri:ea.com:eala:asset) xpointer(/n:HeadlightDraw2/child::*)"/>
```
报两条 `Unknown attribute "href" / "xpointer" for <include>`(`unknown-attribute`),
hover 同时显示 `Unknown attribute for this element.`。
这个元素属于 **W3C XInclude 命名空间**(`xmlns:xi="http://www.w3.org/2001/XInclude"`),
并不是 EA `uri:ea.com:eala:asset` XSD 的一部分。同一行在第二轮“问题 C”处理过
(嵌套 `xi:include` 的索引与导航),但那轮没有覆盖 unknown-attribute 诊断,属于遗留缺口。
### 根因
诊断的属性校验没有像元素校验那样排除外来命名空间:
- 元素校验已有 `!el.name.startsWith("xi:")` 守卫(所以 `<include>` 本身不报 unknown element);
- 属性校验只跳过 `xmlns*` / `xai:` / `xi:` 前缀的属性名,而 `href`、`xpointer` 是不带
前缀的普通属性名;
- `<xi:include>` 解析类型为 null(XSD 模型不含该元素),knownAttrs 为空 → 任何属性
都被判为 unknown。
### 修复
1. `schemaModel` 新增两个纯函数:
- `isXsdElementName`:`xi:` 前缀元素不属于 EA XSD 模型;
- `isXsdAttributeName`:EA XSD 属性不带命名空间前缀,带前缀(`xai:`、`xi:`、
`xlink:`、`xml:`、`xsi:`、`xmlns:*`)的都是命名空间机制,不做 schema 校验。
2. `diagnostics`:`xi:` 前缀元素整体跳过 schema 校验(元素与属性都不再误报);
前缀属性名统一跳过。
3. `hover`:`xi:include` 元素/属性给出 XInclude 说明;`href` 值悬停像
`<Include source>` 一样解析目标文件(Ctrl+点击跳转此前已可用)。
### 验证
- 真实文件全量扫描:0 未知元素、0 未知属性(修复前 `href`/`xpointer` 两条必现);
- 新增测试:`isXsdElementName` / `isXsdAttributeName` 断言;`xi:include` 解析类型为
null 且不参与校验;全量 39/39 通过。
### 后续(架构方向,待确认)
用户提出“先展开 `xi:include`(类比 C++ 宏展开),再处理 mod XML 解析”。该方向与第二轮
遗留的“虚拟合并”开放项一致,设计要点:
- 构建**逻辑树**而非文本拼接:把目标文件选中内容(`xpointer` 子集)作为子节点拼入父
元素,节点保留源文件与原始偏移,避免文本级拼接导致的偏移断裂;
- 展开范围:`xi:include` 与 EA `<Include type="all">`(内容合并);`instance` /
`reference` 是可见性 / 编译产物语义,不拼树;`inheritFrom` + `joinAction` 是属性级
继承合并,不是宏展开;
- 收益:跨 include 的上下文类型解析、包含内容的结构校验、以及后续“GameObject 内模块
id 局部作用域”(HeadlightDraw2 的模块也是该 GameObject 的模块);
- 风险:include 环 / 深度限制、大文件性能、`xpointer` 仅支持现有子集形式
(`/n:Name/child::*`)。
---
## 十、问题分析(第五轮,2026-08-01):`xi:include` 的 `href` / `xpointer` 被误报为未知属性
### 问题
同一 GameObject.xml 第 246–249 行:
```xml
<!-- include Headlight draw module. -->
<xi:include
href="DATA:Includes/HeadlightDraw2.xml"
xpointer="xmlns(n=uri:ea.com:eala:asset) xpointer(/n:HeadlightDraw2/child::*)"/>
```
报两条 `Unknown attribute "href" / "xpointer" for <include>`(unknown-attribute),
hover 显示 `Unknown attribute for this element.`。
**与第二轮的关系**:第二轮“问题 C”处理的正是同一行的嵌套 `xi:include`——但那一轮修的是
**索引器**(嵌套 include 不再被静默忽略、缺失目标产生 include-not-found、目标内容进索引),
本轮这处 **unknown-attribute 诊断**是当时未覆盖的遗留问题。
### 根因
`xi:include` 属于 W3C XInclude 命名空间(`http://www.w3.org/2001/XInclude`),
**不是 RA3 XSD(`uri:ea.com:eala:asset`)定义的元素**:
- 未知元素检查已通过 `el.name.startsWith("xi:")` 跳过,所以没有 unknown-element 误报;
- 但属性检查没有同类守卫:`xi:include` 解析类型为 null → `knownAttrs` 为空 →
`href`、`xpointer` 两个非 `xi:` 前缀的属性名全部落入 unknown-attribute 分支。
复现证据(真实文件):
```
xi:include found: true | parent: Draws
resolved element type: null
known attribute names: (none)
attr href: known=false -> would flag unknown-attribute: true
```
### 修复
1. **模型层新增命名空间守卫**(`schemaModel.ts`):
- `isXsdElementName(name)`:`xi:` 前缀(XInclude)等外来命名空间元素不属于 XSD 模型;
- `isXsdAttributeName(name)`:EA XSD 属性一律无前缀,带前缀的属性
(`xai:`、`xi:`、`xlink:`、`xml:`、`xsi:`、`xmlns:*`)都是命名空间机制,不做未知属性校验。
2. **诊断**(`diagnostics.ts`):外来命名空间元素的未知元素/未知属性检查整体跳过
(`href`、`xpointer` 不再误报);属性名带前缀的一律跳过校验(比原先只跳过
`xmlns`/`xai:`/`xi:` 更完整)。
3. **hover**(`hover.ts`):`xi:include` 的元素/属性悬停显示 XInclude 说明;
`xi:include@href` 与 `Include@source` 一样显示解析后的目标文件(与第二轮已可用的
Ctrl+点击跳转对齐)。
**测试(37 → 39,全部通过)**:
- `schemaModel.test.mjs`:`isXsdElementName` / `isXsdAttributeName` 判定
(`xi:include` 非 XSD 元素;`href`/`xpointer` 是合法属性名形态;
`xai:joinAction`、`xlink:href`、`xmlns:xi` 等带前缀属性不校验);
- `refs.test.mjs`:GameObject → Draws → `xi:include` 实景结构解析类型为 null、
元素被判定为外来命名空间,`href`/`xpointer` 不会进入未知属性分支;
- 实机复验:整份 GameObject.xml 0 个 unknown-attribute 残留。
> **架构讨论(用户提议)**:把 XML 先“宏展开”成不含 `xi:include` 的版本再解析。
> 这与 BAB 编译时的实际行为一致(`defaultscript.cs` 把整个 Mod 合并成一份大 XML),
> 也是实现“GameObject 内模块 id 局部作用域解析”(第四轮遗留)的正确地基——展开后一个
> GameObject 连同 include 进来的兄弟模块都在同一棵树里,`AttachModuleId` 等模块引用才能
> 静态判定。设计备忘(现状 / 逻辑树方案 / 展开范围 / 落地点 / 检查清单)已整理在
> `docs/plan.md` 第六节,等待确认后作为下一阶段实现。
+89 -6
View File
@@ -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 局部引用、`xi:include` 不校验、`Side="Allies"` 命中 manifest 的 `PlayerTemplate`)。
> 注:`D:\Mods\CoronaMod` 位于移动硬盘,当前未连接;GenEvoTest / Corona 的回归需在 D: 盘可用时补跑(用户会另行通知)。
@@ -141,4 +142,86 @@ test/
- 假设 SDK 路径默认 `C:\Apps\RA3-MODSDK-X`(与 prompts 一致),可在设置中修改。
- 假设补全/导航以“文本语义分析”为主,不做完整 XSD 校验(BAB 才是权威校验器)。
- 开放:是否发布到 VS Code Marketplace(需要 publisher)——本期先保证本地 `vsce package` 可安装。
- 开放:嵌套 `xi:include` 内容的"虚拟合并"进父文档(用于父文档内的补全/诊断感知被内联内容)——当前仅保证目标文件可索引、可导航、缺失可诊断。
- 开放:**“宏展开”式虚拟合并**(用户提议,方向已确认):解析前先把 `xi:include`
(以及顶层 `<Include type="all">`、`inheritFrom` 继承合并)展开成不含 include 的
文档树,再对展开后的树做 XSD 校验、补全与诊断——与 BAB 编译时把整个 Mod 合并成
一份大 XML 的行为一致。展开树需携带**来源追溯**(错误/跳转仍定位到原始文件),
并处理 xpointer 子集解析与 include 循环。当前仅做到:目标文件可索引、可导航、
缺失可诊断;`xi:include` 本身不参与 XSD 校验(第五轮)。
该功能也是“GameObject 内模块 id 局部作用域解析”(第四轮遗留)的地基。
详细设计备忘见下一节。
## 六、include 展开设计备忘(2026-08-01,待实施)
> 目的:集中记录 include 处理相关的现状、结论与设计,下次遇到 include 问题时从这里继续,
> 并在实施后把结果回写本节。
### 1. 现状(截至第五轮,已实现)
| 能力 | 状态 |
|---|---|
| `<Include type="all">` / `instance` 递归索引(顶层资产、流、Define) | 已实现(indexer `walk`) |
| `reference` → builtmods manifest 解析 / 缺失回退占位 XML | 已实现 |
| 嵌套 `xi:include`(任意层级):目标可索引、缺失报 `include-not-found`、Ctrl+点击跳转、`href` hover 解析目标 | 已实现(第二轮 + 第五轮) |
| `xi:include` 及其属性不参与 XSD 校验(外来命名空间守卫 `isXsdElementName` / `isXsdAttributeName`) | 已实现(第五轮) |
| include 目标内容“虚拟合并”进父文档的逻辑树 | **未实现**(本文档主题) |
### 2. 已确认的方向
先展开成不含 include 的文档树(类比 C++ 宏展开),再对展开后的树做 mod XML 解析。
BAB(`defaultscript.cs`)编译时正是这样把整个 Mod 合并成一份大 XML 的。
### 3. 关键设计决策:逻辑树拼接,不做文本拼接
- **不要**把 include 目标展开成文本再整体重新解析:源码偏移会断裂,诊断 / 跳转 / hover /
补全全部无法映射回原始文件。
- **要做**的是:解析器逐文件解析(现状不变);展开器把目标文件选中节点按 `xpointer`
子集挂进父元素,节点保留各自的源文件与原始偏移(来源追溯)。后续分析跑在逻辑树上,
范围映射按节点 `sourceFile` 回到对应文件的 lineMap。
- 现有 `parseXml` 已记录标签 / 属性 / 值的起止偏移,`XmlElement` 结构可直接复用;拼接时
用浅拷贝节点壳并重建 parent 链,避免破坏目标文件缓存树自身的 parent 指针。
### 4. 展开范围
| 构造 | 拼入逻辑树 | 理由 |
|---|---|---|
| `xi:include` | ✅ | 内容并入父元素(HeadlightDraw2 场景) |
| EA `<Include type="all">` | ✅ | BAB“内容合并”,等价于复制进来 |
| `type="instance"` | ❌ | 只影响编译可见性;拼树会把 BaseVehicle 的顶层资产错误塞进当前文档 |
| `type="reference"` | ❌ | manifest 编译产物,无文本内容 |
| `inheritFrom` + `xai:joinAction` | 单独一轮 | 元素级继承深合并(Replace/Remove),不是宏展开 |
### 5. 落地位置与接入点
- 新纯模块(与编辑器解耦,呼应 P1):输入 `(parse 树, resolveSource 回调, readDocument 回调)`,
输出逻辑树(root + elements 扁平列表,沿用 diagnostics 的遍历形态)。
- 接入:diagnostics / hover / navigation / completion 目前各自 `parseXml(text)`;
改为 parse 后过 expander 取逻辑树;范围映射按节点 `sourceFile` 选对应文件的 lineMap。
- 按需展开(当前打开文档)+ 按文件缓存(复用 indexer `readDocument` 的 LRU);
环 / 深度守卫复用现有 `visitedAll` 与深度限制思路(建议最大深度 64)。
- `xpointer`:仅支持 mod 实际使用的 `xmlns(n=...) xpointer(/n:Name/child::*)` 子集
(现有 `findXPointerContainer` 正则已覆盖);完整 XPath 暂不支持,遇到新形式先记录到本节。
### 6. 后续收益(承接第四 / 五轮遗留)
- **GameObject 内模块 id 局部作用域**:展开后 include 进来的兄弟模块(HeadlightDraw2)
与本体模块同树,`AttachModuleId` / `ModuleId` / `AutoResolveBody` 等 Poid 引用
才能静态解析与诊断(第四轮遗留);
- 跨 include 的上下文类型解析与结构校验;
- 顶层 `type="all"` 合并后,当前文档视角的重复 id / 引用诊断更接近 BAB 结果。
### 7. 风险与边界
- 大文件性能(Corona 约 7500 文件 / 38MB):只展开当前文档的可达链,不全局展开;
- include 环 / 深度:visited 集合 + 最大深度;
- 被包含内容的诊断上报位置:建议按节点源文件 URI 上报(与偏移一致),包含点的 `href`
上只报“缺失 / 无法解析 / 环”类问题;
- `inheritFrom` 深合并(`xai:joinAction` 的 Replace/Remove 语义)单独设计,别与宏展开混在一轮。
### 8. 下次遇到 include 问题的检查清单
1. 现象发生在哪一层:索引(indexer)、诊断(diagnostics)、导航 / hover、还是补全?
2. 现状能力是否已覆盖(见第 1 节表格);
3. 涉及内容是否跨文件(需要逻辑树)还是本文件内(当前解析树即可);
4. 若要展开:先实现第 5 节的纯模块与单测(fixture 增加 HeadlightDraw2 场景),再接入 feature;
5. 把新结论回写本节与 `docs/analysis-issues.md`。
+7
View File
@@ -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);
}
+38 -30
View File
@@ -115,8 +115,12 @@ export class Ra3Diagnostics {
}
}
// Elements outside the EA asset namespace (e.g. XInclude <xi:include>)
// are not part of the RA3 XSD model; skip their validation entirely.
const isXsdElement = model.isXsdElementName(el.name);
// Unknown element.
if (settings.diagnoseUnknownElements && !el.name.startsWith("xi:")) {
if (settings.diagnoseUnknownElements && isXsdElement) {
const knownType = model.elementTypeName(local);
if (!knownType) {
diags.push(
@@ -131,38 +135,42 @@ export class Ra3Diagnostics {
}
// Attributes.
const elType = resolveElementType(el);
const knownAttrs = model.attributesOfType(elType);
const knownNames = new Set(knownAttrs.map((a) => a.name));
for (const attr of el.attrs) {
const aName = attr.name;
if (aName.startsWith("xmlns") || aName.startsWith("xai:") || aName.startsWith("xi:")) {
continue;
}
if (settings.diagnoseUnknownElements && !knownNames.has(aName)) {
diags.push(
this.diag(
new vscode.Range(
document.positionAt(attr.nameStart),
document.positionAt(attr.nameEnd),
if (isXsdElement) {
const elType = resolveElementType(el);
const knownAttrs = model.attributesOfType(elType);
const knownNames = new Set(knownAttrs.map((a) => a.name));
for (const attr of el.attrs) {
const aName = attr.name;
// Namespace declarations and prefixed attributes (xai:, xi:,
// xlink:, xml:, xsi:, xmlns:*) are not defined by the EA XSD.
if (aName.startsWith("xmlns") || !model.isXsdAttributeName(aName)) {
continue;
}
if (settings.diagnoseUnknownElements && !knownNames.has(aName)) {
diags.push(
this.diag(
new vscode.Range(
document.positionAt(attr.nameStart),
document.positionAt(attr.nameEnd),
),
`Unknown attribute "${aName}" for <${local}>`,
vscode.DiagnosticSeverity.Warning,
"unknown-attribute",
),
`Unknown attribute "${aName}" for <${local}>`,
vscode.DiagnosticSeverity.Warning,
"unknown-attribute",
),
);
}
if (!attr.hasValue) continue;
this.checkValueReferences(
elType,
attr.name,
attr.value,
attr,
document,
idx,
diags,
);
}
if (!attr.hasValue) continue;
this.checkValueReferences(
elType,
attr.name,
attr.value,
attr,
document,
idx,
diags,
);
}
// Include-specific checks.
+24 -3
View File
@@ -29,7 +29,7 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
// Attribute name.
for (const attr of el.attrs) {
if (offset >= attr.nameStart && offset <= attr.nameEnd) {
return this.attributeHover(elType, attr.name);
return this.attributeHover(el, elType, attr.name);
}
}
// Attribute value.
@@ -47,6 +47,14 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
}
private elementHover(name: string): vscode.Hover | null {
if (name.startsWith("xi:")) {
const md = new vscode.MarkdownString();
md.appendCodeblock(`<${name}>`, "xml");
md.appendMarkdown(
"XInclude element (W3C XInclude namespace) — not part of the RA3 XSD model.",
);
return new vscode.Hover(md);
}
const type = model.elementTypeName(name);
const info = type ? model.typeInfo(type) : undefined;
const md = new vscode.MarkdownString();
@@ -68,7 +76,11 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
return new vscode.Hover(md);
}
private attributeHover(elementType: string | null, attrName: string): vscode.Hover | null {
private attributeHover(
el: { name: string },
elementType: string | null,
attrName: string,
): vscode.Hover | null {
const attrs = model.attributesOfType(elementType);
const attr = attrs.find((a) => a.name === attrName);
const md = new vscode.MarkdownString();
@@ -78,6 +90,12 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
md.appendMarkdown(`Namespace/instance attribute.`);
return new vscode.Hover(md);
}
if (!model.isXsdElementName(el.name)) {
md.appendMarkdown(
`XInclude attribute (W3C XInclude namespace) — not part of the RA3 XSD model.`,
);
return new vscode.Hover(md);
}
md.appendMarkdown("Unknown attribute for this element.");
return new vscode.Hover(md);
}
@@ -117,7 +135,10 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
}
// Include source.
if (el.name === "Include" && attrName === "source") {
if (
(el.name === "Include" && attrName === "source") ||
(el.name === "xi:include" && attrName === "href")
) {
const resolved = idx
? resolveSource(
value,
+39 -1
View File
@@ -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
+23
View File
@@ -212,3 +212,26 @@ export const STRUCTURAL_ELEMENTS = [
"Defines",
"Define",
];
/**
* Element names that live outside the RA3 XSD model's namespace. The model
* only covers `uri:ea.com:eala:asset`; other namespaces (so far the W3C
* XInclude `xi:`) must not be validated against it.
*/
const FOREIGN_ELEMENT_PREFIXES = ["xi:"];
/** True when an element name belongs to the RA3 XSD model's namespace. */
export function isXsdElementName(name: string): boolean {
const lower = name.toLowerCase();
return !FOREIGN_ELEMENT_PREFIXES.some((p) => lower.startsWith(p));
}
/**
* True when an attribute name can be validated against the RA3 XSD model.
* EA schema attributes are unprefixed; prefixed attributes (`xai:`,
* `xi:`, `xlink:`, `xml:`, `xsi:`, `xmlns:*`) are namespace machinery and
* are not defined by the XSD.
*/
export function isXsdAttributeName(name: string): boolean {
return !name.includes(":");
}
+147 -5
View File
@@ -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,118 @@ 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,
);
});
test("xi:include elements are outside the XSD model and unvalidated", () => {
// The nested XInclude inside a GameObject draw list (HeadlightDraw2.xml)
// must resolve to no XSD type, so href/xpointer can never be flagged as
// unknown attributes.
const xml = `
<AssetDeclaration>
<GameObject id="GuardianTank">
<Draws>
<xi:include
href="DATA:Includes/HeadlightDraw2.xml"
xpointer="xmlns(n=uri:ea.com:eala:asset) xpointer(/n:HeadlightDraw2/child::*)" />
</Draws>
</GameObject>
</AssetDeclaration>`;
const doc = parseXml(xml);
const inc = doc.elements.find((e) => e.name === "xi:include");
assert.ok(inc);
assert.equal(model.isXsdElementName(inc.name), false);
assert.equal(resolveElementType(inc), null);
// The XInclude attributes themselves are unprefixed (model-valid name
// shape); the element-level foreign check is what keeps them quiet.
for (const a of inc.attrs) {
assert.equal(model.isXsdAttributeName(a.name), true, a.name);
}
});
+47
View File
@@ -17,12 +17,59 @@ 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");
assert.deepEqual(type?.enumValues, ["reference", "instance", "all"]);
});
test("foreign namespaces are excluded from XSD validation", () => {
// XInclude elements (xi:include / xi:fallback) come from the W3C XInclude
// namespace, not from the EA asset XSD.
assert.equal(model.isXsdElementName("xi:include"), false);
assert.equal(model.isXsdElementName("xi:fallback"), false);
assert.equal(model.isXsdElementName("GameObject"), true);
assert.equal(model.isXsdElementName("AssetDeclaration"), true);
// EA schema attributes are unprefixed; prefixed attributes (xai:, xi:,
// xlink:, xml:, xsi:, xmlns:*) are namespace machinery.
assert.equal(model.isXsdAttributeName("href"), true);
assert.equal(model.isXsdAttributeName("xpointer"), true);
assert.equal(model.isXsdAttributeName("xai:joinAction"), false);
assert.equal(model.isXsdAttributeName("xlink:href"), false);
assert.equal(model.isXsdAttributeName("xml:space"), false);
assert.equal(model.isXsdAttributeName("xmlns:xi"), false);
});
test("type assignability follows inheritance", () => {
assert.ok(model.isAssignableTo("GameObject", "BaseInheritableAsset"));
assert.ok(model.isAssignableTo("GameObject", "GameObject"));
+7 -1
View File
@@ -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,