mcpField
F
fix-linter-blind-spots

fix-linter-blind-spots

github.com/ui5开发44143 次安装1 周前更新
代码仓库

关于 fix-linter-blind-spots

面向 UI5 开发者的技能,用于检测并修复 UI5 linter 未发现的 JavaScript 全局命名空间模式,包括旧式 QUnit 断言和测试模拟。

主要功能

  1. 01扫描 JavaScript 文件
  2. 02检测应用命名空间全局引用
  3. 03将全局引用改为 AMD 导入
  4. 04更新 QUnit 断言
  5. 05重构基于命名空间的模拟

适用场景

  1. 01为严格 AMD 加载准备 UI5 代码
  2. 02修复模块中的应用命名空间引用
  3. 03将测试更新至 QUnit 2.x
  4. 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_assertion
  • skipped[] —— 有意忽略的命名空间出现位置(字符串/注释中)
  • 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。

修复步骤:

  1. 解析 sap.ui.define 依赖数组和函数参数列表
  2. 找出与所引用模块对应的参数
  3. 如果参数名称与模块名称不同:
    • 将函数参数重命名为与模块短名称一致
    • 在文件中将所有引用从旧参数名称更新为新参数名称
  4. 将全局引用替换为(现在正确的)参数名称
  5. 删除因此产生的自赋值行(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 的依赖项数组

实施步骤

  1. 读取 manifest.json 获取命名空间。计算 <NAMESPACE>、<NAMESPACE-WITH-DOTS>、<NS-ESCAPED>
  2. 运行检测脚本:node <skill-dir>/scripts/detect-blind-spots.js <project-root>
  3. 应用模式 1(全局赋值)——筛选 type: "global_assignment" 和 type: "global_return" 的发现项。必须先完成此步骤,再应用模式 2,因为模式 2 依赖模块返回局部变量
  4. 应用模式 2(跨模块全局引用)——筛选 type: "global_read" 和 type: "global_method_call" 的发现项。这会解决大多数命名空间引用
  5. 在修复每个模式 2 问题时,应用模式 3(参数名称验证)和模式 4(原子性验证)
  6. 应用模式 5–6(QUnit 断言)——筛选 type: "missing_assert_param"、type: "qunit_global_assertion"、type: "bare_global_assertion" 的发现项。使用每个发现项中的 replacement 字段
  7. 应用模式 7(Sinon 模拟)——筛选 type: "global_mock" 的发现项
  8. 应用模式 8(旧版 jQuery 依赖)——筛选 type: "legacy_jquery_dep" 的发现项。将每处出现都替换为 replacement 字段中的值
  9. 重新运行检测脚本,并确认 findings[] 为空(或仅包含无法修复的项目,例如需要人工审查的模式 7 项目)
  10. 运行 npx @ui5/linter --details 进行二次检查——不应引入新的错误
  11. 对于无法自动修复的发现项(复杂的模拟模式、含糊不清的模块边界),在 MODERNIZATION-ISSUES.md 中记录文件路径、行号和原因

完成门槛

在报告此技能已完成之前,请确认以下所有事项:

  1. 检测脚本未发现任何问题: 重新运行 node <skill-dir>/scripts/detect-blind-spots.js <project-root>,并确认 summary.total === 0(或所有剩余项目都已在 MODERNIZATION-ISSUES.md 中说明并附上理由)
  2. 没有新增的 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 — 测试基础设施现代化,通常会揭示这些模式

评论

支持 Markdown