@utoo/lint 0.1.4 → 0.2.1

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 CHANGED
@@ -41,6 +41,27 @@ Use machine-readable output:
41
41
  pnpm exec utoo-lint --format=json src
42
42
  ```
43
43
 
44
+ ## Autofix
45
+
46
+ Apply safe fixes from all enabled rules that support autofix:
47
+
48
+ ```bash
49
+ pnpm exec utoo-lint --fix src
50
+ ```
51
+
52
+ Preview the result without changing files:
53
+
54
+ ```bash
55
+ pnpm exec utoo-lint --fix-dry-run --format=json src
56
+ ```
57
+
58
+ `--fix` writes fixed source back to disk. `--fix-dry-run` never writes files;
59
+ with JSON output, changed sources are returned in the `outputs` array. Fixes run
60
+ until the source is stable, subject to a safety pass limit. Diagnostics remain
61
+ when a rule or a particular code shape cannot be fixed safely. The current
62
+ [rule status](https://github.com/fireairforce/utoo-lint/blob/main/docs/rule-status.md)
63
+ lists autofix coverage by rule.
64
+
44
65
  ## Configuration
45
66
 
46
67
  Create a `utoo.json` file:
@@ -97,6 +118,19 @@ manually.
97
118
 
98
119
  CommonJS is supported through `require("@utoo/lint")`.
99
120
 
121
+ Set `fix: true` to compute fixed output through the JavaScript API. Raw
122
+ `lintFiles()` and `lintText()` reports expose changed sources in `outputs`
123
+ without writing them. The ESLint-compatible API follows the usual two-step
124
+ flow:
125
+
126
+ ```js
127
+ import { ESLint } from "@utoo/lint";
128
+
129
+ const eslint = new ESLint({ fix: true });
130
+ const results = await eslint.lintFiles(["src"]);
131
+ await ESLint.outputFixes(results);
132
+ ```
133
+
100
134
  ## Binary Resolution
101
135
 
102
136
  `@utoo/lint` installs a platform-specific optional dependency containing the
package/index.cjs CHANGED
@@ -417,6 +417,7 @@ const BUILTIN_RULE_IDS = [
417
417
  "@typescript-eslint/prefer-namespace-keyword",
418
418
  "@typescript-eslint/restrict-plus-operands"
419
419
  ];
420
+ const FIXABLE_BUILTIN_RULE_IDS = new Set(["no-extra-semi", "@typescript-eslint/no-extra-semi"]);
420
421
  const BUILTIN_RULES = new Map(BUILTIN_RULE_IDS.map((ruleId) => [ruleId, createBuiltinRule(ruleId)]));
421
422
 
422
423
  class UtooLint {
@@ -882,12 +883,12 @@ function translateFishlintArgs(args = [], options = {}) {
882
883
  continue;
883
884
  }
884
885
  if (arg === "--fix") {
885
- warn("utoo-lint: fishlint --fix is ignored because utoo-lint does not apply fixes yet.");
886
+ translated.push("--fix");
886
887
  index += 1;
887
888
  continue;
888
889
  }
889
890
  if (arg === "--fix-dry-run") {
890
- warn("utoo-lint: fishlint --fix-dry-run is ignored because utoo-lint does not apply fixes yet.");
891
+ translated.push("--fix-dry-run");
891
892
  index += 1;
892
893
  continue;
893
894
  }
@@ -1067,6 +1068,7 @@ function lintFiles(paths, options = {}) {
1067
1068
  if (!resolvedOptions.deferDiagnosticConfigFiltering) {
1068
1069
  report.filePaths = normalizeReportFilePaths(report.filePaths, resolvedOptions);
1069
1070
  report.diagnostics = normalizeDiagnosticFilePaths(report.diagnostics, resolvedOptions);
1071
+ report.outputs = normalizeReportOutputs(report.outputs, resolvedOptions);
1070
1072
  report.diagnostics = normalizeReportDiagnostics(report.diagnostics, resolvedOptions);
1071
1073
  report.exitCode = exitCodeForDiagnostics(report.diagnostics);
1072
1074
  }
@@ -1115,6 +1117,10 @@ function lintText(code, options = {}) {
1115
1117
  deferDiagnosticConfigFiltering: true
1116
1118
  });
1117
1119
  report.filePaths = (report.filePaths ?? []).map((filePath) => (filePath === tempFile ? requestedPath : filePath));
1120
+ report.outputs = (report.outputs ?? []).map((fixed) => ({
1121
+ ...fixed,
1122
+ filePath: fixed.filePath === tempFile ? requestedPath : fixed.filePath
1123
+ }));
1118
1124
  report.diagnostics = report.diagnostics.map((diagnostic) => ({
1119
1125
  ...diagnostic,
1120
1126
  filePath: requestedPath
@@ -1165,6 +1171,10 @@ function buildNativeLintArgs(paths, options) {
1165
1171
  cliArgs.push(`--format=${options.format}`);
1166
1172
  }
1167
1173
 
1174
+ if (options.fix) {
1175
+ cliArgs.push("--fix-dry-run");
1176
+ }
1177
+
1168
1178
  if (options.extraArgs) {
1169
1179
  cliArgs.push(...normalizeStringArray(options.extraArgs, "extraArgs"));
1170
1180
  }
@@ -1882,7 +1892,9 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
1882
1892
  if (customRules.length === 0) {
1883
1893
  return results;
1884
1894
  }
1885
- const sourceCode = createLinterSourceCode(code);
1895
+ let result = results.find((item) => item.filePath === filePath);
1896
+ const effectiveCode = typeof result?.output === "string" ? result.output : code;
1897
+ const sourceCode = createLinterSourceCode(effectiveCode);
1886
1898
  const messages = runCustomLinterRules(customRules, sourceCode, {
1887
1899
  cwd: options.cwd,
1888
1900
  filename: filePath,
@@ -1893,14 +1905,13 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
1893
1905
  return results;
1894
1906
  }
1895
1907
 
1896
- let result = results.find((item) => item.filePath === filePath);
1897
1908
  if (!result) {
1898
- result = emptyESLintResult(filePath, code);
1909
+ result = emptyESLintResult(filePath, effectiveCode);
1899
1910
  results.push(result);
1900
1911
  }
1901
1912
  result.messages.push(...filtered.messages);
1902
1913
  result.suppressedMessages.push(...filtered.suppressedMessages);
1903
- applyResultFixes(result, code, options);
1914
+ applyResultFixes(result, effectiveCode, options);
1904
1915
  finalizeESLintResult(result);
1905
1916
  return results;
1906
1917
  }
@@ -1913,11 +1924,13 @@ function appendCustomLintFileMessages(results, config, options) {
1913
1924
  if (customRules.length === 0) {
1914
1925
  continue;
1915
1926
  }
1916
- let code;
1917
- try {
1918
- code = readFileSync(filePath, "utf8");
1919
- } catch {
1920
- continue;
1927
+ let code = result.output;
1928
+ if (typeof code !== "string") {
1929
+ try {
1930
+ code = readFileSync(filePath, "utf8");
1931
+ } catch {
1932
+ continue;
1933
+ }
1921
1934
  }
1922
1935
  const sourceCode = createLinterSourceCode(code);
1923
1936
  const messages = runCustomLinterRules(customRules, sourceCode, {
@@ -3471,6 +3484,14 @@ function reportToESLintResults(report, textOptions = {}) {
3471
3484
  byFile.get(filePath).messages.push(diagnosticToESLintMessage(diagnostic, ruleSeverities));
3472
3485
  }
3473
3486
 
3487
+ for (const fixed of report.outputs ?? []) {
3488
+ const filePath = textOptions.filePath ?? normalizeESLintFilePath(fixed.filePath, textOptions.cwd);
3489
+ if (!byFile.has(filePath)) {
3490
+ byFile.set(filePath, emptyESLintResult(filePath, textOptions.source));
3491
+ }
3492
+ byFile.get(filePath).output = fixed.output;
3493
+ }
3494
+
3474
3495
  if (textOptions.filePath && textOptions.includeEmptyTextResult !== false && !byFile.has(textOptions.filePath)) {
3475
3496
  byFile.set(textOptions.filePath, emptyESLintResult(textOptions.filePath, textOptions.source));
3476
3497
  }
@@ -3517,6 +3538,13 @@ function normalizeReportFilePaths(filePaths, options = {}) {
3517
3538
  return (filePaths ?? []).map((filePath) => normalizeESLintFilePath(filePath, options.cwd));
3518
3539
  }
3519
3540
 
3541
+ function normalizeReportOutputs(outputs, options = {}) {
3542
+ return (outputs ?? []).map((fixed) => ({
3543
+ ...fixed,
3544
+ filePath: normalizeESLintFilePath(fixed.filePath, options.cwd)
3545
+ }));
3546
+ }
3547
+
3520
3548
  function normalizeDiagnosticFilePaths(diagnostics, options = {}) {
3521
3549
  return (diagnostics ?? []).map((diagnostic) => ({
3522
3550
  ...diagnostic,
@@ -4035,11 +4063,15 @@ function getErrorResults(results) {
4035
4063
  }
4036
4064
 
4037
4065
  function ruleMetaForRuleId(ruleId) {
4038
- return {
4066
+ const meta = {
4039
4067
  docs: {
4040
4068
  url: ruleDocsUrl(ruleId)
4041
4069
  }
4042
4070
  };
4071
+ if (FIXABLE_BUILTIN_RULE_IDS.has(ruleId)) {
4072
+ meta.fixable = "code";
4073
+ }
4074
+ return meta;
4043
4075
  }
4044
4076
 
4045
4077
  function createBuiltinRule(ruleId) {
@@ -4114,7 +4146,7 @@ function emptyESLintResult(filePath, source) {
4114
4146
  }
4115
4147
 
4116
4148
  function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
4117
- return {
4149
+ const message = {
4118
4150
  ruleId: diagnostic.ruleId,
4119
4151
  severity: ruleSeverities?.get(diagnostic.ruleId) ?? (diagnostic.severity === "error" ? 2 : 1),
4120
4152
  message: diagnostic.message,
@@ -4122,6 +4154,14 @@ function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
4122
4154
  column: diagnostic.column,
4123
4155
  nodeType: null
4124
4156
  };
4157
+ const fixes = (diagnostic.fixes ?? []).map((fix) => ({
4158
+ range: fix.range,
4159
+ text: fix.text
4160
+ }));
4161
+ if (fixes.length > 0) {
4162
+ message.fix = fixes.length === 1 ? fixes[0] : fixes;
4163
+ }
4164
+ return message;
4125
4165
  }
4126
4166
 
4127
4167
  function finalizeESLintResult(result) {
package/index.d.ts CHANGED
@@ -38,6 +38,7 @@ export interface LintReport {
38
38
  files: number;
39
39
  filePaths: string[];
40
40
  diagnostics: LintMessage[];
41
+ outputs?: Array<{ filePath: string; output: string }>;
41
42
  exitCode: number;
42
43
  stderr?: string;
43
44
  }
package/index.js CHANGED
@@ -420,6 +420,7 @@ const BUILTIN_RULE_IDS = [
420
420
  "@typescript-eslint/prefer-namespace-keyword",
421
421
  "@typescript-eslint/restrict-plus-operands"
422
422
  ];
423
+ const FIXABLE_BUILTIN_RULE_IDS = new Set(["no-extra-semi", "@typescript-eslint/no-extra-semi"]);
423
424
  const BUILTIN_RULES = new Map(BUILTIN_RULE_IDS.map((ruleId) => [ruleId, createBuiltinRule(ruleId)]));
424
425
 
425
426
  export class UtooLint {
@@ -832,7 +833,9 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
832
833
  if (customRules.length === 0) {
833
834
  return results;
834
835
  }
835
- const sourceCode = createLinterSourceCode(code);
836
+ let result = results.find((item) => item.filePath === filePath);
837
+ const effectiveCode = typeof result?.output === "string" ? result.output : code;
838
+ const sourceCode = createLinterSourceCode(effectiveCode);
836
839
  const messages = runCustomLinterRules(customRules, sourceCode, {
837
840
  cwd: options.cwd,
838
841
  filename: filePath,
@@ -843,14 +846,13 @@ function appendCustomLintTextMessages(results, code, filePath, config, rules, op
843
846
  return results;
844
847
  }
845
848
 
846
- let result = results.find((item) => item.filePath === filePath);
847
849
  if (!result) {
848
- result = emptyESLintResult(filePath, code);
850
+ result = emptyESLintResult(filePath, effectiveCode);
849
851
  results.push(result);
850
852
  }
851
853
  result.messages.push(...filtered.messages);
852
854
  result.suppressedMessages.push(...filtered.suppressedMessages);
853
- applyResultFixes(result, code, options);
855
+ applyResultFixes(result, effectiveCode, options);
854
856
  finalizeESLintResult(result);
855
857
  return results;
856
858
  }
@@ -863,11 +865,13 @@ function appendCustomLintFileMessages(results, config, options) {
863
865
  if (customRules.length === 0) {
864
866
  continue;
865
867
  }
866
- let code;
867
- try {
868
- code = readFileSync(filePath, "utf8");
869
- } catch {
870
- continue;
868
+ let code = result.output;
869
+ if (typeof code !== "string") {
870
+ try {
871
+ code = readFileSync(filePath, "utf8");
872
+ } catch {
873
+ continue;
874
+ }
871
875
  }
872
876
  const sourceCode = createLinterSourceCode(code);
873
877
  const messages = runCustomLinterRules(customRules, sourceCode, {
@@ -2715,12 +2719,12 @@ export function translateFishlintArgs(args = [], options = {}) {
2715
2719
  continue;
2716
2720
  }
2717
2721
  if (arg === "--fix") {
2718
- warn("utoo-lint: fishlint --fix is ignored because utoo-lint does not apply fixes yet.");
2722
+ translated.push("--fix");
2719
2723
  index += 1;
2720
2724
  continue;
2721
2725
  }
2722
2726
  if (arg === "--fix-dry-run") {
2723
- warn("utoo-lint: fishlint --fix-dry-run is ignored because utoo-lint does not apply fixes yet.");
2727
+ translated.push("--fix-dry-run");
2724
2728
  index += 1;
2725
2729
  continue;
2726
2730
  }
@@ -2900,6 +2904,7 @@ export function lintFiles(paths, options = {}) {
2900
2904
  if (!resolvedOptions.deferDiagnosticConfigFiltering) {
2901
2905
  report.filePaths = normalizeReportFilePaths(report.filePaths, resolvedOptions);
2902
2906
  report.diagnostics = normalizeDiagnosticFilePaths(report.diagnostics, resolvedOptions);
2907
+ report.outputs = normalizeReportOutputs(report.outputs, resolvedOptions);
2903
2908
  report.diagnostics = normalizeReportDiagnostics(report.diagnostics, resolvedOptions);
2904
2909
  report.exitCode = exitCodeForDiagnostics(report.diagnostics);
2905
2910
  }
@@ -2948,6 +2953,10 @@ export function lintText(code, options = {}) {
2948
2953
  deferDiagnosticConfigFiltering: true
2949
2954
  });
2950
2955
  report.filePaths = (report.filePaths ?? []).map((filePath) => (filePath === tempFile ? requestedPath : filePath));
2956
+ report.outputs = (report.outputs ?? []).map((fixed) => ({
2957
+ ...fixed,
2958
+ filePath: fixed.filePath === tempFile ? requestedPath : fixed.filePath
2959
+ }));
2951
2960
  report.diagnostics = report.diagnostics.map((diagnostic) => ({
2952
2961
  ...diagnostic,
2953
2962
  filePath: requestedPath
@@ -2998,6 +3007,10 @@ function buildNativeLintArgs(paths, options) {
2998
3007
  cliArgs.push(`--format=${options.format}`);
2999
3008
  }
3000
3009
 
3010
+ if (options.fix) {
3011
+ cliArgs.push("--fix-dry-run");
3012
+ }
3013
+
3001
3014
  if (options.extraArgs) {
3002
3015
  cliArgs.push(...normalizeStringArray(options.extraArgs, "extraArgs"));
3003
3016
  }
@@ -3474,6 +3487,14 @@ function reportToESLintResults(report, textOptions = {}) {
3474
3487
  byFile.get(filePath).messages.push(diagnosticToESLintMessage(diagnostic, ruleSeverities));
3475
3488
  }
3476
3489
 
3490
+ for (const fixed of report.outputs ?? []) {
3491
+ const filePath = textOptions.filePath ?? normalizeESLintFilePath(fixed.filePath, textOptions.cwd);
3492
+ if (!byFile.has(filePath)) {
3493
+ byFile.set(filePath, emptyESLintResult(filePath, textOptions.source));
3494
+ }
3495
+ byFile.get(filePath).output = fixed.output;
3496
+ }
3497
+
3477
3498
  if (textOptions.filePath && textOptions.includeEmptyTextResult !== false && !byFile.has(textOptions.filePath)) {
3478
3499
  byFile.set(textOptions.filePath, emptyESLintResult(textOptions.filePath, textOptions.source));
3479
3500
  }
@@ -3520,6 +3541,13 @@ function normalizeReportFilePaths(filePaths, options = {}) {
3520
3541
  return (filePaths ?? []).map((filePath) => normalizeESLintFilePath(filePath, options.cwd));
3521
3542
  }
3522
3543
 
3544
+ function normalizeReportOutputs(outputs, options = {}) {
3545
+ return (outputs ?? []).map((fixed) => ({
3546
+ ...fixed,
3547
+ filePath: normalizeESLintFilePath(fixed.filePath, options.cwd)
3548
+ }));
3549
+ }
3550
+
3523
3551
  function normalizeDiagnosticFilePaths(diagnostics, options = {}) {
3524
3552
  return (diagnostics ?? []).map((diagnostic) => ({
3525
3553
  ...diagnostic,
@@ -4038,11 +4066,15 @@ function getErrorResults(results) {
4038
4066
  }
4039
4067
 
4040
4068
  function ruleMetaForRuleId(ruleId) {
4041
- return {
4069
+ const meta = {
4042
4070
  docs: {
4043
4071
  url: ruleDocsUrl(ruleId)
4044
4072
  }
4045
4073
  };
4074
+ if (FIXABLE_BUILTIN_RULE_IDS.has(ruleId)) {
4075
+ meta.fixable = "code";
4076
+ }
4077
+ return meta;
4046
4078
  }
4047
4079
 
4048
4080
  function createBuiltinRule(ruleId) {
@@ -4117,7 +4149,7 @@ function emptyESLintResult(filePath, source) {
4117
4149
  }
4118
4150
 
4119
4151
  function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
4120
- return {
4152
+ const message = {
4121
4153
  ruleId: diagnostic.ruleId,
4122
4154
  severity: ruleSeverities?.get(diagnostic.ruleId) ?? (diagnostic.severity === "error" ? 2 : 1),
4123
4155
  message: diagnostic.message,
@@ -4125,6 +4157,14 @@ function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
4125
4157
  column: diagnostic.column,
4126
4158
  nodeType: null
4127
4159
  };
4160
+ const fixes = (diagnostic.fixes ?? []).map((fix) => ({
4161
+ range: fix.range,
4162
+ text: fix.text
4163
+ }));
4164
+ if (fixes.length > 0) {
4165
+ message.fix = fixes.length === 1 ? fixes[0] : fixes;
4166
+ }
4167
+ return message;
4128
4168
  }
4129
4169
 
4130
4170
  function finalizeESLintResult(result) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utoo/lint",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Experimental JavaScript and TypeScript linter powered by Yuku",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,11 +70,11 @@
70
70
  "use-at-your-own-risk.js"
71
71
  ],
72
72
  "optionalDependencies": {
73
- "@utoo/lint-darwin-arm64": "0.1.4",
74
- "@utoo/lint-darwin-x64": "0.1.4",
75
- "@utoo/lint-linux-arm64": "0.1.4",
76
- "@utoo/lint-linux-x64": "0.1.4",
77
- "@utoo/lint-win32-x64": "0.1.4"
73
+ "@utoo/lint-darwin-arm64": "0.2.1",
74
+ "@utoo/lint-darwin-x64": "0.2.1",
75
+ "@utoo/lint-linux-arm64": "0.2.1",
76
+ "@utoo/lint-linux-x64": "0.2.1",
77
+ "@utoo/lint-win32-x64": "0.2.1"
78
78
  },
79
79
  "publishConfig": {
80
80
  "access": "public",