@book000/eslint-config 1.16.1 → 1.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.mjs +45 -12
- package/package.json +1 -1
- package/test.mjs +160 -44
package/index.mjs
CHANGED
|
@@ -48,12 +48,55 @@ export default tseslint.config(
|
|
|
48
48
|
// クラスメンバーの順序(private before public)を強制しない。
|
|
49
49
|
// public-first の慣習は API の可読性として広く使われているため。
|
|
50
50
|
"unicorn/consistent-class-member-order": "off",
|
|
51
|
+
// 依存プロジェクト横断での実運用調査 (tomacheese/book000/jaoafa 各オーガニゼーション規模) の
|
|
52
|
+
// 結果、誤検知率が高く実利が薄いと判断したルールを以下のとおり無効化する。
|
|
53
|
+
//
|
|
54
|
+
// .then()/.catch()/.finally() チェーンを await へ書き換えるよう強制するが、
|
|
55
|
+
// 関数自体を async 化したくない薄いラッパー関数 (Promise を返すだけの関数) まで
|
|
56
|
+
// 書き換え対象になり実利が薄いため無効化する
|
|
57
|
+
"unicorn/prefer-await": "off",
|
|
58
|
+
// is/has/should 等のプレフィックスを強制するが、ドメイン語彙由来の真偽値変数名
|
|
59
|
+
// (enabled, visible 等) まで誤検知するため無効化する
|
|
60
|
+
"unicorn/consistent-boolean-name": "off",
|
|
61
|
+
// get/set/create 等の動詞プレフィックスを持つ関数以外の値 (変数・プロパティ等) を
|
|
62
|
+
// 検出するが、removeButton (削除用ボタン要素を指す変数名) のようなドメイン語彙由来の
|
|
63
|
+
// 命名まで誤検知するため無効化する
|
|
64
|
+
"unicorn/no-non-function-verb-prefix": "off",
|
|
65
|
+
// Number() による型強制を強制するが、parseInt 等との挙動差 (NaN の扱い等) により
|
|
66
|
+
// 意図せず動作が変わる危険があるため無効化する
|
|
67
|
+
"unicorn/prefer-number-coercion": "off",
|
|
68
|
+
// モジュールトップレベル変数へのキャッシュ/メモ化目的の代入という一般的な
|
|
69
|
+
// パターンまで誤検知するため無効化する
|
|
70
|
+
"unicorn/no-top-level-assignment-in-function": "off",
|
|
71
|
+
// \uXXXX と \u{XXXX} は等価であり、既存コードへの機械的な書き換えを強制する
|
|
72
|
+
// だけで実利が薄いため無効化する
|
|
73
|
+
"unicorn/prefer-unicode-code-point-escapes": "off",
|
|
74
|
+
// ネストしたループ内の break による意図的な早期脱出パターンまで誤検知するため
|
|
75
|
+
// 無効化する
|
|
76
|
+
"unicorn/no-break-in-nested-loop": "off",
|
|
77
|
+
// 継承・拡張を意図した static メソッド内でのクラス名参照という正当な
|
|
78
|
+
// パターンまで誤検知するため無効化する
|
|
79
|
+
"unicorn/class-reference-in-static-methods": "off",
|
|
80
|
+
// while (true) の先頭で break 条件を判定するコードを、条件を while() の式へ
|
|
81
|
+
// 移すよう強制するが、ループ内で算出した値を break 条件に使うイディオム
|
|
82
|
+
// (while (true) { const chunk = readNext(); if (!chunk) break; ... }) まで
|
|
83
|
+
// 誤検知するため無効化する
|
|
84
|
+
"unicorn/prefer-while-loop-condition": "off",
|
|
85
|
+
// location.href への代入と location.assign() はほぼ等価であり、機械的な
|
|
86
|
+
// 書き換えを強制するだけで実利が薄いため無効化する
|
|
87
|
+
"unicorn/prefer-location-assign": "off",
|
|
88
|
+
// 呼び出しの引数として渡された呼び出しの入れ子の深さを検出するが、
|
|
89
|
+
// z.array(z.object({ ... })) のような Zod 等のスキーマ定義における正当な
|
|
90
|
+
// 入れ子呼び出しまで誤検知するため無効化する
|
|
91
|
+
"unicorn/max-nested-calls": "off",
|
|
92
|
+
// globalThis/window/global のプロパティへの代入は、テストやモックの
|
|
93
|
+
// セットアップにおける標準的なイディオム (globalThis.fetch = mock 等) であり、
|
|
94
|
+
// 実運用コードでの誤検知率が高いため無効化する
|
|
95
|
+
"unicorn/no-global-object-property-assignment": "off",
|
|
51
96
|
//
|
|
52
97
|
// 以下、上記以外の flat/recommended 収録ルールをすべて明示的に error とする
|
|
53
98
|
"unicorn/better-dom-traversing": "error",
|
|
54
|
-
"unicorn/class-reference-in-static-methods": "error",
|
|
55
99
|
"unicorn/consistent-assert": "error",
|
|
56
|
-
"unicorn/consistent-boolean-name": "error",
|
|
57
100
|
"unicorn/consistent-compound-words": "error",
|
|
58
101
|
"unicorn/consistent-conditional-object-spread": "error",
|
|
59
102
|
"unicorn/consistent-date-clone": "error",
|
|
@@ -75,7 +118,6 @@ export default tseslint.config(
|
|
|
75
118
|
"unicorn/import-style": "error",
|
|
76
119
|
"unicorn/isolated-functions": "error",
|
|
77
120
|
"unicorn/logical-assignment-operators": "error",
|
|
78
|
-
"unicorn/max-nested-calls": "error",
|
|
79
121
|
"unicorn/new-for-builtins": "error",
|
|
80
122
|
"unicorn/no-abusive-eslint-disable": "error",
|
|
81
123
|
"unicorn/no-accessor-recursion": "error",
|
|
@@ -95,7 +137,6 @@ export default tseslint.config(
|
|
|
95
137
|
"unicorn/no-await-in-promise-methods": "error",
|
|
96
138
|
"unicorn/no-blob-to-file": "error",
|
|
97
139
|
"unicorn/no-boolean-sort-comparator": "error",
|
|
98
|
-
"unicorn/no-break-in-nested-loop": "error",
|
|
99
140
|
"unicorn/no-canvas-to-image": "error",
|
|
100
141
|
"unicorn/no-chained-comparison": "error",
|
|
101
142
|
"unicorn/no-collection-bracket-access": "error",
|
|
@@ -116,7 +157,6 @@ export default tseslint.config(
|
|
|
116
157
|
"unicorn/no-exports-in-scripts": "error",
|
|
117
158
|
"unicorn/no-for-each": "error",
|
|
118
159
|
"unicorn/no-for-loop": "error",
|
|
119
|
-
"unicorn/no-global-object-property-assignment": "error",
|
|
120
160
|
"unicorn/no-immediate-mutation": "error",
|
|
121
161
|
"unicorn/no-impossible-length-comparison": "error",
|
|
122
162
|
"unicorn/no-incorrect-query-selector": "error",
|
|
@@ -142,7 +182,6 @@ export default tseslint.config(
|
|
|
142
182
|
"unicorn/no-nested-ternary": "error",
|
|
143
183
|
"unicorn/no-new-array": "error",
|
|
144
184
|
"unicorn/no-new-buffer": "error",
|
|
145
|
-
"unicorn/no-non-function-verb-prefix": "error",
|
|
146
185
|
"unicorn/no-nonstandard-builtin-properties": "error",
|
|
147
186
|
"unicorn/no-object-as-default-parameter": "error",
|
|
148
187
|
"unicorn/no-object-methods-with-collections": "error",
|
|
@@ -157,7 +196,6 @@ export default tseslint.config(
|
|
|
157
196
|
"unicorn/no-thenable": "error",
|
|
158
197
|
"unicorn/no-this-assignment": "error",
|
|
159
198
|
"unicorn/no-this-outside-of-class": "error",
|
|
160
|
-
"unicorn/no-top-level-assignment-in-function": "error",
|
|
161
199
|
"unicorn/no-top-level-side-effects": "error",
|
|
162
200
|
"unicorn/no-typeof-undefined": "error",
|
|
163
201
|
"unicorn/no-uncalled-method": "error",
|
|
@@ -223,7 +261,6 @@ export default tseslint.config(
|
|
|
223
261
|
"unicorn/prefer-array-slice": "error",
|
|
224
262
|
"unicorn/prefer-array-some": "error",
|
|
225
263
|
"unicorn/prefer-at": "error",
|
|
226
|
-
"unicorn/prefer-await": "error",
|
|
227
264
|
"unicorn/prefer-bigint-literals": "error",
|
|
228
265
|
"unicorn/prefer-blob-reading-methods": "error",
|
|
229
266
|
"unicorn/prefer-block-statement-over-iife": "error",
|
|
@@ -259,7 +296,6 @@ export default tseslint.config(
|
|
|
259
296
|
"unicorn/prefer-iterator-to-array": "error",
|
|
260
297
|
"unicorn/prefer-iterator-to-array-at-end": "error",
|
|
261
298
|
"unicorn/prefer-keyboard-event-key": "error",
|
|
262
|
-
"unicorn/prefer-location-assign": "error",
|
|
263
299
|
"unicorn/prefer-logical-operator-over-ternary": "error",
|
|
264
300
|
"unicorn/prefer-map-from-entries": "error",
|
|
265
301
|
"unicorn/prefer-math-abs": "error",
|
|
@@ -273,7 +309,6 @@ export default tseslint.config(
|
|
|
273
309
|
"unicorn/prefer-native-coercion-functions": "error",
|
|
274
310
|
"unicorn/prefer-negative-index": "error",
|
|
275
311
|
"unicorn/prefer-node-protocol": "error",
|
|
276
|
-
"unicorn/prefer-number-coercion": "error",
|
|
277
312
|
"unicorn/prefer-number-is-safe-integer": "error",
|
|
278
313
|
"unicorn/prefer-number-properties": "error",
|
|
279
314
|
"unicorn/prefer-object-define-properties": "error",
|
|
@@ -321,11 +356,9 @@ export default tseslint.config(
|
|
|
321
356
|
"unicorn/prefer-type-error": "error",
|
|
322
357
|
"unicorn/prefer-type-literal-last": "error",
|
|
323
358
|
"unicorn/prefer-unary-minus": "error",
|
|
324
|
-
"unicorn/prefer-unicode-code-point-escapes": "error",
|
|
325
359
|
"unicorn/prefer-url-can-parse": "error",
|
|
326
360
|
"unicorn/prefer-url-href": "error",
|
|
327
361
|
"unicorn/prefer-url-search-parameters": "error",
|
|
328
|
-
"unicorn/prefer-while-loop-condition": "error",
|
|
329
362
|
"unicorn/relative-url-style": "error",
|
|
330
363
|
"unicorn/require-array-join-separator": "error",
|
|
331
364
|
"unicorn/require-array-sort-compare": "error",
|
package/package.json
CHANGED
package/test.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Node.jsスクリプトでESLint flat config(index.mjs)の動作を検証するサンプル
|
|
2
|
-
import {
|
|
2
|
+
import { execFile } from "child_process";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
5
8
|
|
|
6
9
|
async function main() {
|
|
7
10
|
const testCases = [
|
|
@@ -218,6 +221,78 @@ async function main() {
|
|
|
218
221
|
shouldError: false,
|
|
219
222
|
rules: ["unicorn/consistent-class-member-order"],
|
|
220
223
|
},
|
|
224
|
+
{
|
|
225
|
+
name: "prefer-await: .then() チェーンを使う非asyncな薄いラッパー関数はOK",
|
|
226
|
+
code: "function getValue(): Promise<number> { return Promise.resolve(1).then((value) => value); }",
|
|
227
|
+
shouldError: false,
|
|
228
|
+
rules: ["unicorn/prefer-await"],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "consistent-boolean-name: is/has等のプレフィックスがない真偽値変数名はOK",
|
|
232
|
+
code: "const enabled: boolean = true;",
|
|
233
|
+
shouldError: false,
|
|
234
|
+
rules: ["unicorn/consistent-boolean-name"],
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "no-non-function-verb-prefix: 動詞プレフィックスを持つ非関数の変数名はOK",
|
|
238
|
+
code: "const removeButton = 1;",
|
|
239
|
+
shouldError: false,
|
|
240
|
+
rules: ["unicorn/no-non-function-verb-prefix"],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "prefer-number-coercion: parseInt による数値変換はOK",
|
|
244
|
+
code: "const n = parseInt('1', 10);",
|
|
245
|
+
shouldError: false,
|
|
246
|
+
rules: ["unicorn/prefer-number-coercion"],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "no-top-level-assignment-in-function: トップレベル変数へのキャッシュ代入はOK",
|
|
250
|
+
code: "let cache: number | undefined; function setCache(): void { cache = 1; }",
|
|
251
|
+
shouldError: false,
|
|
252
|
+
rules: ["unicorn/no-top-level-assignment-in-function"],
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "prefer-unicode-code-point-escapes: \\uXXXX形式のエスケープはOK",
|
|
256
|
+
code: "const s = '\\u00e9';",
|
|
257
|
+
shouldError: false,
|
|
258
|
+
rules: ["unicorn/prefer-unicode-code-point-escapes"],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "no-break-in-nested-loop: ネストしたループ内のbreakによる早期脱出はOK",
|
|
262
|
+
code: "for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (j === 1) break; } }",
|
|
263
|
+
shouldError: false,
|
|
264
|
+
rules: ["unicorn/no-break-in-nested-loop"],
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: "class-reference-in-static-methods: staticメソッド内での非呼び出し位置のクラス名参照はOK",
|
|
268
|
+
code: "class Foo { static bar = 1; static create(): number { return Foo.bar; } }",
|
|
269
|
+
shouldError: false,
|
|
270
|
+
rules: ["unicorn/class-reference-in-static-methods"],
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "prefer-while-loop-condition: while(true) + 内部breakによる終了判定はOK",
|
|
274
|
+
code: "let i = 0; while (true) { if (i >= 5) break; i++; }",
|
|
275
|
+
shouldError: false,
|
|
276
|
+
rules: ["unicorn/prefer-while-loop-condition"],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "prefer-location-assign: location.hrefへの代入はOK",
|
|
280
|
+
code: "location.href = 'https://example.com';",
|
|
281
|
+
shouldError: false,
|
|
282
|
+
rules: ["unicorn/prefer-location-assign"],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "max-nested-calls: スキーマ定義等のメソッドチェーンによる深いネストはOK",
|
|
286
|
+
code: "function a(x: number) { return b(x); } function b(x: number) { return c(x); } function c(x: number) { return d(x); } function d(x: number) { return x; } const result = a(b(c(d(1))));",
|
|
287
|
+
shouldError: false,
|
|
288
|
+
rules: ["unicorn/max-nested-calls"],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "no-global-object-property-assignment: globalThisプロパティへの代入(テストのモック等)はOK",
|
|
292
|
+
code: "globalThis.fetch = (() => Promise.resolve(new Response())) as typeof fetch;",
|
|
293
|
+
shouldError: false,
|
|
294
|
+
rules: ["unicorn/no-global-object-property-assignment"],
|
|
295
|
+
},
|
|
221
296
|
{
|
|
222
297
|
name: "filename-case: checkDirectories: false のため kebab-case でないディレクトリ名もエラーにならない(OK)",
|
|
223
298
|
code: "export const a = 1;",
|
|
@@ -276,57 +351,98 @@ async function main() {
|
|
|
276
351
|
const flatConfigPath = path.join(process.cwd(), "eslint.config.mjs");
|
|
277
352
|
fs.copyFileSync(path.join(process.cwd(), "index.mjs"), flatConfigPath);
|
|
278
353
|
|
|
279
|
-
//
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
exec(
|
|
288
|
-
`npx eslint --no-cache --ext .ts ${tmpFilePath}`,
|
|
289
|
-
(error, stdout, stderr) => {
|
|
290
|
-
const output = (stdout || "") + (stderr || "");
|
|
291
|
-
// テスト項目ごとに対象ルールのみを判定
|
|
292
|
-
const errorLines = output
|
|
293
|
-
.split("\n")
|
|
294
|
-
.filter((line) => line.match(/error/));
|
|
295
|
-
let errorCount = 0;
|
|
296
|
-
let ignoredErrors = [];
|
|
297
|
-
const relevantErrors = [];
|
|
298
|
-
for (const line of errorLines) {
|
|
299
|
-
const ruleMatch = line.match(/\s([\w@\-/]+)$/);
|
|
300
|
-
const rule = ruleMatch ? ruleMatch[1] : null;
|
|
301
|
-
if (rule && rules.includes(rule)) {
|
|
302
|
-
errorCount = 1;
|
|
303
|
-
relevantErrors.push(line);
|
|
304
|
-
} else {
|
|
305
|
-
ignoredErrors.push(line);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
fs.unlinkSync(tmpFilePath);
|
|
309
|
-
resolve({
|
|
310
|
-
name,
|
|
311
|
-
shouldError,
|
|
312
|
-
errorCount,
|
|
313
|
-
output,
|
|
314
|
-
relevantErrors,
|
|
315
|
-
ignoredErrors,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
);
|
|
319
|
-
});
|
|
354
|
+
// テスト用一時ファイルをすべて書き出す (プロセス起動は後で 1 回だけ行う)
|
|
355
|
+
const tmpFilePaths = testCases.map((testCase, i) => {
|
|
356
|
+
const { code, dir } = testCase;
|
|
357
|
+
const kebabName = `test-${i}.ts`;
|
|
358
|
+
const targetDir = dir ? path.join(srcDir, dir) : tmpDir;
|
|
359
|
+
const tmpFilePath = path.join(targetDir, kebabName);
|
|
360
|
+
fs.writeFileSync(tmpFilePath, code);
|
|
361
|
+
return tmpFilePath;
|
|
320
362
|
});
|
|
321
363
|
|
|
322
|
-
|
|
364
|
+
// 全テストケースを 1 回の eslint 起動でまとめて検証する。
|
|
365
|
+
// 以前はテストケースごとに `npx eslint` を並列起動していたため、
|
|
366
|
+
// type-aware (typescript-eslint) なパーサ初期化がテストケース数分同時に走り、
|
|
367
|
+
// メモリ枯渇・OOM killer の引き金になっていた。
|
|
368
|
+
// 1 プロセス・1 回の TypeScript プログラム初期化にまとめることで、
|
|
369
|
+
// プロセス数をテストケース数から 1 に減らし、メモリ使用量を大幅に下げる。
|
|
370
|
+
const eslintBinPath = path.join(
|
|
371
|
+
process.cwd(),
|
|
372
|
+
"node_modules",
|
|
373
|
+
"eslint",
|
|
374
|
+
"bin",
|
|
375
|
+
"eslint.js"
|
|
376
|
+
);
|
|
377
|
+
let jsonOutput = "";
|
|
378
|
+
try {
|
|
379
|
+
const { stdout } = await execFileAsync(
|
|
380
|
+
process.execPath,
|
|
381
|
+
[
|
|
382
|
+
eslintBinPath,
|
|
383
|
+
"--no-cache",
|
|
384
|
+
"--ext",
|
|
385
|
+
".ts",
|
|
386
|
+
"--format",
|
|
387
|
+
"json",
|
|
388
|
+
...tmpFilePaths,
|
|
389
|
+
],
|
|
390
|
+
{ maxBuffer: 1024 * 1024 * 50 }
|
|
391
|
+
);
|
|
392
|
+
jsonOutput = stdout;
|
|
393
|
+
} catch (error) {
|
|
394
|
+
// eslint はテストケースに期待どおりのエラーがあると非ゼロの exit code を返すが、
|
|
395
|
+
// これはテストとして期待される挙動なので、JSON 形式の stdout が取れていれば
|
|
396
|
+
// 正常系として扱う。設定エラー等で stdout に JSON が出力されない場合は
|
|
397
|
+
// ここでは判別せず、後続の JSON.parse で例外として検出させる。
|
|
398
|
+
if (error.stdout) {
|
|
399
|
+
jsonOutput = error.stdout;
|
|
400
|
+
} else {
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const eslintResults = JSON.parse(jsonOutput);
|
|
406
|
+
const resultsByFilePath = new Map(
|
|
407
|
+
eslintResults.map((result) => [result.filePath, result])
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const results = testCases.map((testCase, i) => {
|
|
411
|
+
const { name, shouldError, rules } = testCase;
|
|
412
|
+
const tmpFilePath = tmpFilePaths[i];
|
|
413
|
+
const fileResult = resultsByFilePath.get(tmpFilePath);
|
|
414
|
+
if (!fileResult) {
|
|
415
|
+
// 対象ファイルが eslint の結果に存在しない場合、空メッセージとして
|
|
416
|
+
// 握りつぶすと「エラー 0 件 = OK」と誤ってパスしてしまう。
|
|
417
|
+
// ファイルが実際には lint されていないことを示すため、例外として検出する。
|
|
418
|
+
throw new Error(
|
|
419
|
+
`ESLint did not return a result for temp file: ${tmpFilePath}`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let errorCount = 0;
|
|
424
|
+
const relevantErrors = [];
|
|
425
|
+
const ignoredErrors = [];
|
|
426
|
+
for (const message of fileResult.messages) {
|
|
427
|
+
const severityLabel = message.severity === 2 ? "error" : "warning";
|
|
428
|
+
const line = ` ${message.line}:${message.column} ${severityLabel} ${message.message} ${message.ruleId ?? ""}`;
|
|
429
|
+
if (message.ruleId && rules.includes(message.ruleId)) {
|
|
430
|
+
errorCount = 1;
|
|
431
|
+
relevantErrors.push(line);
|
|
432
|
+
} else {
|
|
433
|
+
ignoredErrors.push(line);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
fs.unlinkSync(tmpFilePath);
|
|
438
|
+
return { name, shouldError, errorCount, relevantErrors, ignoredErrors };
|
|
439
|
+
});
|
|
323
440
|
let pass = 0,
|
|
324
441
|
fail = 0;
|
|
325
442
|
for (const {
|
|
326
443
|
name,
|
|
327
444
|
shouldError,
|
|
328
445
|
errorCount,
|
|
329
|
-
output,
|
|
330
446
|
relevantErrors,
|
|
331
447
|
ignoredErrors,
|
|
332
448
|
} of results) {
|