@utoo/lint 0.2.8 → 0.3.4
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/README.md +201 -172
- package/bin/fishlint.js +23 -3
- package/bin/migrate.js +555 -39
- package/bin/utoo-lint.js +15 -15
- package/configs/frontend.d.ts +54 -2
- package/configs/frontend.json +16 -1
- package/index.cjs +277 -60
- package/index.d.ts +14 -1
- package/index.js +277 -60
- package/package.json +13 -11
- package/schema.json +89 -36
package/bin/migrate.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, extname, resolve as resolvePath } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, extname, relative, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
|
|
6
|
+
import { Legacy } from "@eslint/eslintrc";
|
|
7
|
+
import eslintJs from "@eslint/js";
|
|
8
|
+
|
|
6
9
|
import { Linter } from "../index.js";
|
|
7
10
|
|
|
8
11
|
const ESLINT_CONFIG_FILENAMES = [
|
|
9
12
|
"eslint.config.js",
|
|
10
13
|
"eslint.config.mjs",
|
|
11
14
|
"eslint.config.cjs",
|
|
15
|
+
".eslintrc.js",
|
|
16
|
+
".eslintrc.cjs",
|
|
12
17
|
".eslintrc.json",
|
|
13
18
|
".eslintrc"
|
|
14
19
|
];
|
|
@@ -16,7 +21,19 @@ const JAVASCRIPT_CONFIG_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]);
|
|
|
16
21
|
const IGNORED_RULES = new Map([
|
|
17
22
|
["prettier/prettier", "formatting is outside utoo-lint"]
|
|
18
23
|
]);
|
|
19
|
-
|
|
24
|
+
// Add aliases only after reviewing the source rule against the target rule's
|
|
25
|
+
// implemented behavior and supported options.
|
|
26
|
+
const REVIEWED_RULE_ALIASES = new Map([
|
|
27
|
+
["no-native-reassign", "no-global-assign"],
|
|
28
|
+
["@typescript-eslint/no-invalid-this", "no-invalid-this"],
|
|
29
|
+
["@eslint-react/no-array-index-key", "react/no-array-index-key"],
|
|
30
|
+
["@eslint-react/dom-no-find-dom-node", "react/no-find-dom-node"],
|
|
31
|
+
["@eslint-react/dom-no-render-return-value", "react/no-render-return-value"],
|
|
32
|
+
["@eslint-react/dom-no-void-elements-with-children", "react/void-dom-elements-no-children"],
|
|
33
|
+
["@eslint-react/rules-of-hooks", "react-hooks/rules-of-hooks"]
|
|
34
|
+
]);
|
|
35
|
+
const SCHEMA_URL = "https://raw.githubusercontent.com/utooland/utoo-lint/main/npm/utoo-lint/schema.json";
|
|
36
|
+
const MIGRATION_SOURCE = Symbol("migrationSource");
|
|
20
37
|
const JAVASCRIPT_CONFIG_LOADER_SCRIPT = `
|
|
21
38
|
import { writeFileSync } from "node:fs";
|
|
22
39
|
import { pathToFileURL } from "node:url";
|
|
@@ -177,44 +194,49 @@ function migrateEslintConfig(options) {
|
|
|
177
194
|
|
|
178
195
|
const eslintConfig = loadEslintConfig(sourcePath, options.cwd);
|
|
179
196
|
const supportedRuleIds = new Set(new Linter().getRules().keys());
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
const migrationInput = migrationInputFromConfig(
|
|
198
|
+
eslintConfig,
|
|
199
|
+
sourcePath,
|
|
200
|
+
realpathSync(dirname(options.output))
|
|
201
|
+
);
|
|
202
|
+
const entries = migrationInput.entries;
|
|
203
|
+
const migratedEntries = [];
|
|
184
204
|
const supportedRules = [];
|
|
185
|
-
|
|
205
|
+
let unsupportedRules = [];
|
|
186
206
|
const ignoredRules = [];
|
|
207
|
+
const translatedRules = [];
|
|
187
208
|
|
|
188
209
|
for (const entry of entries) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
210
|
+
const rules = {};
|
|
211
|
+
if (entry.rules && typeof entry.rules === "object") {
|
|
212
|
+
for (const [ruleId, ruleConfig] of Object.entries(entry.rules)) {
|
|
213
|
+
const migratedRuleId = REVIEWED_RULE_ALIASES.get(ruleId) ?? ruleId;
|
|
214
|
+
if (IGNORED_RULES.has(ruleId)) {
|
|
215
|
+
ignoredRules.push({ ruleId, reason: IGNORED_RULES.get(ruleId) });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!supportedRuleIds.has(migratedRuleId)) {
|
|
219
|
+
if (isRuleDisabled(ruleConfig)) {
|
|
220
|
+
unsupportedRules = removeDisabledUnsupportedRule(unsupportedRules, ruleId, entry);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const source = entry[MIGRATION_SOURCE];
|
|
224
|
+
unsupportedRules.push({ ruleId, scope: migrationRuleScope(entry), source });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
rules[migratedRuleId] = migratableRuleConfig(ruleConfig);
|
|
228
|
+
supportedRules.push(migratedRuleId);
|
|
229
|
+
if (migratedRuleId !== ruleId) {
|
|
230
|
+
translatedRules.push({ sourceRuleId: ruleId, targetRuleId: migratedRuleId });
|
|
231
|
+
}
|
|
202
232
|
}
|
|
203
|
-
rules[ruleId] = migratableRuleConfig(ruleConfig);
|
|
204
|
-
supportedRules.push(ruleId);
|
|
205
233
|
}
|
|
234
|
+
migratedEntries.push(migratableConfigEntry(entry, rules));
|
|
206
235
|
}
|
|
207
236
|
|
|
208
|
-
const config =
|
|
209
|
-
$schema: SCHEMA_URL
|
|
210
|
-
|
|
211
|
-
if (files.size > 0) {
|
|
212
|
-
config.files = [...files];
|
|
213
|
-
}
|
|
214
|
-
if (ignores.size > 0) {
|
|
215
|
-
config.ignores = [...ignores];
|
|
216
|
-
}
|
|
217
|
-
config.rules = sortObjectByKey(rules);
|
|
237
|
+
const config = Array.isArray(eslintConfig) || migratedEntries.length > 1
|
|
238
|
+
? [{ $schema: SCHEMA_URL }, ...migratedEntries]
|
|
239
|
+
: { $schema: SCHEMA_URL, ...(migratedEntries[0] ?? { rules: {} }) };
|
|
218
240
|
|
|
219
241
|
return {
|
|
220
242
|
config,
|
|
@@ -222,13 +244,410 @@ function migrateEslintConfig(options) {
|
|
|
222
244
|
source: sourcePath,
|
|
223
245
|
output: options.print ? null : options.output,
|
|
224
246
|
supportedRules: uniqueSorted(supportedRules),
|
|
225
|
-
unsupportedRules: uniqueSorted(unsupportedRules),
|
|
247
|
+
unsupportedRules: uniqueSorted(unsupportedRules.map((rule) => rule.ruleId)),
|
|
226
248
|
ignoredRules: uniqueByRuleId(ignoredRules),
|
|
249
|
+
translatedRules: uniqueTranslatedRules(translatedRules),
|
|
250
|
+
inheritedSources: migrationInput.inheritedSources,
|
|
251
|
+
unsupportedInheritedRules: uniqueUnsupportedInheritedRules(
|
|
252
|
+
unsupportedRules
|
|
253
|
+
.filter((rule) => rule.source?.inherited)
|
|
254
|
+
.map((rule) => ({
|
|
255
|
+
ruleId: rule.ruleId,
|
|
256
|
+
sourceName: rule.source.name,
|
|
257
|
+
sourcePath: rule.source.filePath
|
|
258
|
+
}))
|
|
259
|
+
),
|
|
227
260
|
optionDroppedRules: []
|
|
228
261
|
}
|
|
229
262
|
};
|
|
230
263
|
}
|
|
231
264
|
|
|
265
|
+
function migrationInputFromConfig(eslintConfig, sourcePath, outputDirectory) {
|
|
266
|
+
if (!isClassicEslintConfig(eslintConfig, sourcePath)) {
|
|
267
|
+
return { entries: normalizeConfigEntries(eslintConfig), inheritedSources: [] };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const basePath = realpathSync(dirname(sourcePath));
|
|
271
|
+
const factory = new MigrationConfigArrayFactory({
|
|
272
|
+
cwd: basePath,
|
|
273
|
+
getEslintRecommendedConfig: () => eslintJs.configs.recommended,
|
|
274
|
+
getEslintAllConfig: () => eslintJs.configs.all
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
let configArray;
|
|
278
|
+
try {
|
|
279
|
+
configArray = factory.create(eslintConfig, {
|
|
280
|
+
basePath,
|
|
281
|
+
filePath: sourcePath,
|
|
282
|
+
name: sourcePath
|
|
283
|
+
});
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw new Error(`utoo-lint migrate eslint: unable to resolve classic config ${sourcePath}: ${error.message}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return migrationInputFromClassicConfigArray(configArray, basePath, outputDirectory);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isClassicEslintConfig(eslintConfig, sourcePath) {
|
|
292
|
+
if (/^\.eslintrc(?:\.|$)/u.test(basename(sourcePath))) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
return Boolean(
|
|
296
|
+
eslintConfig &&
|
|
297
|
+
!Array.isArray(eslintConfig) &&
|
|
298
|
+
typeof eslintConfig === "object" &&
|
|
299
|
+
(eslintConfig.extends || eslintConfig.overrides || eslintConfig.ignorePatterns)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
class MigrationConfigArrayFactory extends Legacy.ConfigArrayFactory {
|
|
304
|
+
activeSources = [];
|
|
305
|
+
|
|
306
|
+
_loadPlugin(name, context) {
|
|
307
|
+
return super._loadPlugin(name, {
|
|
308
|
+
...context,
|
|
309
|
+
pluginBasePath: context.filePath ? dirname(context.filePath) : context.pluginBasePath
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
*_normalizeConfigData(configData, context) {
|
|
314
|
+
const source = classicContextSource(context);
|
|
315
|
+
const cycleIndex = this.activeSources.findIndex((active) => active.key === source.key);
|
|
316
|
+
if (cycleIndex !== -1) {
|
|
317
|
+
const chain = [...this.activeSources.slice(cycleIndex), source].map((active) => active.label).join(" -> ");
|
|
318
|
+
throw new Error(`Circular ESLint extends chain detected: ${chain}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.activeSources.push(source);
|
|
322
|
+
try {
|
|
323
|
+
yield* super._normalizeConfigData(configData, context);
|
|
324
|
+
} finally {
|
|
325
|
+
this.activeSources.pop();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function classicContextSource(context) {
|
|
331
|
+
const label = classicSourceName(context.name ?? context.filePath ?? "inline config");
|
|
332
|
+
const key = label.startsWith("plugin:")
|
|
333
|
+
? `plugin:${context.filePath ?? ""}:${label}`
|
|
334
|
+
: context.filePath
|
|
335
|
+
? `file:${resolvePath(context.filePath)}`
|
|
336
|
+
: `name:${label}`;
|
|
337
|
+
return { key, label };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function migrationInputFromClassicConfigArray(configArray, basePath, outputDirectory) {
|
|
341
|
+
const entries = [];
|
|
342
|
+
const inheritedSources = [];
|
|
343
|
+
const records = [...configArray].map((element) => ({
|
|
344
|
+
element,
|
|
345
|
+
source: classicElementSource(element),
|
|
346
|
+
selector: rebaseClassicSelector(classicSelector(element.criteria), basePath, outputDirectory),
|
|
347
|
+
ignorePatterns: element.ignorePattern?.getPatternsRelativeTo(basePath) ?? []
|
|
348
|
+
}));
|
|
349
|
+
const scopedIgnores = new Map();
|
|
350
|
+
|
|
351
|
+
for (const record of records) {
|
|
352
|
+
if (record.element.criteria && record.ignorePatterns.length > 0) {
|
|
353
|
+
const key = classicOverrideChainKey(record.element);
|
|
354
|
+
scopedIgnores.set(key, uniquePatterns([...(scopedIgnores.get(key) ?? []), ...record.ignorePatterns]));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const { element, source, selector, ignorePatterns } of records) {
|
|
359
|
+
if (source.inherited) {
|
|
360
|
+
inheritedSources.push({ name: source.name, filePath: source.filePath });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const effectiveIgnorePatterns = (element.criteria
|
|
364
|
+
? scopedIgnores.get(classicOverrideChainKey(element)) ?? []
|
|
365
|
+
: ignorePatterns).map((pattern) => rebaseClassicPattern(pattern, basePath, outputDirectory, true));
|
|
366
|
+
const rebasedIgnorePatterns = ignorePatterns.map(
|
|
367
|
+
(pattern) => rebaseClassicPattern(pattern, basePath, outputDirectory, true)
|
|
368
|
+
);
|
|
369
|
+
const hasRules = element.rules && typeof element.rules === "object";
|
|
370
|
+
|
|
371
|
+
if (rebasedIgnorePatterns.length > 0 && !element.criteria) {
|
|
372
|
+
entries.push(withMigrationSource({ ignores: rebasedIgnorePatterns }, source));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (hasRules) {
|
|
376
|
+
const entry = { ...selector, rules: element.rules };
|
|
377
|
+
if (effectiveIgnorePatterns.length > 0 && element.criteria) {
|
|
378
|
+
entry.ignores = uniquePatterns([...(entry.ignores ?? []), ...effectiveIgnorePatterns]);
|
|
379
|
+
}
|
|
380
|
+
entries.push(withMigrationSource(entry, source));
|
|
381
|
+
} else if (rebasedIgnorePatterns.length > 0 && element.criteria) {
|
|
382
|
+
entries.push(withMigrationSource({ ...selector, ignores: [...(selector.ignores ?? []), ...rebasedIgnorePatterns] }, source));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
entries,
|
|
388
|
+
inheritedSources: uniqueInheritedSources(inheritedSources)
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function classicOverrideChainKey(element) {
|
|
393
|
+
const segments = String(element.name ?? "").split(" » ");
|
|
394
|
+
let overrideIndex = -1;
|
|
395
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
396
|
+
if (/#overrides\[\d+\]/u.test(segments[index])) {
|
|
397
|
+
overrideIndex = index;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return overrideIndex === -1
|
|
401
|
+
? `criteria:${classicCriteriaKey(element.criteria)}`
|
|
402
|
+
: `override:${segments.slice(0, overrideIndex + 1).join(" » ")}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function rebaseClassicSelector(selector, basePath, outputDirectory) {
|
|
406
|
+
return {
|
|
407
|
+
...(selector.files ? {
|
|
408
|
+
files: selector.files.map((value) => Array.isArray(value)
|
|
409
|
+
? value.map((pattern) => rebaseClassicPattern(pattern, basePath, outputDirectory))
|
|
410
|
+
: rebaseClassicPattern(value, basePath, outputDirectory))
|
|
411
|
+
} : {}),
|
|
412
|
+
...(selector.ignores ? {
|
|
413
|
+
ignores: selector.ignores.map(
|
|
414
|
+
(pattern) => rebaseClassicPattern(pattern, basePath, outputDirectory, true)
|
|
415
|
+
)
|
|
416
|
+
} : {})
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function rebaseClassicPattern(pattern, basePath, outputDirectory, preserveBasenameMatch = false) {
|
|
421
|
+
const negated = pattern.startsWith("!");
|
|
422
|
+
const unsignedPattern = negated ? pattern.slice(1) : pattern;
|
|
423
|
+
const rootAnchored = unsignedPattern.startsWith("/");
|
|
424
|
+
const value = rootAnchored ? unsignedPattern.slice(1) : unsignedPattern;
|
|
425
|
+
if (preserveBasenameMatch && !rootAnchored && !value.includes("/")) {
|
|
426
|
+
return pattern;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const absolutePattern = resolvePath(basePath, value);
|
|
430
|
+
const relativePattern = normalizeMigrationPath(relative(outputDirectory, absolutePattern));
|
|
431
|
+
const rebased = relativePattern === ".." || relativePattern.startsWith("../")
|
|
432
|
+
? normalizeMigrationPath(absolutePattern)
|
|
433
|
+
: relativePattern || ".";
|
|
434
|
+
const anchoredPattern = rootAnchored && !rebased.includes("/") ? `/${rebased}` : rebased;
|
|
435
|
+
return negated ? `!${anchoredPattern}` : anchoredPattern;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function normalizeMigrationPath(path) {
|
|
439
|
+
return path.replaceAll("\\", "/");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function classicCriteriaKey(criteria) {
|
|
443
|
+
return JSON.stringify(classicSelector(criteria));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function classicSelector(criteria) {
|
|
447
|
+
if (!criteria) {
|
|
448
|
+
return {};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const includeGroups = [];
|
|
452
|
+
const ignores = [];
|
|
453
|
+
for (const patternGroup of criteria.patterns) {
|
|
454
|
+
includeGroups.push(uniquePatterns(
|
|
455
|
+
(patternGroup.includes ?? []).flatMap((matcher) => expandClassicExtglobs(classicIncludePattern(matcher)))
|
|
456
|
+
));
|
|
457
|
+
ignores.push(...(patternGroup.excludes ?? []).flatMap(
|
|
458
|
+
(matcher) => expandClassicExtglobs(classicExcludePattern(matcher))
|
|
459
|
+
));
|
|
460
|
+
}
|
|
461
|
+
const files = classicFileSelectors(includeGroups);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
...(files.length > 0 ? { files } : {}),
|
|
465
|
+
...(ignores.length > 0 ? { ignores: uniquePatterns(ignores) } : {})
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function classicIncludePattern(matcher) {
|
|
470
|
+
return matcher.options?.matchBase && !matcher.pattern.includes("/")
|
|
471
|
+
? `**/${matcher.pattern}`
|
|
472
|
+
: matcher.pattern;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function classicExcludePattern(matcher) {
|
|
476
|
+
return matcher.options?.matchBase === false && !matcher.pattern.includes("/")
|
|
477
|
+
? `/${matcher.pattern}`
|
|
478
|
+
: matcher.pattern;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function expandClassicExtglobs(pattern) {
|
|
482
|
+
let expansions = [pattern];
|
|
483
|
+
while (expansions.some((value) => /@\([^()]*\)/u.test(value))) {
|
|
484
|
+
expansions = expansions.flatMap((value) => {
|
|
485
|
+
const match = /@\(([^()]*)\)/u.exec(value);
|
|
486
|
+
if (!match) {
|
|
487
|
+
return value;
|
|
488
|
+
}
|
|
489
|
+
const alternatives = match[1].split("|");
|
|
490
|
+
if (alternatives.some((alternative) => !alternative || /[*?[\]{}()]/u.test(alternative))) {
|
|
491
|
+
throw unsupportedClassicExtglobError(pattern);
|
|
492
|
+
}
|
|
493
|
+
return alternatives.map((alternative) =>
|
|
494
|
+
`${value.slice(0, match.index)}${alternative}${value.slice(match.index + match[0].length)}`
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (/[?+*!@]\(/u.test(expansions[0])) {
|
|
499
|
+
throw unsupportedClassicExtglobError(pattern);
|
|
500
|
+
}
|
|
501
|
+
return uniquePatterns(expansions);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function unsupportedClassicExtglobError(pattern) {
|
|
505
|
+
return new Error(
|
|
506
|
+
`utoo-lint migrate eslint: cannot migrate classic selector pattern "${pattern}": ` +
|
|
507
|
+
"only literal @(one|two) extglob alternatives are supported"
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function classicFileSelectors(includeGroups) {
|
|
512
|
+
const groups = includeGroups.filter((group) => group.length > 0);
|
|
513
|
+
if (groups.length <= 1) {
|
|
514
|
+
return groups[0] ?? [];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
let selectors = [[]];
|
|
518
|
+
for (const group of groups) {
|
|
519
|
+
selectors = selectors.flatMap((selector) => group.map((pattern) => [...selector, pattern]));
|
|
520
|
+
}
|
|
521
|
+
return selectors;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function uniquePatterns(patterns) {
|
|
525
|
+
return [...new Set(patterns)];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function migrationRuleScope(entry) {
|
|
529
|
+
if (!entry.files && !entry.ignores) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
files: migratableFilePatterns(entry.files),
|
|
534
|
+
ignores: migratablePatterns(entry.ignores)
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function removeDisabledUnsupportedRule(rules, ruleId, entry) {
|
|
539
|
+
const disabledScope = migrationRuleScope(entry);
|
|
540
|
+
const remaining = [];
|
|
541
|
+
for (const rule of rules) {
|
|
542
|
+
if (rule.ruleId !== ruleId) {
|
|
543
|
+
remaining.push(rule);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const scope = subtractDisabledScope(rule.scope, disabledScope);
|
|
547
|
+
if (scope !== undefined) {
|
|
548
|
+
remaining.push({ ...rule, scope });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return remaining;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function subtractDisabledScope(enabledScope, disabledScope) {
|
|
555
|
+
if (disabledScope === null) {
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
if (enabledScope === null) {
|
|
559
|
+
return enabledScope;
|
|
560
|
+
}
|
|
561
|
+
if (
|
|
562
|
+
disabledScope.ignores.length > 0 &&
|
|
563
|
+
JSON.stringify(enabledScope.ignores) !== JSON.stringify(disabledScope.ignores)
|
|
564
|
+
) {
|
|
565
|
+
return enabledScope;
|
|
566
|
+
}
|
|
567
|
+
if (disabledScope.files.length === 0) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
if (enabledScope.files.length === 0) {
|
|
571
|
+
return enabledScope;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const disabledGroups = disabledScope.files.map((selector) => Array.isArray(selector) ? selector : [selector]);
|
|
575
|
+
const files = enabledScope.files.filter((selector) => {
|
|
576
|
+
const enabledGroup = Array.isArray(selector) ? selector : [selector];
|
|
577
|
+
return !disabledGroups.some((disabledGroup) => selectorGroupCovers(enabledGroup, disabledGroup));
|
|
578
|
+
});
|
|
579
|
+
return files.length === 0 ? undefined : { ...enabledScope, files };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function selectorGroupCovers(enabledGroup, disabledGroup) {
|
|
583
|
+
return disabledGroup.every((disabledPattern) => enabledGroup.some((enabledPattern) =>
|
|
584
|
+
globPatternCovers(enabledPattern, disabledPattern)
|
|
585
|
+
));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function globPatternCovers(enabledPattern, disabledPattern) {
|
|
589
|
+
if (enabledPattern === disabledPattern) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
const enabledSegments = enabledPattern.split("/");
|
|
593
|
+
const disabledSegments = disabledPattern.split("/");
|
|
594
|
+
return globSegmentsCover(enabledSegments, disabledSegments, 0, 0);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function globSegmentsCover(enabled, disabled, enabledIndex, disabledIndex) {
|
|
598
|
+
if (disabledIndex === disabled.length) {
|
|
599
|
+
return enabledIndex === enabled.length;
|
|
600
|
+
}
|
|
601
|
+
if (disabled[disabledIndex] === "**") {
|
|
602
|
+
if (disabledIndex === disabled.length - 1) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
for (let index = enabledIndex; index <= enabled.length; index += 1) {
|
|
606
|
+
if (globSegmentsCover(enabled, disabled, index, disabledIndex + 1)) {
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
if (enabledIndex === enabled.length || enabled[enabledIndex] === "**") {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
return globSegmentCovers(enabled[enabledIndex], disabled[disabledIndex]) &&
|
|
616
|
+
globSegmentsCover(enabled, disabled, enabledIndex + 1, disabledIndex + 1);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function globSegmentCovers(enabledSegment, disabledSegment) {
|
|
620
|
+
if (enabledSegment === disabledSegment) {
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
if (/[?[{]/u.test(disabledSegment)) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
const expression = new RegExp(`^${disabledSegment
|
|
627
|
+
.split("*")
|
|
628
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/gu, "\\$&"))
|
|
629
|
+
.join(".*")}$`);
|
|
630
|
+
return expression.test(enabledSegment);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function classicElementSource(element) {
|
|
634
|
+
const name = classicSourceName(element.name ?? element.filePath ?? "inline config");
|
|
635
|
+
return {
|
|
636
|
+
inherited: String(element.name ?? "").includes(" » "),
|
|
637
|
+
name,
|
|
638
|
+
filePath: element.filePath || null
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function classicSourceName(name) {
|
|
643
|
+
return String(name).split(" » ").at(-1).replace(/#overrides\[\d+\]$/u, "");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function withMigrationSource(entry, source) {
|
|
647
|
+
Object.defineProperty(entry, MIGRATION_SOURCE, { value: source });
|
|
648
|
+
return entry;
|
|
649
|
+
}
|
|
650
|
+
|
|
232
651
|
function findEslintConfig(cwd) {
|
|
233
652
|
let current = cwd;
|
|
234
653
|
while (true) {
|
|
@@ -282,21 +701,75 @@ function normalizeConfigEntries(config) {
|
|
|
282
701
|
return [];
|
|
283
702
|
}
|
|
284
703
|
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
704
|
+
function migratableConfigEntry(entry, rules) {
|
|
705
|
+
const migrated = {};
|
|
706
|
+
if (typeof entry.name === "string") {
|
|
707
|
+
migrated.name = entry.name;
|
|
708
|
+
}
|
|
709
|
+
const files = migratableFilePatterns(entry.files);
|
|
710
|
+
if (files.length > 0) {
|
|
711
|
+
migrated.files = files;
|
|
712
|
+
}
|
|
713
|
+
const ignores = migratablePatterns(entry.ignores);
|
|
714
|
+
if (ignores.length > 0) {
|
|
715
|
+
migrated.ignores = ignores;
|
|
716
|
+
}
|
|
717
|
+
if (!isGlobalIgnoreEntry(entry)) {
|
|
718
|
+
migrated.rules = sortObjectByKey(rules);
|
|
719
|
+
}
|
|
720
|
+
return migrated;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function migratableFilePatterns(value) {
|
|
724
|
+
const result = [];
|
|
725
|
+
const seen = new Set();
|
|
726
|
+
for (const pattern of Array.isArray(value) ? value : [value]) {
|
|
727
|
+
const migrated = Array.isArray(pattern)
|
|
728
|
+
? migratablePatterns(pattern)
|
|
729
|
+
: typeof pattern === "string"
|
|
730
|
+
? pattern
|
|
731
|
+
: null;
|
|
732
|
+
if (migrated === null || (Array.isArray(migrated) && migrated.length === 0)) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const key = JSON.stringify(migrated);
|
|
736
|
+
if (!seen.has(key)) {
|
|
737
|
+
seen.add(key);
|
|
738
|
+
result.push(migrated);
|
|
739
|
+
}
|
|
288
740
|
}
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function migratablePatterns(value) {
|
|
745
|
+
const patterns = [];
|
|
746
|
+
collectMigratablePatterns(value, patterns);
|
|
747
|
+
return [...new Set(patterns)];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function collectMigratablePatterns(value, patterns) {
|
|
289
751
|
for (const pattern of Array.isArray(value) ? value : [value]) {
|
|
290
|
-
if (
|
|
291
|
-
|
|
752
|
+
if (Array.isArray(pattern)) {
|
|
753
|
+
collectMigratablePatterns(pattern, patterns);
|
|
754
|
+
} else if (typeof pattern === "string") {
|
|
755
|
+
patterns.push(pattern);
|
|
292
756
|
}
|
|
293
757
|
}
|
|
294
758
|
}
|
|
295
759
|
|
|
760
|
+
function isGlobalIgnoreEntry(entry) {
|
|
761
|
+
return Boolean(entry.ignores) && Object.keys(entry).every((key) => key === "name" || key === "ignores");
|
|
762
|
+
}
|
|
763
|
+
|
|
296
764
|
function migratableRuleConfig(ruleConfig) {
|
|
297
765
|
return Array.isArray(ruleConfig) ? [...ruleConfig] : ruleConfig;
|
|
298
766
|
}
|
|
299
767
|
|
|
768
|
+
function isRuleDisabled(ruleConfig) {
|
|
769
|
+
const severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
|
|
770
|
+
return severity === "off" || severity === 0 || severity === false;
|
|
771
|
+
}
|
|
772
|
+
|
|
300
773
|
function sortObjectByKey(object) {
|
|
301
774
|
return Object.fromEntries(Object.entries(object).sort(([left], [right]) => left.localeCompare(right)));
|
|
302
775
|
}
|
|
@@ -313,6 +786,36 @@ function uniqueByRuleId(values) {
|
|
|
313
786
|
return [...result.values()].sort((left, right) => left.ruleId.localeCompare(right.ruleId));
|
|
314
787
|
}
|
|
315
788
|
|
|
789
|
+
function uniqueTranslatedRules(values) {
|
|
790
|
+
const result = new Map();
|
|
791
|
+
for (const value of values) {
|
|
792
|
+
result.set(value.sourceRuleId, value);
|
|
793
|
+
}
|
|
794
|
+
return [...result.values()].sort((left, right) => left.sourceRuleId.localeCompare(right.sourceRuleId));
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function uniqueInheritedSources(values) {
|
|
798
|
+
const result = new Map();
|
|
799
|
+
for (const value of values) {
|
|
800
|
+
result.set(`${value.name}\0${value.filePath ?? ""}`, value);
|
|
801
|
+
}
|
|
802
|
+
return [...result.values()].sort((left, right) =>
|
|
803
|
+
left.name.localeCompare(right.name) || String(left.filePath ?? "").localeCompare(String(right.filePath ?? ""))
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function uniqueUnsupportedInheritedRules(values) {
|
|
808
|
+
const result = new Map();
|
|
809
|
+
for (const value of values) {
|
|
810
|
+
result.set(`${value.ruleId}\0${value.sourceName}\0${value.sourcePath ?? ""}`, value);
|
|
811
|
+
}
|
|
812
|
+
return [...result.values()].sort((left, right) =>
|
|
813
|
+
left.ruleId.localeCompare(right.ruleId) ||
|
|
814
|
+
left.sourceName.localeCompare(right.sourceName) ||
|
|
815
|
+
String(left.sourcePath ?? "").localeCompare(String(right.sourcePath ?? ""))
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
316
819
|
function writeReport(report, options, stream) {
|
|
317
820
|
if (options.report === "json") {
|
|
318
821
|
stream.write(JSON.stringify(report, null, 2) + "\n");
|
|
@@ -324,12 +827,25 @@ function writeReport(report, options, stream) {
|
|
|
324
827
|
stream.write(`utoo-lint migrate eslint: wrote ${report.output}\n`);
|
|
325
828
|
}
|
|
326
829
|
stream.write(`utoo-lint migrate eslint: migrated ${report.supportedRules.length} supported rule(s)\n`);
|
|
830
|
+
if (report.inheritedSources.length > 0) {
|
|
831
|
+
stream.write(`utoo-lint migrate eslint: resolved ${report.inheritedSources.length} inherited config source(s): ${report.inheritedSources.map(formatInheritedSource).join(", ")}\n`);
|
|
832
|
+
}
|
|
833
|
+
if (report.translatedRules.length > 0) {
|
|
834
|
+
stream.write(`utoo-lint migrate eslint: translated ${report.translatedRules.length} rule alias(es): ${report.translatedRules.map((rule) => `${rule.sourceRuleId} -> ${rule.targetRuleId}`).join(", ")}\n`);
|
|
835
|
+
}
|
|
327
836
|
if (report.ignoredRules.length > 0) {
|
|
328
837
|
stream.write(`utoo-lint migrate eslint: ignored ${report.ignoredRules.length} rule(s): ${report.ignoredRules.map((rule) => rule.ruleId).join(", ")}\n`);
|
|
329
838
|
}
|
|
330
839
|
if (report.unsupportedRules.length > 0) {
|
|
331
840
|
stream.write(`utoo-lint migrate eslint: ${report.unsupportedRules.length} unsupported rule(s): ${report.unsupportedRules.join(", ")}\n`);
|
|
332
841
|
}
|
|
842
|
+
if (report.unsupportedInheritedRules.length > 0) {
|
|
843
|
+
stream.write(`utoo-lint migrate eslint: unsupported inherited rule source(s): ${report.unsupportedInheritedRules.map((rule) => `${rule.ruleId} from ${formatInheritedSource({ name: rule.sourceName, filePath: rule.sourcePath })}`).join(", ")}\n`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function formatInheritedSource(source) {
|
|
848
|
+
return source.filePath ? `${source.name} (${pathToFileURL(source.filePath).href})` : source.name;
|
|
333
849
|
}
|
|
334
850
|
|
|
335
851
|
function printMigrateHelp() {
|