@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/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
- const SCHEMA_URL = "https://raw.githubusercontent.com/fireairforce/utoo-lint/main/npm/utoo-lint/schema.json";
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 entries = normalizeConfigEntries(eslintConfig);
181
- const rules = {};
182
- const files = new Set();
183
- const ignores = new Set();
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
- const unsupportedRules = [];
205
+ let unsupportedRules = [];
186
206
  const ignoredRules = [];
207
+ const translatedRules = [];
187
208
 
188
209
  for (const entry of entries) {
189
- addPatterns(files, entry.files);
190
- addPatterns(ignores, entry.ignores);
191
- if (!entry.rules || typeof entry.rules !== "object") {
192
- continue;
193
- }
194
- for (const [ruleId, ruleConfig] of Object.entries(entry.rules)) {
195
- if (IGNORED_RULES.has(ruleId)) {
196
- ignoredRules.push({ ruleId, reason: IGNORED_RULES.get(ruleId) });
197
- continue;
198
- }
199
- if (!supportedRuleIds.has(ruleId)) {
200
- unsupportedRules.push(ruleId);
201
- continue;
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 addPatterns(target, value) {
286
- if (!value) {
287
- return;
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 (typeof pattern === "string") {
291
- target.add(pattern);
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() {