fix-linter-blind-spots について
UI5 開発者向けのスキルです。UI5 リンターが検出しない JavaScript のグローバル名前空間パターンを修正し、旧式 QUnit 断言やテストモックにも対応します。
主な機能
- 01JavaScript ファイルのスキャン
- 02アプリ名前空間のグローバル検出
- 03グローバル参照を AMD インポートに置換
- 04QUnit 断言の更新
- 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 により修正します。このスキルでは修正しないでください。
これらのパターンは、グローバル名前空間チェーンを構築する 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、標準エラー出力には進捗情報):
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 により修正します。)
検出: 検出スクリプトの findings を使い、type: "global_assignment" および type: "global_return" で絞り込みます。
修正前:
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 ファイルが、インポートする代わりにグローバル名前空間のチェーンを介して別のモジュールを参照しています。次の 3 つのサブケースがあります。
検出: 検出スクリプトの 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" を参照してください。
テストファイルでは、<NAMESPACE>/test/ の代わりに test-resources/<NAMESPACE>/ プレフィックスを使用します。
サブケース 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 に置き換えても失敗します。パラメーターは DataService ではなく Formatter です。
修正手順:
sap.ui.defineの依存関係配列と関数パラメーターリストを解析します- 参照対象のモジュールに対応するパラメーターを特定します
- パラメーター名がモジュール名と異なる場合:
- 関数パラメーター名をモジュールの短縮名に変更します
- ファイル内の古いパラメーター名への参照をすべて新しい名前に更新します
- グローバル参照を(正しくなった)パラメーター名に置き換えます
- 結果として生じた自己代入の行(
var X = X;)があれば削除します
パターン 4:ケース 1c の一体性 — インポートせずに名前だけを変更しない
グローバルからローカルへの名前変更は、必ずインポートと組み合わせる必要があります。宣言のない名前変更済みの変数は ReferenceError を引き起こし、グローバルのままにするよりも問題が大きくなります。
ルール:
- グローバル参照を置き換えるときは、必ず
sap.ui.defineの依存関係を追加します。この段階では循環依存を検出しようとしないでください。ここで導入された循環依存は、すべての修正フェーズが完了した後、フェーズ 3 のステップ 3.3 でfix-cyclic-depsによって検出・解決されます。 - ファイルの修正後、追加した変数名をすべて grep で検索し、関数パラメーター、
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" でフィルタリングします。各検出結果には assertionName と replacement(ファイルの場所に応じた assert.<name> または Opa5.assert.<name>)が含まれます。
コンテキストの検出:
test/opa/またはtest/integration/配下のファイルで、waitForの success/error コールバック内にある場合 →Opa5.assert.<assertion>test/unit/配下のファイル、またはQUnit.testコールバック内の.qunit.jsファイルの場合 →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();
});
});
sinon は Test Starter のインフラストラクチャを通じてグローバルに利用できるため、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" でフィルタリングします。各検出結果には、正しい値("sap/ui/thirdparty/jquery")を示す replacement フィールドが含まれます。
変更前:
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 に記録されていることを確認します) - 新しいリンターエラーがないこと:
npx @ui5/linter --detailsを実行し、新しいエラーが発生していないことを確認します
関連スキル
- fix-js-globals — 同じ変換(パターン 1~4)を適用しますが、リンターが
no-globalsエラー(つまりsap.*グローバル)として明示的に報告するケースのみが対象です。このスキルはリンターでは検出できないアプリ名前空間のグローバルを扱います。どちらのスキルも同種の修正を行いますが、問題の検出方法が異なります。 - fix-cyclic-deps —
sap.ui.defineの依存関係がパターン 2 で追加されたことで生じた循環依存関係を検出して解決するため、このスキルの後(フェーズ 3、ステップ 3.3)に実行します - modernize-test-starter — こうしたパターンが明らかになることの多い、テストインフラストラクチャのモダナイゼーションです