@utoo/lint 0.3.4 → 0.3.5

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.cjs CHANGED
@@ -14,6 +14,7 @@ const version = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8")
14
14
 
15
15
  const LINTABLE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"]);
16
16
  const MAX_AUTOFIX_PASSES = 10;
17
+ const CONFIG_DISCOVERY_CACHE = Symbol("configDiscoveryCache");
17
18
  const JS_KEYWORDS = new Set([
18
19
  "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete",
19
20
  "do", "else", "export", "extends", "finally", "for", "function", "if", "import", "in",
@@ -569,7 +570,7 @@ class UtooLint {
569
570
  }
570
571
 
571
572
  async isPathIgnored(filePath) {
572
- return isPathIgnored(filePath, mergeLintOptions(this.options, {}));
573
+ return isPathIgnored(filePath, withConfigCache(mergeLintOptions(this.options, {})));
573
574
  }
574
575
 
575
576
  async calculateConfigForFile(filePath) {
@@ -577,7 +578,7 @@ class UtooLint {
577
578
  }
578
579
 
579
580
  async findConfigFile(filePath) {
580
- const options = eslintConstructorOptions(this.options);
581
+ const options = withConfigCache(eslintConstructorOptions(this.options));
581
582
  if (options.noConfig) {
582
583
  return undefined;
583
584
  }
@@ -711,7 +712,7 @@ class CLIEngine {
711
712
  }
712
713
 
713
714
  isPathIgnored(filePath) {
714
- return isPathIgnored(filePath, eslintConstructorOptions(this.options));
715
+ return isPathIgnored(filePath, withConfigCache(eslintConstructorOptions(this.options)));
715
716
  }
716
717
 
717
718
  getConfigForFile(filePath) {
@@ -1210,6 +1211,12 @@ function lintFiles(paths, options = {}) {
1210
1211
  return { files: 0, filePaths: [], diagnostics: ignoredDiagnostics, suppressedDiagnostics: [], exitCode: exitCodeForDiagnostics(ignoredDiagnostics) };
1211
1212
  }
1212
1213
 
1214
+ const nativeFlatOptions = nativeFlatConfigOptions(options);
1215
+ if (nativeFlatOptions) {
1216
+ const report = runNativeLintReport(lintPaths, nativeFlatOptions);
1217
+ return finalizeLintReport(report, ignoredDiagnostics, options);
1218
+ }
1219
+
1213
1220
  const configRuns = nativeConfigRunsForFiles(expandNativeConfigRunPaths(lintPaths, options), options);
1214
1221
  if (configRuns) {
1215
1222
  const report = mergeNativeReports(
@@ -1222,6 +1229,76 @@ function lintFiles(paths, options = {}) {
1222
1229
  return finalizeLintReport(report, ignoredDiagnostics, options);
1223
1230
  }
1224
1231
 
1232
+ function nativeFlatConfigOptions(options) {
1233
+ if (options.rules || options.extraArgs?.some((arg) => arg.startsWith("--") && arg !== "--fix" && arg !== "--fix-dry-run")) {
1234
+ return undefined;
1235
+ }
1236
+
1237
+ const fileConfigPath = options.noConfig ? undefined : configPathForOptions(options);
1238
+ const inlineConfigs = [options.baseConfig, options.overrideConfig].filter(Boolean);
1239
+ if (fileConfigPath && inlineConfigs.length > 0) {
1240
+ return undefined;
1241
+ }
1242
+
1243
+ let config;
1244
+ let configRoot;
1245
+ if (fileConfigPath) {
1246
+ config = readConfig(fileConfigPath, options.cwd, options.configCache);
1247
+ configRoot = dirname(fileConfigPath);
1248
+ } else if (inlineConfigs.length > 0) {
1249
+ config = inlineConfigs.length === 1 ? inlineConfigs[0] : inlineConfigs;
1250
+ configRoot = resolvePath(options.cwd ?? process.cwd());
1251
+ } else {
1252
+ return undefined;
1253
+ }
1254
+
1255
+ if (configCanUseNativeRulesFastPath(config)) {
1256
+ return undefined;
1257
+ }
1258
+
1259
+ const serializedConfig = nativeSerializableConfig(config);
1260
+ return {
1261
+ ...options,
1262
+ nativeFlatConfigData: Array.isArray(serializedConfig) ? serializedConfig : [serializedConfig],
1263
+ nativeConfigRoot: configRoot,
1264
+ nativeConfigCwd: resolvePath(options.cwd ?? process.cwd())
1265
+ };
1266
+ }
1267
+
1268
+ function configCanUseNativeRulesFastPath(config) {
1269
+ const entries = Array.isArray(config) ? config : [config];
1270
+ let hasEnabledRule = false;
1271
+ for (const entry of entries) {
1272
+ if (!entry || typeof entry !== "object" || Object.keys(entry.settings ?? {}).length > 0) {
1273
+ return false;
1274
+ }
1275
+ for (const ruleConfig of Object.values(entry.rules ?? {})) {
1276
+ if (ruleConfigSeverity(ruleConfig) === 0) {
1277
+ continue;
1278
+ }
1279
+ if (!isPureRuleSeverity(ruleConfig)) {
1280
+ return false;
1281
+ }
1282
+ hasEnabledRule = true;
1283
+ }
1284
+ }
1285
+ return hasEnabledRule;
1286
+ }
1287
+
1288
+ function nativeSerializableConfig(config) {
1289
+ if (Array.isArray(config)) {
1290
+ return config.map(nativeSerializableConfig);
1291
+ }
1292
+ if (!config || typeof config !== "object") {
1293
+ return config;
1294
+ }
1295
+ return Object.fromEntries(
1296
+ ["$schema", "name", "files", "ignores", "rules", "settings"]
1297
+ .filter((key) => config[key] !== undefined)
1298
+ .map((key) => [key, config[key]])
1299
+ );
1300
+ }
1301
+
1225
1302
  function expandNativeConfigRunPaths(paths, options) {
1226
1303
  const cwd = options.cwd ?? process.cwd();
1227
1304
  const patterns = ignoreDisabled(options) ? [] : ignorePatternsForOptions(options, cwd);
@@ -1326,6 +1403,22 @@ function nativeConfigRunsForFiles(paths, options) {
1326
1403
  };
1327
1404
  }
1328
1405
 
1406
+ const selectedRules = severityOnlyNativeRuleNames(group.rules, group.settings);
1407
+ if (selectedRules) {
1408
+ return {
1409
+ paths: group.paths,
1410
+ options: {
1411
+ ...options,
1412
+ config: undefined,
1413
+ noConfig: true,
1414
+ baseConfig: undefined,
1415
+ overrideConfig: undefined,
1416
+ rules: selectedRules,
1417
+ forceMaterializedConfig: undefined
1418
+ }
1419
+ };
1420
+ }
1421
+
1329
1422
  return {
1330
1423
  paths: group.paths,
1331
1424
  options: {
@@ -1343,6 +1436,40 @@ function nativeConfigRunsForFiles(paths, options) {
1343
1436
  });
1344
1437
  }
1345
1438
 
1439
+ function severityOnlyNativeRuleNames(rules, settings) {
1440
+ if (Object.keys(settings ?? {}).length > 0) {
1441
+ return undefined;
1442
+ }
1443
+
1444
+ const selected = [];
1445
+ for (const [rule, config] of Object.entries(rules ?? {})) {
1446
+ if (ruleConfigSeverity(config) === 0) {
1447
+ continue;
1448
+ }
1449
+ if (!isPureRuleSeverity(config)) {
1450
+ return undefined;
1451
+ }
1452
+ selected.push(rule);
1453
+ }
1454
+ return selected.length > 0 ? selected : undefined;
1455
+ }
1456
+
1457
+ function isPureRuleSeverity(config) {
1458
+ if (Array.isArray(config)) {
1459
+ if (config.length !== 1) {
1460
+ return false;
1461
+ }
1462
+ [config] = config;
1463
+ }
1464
+ if (typeof config === "boolean") {
1465
+ return true;
1466
+ }
1467
+ if (typeof config === "number") {
1468
+ return config === 0 || config === 1 || config === 2;
1469
+ }
1470
+ return typeof config === "string" && ["off", "warn", "warning", "error", "0", "1", "2"].includes(config.toLowerCase());
1471
+ }
1472
+
1346
1473
  function allDisabledNativeRules(rules) {
1347
1474
  return {
1348
1475
  ...Object.fromEntries(NATIVE_RULE_IDS.map((rule) => [rule, "off"])),
@@ -1459,7 +1586,7 @@ function buildNativeLintArgs(paths, options) {
1459
1586
  if (options.noConfig) {
1460
1587
  cliArgs.push("--no-config");
1461
1588
  }
1462
- if (!options.rules && !options.forceMaterializedConfig && (options.config || options.baseConfig || options.overrideConfig)) {
1589
+ if (!options.nativeFlatConfigData && !options.rules && !options.forceMaterializedConfig && (options.config || options.baseConfig || options.overrideConfig)) {
1463
1590
  const configForRuleSelection = options.noConfig
1464
1591
  ? undefined
1465
1592
  : options.config ? readConfig(options.config, options.cwd, options.configCache) : undefined;
@@ -1475,6 +1602,12 @@ function buildNativeLintArgs(paths, options) {
1475
1602
  if (options.config) {
1476
1603
  cliArgs.push(`--config=${options.config}`);
1477
1604
  }
1605
+ if (options.nativeConfigRoot) {
1606
+ cliArgs.push(`--config-root=${options.nativeConfigRoot}`);
1607
+ }
1608
+ if (options.nativeConfigCwd) {
1609
+ cliArgs.push(`--config-cwd=${options.nativeConfigCwd}`);
1610
+ }
1478
1611
  if (options.rules) {
1479
1612
  const rules = Array.isArray(options.rules) ? options.rules.join(",") : options.rules;
1480
1613
  if (rules) {
@@ -1509,9 +1642,17 @@ function mergeLintOptions(base, override) {
1509
1642
  }
1510
1643
 
1511
1644
  function withConfigCache(options) {
1512
- // Executable configs are otherwise re-evaluated at every per-file lookup.
1513
- // Keep the cache on the options object so one public invocation shares it.
1514
- return options.configCache ? options : { ...options, configCache: new Map() };
1645
+ if (options.configCache && options[CONFIG_DISCOVERY_CACHE]) {
1646
+ return options;
1647
+ }
1648
+
1649
+ return {
1650
+ ...options,
1651
+ // Executable configs are otherwise re-evaluated at every per-file lookup.
1652
+ configCache: options.configCache ?? new Map(),
1653
+ // Discovery results must not survive into a later public invocation.
1654
+ [CONFIG_DISCOVERY_CACHE]: new Map()
1655
+ };
1515
1656
  }
1516
1657
 
1517
1658
  function eslintConstructorOptions(options) {
@@ -1852,7 +1993,7 @@ function configPathForOptions(options) {
1852
1993
  }
1853
1994
 
1854
1995
  const cwd = options.cwd ?? process.cwd();
1855
- return configPathFromDirectory(cwd);
1996
+ return configPathFromDirectory(cwd, options);
1856
1997
  }
1857
1998
 
1858
1999
  function configPathForFile(options, filePath) {
@@ -1862,7 +2003,7 @@ function configPathForFile(options, filePath) {
1862
2003
  if (options.config) {
1863
2004
  return resolvePath(options.cwd ?? process.cwd(), options.config);
1864
2005
  }
1865
- return configPathFromDirectory(configSearchDirectoryForFile(filePath, options.cwd));
2006
+ return configPathFromDirectory(configSearchDirectoryForFile(filePath, options.cwd), options);
1866
2007
  }
1867
2008
 
1868
2009
  function configSearchDirectoryForFile(filePath, cwd) {
@@ -1877,11 +2018,29 @@ function configSearchDirectoryForFile(filePath, cwd) {
1877
2018
  return dirname(absolute);
1878
2019
  }
1879
2020
 
1880
- function configPathFromDirectory(directory) {
1881
- return findConfigPathFromDirectory(directory);
2021
+ function configPathFromDirectory(directory, options) {
2022
+ return findConfigPathFromDirectory(directory, options[CONFIG_DISCOVERY_CACHE]);
1882
2023
  }
1883
2024
 
1884
2025
  function withTemporaryConfig(options, callback) {
2026
+ if (options.nativeFlatConfigData !== undefined) {
2027
+ const tmp = mkdtempSync(join(tmpdir(), "utoo-lint-flat-config-"));
2028
+ const configPath = join(tmp, "utlint.config.json");
2029
+ try {
2030
+ writeFileSync(configPath, JSON.stringify(options.nativeFlatConfigData));
2031
+ return callback({
2032
+ ...options,
2033
+ config: configPath,
2034
+ noConfig: false,
2035
+ baseConfig: undefined,
2036
+ overrideConfig: undefined,
2037
+ forceMaterializedConfig: true
2038
+ });
2039
+ } finally {
2040
+ rmSync(tmp, { recursive: true, force: true });
2041
+ }
2042
+ }
2043
+
1885
2044
  const fileConfigPath = !options.noConfig ? configPathForOptions(options) : undefined;
1886
2045
  const shouldMaterializeFileConfig = Boolean(fileConfigPath);
1887
2046
  const fileConfig = shouldMaterializeFileConfig
@@ -2063,15 +2222,20 @@ class Linter {
2063
2222
  filename: filePath,
2064
2223
  config: calculatedConfig({ cwd: verifyOptions.cwd, noConfig: true, overrideConfig: config }, normalizedFilePath)
2065
2224
  });
2225
+ const nativeRuleFilter = applyDisableDirectives(
2226
+ (report.diagnostics ?? []).map((diagnostic) => diagnosticToESLintMessage(diagnostic, ruleSeverities)),
2227
+ sourceCode
2228
+ );
2066
2229
  const customRuleFilter = applyDisableDirectives(customRuleMessages, sourceCode);
2067
2230
  this.suppressedMessages = [
2068
2231
  ...(report.suppressedDiagnostics ?? []).map((diagnostic) => suppressedDiagnosticToESLintMessage(diagnostic, ruleSeverities)),
2232
+ ...nativeRuleFilter.suppressedMessages,
2069
2233
  ...customRuleFilter.suppressedMessages
2070
2234
  ];
2071
2235
  this.times = { passes: [] };
2072
2236
  this.fixPassCount = 0;
2073
2237
  return [
2074
- ...(report.diagnostics ?? []).map((diagnostic) => diagnosticToESLintMessage(diagnostic, ruleSeverities)),
2238
+ ...nativeRuleFilter.messages,
2075
2239
  ...customRuleFilter.messages
2076
2240
  ];
2077
2241
  }
@@ -2362,6 +2526,7 @@ function applyDisableDirectives(messages, sourceCode) {
2362
2526
  }
2363
2527
  const directives = sourceCode.getDisableDirectives()
2364
2528
  .filter((directive) => directive.node?.loc?.start?.line)
2529
+ .filter((directive) => directive.type !== "eslint-disable-line" || directive.node.loc.start.line === directive.node.loc.end.line)
2365
2530
  .sort((left, right) => left.node.loc.start.line - right.node.loc.start.line);
2366
2531
  const utlintDirectives = sourceCode.getAllComments()
2367
2532
  .map((comment) => utlintDirectiveFromComment(comment, sourceCode))
@@ -2374,6 +2539,10 @@ function applyDisableDirectives(messages, sourceCode) {
2374
2539
  const kept = [];
2375
2540
  const suppressed = [];
2376
2541
  for (const message of messages) {
2542
+ if (!message.ruleId || message.ruleId === "parse" || message.ruleId === "io") {
2543
+ kept.push(message);
2544
+ continue;
2545
+ }
2377
2546
  const directive = utlintDirectiveForMessage(message, utlintDirectives) ?? disableDirectiveForMessage(message, directives);
2378
2547
  if (directive) {
2379
2548
  suppressed.push({
@@ -3954,12 +4123,38 @@ function reportToESLintResults(report, textOptions = {}) {
3954
4123
  }
3955
4124
 
3956
4125
  for (const result of byFile.values()) {
4126
+ applyCompatibilityDisableDirectives(result, textOptions);
3957
4127
  finalizeESLintResult(result);
3958
4128
  }
3959
4129
 
3960
4130
  return [...byFile.values()];
3961
4131
  }
3962
4132
 
4133
+ function applyCompatibilityDisableDirectives(result, textOptions) {
4134
+ if (result.messages.length === 0) {
4135
+ return;
4136
+ }
4137
+
4138
+ let source = result.source;
4139
+ if (typeof source !== "string" && textOptions.filePath === result.filePath) {
4140
+ source = textOptions.source;
4141
+ }
4142
+ if (typeof source !== "string") {
4143
+ try {
4144
+ source = readFileSync(result.filePath, "utf8");
4145
+ } catch {
4146
+ return;
4147
+ }
4148
+ }
4149
+ if (!source.includes("eslint-")) {
4150
+ return;
4151
+ }
4152
+
4153
+ const filtered = applyDisableDirectives(result.messages, createLinterSourceCode(source));
4154
+ result.messages = filtered.messages;
4155
+ result.suppressedMessages.push(...filtered.suppressedMessages);
4156
+ }
4157
+
3963
4158
  function normalizeReportDiagnostics(diagnostics, options = {}) {
3964
4159
  return (diagnostics ?? []).flatMap((diagnostic) => {
3965
4160
  if (!diagnostic?.ruleId) {
@@ -4709,10 +4904,12 @@ function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
4709
4904
  function suppressedDiagnosticToESLintMessage(diagnostic, ruleSeverities) {
4710
4905
  return {
4711
4906
  ...diagnosticToESLintMessage(diagnostic, ruleSeverities),
4712
- suppressions: [{
4713
- kind: diagnostic.suppression?.kind ?? "directive",
4714
- justification: diagnostic.suppression?.justification ?? ""
4715
- }]
4907
+ suppressions: diagnostic.suppressions?.length > 0
4908
+ ? diagnostic.suppressions
4909
+ : [{
4910
+ kind: diagnostic.suppression?.kind ?? "directive",
4911
+ justification: diagnostic.suppression?.justification ?? ""
4912
+ }]
4716
4913
  };
4717
4914
  }
4718
4915
 
package/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface LintSuppression {
17
17
  justification: string;
18
18
  }
19
19
 
20
- export interface LintDiagnostic extends Omit<LintMessage, "severity" | "suppressions"> {
20
+ export interface LintDiagnostic extends Omit<LintMessage, "severity"> {
21
21
  severity: "warning" | "error";
22
22
  suppression?: LintSuppression;
23
23
  }
package/index.js CHANGED
@@ -17,6 +17,7 @@ export const version = JSON.parse(readFileSync(new URL("./package.json", import.
17
17
 
18
18
  const LINTABLE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"]);
19
19
  const MAX_AUTOFIX_PASSES = 10;
20
+ const CONFIG_DISCOVERY_CACHE = Symbol("configDiscoveryCache");
20
21
  const JS_KEYWORDS = new Set([
21
22
  "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete",
22
23
  "do", "else", "export", "extends", "finally", "for", "function", "if", "import", "in",
@@ -572,7 +573,7 @@ export class UtooLint {
572
573
  }
573
574
 
574
575
  async isPathIgnored(filePath) {
575
- return isPathIgnored(filePath, mergeLintOptions(this.options, {}));
576
+ return isPathIgnored(filePath, withConfigCache(mergeLintOptions(this.options, {})));
576
577
  }
577
578
 
578
579
  async calculateConfigForFile(filePath) {
@@ -580,7 +581,7 @@ export class UtooLint {
580
581
  }
581
582
 
582
583
  async findConfigFile(filePath) {
583
- const options = eslintConstructorOptions(this.options);
584
+ const options = withConfigCache(eslintConstructorOptions(this.options));
584
585
  if (options.noConfig) {
585
586
  return undefined;
586
587
  }
@@ -679,15 +680,20 @@ export class Linter {
679
680
  filename: filePath,
680
681
  config: calculatedConfig({ cwd: verifyOptions.cwd, noConfig: true, overrideConfig: config }, normalizedFilePath)
681
682
  });
683
+ const nativeRuleFilter = applyDisableDirectives(
684
+ (report.diagnostics ?? []).map((diagnostic) => diagnosticToESLintMessage(diagnostic, ruleSeverities)),
685
+ sourceCode
686
+ );
682
687
  const customRuleFilter = applyDisableDirectives(customRuleMessages, sourceCode);
683
688
  this.suppressedMessages = [
684
689
  ...(report.suppressedDiagnostics ?? []).map((diagnostic) => suppressedDiagnosticToESLintMessage(diagnostic, ruleSeverities)),
690
+ ...nativeRuleFilter.suppressedMessages,
685
691
  ...customRuleFilter.suppressedMessages
686
692
  ];
687
693
  this.times = { passes: [] };
688
694
  this.fixPassCount = 0;
689
695
  return [
690
- ...(report.diagnostics ?? []).map((diagnostic) => diagnosticToESLintMessage(diagnostic, ruleSeverities)),
696
+ ...nativeRuleFilter.messages,
691
697
  ...customRuleFilter.messages
692
698
  ];
693
699
  }
@@ -978,6 +984,7 @@ function applyDisableDirectives(messages, sourceCode) {
978
984
  }
979
985
  const directives = sourceCode.getDisableDirectives()
980
986
  .filter((directive) => directive.node?.loc?.start?.line)
987
+ .filter((directive) => directive.type !== "eslint-disable-line" || directive.node.loc.start.line === directive.node.loc.end.line)
981
988
  .sort((left, right) => left.node.loc.start.line - right.node.loc.start.line);
982
989
  const utlintDirectives = sourceCode.getAllComments()
983
990
  .map((comment) => utlintDirectiveFromComment(comment, sourceCode))
@@ -990,6 +997,10 @@ function applyDisableDirectives(messages, sourceCode) {
990
997
  const kept = [];
991
998
  const suppressed = [];
992
999
  for (const message of messages) {
1000
+ if (!message.ruleId || message.ruleId === "parse" || message.ruleId === "io") {
1001
+ kept.push(message);
1002
+ continue;
1003
+ }
993
1004
  const directive = utlintDirectiveForMessage(message, utlintDirectives) ?? disableDirectiveForMessage(message, directives);
994
1005
  if (directive) {
995
1006
  suppressed.push({
@@ -2600,7 +2611,7 @@ export class CLIEngine {
2600
2611
  }
2601
2612
 
2602
2613
  isPathIgnored(filePath) {
2603
- return isPathIgnored(filePath, eslintConstructorOptions(this.options));
2614
+ return isPathIgnored(filePath, withConfigCache(eslintConstructorOptions(this.options)));
2604
2615
  }
2605
2616
 
2606
2617
  getConfigForFile(filePath) {
@@ -3099,6 +3110,12 @@ export function lintFiles(paths, options = {}) {
3099
3110
  return { files: 0, filePaths: [], diagnostics: ignoredDiagnostics, suppressedDiagnostics: [], exitCode: exitCodeForDiagnostics(ignoredDiagnostics) };
3100
3111
  }
3101
3112
 
3113
+ const nativeFlatOptions = nativeFlatConfigOptions(options);
3114
+ if (nativeFlatOptions) {
3115
+ const report = runNativeLintReport(lintPaths, nativeFlatOptions);
3116
+ return finalizeLintReport(report, ignoredDiagnostics, options);
3117
+ }
3118
+
3102
3119
  const configRuns = nativeConfigRunsForFiles(expandNativeConfigRunPaths(lintPaths, options), options);
3103
3120
  if (configRuns) {
3104
3121
  const report = mergeNativeReports(
@@ -3111,6 +3128,76 @@ export function lintFiles(paths, options = {}) {
3111
3128
  return finalizeLintReport(report, ignoredDiagnostics, options);
3112
3129
  }
3113
3130
 
3131
+ function nativeFlatConfigOptions(options) {
3132
+ if (options.rules || options.extraArgs?.some((arg) => arg.startsWith("--") && arg !== "--fix" && arg !== "--fix-dry-run")) {
3133
+ return undefined;
3134
+ }
3135
+
3136
+ const fileConfigPath = options.noConfig ? undefined : configPathForOptions(options);
3137
+ const inlineConfigs = [options.baseConfig, options.overrideConfig].filter(Boolean);
3138
+ if (fileConfigPath && inlineConfigs.length > 0) {
3139
+ return undefined;
3140
+ }
3141
+
3142
+ let config;
3143
+ let configRoot;
3144
+ if (fileConfigPath) {
3145
+ config = readConfig(fileConfigPath, options.cwd, options.configCache);
3146
+ configRoot = dirname(fileConfigPath);
3147
+ } else if (inlineConfigs.length > 0) {
3148
+ config = inlineConfigs.length === 1 ? inlineConfigs[0] : inlineConfigs;
3149
+ configRoot = resolvePath(options.cwd ?? process.cwd());
3150
+ } else {
3151
+ return undefined;
3152
+ }
3153
+
3154
+ if (configCanUseNativeRulesFastPath(config)) {
3155
+ return undefined;
3156
+ }
3157
+
3158
+ const serializedConfig = nativeSerializableConfig(config);
3159
+ return {
3160
+ ...options,
3161
+ nativeFlatConfigData: Array.isArray(serializedConfig) ? serializedConfig : [serializedConfig],
3162
+ nativeConfigRoot: configRoot,
3163
+ nativeConfigCwd: resolvePath(options.cwd ?? process.cwd())
3164
+ };
3165
+ }
3166
+
3167
+ function configCanUseNativeRulesFastPath(config) {
3168
+ const entries = Array.isArray(config) ? config : [config];
3169
+ let hasEnabledRule = false;
3170
+ for (const entry of entries) {
3171
+ if (!entry || typeof entry !== "object" || Object.keys(entry.settings ?? {}).length > 0) {
3172
+ return false;
3173
+ }
3174
+ for (const ruleConfig of Object.values(entry.rules ?? {})) {
3175
+ if (ruleConfigSeverity(ruleConfig) === 0) {
3176
+ continue;
3177
+ }
3178
+ if (!isPureRuleSeverity(ruleConfig)) {
3179
+ return false;
3180
+ }
3181
+ hasEnabledRule = true;
3182
+ }
3183
+ }
3184
+ return hasEnabledRule;
3185
+ }
3186
+
3187
+ function nativeSerializableConfig(config) {
3188
+ if (Array.isArray(config)) {
3189
+ return config.map(nativeSerializableConfig);
3190
+ }
3191
+ if (!config || typeof config !== "object") {
3192
+ return config;
3193
+ }
3194
+ return Object.fromEntries(
3195
+ ["$schema", "name", "files", "ignores", "rules", "settings"]
3196
+ .filter((key) => config[key] !== undefined)
3197
+ .map((key) => [key, config[key]])
3198
+ );
3199
+ }
3200
+
3114
3201
  function expandNativeConfigRunPaths(paths, options) {
3115
3202
  const cwd = options.cwd ?? process.cwd();
3116
3203
  const patterns = ignoreDisabled(options) ? [] : ignorePatternsForOptions(options, cwd);
@@ -3215,6 +3302,22 @@ function nativeConfigRunsForFiles(paths, options) {
3215
3302
  };
3216
3303
  }
3217
3304
 
3305
+ const selectedRules = severityOnlyNativeRuleNames(group.rules, group.settings);
3306
+ if (selectedRules) {
3307
+ return {
3308
+ paths: group.paths,
3309
+ options: {
3310
+ ...options,
3311
+ config: undefined,
3312
+ noConfig: true,
3313
+ baseConfig: undefined,
3314
+ overrideConfig: undefined,
3315
+ rules: selectedRules,
3316
+ forceMaterializedConfig: undefined
3317
+ }
3318
+ };
3319
+ }
3320
+
3218
3321
  return {
3219
3322
  paths: group.paths,
3220
3323
  options: {
@@ -3232,6 +3335,40 @@ function nativeConfigRunsForFiles(paths, options) {
3232
3335
  });
3233
3336
  }
3234
3337
 
3338
+ function severityOnlyNativeRuleNames(rules, settings) {
3339
+ if (Object.keys(settings ?? {}).length > 0) {
3340
+ return undefined;
3341
+ }
3342
+
3343
+ const selected = [];
3344
+ for (const [rule, config] of Object.entries(rules ?? {})) {
3345
+ if (ruleConfigSeverity(config) === 0) {
3346
+ continue;
3347
+ }
3348
+ if (!isPureRuleSeverity(config)) {
3349
+ return undefined;
3350
+ }
3351
+ selected.push(rule);
3352
+ }
3353
+ return selected.length > 0 ? selected : undefined;
3354
+ }
3355
+
3356
+ function isPureRuleSeverity(config) {
3357
+ if (Array.isArray(config)) {
3358
+ if (config.length !== 1) {
3359
+ return false;
3360
+ }
3361
+ [config] = config;
3362
+ }
3363
+ if (typeof config === "boolean") {
3364
+ return true;
3365
+ }
3366
+ if (typeof config === "number") {
3367
+ return config === 0 || config === 1 || config === 2;
3368
+ }
3369
+ return typeof config === "string" && ["off", "warn", "warning", "error", "0", "1", "2"].includes(config.toLowerCase());
3370
+ }
3371
+
3235
3372
  function allDisabledNativeRules(rules) {
3236
3373
  return {
3237
3374
  ...Object.fromEntries(NATIVE_RULE_IDS.map((rule) => [rule, "off"])),
@@ -3348,7 +3485,7 @@ function buildNativeLintArgs(paths, options) {
3348
3485
  if (options.noConfig) {
3349
3486
  cliArgs.push("--no-config");
3350
3487
  }
3351
- if (!options.rules && !options.forceMaterializedConfig && (options.config || options.baseConfig || options.overrideConfig)) {
3488
+ if (!options.nativeFlatConfigData && !options.rules && !options.forceMaterializedConfig && (options.config || options.baseConfig || options.overrideConfig)) {
3352
3489
  const configForRuleSelection = options.noConfig
3353
3490
  ? undefined
3354
3491
  : options.config ? readConfig(options.config, options.cwd, options.configCache) : undefined;
@@ -3364,6 +3501,12 @@ function buildNativeLintArgs(paths, options) {
3364
3501
  if (options.config) {
3365
3502
  cliArgs.push(`--config=${options.config}`);
3366
3503
  }
3504
+ if (options.nativeConfigRoot) {
3505
+ cliArgs.push(`--config-root=${options.nativeConfigRoot}`);
3506
+ }
3507
+ if (options.nativeConfigCwd) {
3508
+ cliArgs.push(`--config-cwd=${options.nativeConfigCwd}`);
3509
+ }
3367
3510
  if (options.rules) {
3368
3511
  const rules = Array.isArray(options.rules) ? options.rules.join(",") : options.rules;
3369
3512
  if (rules) {
@@ -3398,9 +3541,17 @@ function mergeLintOptions(base, override) {
3398
3541
  }
3399
3542
 
3400
3543
  function withConfigCache(options) {
3401
- // Executable configs are otherwise re-evaluated at every per-file lookup.
3402
- // Keep the cache on the options object so one public invocation shares it.
3403
- return options.configCache ? options : { ...options, configCache: new Map() };
3544
+ if (options.configCache && options[CONFIG_DISCOVERY_CACHE]) {
3545
+ return options;
3546
+ }
3547
+
3548
+ return {
3549
+ ...options,
3550
+ // Executable configs are otherwise re-evaluated at every per-file lookup.
3551
+ configCache: options.configCache ?? new Map(),
3552
+ // Discovery results must not survive into a later public invocation.
3553
+ [CONFIG_DISCOVERY_CACHE]: new Map()
3554
+ };
3404
3555
  }
3405
3556
 
3406
3557
  function eslintConstructorOptions(options) {
@@ -3741,7 +3892,7 @@ function configPathForOptions(options) {
3741
3892
  }
3742
3893
 
3743
3894
  const cwd = options.cwd ?? process.cwd();
3744
- return configPathFromDirectory(cwd);
3895
+ return configPathFromDirectory(cwd, options);
3745
3896
  }
3746
3897
 
3747
3898
  function configPathForFile(options, filePath) {
@@ -3751,7 +3902,7 @@ function configPathForFile(options, filePath) {
3751
3902
  if (options.config) {
3752
3903
  return resolvePath(options.cwd ?? process.cwd(), options.config);
3753
3904
  }
3754
- return configPathFromDirectory(configSearchDirectoryForFile(filePath, options.cwd));
3905
+ return configPathFromDirectory(configSearchDirectoryForFile(filePath, options.cwd), options);
3755
3906
  }
3756
3907
 
3757
3908
  function configSearchDirectoryForFile(filePath, cwd) {
@@ -3766,11 +3917,29 @@ function configSearchDirectoryForFile(filePath, cwd) {
3766
3917
  return dirname(absolute);
3767
3918
  }
3768
3919
 
3769
- function configPathFromDirectory(directory) {
3770
- return findConfigPathFromDirectory(directory);
3920
+ function configPathFromDirectory(directory, options) {
3921
+ return findConfigPathFromDirectory(directory, options[CONFIG_DISCOVERY_CACHE]);
3771
3922
  }
3772
3923
 
3773
3924
  function withTemporaryConfig(options, callback) {
3925
+ if (options.nativeFlatConfigData !== undefined) {
3926
+ const tmp = mkdtempSync(join(tmpdir(), "utoo-lint-flat-config-"));
3927
+ const configPath = join(tmp, "utlint.config.json");
3928
+ try {
3929
+ writeFileSync(configPath, JSON.stringify(options.nativeFlatConfigData));
3930
+ return callback({
3931
+ ...options,
3932
+ config: configPath,
3933
+ noConfig: false,
3934
+ baseConfig: undefined,
3935
+ overrideConfig: undefined,
3936
+ forceMaterializedConfig: true
3937
+ });
3938
+ } finally {
3939
+ rmSync(tmp, { recursive: true, force: true });
3940
+ }
3941
+ }
3942
+
3774
3943
  const fileConfigPath = !options.noConfig ? configPathForOptions(options) : undefined;
3775
3944
  const shouldMaterializeFileConfig = Boolean(fileConfigPath);
3776
3945
  const fileConfig = shouldMaterializeFileConfig
@@ -3957,12 +4126,38 @@ function reportToESLintResults(report, textOptions = {}) {
3957
4126
  }
3958
4127
 
3959
4128
  for (const result of byFile.values()) {
4129
+ applyCompatibilityDisableDirectives(result, textOptions);
3960
4130
  finalizeESLintResult(result);
3961
4131
  }
3962
4132
 
3963
4133
  return [...byFile.values()];
3964
4134
  }
3965
4135
 
4136
+ function applyCompatibilityDisableDirectives(result, textOptions) {
4137
+ if (result.messages.length === 0) {
4138
+ return;
4139
+ }
4140
+
4141
+ let source = result.source;
4142
+ if (typeof source !== "string" && textOptions.filePath === result.filePath) {
4143
+ source = textOptions.source;
4144
+ }
4145
+ if (typeof source !== "string") {
4146
+ try {
4147
+ source = readFileSync(result.filePath, "utf8");
4148
+ } catch {
4149
+ return;
4150
+ }
4151
+ }
4152
+ if (!source.includes("eslint-")) {
4153
+ return;
4154
+ }
4155
+
4156
+ const filtered = applyDisableDirectives(result.messages, createLinterSourceCode(source));
4157
+ result.messages = filtered.messages;
4158
+ result.suppressedMessages.push(...filtered.suppressedMessages);
4159
+ }
4160
+
3966
4161
  function normalizeReportDiagnostics(diagnostics, options = {}) {
3967
4162
  return (diagnostics ?? []).flatMap((diagnostic) => {
3968
4163
  if (!diagnostic?.ruleId) {
@@ -4712,10 +4907,12 @@ function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
4712
4907
  function suppressedDiagnosticToESLintMessage(diagnostic, ruleSeverities) {
4713
4908
  return {
4714
4909
  ...diagnosticToESLintMessage(diagnostic, ruleSeverities),
4715
- suppressions: [{
4716
- kind: diagnostic.suppression?.kind ?? "directive",
4717
- justification: diagnostic.suppression?.justification ?? ""
4718
- }]
4910
+ suppressions: diagnostic.suppressions?.length > 0
4911
+ ? diagnostic.suppressions
4912
+ : [{
4913
+ kind: diagnostic.suppression?.kind ?? "directive",
4914
+ justification: diagnostic.suppression?.justification ?? ""
4915
+ }]
4719
4916
  };
4720
4917
  }
4721
4918
 
@@ -13,23 +13,36 @@ const TYPESCRIPT_CONFIG_EXTENSIONS = new Set([".ts", ".mts", ".cts"]);
13
13
  const JAVASCRIPT_CONFIG_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]);
14
14
  const loaderPath = join(__dirname, "load-config.js");
15
15
 
16
- function findConfigPath(directory) {
16
+ function findConfigPath(directory, cache) {
17
17
  let current = resolvePath(directory);
18
+ const visited = [];
18
19
  while (true) {
20
+ if (cache?.has(current)) {
21
+ return cacheConfigPath(cache, visited, cache.get(current));
22
+ }
23
+
24
+ visited.push(current);
19
25
  for (const filename of CONFIG_FILENAMES) {
20
26
  const candidate = resolvePath(current, filename);
21
27
  if (isFile(candidate)) {
22
- return candidate;
28
+ return cacheConfigPath(cache, visited, candidate);
23
29
  }
24
30
  }
25
31
  const parent = dirname(current);
26
32
  if (parent === current) {
27
- return undefined;
33
+ return cacheConfigPath(cache, visited, undefined);
28
34
  }
29
35
  current = parent;
30
36
  }
31
37
  }
32
38
 
39
+ function cacheConfigPath(cache, directories, configPath) {
40
+ for (const directory of directories) {
41
+ cache?.set(directory, configPath);
42
+ }
43
+ return configPath;
44
+ }
45
+
33
46
  function isFile(path) {
34
47
  try {
35
48
  return statSync(path).isFile();
@@ -14,23 +14,36 @@ const TYPESCRIPT_CONFIG_EXTENSIONS = new Set([".ts", ".mts", ".cts"]);
14
14
  const JAVASCRIPT_CONFIG_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]);
15
15
  const loaderPath = fileURLToPath(new URL("./load-config.js", import.meta.url));
16
16
 
17
- export function findConfigPath(directory) {
17
+ export function findConfigPath(directory, cache) {
18
18
  let current = resolvePath(directory);
19
+ const visited = [];
19
20
  while (true) {
21
+ if (cache?.has(current)) {
22
+ return cacheConfigPath(cache, visited, cache.get(current));
23
+ }
24
+
25
+ visited.push(current);
20
26
  for (const filename of CONFIG_FILENAMES) {
21
27
  const candidate = resolvePath(current, filename);
22
28
  if (isFile(candidate)) {
23
- return candidate;
29
+ return cacheConfigPath(cache, visited, candidate);
24
30
  }
25
31
  }
26
32
  const parent = dirname(current);
27
33
  if (parent === current) {
28
- return undefined;
34
+ return cacheConfigPath(cache, visited, undefined);
29
35
  }
30
36
  current = parent;
31
37
  }
32
38
  }
33
39
 
40
+ function cacheConfigPath(cache, directories, configPath) {
41
+ for (const directory of directories) {
42
+ cache?.set(directory, configPath);
43
+ }
44
+ return configPath;
45
+ }
46
+
34
47
  function isFile(path) {
35
48
  try {
36
49
  return statSync(path).isFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utoo/lint",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "High-performance JavaScript and TypeScript linter powered by Yuku",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -76,11 +76,11 @@
76
76
  "use-at-your-own-risk.js"
77
77
  ],
78
78
  "optionalDependencies": {
79
- "@utoo/lint-darwin-arm64": "0.3.4",
80
- "@utoo/lint-darwin-x64": "0.3.4",
81
- "@utoo/lint-linux-arm64": "0.3.4",
82
- "@utoo/lint-linux-x64": "0.3.4",
83
- "@utoo/lint-win32-x64": "0.3.4"
79
+ "@utoo/lint-darwin-arm64": "0.3.5",
80
+ "@utoo/lint-darwin-x64": "0.3.5",
81
+ "@utoo/lint-linux-arm64": "0.3.5",
82
+ "@utoo/lint-linux-x64": "0.3.5",
83
+ "@utoo/lint-win32-x64": "0.3.5"
84
84
  },
85
85
  "dependencies": {
86
86
  "@eslint/eslintrc": "2.1.4",