关于 fix-linter-blind-spots
面向 UI5 开发者的技能,用于检测并修复 UI5 linter 未发现的 JavaScript 全局命名空间模式,包括旧式 QUnit 断言和测试模拟。
主要功能
- 01扫描 JavaScript 文件
- 02检测应用命名空间全局引用
- 03将全局引用改为 AMD 导入
- 04更新 QUnit 断言
- 05重构基于命名空间的模拟
适用场景
- 01为严格 AMD 加载准备 UI5 代码
- 02修复模块中的应用命名空间引用
- 03将测试更新至 QUnit 2.x
- 04替换全局链式测试模拟
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ui5/ plugins-coding-agents --skill fix-linter-blind-spots安装后在 Claude Code 中输入/fix-linter-blind-spots确认已加载。
附带文件scripts/detect-blind-spots.jsscripts/detect-blind-spots.test.js
详细介绍
修复检查器的盲区
此技能用于修复会导致运行时故障、但不会被 UI5 检查器报告的模式。检查器的 no-globals 规则只会检查 JS 文件中的 sap.* 命名空间。JavaScript 中特定于应用的全局命名空间模式——赋值、跨模块引用、QUnit 1.x 断言,以及通过全局链进行 sinon 模拟——检查器都无法发现。
重要——范围说明: 此技能仅处理 JavaScript 文件中的应用命名空间全局变量。XML 文件中的应用命名空间全局变量(使用带点号的应用路径的事件处理程序、格式化器、工厂函数)会由检查器的 no-globals 规则报告,并在第 3 阶段通过 fix-xml-globals 处理,不由此技能处理。请勿在此处修复 XML 应用命名空间全局变量。
这些模式在旧版引导模型中有效(该模型通过 jQuery.sap.declare 构建全局命名空间链),但在严格的 AMD 模块加载模式下(Test Starter、现代 UI5)会失效。
范围
扫描所有 .js 文件:
- 应用源代码:
webapp/controller/、webapp/utils/、webapp/model/等。 - 测试文件:
webapp/test/unit/、webapp/test/integration/、webapp/test/opa/等。
前置条件
读取 manifest.json 获取命名空间:
<NAMESPACE>—— 使用斜杠(例如,com/example/app)<NAMESPACE-WITH-DOTS>—— 使用点号(例如,com.example.app)
检测——初始扫描
从 <skill-dir>/scripts/detect-blind-spots.js 运行捆绑的检测脚本:
node <skill-dir>/scripts/detect-blind-spots.js <project-root>
脚本使用字符级扫描器,区分裸代码中的命名空间出现位置(检测结果)与字符串字面量或注释中的出现位置(跳过)。这样可以避免误报,例如 sap.ui.controller("com.example.app.controller.Detail") 或 Controller.extend("com.example.app.controller.Main", {,这些是字符串参数,而非全局代码引用。
输出(JSON 输出到 stdout,进度输出到 stderr):
findings[]—— 按类型分类的可处理项目:global_assignment、global_read、global_return、global_mock、global_method_call、missing_assert_param、qunit_global_assertion、bare_global_assertionskipped[]—— 有意忽略的命名空间出现位置(字符串/注释中)summary—— 按类型统计的数量
每个命名空间检测结果都包含 moduleName(提取出的短名称,例如 "DataService")和 namespacePath(完整的点分隔路径,例如 "com.example.app.utils.DataService")。
要修复的模式
按以下顺序修复模式——模式 1 必须先于模式 2 完成(因为模式 2 依赖模式 1 修复后模块中的正确 return 语句)。模式 3–4 在每次修复模式 2 时应用。
模式 1:sap.ui.define 中的全局命名空间赋值——仅限 JS(情况 1b)
由 sap.ui.define 包装的 JS 文件向全局命名空间赋值并返回该全局引用。这是移除 jQuery.sap.declare 后遗留的代码,全局赋值未被清理。
范围: 仅限 JavaScript 文件。检查器不会报告 JS 中的应用命名空间全局变量。(XML 文件中的应用命名空间全局变量会由检查器报告,并通过第 3 阶段的 fix-xml-globals 修复。)
检测: 使用检测脚本中按 type: "global_assignment" 和 type: "global_return" 筛选后的 findings。
修改前:
sap.ui.define(["sap/base/Log"], function(Log) {
"use strict";
com.example.app.utils.DataService = {
fetchData: function() { Log.info("fetching"); },
processData: function(aItems) { return aItems.map(/*...*/); }
};
return com.example.app.utils.DataService;
});
修改后:
sap.ui.define(["sap/base/Log"], function(Log) {
"use strict";
var DataService = {
fetchData: function() { Log.info("fetching"); },
processData: function(aItems) { return aItems.map(/*...*/); }
};
return DataService;
});
规则:
- 从命名空间路径的最后一段提取短名称
- 将
<FULL.NAMESPACE.PATH>.ModuleName = {替换为var ModuleName = { - 将
return <FULL.NAMESPACE.PATH>.ModuleName;替换为return ModuleName; - 将文件内其他所有对完整命名空间的引用替换为局部变量
var关键字至关重要——省略它会创建隐式全局变量
模式 2:JS 中跨模块的全局命名空间引用(案例 1c 扩展)
JS 文件通过全局命名空间链引用另一个模块,而不是导入该模块。分为三个子情况:
检测: 使用检测脚本中的 findings,并按 type: "global_read"、type: "global_method_call" 或 type: "global_mock" 筛选。脚本的字符级扫描器会自动跳过字符串字面量和注释中的命名空间,不需要手动筛选。
子情况 A — 模块已通过其他名称导入:
// Before: dep loaded as oDependency but also accessed via global
sap.ui.define(["com/example/app/utils/Helper"], function(oDependency) {
var oHelper = com.example.app.utils.Helper; // redundant global read
oHelper.doSomething();
});
// After: delete the redundant line, use the function parameter
sap.ui.define(["com/example/app/utils/Helper"], function(oDependency) {
oDependency.doSomething();
});
如果参数名称与模块名称不匹配(如这里的 oDependency 和 Helper),还要应用模式 3 进行重命名。
子情况 B — 模块未导入:
// Before: no import, accessed via global
sap.ui.define(["sap/base/Log"], function(Log) {
var oHelper = com.example.app.utils.Helper;
oHelper.doSomething();
});
// After: add as dependency
sap.ui.define([
"com/example/app/utils/Helper",
"sap/base/Log"
], function(Helper, Log) {
Helper.doSomething();
});
重要 — 应用子情况 B 之前,请阅读目标模块的 return 语句,以确定正确的替换模式。并非所有模块都导出可用值——有些模块仅产生副作用(没有 AMD 导出)。对于仅产生副作用的模块,应先修复目标模块:添加 return 语句并移除全局命名空间赋值,然后再正常导入。完整的决策表和操作步骤请参见 fix-js-globals/SKILL.md § Case 1 → "Before replacing global access, read the target module's return statement"。
对于测试文件,使用 test-resources/<NAMESPACE>/ 前缀,而不是 <NAMESPACE>/test/。
子情况 C — 自引用(模块通过全局命名空间引用自身):
// Before: module object assigned to local var, then referenced via global
var DataService = { /*...*/ };
var svc = com.example.app.utils.DataService; // self-reference
svc.process();
// After: use the local variable directly
var DataService = { /*...*/ };
DataService.process();
检测脚本会自动跳过的内容 — 字符串字面量:
以下内容包含命名空间,但不是代码引用(会出现在脚本的 skipped[] 输出中):
- 片段名称:
"com.example.app.fragment.Dialog" - 类扩展名称:
Controller.extend("com.example.app.controller.Main", { - 日志消息:
Log.info("com.example.app.utils.Helper initialized") - XML 片段路径:
"com/example/app/fragment/Dialog" - UI5 API 字符串参数:
sap.ui.controller("com.example.app.controller.Detail")
只修复将命名空间用作值的模式(赋给变量、用于属性访问链,或作为函数参数传入且该参数需要的是对象而非字符串)。
模式 3:案例 1c 参数名称验证
将全局引用替换为局部变量名之前,请验证 sap.ui.define 函数参数名称与模块的短名称匹配。
问题场景:
sap.ui.define(["com/example/app/utils/DataService"], function(Formatter) {
// Parameter named "Formatter" but module is "DataService"
var oSvc = com.example.app.utils.DataService; // global read
});
直接将全局引用替换为 DataService 会失败——参数名称是 Formatter,不是 DataService。
修复步骤:
- 解析
sap.ui.define依赖数组和函数参数列表 - 找出与所引用模块对应的参数
- 如果参数名称与模块名称不同:
- 将函数参数重命名为与模块短名称一致
- 在文件中将所有引用从旧参数名称更新为新参数名称
- 将全局引用替换为(现在正确的)参数名称
- 删除因此产生的自赋值行(
var X = X;)
模式 4:案例 1c 原子性——重命名时必须同时导入
每次将全局引用重命名为局部引用时,都必须同时添加导入。重命名后的变量若没有声明,会导致 ReferenceError——比保留全局引用更糟。
规则:
- 替换全局引用时,始终添加
sap.ui.define依赖。此阶段不要尝试检测循环依赖——在此处引入的循环依赖会在所有修复阶段完成后,于第 3 阶段第 3.3 步由fix-cyclic-deps检测并解决。 - 修复文件后,验证:搜索每个新引入的变量名,并确认其对应函数参数、
var/let/const声明或sap.ui.require调用 - 无需导入的常见全局对象:
window、document、console、sap、jQuery、QUnit、sinon
模式 5:QUnit.test 回调中缺少 assert 参数
QUnit 2.x(由 Test Starter 强制要求)需要将 assert 作为函数参数。QUnit 1.x 则将其设为全局变量。
检测: 使用检测脚本中的 findings,并按 type: "missing_assert_param" 进行筛选。
修改前:
QUnit.test("formats value correctly", function() {
assert.equal(formatter.format(1), "One");
assert.ok(formatter.isValid("test"));
});
修改后:
QUnit.test("formats value correctly", function(assert) {
assert.equal(formatter.format(1), "One");
assert.ok(formatter.isValid("test"));
});
模式 6:QUnit 全局断言 → assert / Opa5.assert
QUnit 1.x 允许直接调用 QUnit 对象上的断言方法(QUnit.ok(...)、QUnit.equal(...))。QUnit 2.x 移除了这些方法——断言应通过 assert 参数(单元测试)或 Opa5.assert(OPA 测试)调用。
检测: 使用检测脚本中的 findings,并按 type: "qunit_global_assertion" 和 type: "bare_global_assertion" 进行筛选。每个 finding 都包含 assertionName 和 replacement(根据文件位置,值为 assert.<name> 或 Opa5.assert.<name>)。
上下文检测:
- 文件位于
test/opa/或test/integration/下,且在waitFor成功/错误回调中 →Opa5.assert.<assertion> - 文件位于
test/unit/下,或.qunit.js文件位于QUnit.test回调中 →assert.<assertion>(使用函数参数)
OPA 示例:
// Before:
success: function(aItems) {
QUnit.ok(aItems.length > 0, "Items found");
ok(true, "Test passed"); // bare global
}
// After:
success: function(aItems) {
Opa5.assert.ok(aItems.length > 0, "Items found");
Opa5.assert.ok(true, "Test passed");
}
使用 Opa5.assert 前,请确认文件导入了 sap/ui/test/Opa5。如果没有,请将其添加为依赖项。
单元测试示例:
// Before:
QUnit.test("test name", function(assert) {
QUnit.ok(result);
QUnit.equal(a, b);
});
// After:
QUnit.test("test name", function(assert) {
assert.ok(result);
assert.equal(a, b);
});
需要替换的完整断言列表:
ok、equal、notEqual、deepEqual、notDeepEqual、strictEqual、notStrictEqual、propEqual、notPropEqual、throws、expect、push
模式 7:全局命名空间模拟 → sinon.stub
通过覆盖全局命名空间链上的属性来模拟依赖项的测试,在该链不存在时会失败。
检测: 使用检测脚本中的 findings,并按 type: "global_mock" 进行筛选。这些检测结果涵盖 jQuery.extend 备份模式和涉及应用命名空间的函数覆盖模式。备份 sap.* 子命名空间(例如 sap.ushell.Container)的模拟模式不在此范围内——这些是框架全局变量,而非应用命名空间全局变量。
查找备份-覆盖-恢复模式:
var oBackup = jQuery.extend(true, {}, com.example.app.utils);
com.example.app.utils.Module.method = function() { /*mock*/ };
// ... test ...
com.example.app.utils = oBackup;
修改前(测试):
sap.ui.define(["com/example/app/model/formatter"], function(Formatter) {
QUnit.test("show button", function(assert) {
var oBackup = jQuery.extend(true, {}, com.example.app.utils);
com.example.app.utils.Actions.getInvalidItems = function() { return []; };
// ... test using Formatter which calls Actions.getInvalidItems ...
com.example.app.utils = oBackup;
});
});
修改后(测试):
sap.ui.define([
"com/example/app/model/formatter",
"com/example/app/utils/Actions"
], function(Formatter, Actions) {
QUnit.test("show button", function(assert) {
var oStub = sinon.stub(Actions, "getInvalidItems").returns([]);
// ... test ...
oStub.restore();
});
});
Test Starter 基础设施已全局提供 sinon,无需将其作为 sap.ui.define 依赖项导入。
还需进行配套修复: 如果应用源模块(例如 Formatter.js)也通过全局命名空间访问被模拟的模块,请将该模块修复为 sap.ui.define 依赖项。否则,sinon 存根无法拦截调用——应用代码访问的将是另一个对象。这些情况已经会出现在检测脚本的 findings[] 中,类型为 global_read 或 global_method_call——请按照模式 2 一并修复。
模式 8:旧版 "jquery.sap.global" 依赖路径
依赖项 "jquery.sap.global" 已弃用,必须替换为 "sap/ui/thirdparty/jquery"。
检测: 使用检测脚本中的 findings,并按 type: "legacy_jquery_dep" 进行筛选。每个 finding 都包含一个 replacement 字段,其值为正确的依赖路径("sap/ui/thirdparty/jquery")。
修改前:
sap.ui.define([
"sap/ui/core/mvc/Controller",
"jquery.sap.global"
], function(Controller, jQuery) {
"use strict";
// ...
});
修改后:
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/thirdparty/jquery"
], function(Controller, jQuery) {
"use strict";
// ...
});
规则:
- 在依赖项数组中,将
"jquery.sap.global"替换为"sap/ui/thirdparty/jquery" - 保持对应的函数参数名称不变(通常为
jQuery) - 此规则适用于
sap.ui.define和sap.ui.require的依赖项数组
实施步骤
- 读取
manifest.json获取命名空间。计算<NAMESPACE>、<NAMESPACE-WITH-DOTS>、<NS-ESCAPED> - 运行检测脚本:
node <skill-dir>/scripts/detect-blind-spots.js <project-root> - 应用模式 1(全局赋值)——筛选
type: "global_assignment"和type: "global_return"的发现项。必须先完成此步骤,再应用模式 2,因为模式 2 依赖模块返回局部变量 - 应用模式 2(跨模块全局引用)——筛选
type: "global_read"和type: "global_method_call"的发现项。这会解决大多数命名空间引用 - 在修复每个模式 2 问题时,应用模式 3(参数名称验证)和模式 4(原子性验证)
- 应用模式 5–6(QUnit 断言)——筛选
type: "missing_assert_param"、type: "qunit_global_assertion"、type: "bare_global_assertion"的发现项。使用每个发现项中的replacement字段 - 应用模式 7(Sinon 模拟)——筛选
type: "global_mock"的发现项 - 应用模式 8(旧版 jQuery 依赖)——筛选
type: "legacy_jquery_dep"的发现项。将每处出现都替换为replacement字段中的值 - 重新运行检测脚本,并确认
findings[]为空(或仅包含无法修复的项目,例如需要人工审查的模式 7 项目) - 运行
npx @ui5/linter --details进行二次检查——不应引入新的错误 - 对于无法自动修复的发现项(复杂的模拟模式、含糊不清的模块边界),在 MODERNIZATION-ISSUES.md 中记录文件路径、行号和原因
完成门槛
在报告此技能已完成之前,请确认以下所有事项:
- 检测脚本未发现任何问题: 重新运行
node <skill-dir>/scripts/detect-blind-spots.js <project-root>,并确认summary.total === 0(或所有剩余项目都已在 MODERNIZATION-ISSUES.md 中说明并附上理由) - 没有新增的 Linter 错误: 运行
npx @ui5/linter --details,并确认没有引入新的错误
相关技能
- fix-js-globals — 应用相同的转换(模式 1–4),但仅处理 Linter 明确报告为
no-globals错误的情况(即sap.*全局变量)。此技能处理 Linter 无法检测的应用命名空间全局变量。两种技能执行相同类型的修复;区别在于问题的发现方式。 - fix-cyclic-deps — 在此技能之后运行(阶段 3,步骤 3.3),用于检测并解决模式 2 中添加的
sap.ui.define依赖边所引入的循环依赖 - modernize-test-starter — 测试基础设施现代化,通常会揭示这些模式