@utoo/lint 0.2.7 → 0.2.9

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
@@ -2,234 +2,226 @@
2
2
 
3
3
  ![utoo-lint logo](assets/utoo-lint-logo.svg)
4
4
 
5
- High performance linter for JavaScript and TypeScript written by Zig.
5
+ [![npm version](https://img.shields.io/npm/v/%40utoo%2Flint?logo=npm)](https://www.npmjs.com/package/@utoo/lint)
6
+ [![Node.js 20+](https://img.shields.io/badge/Node.js-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
7
+ [![CI](https://github.com/utooland/utoo-lint/actions/workflows/ci.yml/badge.svg)](https://github.com/utooland/utoo-lint/actions/workflows/ci.yml)
8
+ [![License](https://img.shields.io/github/license/utooland/utoo-lint)](https://github.com/utooland/utoo-lint/blob/main/LICENSE)
6
9
 
7
- ## Install
10
+ A high-performance linter for JavaScript and TypeScript, written in Zig and
11
+ powered by [Yuku](https://github.com/yuku-toolchain/yuku).
8
12
 
9
- ```bash
10
- pnpm add -D @utoo/lint
11
- ```
13
+ `@utoo/lint` combines a native lint engine with familiar configuration, CLI,
14
+ and JavaScript APIs.
12
15
 
13
- ## CLI
16
+ [Installation](#installation) · [Quick start](#quick-start) ·
17
+ [Rules](https://github.com/utooland/utoo-lint/blob/main/docs/rule-status.md) ·
18
+ [Configuration](https://github.com/utooland/utoo-lint/blob/main/docs/configuration.md) ·
19
+ [GitHub](https://github.com/utooland/utoo-lint)
14
20
 
15
- Run the linter with:
21
+ ## Why utoo-lint?
16
22
 
17
- ```bash
18
- pnpm exec utoo-lint src
19
- ```
23
+ - **Native performance** — Zig and Yuku power parsing, semantic analysis, and
24
+ parallel file linting.
25
+ - **Broad rule coverage** — More than 300 rules cover JavaScript, TypeScript,
26
+ React, JSX accessibility, and imports.
27
+ - **Familiar configuration** — Use typed `utlint.config.ts` or static
28
+ `utlint.config.json`, with ESLint-style rule names and severities.
29
+ - **Practical workflows** — Safe autofix, suppression comments, JSON output,
30
+ and ESM/CommonJS APIs are included.
31
+ - **Portable installation** — Prebuilt binaries are available for macOS,
32
+ Linux, and Windows on supported architectures.
20
33
 
21
- If no target is provided, `utoo-lint` uses `files` from the selected
22
- `utlint.config.ts` or `utlint.config.json`. When no config file or `files` entry
23
- exists, it scans the current directory. It skips `.git`, `.zig-cache`,
24
- `node_modules`, `vendor`, and `zig-out`.
34
+ ## Installation
25
35
 
26
- Use a config file:
36
+ `@utoo/lint` requires Node.js 20 or later.
27
37
 
28
- ```bash
29
- pnpm exec utoo-lint --config=utlint.config.json src
30
- ```
38
+ ### utoo
31
39
 
32
- Run a focused rule set:
40
+ [utoo](https://utoo.land/en/docs/utoo) is a fast, npm-compatible package
41
+ manager written in Rust. Install it once if it is not already available:
33
42
 
34
43
  ```bash
35
- pnpm exec utoo-lint --rules=no-debugger,no-console src
44
+ npm install -g utoo
36
45
  ```
37
46
 
38
- Use machine-readable output:
47
+ Add `@utoo/lint` as a development dependency:
39
48
 
40
49
  ```bash
41
- pnpm exec utoo-lint --format=json src
50
+ ut install @utoo/lint -D
42
51
  ```
43
52
 
44
- The default terminal output groups diagnostics by file, aligns their location
45
- and severity, and summarizes errors and warnings separately. Color is detected
46
- automatically; pass `--color` to force it or `--no-color` to disable it. JSON
47
- output remains stable for editor and CI integrations.
48
-
49
- ## Autofix
53
+ In an existing project, run `ut install` without a package name to install all
54
+ dependencies declared in `package.json`.
50
55
 
51
- Apply safe fixes from all enabled rules that support autofix:
56
+ ### Other package managers
52
57
 
53
58
  ```bash
54
- pnpm exec utoo-lint --fix src
59
+ pnpm add -D @utoo/lint
60
+ npm install --save-dev @utoo/lint
55
61
  ```
56
62
 
57
- Preview the result without changing files:
63
+ The package selects the native binary for the current platform. No Zig
64
+ toolchain is required.
65
+
66
+ ## Quick start
67
+
68
+ ### Run with the default rules
58
69
 
59
70
  ```bash
60
- pnpm exec utoo-lint --fix-dry-run --format=json src
71
+ utx @utoo/lint src
61
72
  ```
62
73
 
63
- `--fix` writes fixed source back to disk. `--fix-dry-run` never writes files;
64
- with JSON output, changed sources are returned in the `outputs` array. Fixes run
65
- until the source is stable, subject to a safety pass limit. Diagnostics remain
66
- when a rule or a particular code shape cannot be fixed safely. The current
67
- [rule status](https://github.com/fireairforce/utoo-lint/blob/main/docs/rule-status.md)
68
- lists autofix coverage by rule.
74
+ Without a config file, `utoo-lint` uses its built-in default rules. It supports
75
+ `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.mts`, and `.cts` files.
69
76
 
70
- ## Configuration
77
+ ### Add a typed config
71
78
 
72
- `utoo-lint` supports two canonical project config formats:
79
+ Create `utlint.config.ts`:
73
80
 
74
- | Config | Use it when | Raw native binary |
75
- | --- | --- | --- |
76
- | `utlint.config.ts` | You want typed authoring, imports, or computed values. | No |
77
- | `utlint.config.json` | You want a static, runtime-independent config. | Yes |
81
+ ```ts
82
+ import { defineConfig, globalIgnores } from "@utoo/lint/config";
83
+ import frontend from "@utoo/lint/configs/frontend";
78
84
 
79
- The npm CLI discovers either format automatically. It can also select one
80
- explicitly:
85
+ export default defineConfig(
86
+ globalIgnores(["dist/", "coverage/"]),
87
+ {
88
+ ...frontend,
89
+ files: ["src/**/*.{js,jsx,ts,tsx}"],
90
+ rules: {
91
+ ...frontend.rules,
92
+ "no-console": "warn"
93
+ }
94
+ }
95
+ );
96
+ ```
97
+
98
+ The CLI discovers the config and uses its `files` patterns:
81
99
 
82
100
  ```bash
83
- pnpm exec utoo-lint --config=utlint.config.ts src
84
- pnpm exec utoo-lint --config=utlint.config.json src
101
+ utx @utoo/lint
85
102
  ```
86
103
 
87
- For a static config readable by both the npm CLI and raw native binary, create
88
- `utlint.config.json`:
104
+ See the
105
+ [configuration guide](https://github.com/utooland/utoo-lint/blob/main/docs/configuration.md)
106
+ for flat config arrays, global ignores, rule options, and config precedence.
107
+
108
+ ### Add package scripts
89
109
 
90
110
  ```json
91
111
  {
92
- "$schema": "https://raw.githubusercontent.com/fireairforce/utoo-lint/main/npm/utoo-lint/schema.json",
93
- "files": ["src/**/*.{js,jsx,ts,tsx}"],
94
- "ignores": ["dist", "node_modules"],
95
- "rules": {
96
- "no-console": "off",
97
- "no-debugger": "error"
112
+ "scripts": {
113
+ "lint": "utoo-lint src",
114
+ "lint:fix": "utoo-lint --fix src"
98
115
  }
99
116
  }
100
117
  ```
101
118
 
102
- For a typed config executed by the npm/Node CLI, create `utlint.config.ts`:
119
+ With utoo, run these scripts as `ut lint` and `ut lint:fix`.
103
120
 
104
- ```ts
105
- import { defineConfig } from "@utoo/lint/config";
106
-
107
- export default defineConfig({
108
- files: ["src/**/*.{js,jsx,ts,tsx}"],
109
- ignores: ["dist", "node_modules"],
110
- rules: {
111
- "no-console": "off",
112
- "no-debugger": "error"
113
- }
114
- });
115
- ```
121
+ ## Common commands
116
122
 
117
- The packaged frontend preset is also available as a typed config import:
123
+ | Task | Command |
124
+ | --- | --- |
125
+ | Lint files | `utx @utoo/lint src` |
126
+ | Apply safe fixes | `utx @utoo/lint --fix src` |
127
+ | Preview fixes as JSON | `utx @utoo/lint --fix-dry-run --format=json src` |
128
+ | Select rules for one run | `utx @utoo/lint --rules=no-debugger,no-unused-vars src` |
129
+ | Use an explicit config | `utx @utoo/lint --config=utlint.config.json src` |
118
130
 
119
- ```ts
120
- import { defineConfig } from "@utoo/lint/config";
121
- import frontend from "@utoo/lint/configs/frontend";
131
+ Run `utx @utoo/lint --help` for all CLI options.
122
132
 
123
- export default defineConfig({
124
- ...frontend,
125
- rules: {
126
- ...frontend.rules,
127
- "no-console": "off",
128
- },
129
- });
130
- ```
133
+ ## Configuration
131
134
 
132
- Use `globalIgnores()` as a separate config entry when files or directories
133
- must be excluded from every entry:
135
+ `utoo-lint` supports two canonical config formats. They are alternative
136
+ representations of one active config and are not merged together.
134
137
 
135
- ```ts
136
- import { defineConfig, globalIgnores } from "@utoo/lint/config";
138
+ | Config | Best for | Raw native binary |
139
+ | --- | --- | --- |
140
+ | `utlint.config.ts` | Typed authoring, imports, presets, and computed values | No |
141
+ | `utlint.config.json` | Static configuration and JSON Schema validation | Yes |
137
142
 
138
- export default defineConfig(
139
- globalIgnores(["dist/", ".next/", "**/generated/"]),
140
- {
141
- files: ["**/*.{js,jsx,ts,tsx}"],
142
- rules: {
143
- "no-debugger": "error"
144
- }
145
- }
146
- );
143
+ Rule severities follow ESLint conventions: `off`, `warn`, `error`, `0`, `1`,
144
+ and `2`. A selected config enables only the rules present in its resolved
145
+ `rules` map.
146
+
147
+ The npm CLI and JavaScript API load both formats. The raw native binary loads
148
+ JSON only. See the
149
+ [configuration guide](https://github.com/utooland/utoo-lint/blob/main/docs/configuration.md)
150
+ for the complete behavior.
151
+
152
+ ## Autofix
153
+
154
+ Apply safe fixes in place:
155
+
156
+ ```bash
157
+ utx @utoo/lint --fix src
147
158
  ```
148
159
 
149
- `globalIgnores()` returns an ignore-only object containing `ignores` and a
150
- generated `name`. Only such global entries prune directories during target
151
- discovery. By contrast, `ignores` beside `files`, `rules`, or another config
152
- key is entry-scoped: it prevents that entry from applying to matching files,
153
- but does not exclude them from other entries. With no CLI targets, global
154
- ignores filter the config's `files` patterns, or the current-directory scan
155
- when no `files` are configured. Directory patterns such as `dist/` and
156
- `.next/` are relative to the config; use `**/generated/` to match at any depth.
157
- This intentionally follows [ESLint flat config ignore semantics](https://eslint.org/docs/latest/use/configure/ignore).
158
-
159
- The two canonical names represent one active config and are not implicitly
160
- merged. For the npm/Node entry point, discovery is nearest-directory-first.
161
- Within the same directory, `utlint.config.ts` takes precedence over
162
- `utlint.config.json`; deprecated `utoo.json` and `utoo-lint.json` files are
163
- checked afterward for temporary backward compatibility. Rule-related CLI
164
- options such as `--rules`, individual rule toggles, and fishlint's `--rule` are
165
- applied after the selected config.
166
-
167
- Project-config `files` and `ignores` patterns are relative to the selected
168
- config file's directory. In a flat config array, they select the entries that
169
- apply to each file; matching entries are combined in order and later rule
170
- values override earlier values. The npm CLI, JavaScript API, and fishlint
171
- compatibility command perform this rule resolution per file.
172
-
173
- TypeScript config is trusted executable code. Its default export must be a
174
- JSON-serializable object or flat config array. The npm wrapper executes it and
175
- materializes the result as JSON before invoking the native binary. The raw
176
- binary does not execute or discover TypeScript; it searches for
177
- `utlint.config.json` and then the legacy JSON names. Direct native use therefore
178
- requires a JSON config. The raw binary applies only its `rules`;
179
- config-driven `files` and `ignores` filtering and default target selection are
180
- npm/Node wrapper features. Pass lint targets explicitly when invoking the raw
181
- binary. As in ESLint, selecting a config disables rules not present in its
182
- resolved `rules` map. When no config is selected, utoo-lint uses its built-in
183
- default rules.
184
-
185
- Rule severities use ESLint's exit behavior: warnings are reported with exit
186
- status 0, while errors return exit status 1. Use fishlint's `--max-warnings`
187
- option when warnings should fail CI.
188
-
189
- Start from the packaged frontend template:
160
+ Preview fixes without writing files:
190
161
 
191
162
  ```bash
192
- cp node_modules/@utoo/lint/configs/frontend.json utlint.config.json
163
+ utx @utoo/lint --fix-dry-run --format=json src
193
164
  ```
194
165
 
195
- ## ESLint Migration
166
+ Fixes run until the source is stable, subject to a safety pass limit. The
167
+ [rule status](https://github.com/utooland/utoo-lint/blob/main/docs/rule-status.md)
168
+ documents autofix support for each rule.
196
169
 
197
- Convert an existing ESLint config into the native utoo format:
170
+ ## Suppression comments
198
171
 
199
- ```bash
200
- pnpm exec utoo-lint migrate eslint --from eslint.config.js --output utlint.config.json
172
+ Use suppression comments for intentional exceptions without disabling a rule
173
+ in project configuration:
174
+
175
+ ```js
176
+ // utlint-ignore no-debugger: generated breakpoint
177
+ debugger;
201
178
  ```
202
179
 
203
- The package also provides compatibility bins for common replacement flows:
180
+ | Directive | Scope |
181
+ | --- | --- |
182
+ | `utlint-ignore [rule]` | The next line of code |
183
+ | `utlint-ignore-all [rule]` | The entire file when placed before any code |
184
+ | `utlint-ignore-start [rule]` | The start of a suppressed range |
185
+ | `utlint-ignore-end [rule]` | The end of a suppressed range |
186
+
187
+ A directive may target one rule ID. Omitting it creates or closes an all-rules
188
+ suppression. Parse errors are never suppressed, and suppressed fixes are not
189
+ applied.
190
+
191
+ See the
192
+ [suppression comments guide](https://github.com/utooland/utoo-lint/blob/main/docs/suppressions.md)
193
+ for range examples and API behavior.
194
+
195
+ ## Migrating from ESLint
196
+
197
+ Convert an existing ESLint config into a native utoo-lint config:
204
198
 
205
199
  ```bash
206
- pnpm exec eslint --config utlint.config.json src
207
- pnpm exec fishlint eslint --disable-setup --config utlint.config.json --glob src
200
+ utx @utoo/lint migrate eslint \
201
+ --from eslint.config.js \
202
+ --output utlint.config.json
208
203
  ```
209
204
 
205
+ The package also exposes `eslint` and `fishlint` compatibility commands for
206
+ incremental replacement workflows. See the
207
+ [migration guide](https://github.com/utooland/utoo-lint/blob/main/docs/eslint-migration.md)
208
+ for supported mappings and known differences.
209
+
210
210
  ## JavaScript API
211
211
 
212
+ The package provides ESM and CommonJS entry points:
213
+
212
214
  ```js
213
215
  import { lintFiles } from "@utoo/lint";
214
216
 
215
217
  const report = lintFiles(["src"], {
216
- config: "utlint.config.json",
217
- rules: ["no-debugger"]
218
+ config: "utlint.config.ts"
218
219
  });
219
220
 
220
221
  console.log(report.diagnostics);
221
222
  ```
222
223
 
223
- Calling `lintFiles()` without patterns follows the same default target behavior
224
- as the CLI, so migration scripts usually do not need to expand config `files`
225
- manually.
226
-
227
- CommonJS is supported through `require("@utoo/lint")`.
228
-
229
- Set `fix: true` to compute fixed output through the JavaScript API. Raw
230
- `lintFiles()` and `lintText()` reports expose changed sources in `outputs`
231
- without writing them. The ESLint-compatible API follows the usual two-step
232
- flow:
224
+ An ESLint-style API is also available:
233
225
 
234
226
  ```js
235
227
  import { ESLint } from "@utoo/lint";
@@ -239,8 +231,33 @@ const results = await eslint.lintFiles(["src"]);
239
231
  await ESLint.outputFixes(results);
240
232
  ```
241
233
 
242
- ## Binary Resolution
234
+ Raw reports include active diagnostics, suppressed diagnostics, fixed outputs,
235
+ and an exit code. The compatibility layer also exports `Linter`, `CLIEngine`,
236
+ `RuleTester`, and `SourceCode` APIs.
237
+
238
+ ## Supported platforms
239
+
240
+ The package installs a platform-specific optional dependency containing the
241
+ native binary.
242
+
243
+ | Operating system | Architectures |
244
+ | --- | --- |
245
+ | macOS | arm64, x64 |
246
+ | Linux | arm64, x64 |
247
+ | Windows | x64 |
248
+
249
+ Set `UTOO_LINT_BIN=/path/to/utoo-lint` to use a custom binary during local
250
+ development.
251
+
252
+ ## Documentation
253
+
254
+ - [Rule status](https://github.com/utooland/utoo-lint/blob/main/docs/rule-status.md)
255
+ - [Configuration](https://github.com/utooland/utoo-lint/blob/main/docs/configuration.md)
256
+ - [Suppression comments](https://github.com/utooland/utoo-lint/blob/main/docs/suppressions.md)
257
+ - [Migrating from ESLint](https://github.com/utooland/utoo-lint/blob/main/docs/eslint-migration.md)
258
+ - [Benchmarks](https://github.com/utooland/utoo-lint/tree/main/benchmarks)
259
+ - [Security policy](https://github.com/utooland/utoo-lint/blob/main/SECURITY.md)
260
+
261
+ ## License
243
262
 
244
- `@utoo/lint` installs a platform-specific optional dependency containing the
245
- native `utoo-lint` binary. Set `UTOO_LINT_BIN=/path/to/utoo-lint` to force a
246
- specific binary during local development.
263
+ [MIT](https://github.com/utooland/utoo-lint/blob/main/LICENSE)
package/bin/migrate.js CHANGED
@@ -16,7 +16,14 @@ const JAVASCRIPT_CONFIG_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]);
16
16
  const IGNORED_RULES = new Map([
17
17
  ["prettier/prettier", "formatting is outside utoo-lint"]
18
18
  ]);
19
- const SCHEMA_URL = "https://raw.githubusercontent.com/fireairforce/utoo-lint/main/npm/utoo-lint/schema.json";
19
+ const RULE_ALIASES = new Map([
20
+ ["@eslint-react/no-array-index-key", "react/no-array-index-key"],
21
+ ["@eslint-react/dom-no-find-dom-node", "react/no-find-dom-node"],
22
+ ["@eslint-react/dom-no-render-return-value", "react/no-render-return-value"],
23
+ ["@eslint-react/dom-no-void-elements-with-children", "react/void-dom-elements-no-children"],
24
+ ["@eslint-react/rules-of-hooks", "react-hooks/rules-of-hooks"]
25
+ ]);
26
+ const SCHEMA_URL = "https://raw.githubusercontent.com/utooland/utoo-lint/main/npm/utoo-lint/schema.json";
20
27
  const JAVASCRIPT_CONFIG_LOADER_SCRIPT = `
21
28
  import { writeFileSync } from "node:fs";
22
29
  import { pathToFileURL } from "node:url";
@@ -178,43 +185,41 @@ function migrateEslintConfig(options) {
178
185
  const eslintConfig = loadEslintConfig(sourcePath, options.cwd);
179
186
  const supportedRuleIds = new Set(new Linter().getRules().keys());
180
187
  const entries = normalizeConfigEntries(eslintConfig);
181
- const rules = {};
182
- const files = new Set();
183
- const ignores = new Set();
188
+ const migratedEntries = [];
184
189
  const supportedRules = [];
185
190
  const unsupportedRules = [];
186
191
  const ignoredRules = [];
192
+ const translatedRules = [];
187
193
 
188
194
  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;
195
+ const rules = {};
196
+ if (entry.rules && typeof entry.rules === "object") {
197
+ for (const [ruleId, ruleConfig] of Object.entries(entry.rules)) {
198
+ const migratedRuleId = RULE_ALIASES.get(ruleId) ?? ruleId;
199
+ if (IGNORED_RULES.has(ruleId)) {
200
+ ignoredRules.push({ ruleId, reason: IGNORED_RULES.get(ruleId) });
201
+ continue;
202
+ }
203
+ if (!supportedRuleIds.has(migratedRuleId)) {
204
+ if (isRuleDisabled(ruleConfig)) {
205
+ continue;
206
+ }
207
+ unsupportedRules.push(ruleId);
208
+ continue;
209
+ }
210
+ rules[migratedRuleId] = migratableRuleConfig(ruleConfig);
211
+ supportedRules.push(migratedRuleId);
212
+ if (migratedRuleId !== ruleId) {
213
+ translatedRules.push({ sourceRuleId: ruleId, targetRuleId: migratedRuleId });
214
+ }
198
215
  }
199
- if (!supportedRuleIds.has(ruleId)) {
200
- unsupportedRules.push(ruleId);
201
- continue;
202
- }
203
- rules[ruleId] = migratableRuleConfig(ruleConfig);
204
- supportedRules.push(ruleId);
205
216
  }
217
+ migratedEntries.push(migratableConfigEntry(entry, rules));
206
218
  }
207
219
 
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);
220
+ const config = Array.isArray(eslintConfig)
221
+ ? [{ $schema: SCHEMA_URL }, ...migratedEntries]
222
+ : { $schema: SCHEMA_URL, ...(migratedEntries[0] ?? { rules: {} }) };
218
223
 
219
224
  return {
220
225
  config,
@@ -224,6 +229,7 @@ function migrateEslintConfig(options) {
224
229
  supportedRules: uniqueSorted(supportedRules),
225
230
  unsupportedRules: uniqueSorted(unsupportedRules),
226
231
  ignoredRules: uniqueByRuleId(ignoredRules),
232
+ translatedRules: uniqueTranslatedRules(translatedRules),
227
233
  optionDroppedRules: []
228
234
  }
229
235
  };
@@ -282,21 +288,54 @@ function normalizeConfigEntries(config) {
282
288
  return [];
283
289
  }
284
290
 
285
- function addPatterns(target, value) {
286
- if (!value) {
287
- return;
291
+ function migratableConfigEntry(entry, rules) {
292
+ const migrated = {};
293
+ if (typeof entry.name === "string") {
294
+ migrated.name = entry.name;
295
+ }
296
+ const files = migratablePatterns(entry.files);
297
+ if (files.length > 0) {
298
+ migrated.files = files;
299
+ }
300
+ const ignores = migratablePatterns(entry.ignores);
301
+ if (ignores.length > 0) {
302
+ migrated.ignores = ignores;
288
303
  }
304
+ if (!isGlobalIgnoreEntry(entry)) {
305
+ migrated.rules = sortObjectByKey(rules);
306
+ }
307
+ return migrated;
308
+ }
309
+
310
+ function migratablePatterns(value) {
311
+ const patterns = [];
312
+ collectMigratablePatterns(value, patterns);
313
+ return [...new Set(patterns)];
314
+ }
315
+
316
+ function collectMigratablePatterns(value, patterns) {
289
317
  for (const pattern of Array.isArray(value) ? value : [value]) {
290
- if (typeof pattern === "string") {
291
- target.add(pattern);
318
+ if (Array.isArray(pattern)) {
319
+ collectMigratablePatterns(pattern, patterns);
320
+ } else if (typeof pattern === "string") {
321
+ patterns.push(pattern);
292
322
  }
293
323
  }
294
324
  }
295
325
 
326
+ function isGlobalIgnoreEntry(entry) {
327
+ return Boolean(entry.ignores) && Object.keys(entry).every((key) => key === "name" || key === "ignores");
328
+ }
329
+
296
330
  function migratableRuleConfig(ruleConfig) {
297
331
  return Array.isArray(ruleConfig) ? [...ruleConfig] : ruleConfig;
298
332
  }
299
333
 
334
+ function isRuleDisabled(ruleConfig) {
335
+ const severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
336
+ return severity === "off" || severity === 0 || severity === false;
337
+ }
338
+
300
339
  function sortObjectByKey(object) {
301
340
  return Object.fromEntries(Object.entries(object).sort(([left], [right]) => left.localeCompare(right)));
302
341
  }
@@ -313,6 +352,14 @@ function uniqueByRuleId(values) {
313
352
  return [...result.values()].sort((left, right) => left.ruleId.localeCompare(right.ruleId));
314
353
  }
315
354
 
355
+ function uniqueTranslatedRules(values) {
356
+ const result = new Map();
357
+ for (const value of values) {
358
+ result.set(value.sourceRuleId, value);
359
+ }
360
+ return [...result.values()].sort((left, right) => left.sourceRuleId.localeCompare(right.sourceRuleId));
361
+ }
362
+
316
363
  function writeReport(report, options, stream) {
317
364
  if (options.report === "json") {
318
365
  stream.write(JSON.stringify(report, null, 2) + "\n");
@@ -324,6 +371,9 @@ function writeReport(report, options, stream) {
324
371
  stream.write(`utoo-lint migrate eslint: wrote ${report.output}\n`);
325
372
  }
326
373
  stream.write(`utoo-lint migrate eslint: migrated ${report.supportedRules.length} supported rule(s)\n`);
374
+ if (report.translatedRules.length > 0) {
375
+ stream.write(`utoo-lint migrate eslint: translated ${report.translatedRules.length} rule alias(es): ${report.translatedRules.map((rule) => `${rule.sourceRuleId} -> ${rule.targetRuleId}`).join(", ")}\n`);
376
+ }
327
377
  if (report.ignoredRules.length > 0) {
328
378
  stream.write(`utoo-lint migrate eslint: ignored ${report.ignoredRules.length} rule(s): ${report.ignoredRules.map((rule) => rule.ruleId).join(", ")}\n`);
329
379
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/fireairforce/utoo-lint/main/npm/utoo-lint/schema.json",
2
+ "$schema": "https://raw.githubusercontent.com/utooland/utoo-lint/main/npm/utoo-lint/schema.json",
3
3
  "rules": {
4
4
  "no-debugger": "error",
5
5
  "no-alert": "error",
package/index.cjs CHANGED
@@ -305,6 +305,11 @@ const BUILTIN_RULE_IDS = [
305
305
  "import/no-named-as-default-member",
306
306
  "import/no-unresolved",
307
307
  "import/no-self-import",
308
+ "promise/no-promise-in-callback",
309
+ "promise/no-return-in-finally",
310
+ "promise/no-return-wrap",
311
+ "promise/param-names",
312
+ "promise/valid-params",
308
313
  "jsx-a11y/alt-text",
309
314
  "jsx-a11y/anchor-has-content",
310
315
  "jsx-a11y/aria-props",
@@ -318,6 +323,11 @@ const BUILTIN_RULE_IDS = [
318
323
  "jsx-a11y/role-has-required-aria-props",
319
324
  "jsx-a11y/role-supports-aria-props",
320
325
  "jsx-a11y/scope",
326
+ "promise/always-return",
327
+ "promise/catch-or-return",
328
+ "promise/no-callback-in-promise",
329
+ "promise/no-nesting",
330
+ "promise/no-new-statics",
321
331
  "react/button-has-type",
322
332
  "react/default-props-match-prop-types",
323
333
  "react/display-name",
@@ -358,7 +368,9 @@ const BUILTIN_RULE_IDS = [
358
368
  "react/self-closing-comp",
359
369
  "react/style-prop-object",
360
370
  "react/void-dom-elements-no-children",
371
+ "react-hooks/exhaustive-deps",
361
372
  "react-hooks/rules-of-hooks",
373
+ "unused-imports/no-unused-imports",
362
374
  "@typescript-eslint/adjacent-overload-signatures",
363
375
  "@typescript-eslint/array-type",
364
376
  "@typescript-eslint/ban-ts-comment",
@@ -376,6 +388,7 @@ const BUILTIN_RULE_IDS = [
376
388
  "@typescript-eslint/no-dupe-class-members",
377
389
  "@typescript-eslint/no-empty-function",
378
390
  "@typescript-eslint/no-empty-interface",
391
+ "@typescript-eslint/no-empty-object-type",
379
392
  "@typescript-eslint/no-duplicate-enum-values",
380
393
  "@typescript-eslint/no-extra-semi",
381
394
  "@typescript-eslint/no-extra-non-null-assertion",
@@ -391,6 +404,7 @@ const BUILTIN_RULE_IDS = [
391
404
  "@typescript-eslint/no-shadow",
392
405
  "@typescript-eslint/no-this-alias",
393
406
  "@typescript-eslint/no-unsafe-declaration-merging",
407
+ "@typescript-eslint/no-unsafe-function-type",
394
408
  "@typescript-eslint/triple-slash-reference",
395
409
  "@typescript-eslint/typedef",
396
410
  "@typescript-eslint/unified-signatures",
@@ -434,7 +448,11 @@ const NATIVE_ONLY_RULE_IDS = [
434
448
  "semantic-errors"
435
449
  ];
436
450
  const NATIVE_RULE_IDS = [...BUILTIN_RULE_IDS, ...NATIVE_ONLY_RULE_IDS];
437
- const FIXABLE_BUILTIN_RULE_IDS = new Set(["no-extra-semi", "@typescript-eslint/no-extra-semi"]);
451
+ const FIXABLE_BUILTIN_RULE_IDS = new Set([
452
+ "no-extra-semi",
453
+ "@typescript-eslint/no-extra-semi",
454
+ "unused-imports/no-unused-imports"
455
+ ]);
438
456
  const BUILTIN_RULES = new Map(BUILTIN_RULE_IDS.map((ruleId) => [ruleId, createBuiltinRule(ruleId)]));
439
457
 
440
458
  class UtooLint {
@@ -514,7 +532,7 @@ class UtooLint {
514
532
  const results = reportToESLintResults(report, {
515
533
  source: code,
516
534
  filePath,
517
- includeEmptyTextResult: report.files !== 0 || (report.diagnostics?.length ?? 0) > 0,
535
+ includeEmptyTextResult: report.files !== 0 || (report.diagnostics?.length ?? 0) > 0 || (report.suppressedDiagnostics?.length ?? 0) > 0,
518
536
  ruleSeverityForFile: (filePath) => ruleSeverityMapForOptions(nativeOptions, filePath)
519
537
  });
520
538
  if (report.files !== 0) {
@@ -641,7 +659,7 @@ class CLIEngine {
641
659
  const results = maybeFilterQuietResults(reportToESLintResults(report, {
642
660
  source: code,
643
661
  filePath: normalizeESLintFilePath(filePath, mergedOptions.cwd),
644
- includeEmptyTextResult: report.files !== 0 || (report.diagnostics?.length ?? 0) > 0,
662
+ includeEmptyTextResult: report.files !== 0 || (report.diagnostics?.length ?? 0) > 0 || (report.suppressedDiagnostics?.length ?? 0) > 0,
645
663
  ruleSeverityForFile: (filePath) => ruleSeverityMapForOptions(mergedOptions, filePath)
646
664
  }), mergedOptions);
647
665
  return resultsToCLIEngineReport(results);
@@ -842,6 +860,7 @@ function publicCliReport(report) {
842
860
  files: report.files ?? 0,
843
861
  filePaths: report.filePaths ?? [],
844
862
  diagnostics: report.diagnostics ?? [],
863
+ suppressedDiagnostics: report.suppressedDiagnostics ?? [],
845
864
  outputs: report.outputs ?? []
846
865
  };
847
866
  }
@@ -1140,7 +1159,7 @@ function lintFiles(paths, options = {}) {
1140
1159
  const ignoredDiagnostics = ignoredLintPathDiagnostics(targets, options);
1141
1160
  const lintPaths = filteredLintPaths(targets, options);
1142
1161
  if (lintPaths.length === 0) {
1143
- return { files: 0, filePaths: [], diagnostics: ignoredDiagnostics, exitCode: exitCodeForDiagnostics(ignoredDiagnostics) };
1162
+ return { files: 0, filePaths: [], diagnostics: ignoredDiagnostics, suppressedDiagnostics: [], exitCode: exitCodeForDiagnostics(ignoredDiagnostics) };
1144
1163
  }
1145
1164
 
1146
1165
  const configRuns = nativeConfigRunsForFiles(expandNativeConfigRunPaths(lintPaths, options), options);
@@ -1205,8 +1224,10 @@ function finalizeLintReport(report, ignoredDiagnostics, options) {
1205
1224
  if (!options.deferDiagnosticConfigFiltering) {
1206
1225
  report.filePaths = normalizeReportFilePaths(report.filePaths, options);
1207
1226
  report.diagnostics = normalizeDiagnosticFilePaths(report.diagnostics, options);
1227
+ report.suppressedDiagnostics = normalizeDiagnosticFilePaths(report.suppressedDiagnostics, options);
1208
1228
  report.outputs = normalizeReportOutputs(report.outputs, options);
1209
1229
  report.diagnostics = normalizeReportDiagnostics(report.diagnostics, options);
1230
+ report.suppressedDiagnostics = normalizeReportDiagnostics(report.suppressedDiagnostics, options);
1210
1231
  report.exitCode = exitCodeForDiagnostics(report.diagnostics);
1211
1232
  }
1212
1233
  return report;
@@ -1303,6 +1324,7 @@ function mergeNativeReports(reports) {
1303
1324
  files: reports.reduce((count, item) => count + (item.files ?? 0), 0),
1304
1325
  filePaths: reports.flatMap((item) => item.filePaths ?? []),
1305
1326
  diagnostics: reports.flatMap((item) => item.diagnostics ?? []),
1327
+ suppressedDiagnostics: reports.flatMap((item) => item.suppressedDiagnostics ?? []),
1306
1328
  outputs: reports.flatMap((item) => item.outputs ?? []),
1307
1329
  exitCode: reports.some((item) => item.exitCode) ? 1 : 0
1308
1330
  };
@@ -1335,6 +1357,7 @@ function lintText(code, options = {}) {
1335
1357
  files: 0,
1336
1358
  filePaths: [],
1337
1359
  diagnostics,
1360
+ suppressedDiagnostics: [],
1338
1361
  exitCode: exitCodeForDiagnostics(diagnostics)
1339
1362
  };
1340
1363
  }
@@ -1356,7 +1379,12 @@ function lintText(code, options = {}) {
1356
1379
  ...diagnostic,
1357
1380
  filePath: requestedPath
1358
1381
  }));
1382
+ report.suppressedDiagnostics = (report.suppressedDiagnostics ?? []).map((diagnostic) => ({
1383
+ ...diagnostic,
1384
+ filePath: requestedPath
1385
+ }));
1359
1386
  report.diagnostics = normalizeReportDiagnostics(report.diagnostics, options);
1387
+ report.suppressedDiagnostics = normalizeReportDiagnostics(report.suppressedDiagnostics, options);
1360
1388
  report.exitCode = exitCodeForDiagnostics(report.diagnostics);
1361
1389
  return report;
1362
1390
  } finally {
@@ -1888,7 +1916,10 @@ class Linter {
1888
1916
  config: calculatedConfig({ cwd: verifyOptions.cwd, noConfig: true, overrideConfig: config }, normalizedFilePath)
1889
1917
  });
1890
1918
  const customRuleFilter = applyDisableDirectives(customRuleMessages, sourceCode);
1891
- this.suppressedMessages = customRuleFilter.suppressedMessages;
1919
+ this.suppressedMessages = [
1920
+ ...(report.suppressedDiagnostics ?? []).map((diagnostic) => suppressedDiagnosticToESLintMessage(diagnostic, ruleSeverities)),
1921
+ ...customRuleFilter.suppressedMessages
1922
+ ];
1892
1923
  this.times = { passes: [] };
1893
1924
  this.fixPassCount = 0;
1894
1925
  return [
@@ -2197,14 +2228,18 @@ function applyDisableDirectives(messages, sourceCode) {
2197
2228
  const directives = sourceCode.getDisableDirectives()
2198
2229
  .filter((directive) => directive.node?.loc?.start?.line)
2199
2230
  .sort((left, right) => left.node.loc.start.line - right.node.loc.start.line);
2200
- if (directives.length === 0) {
2231
+ const utlintDirectives = sourceCode.getAllComments()
2232
+ .map((comment) => utlintDirectiveFromComment(comment, sourceCode))
2233
+ .filter(Boolean)
2234
+ .sort((left, right) => left.node.range[0] - right.node.range[0]);
2235
+ if (directives.length === 0 && utlintDirectives.length === 0) {
2201
2236
  return { messages, suppressedMessages: [] };
2202
2237
  }
2203
2238
 
2204
2239
  const kept = [];
2205
2240
  const suppressed = [];
2206
2241
  for (const message of messages) {
2207
- const directive = disableDirectiveForMessage(message, directives);
2242
+ const directive = utlintDirectiveForMessage(message, utlintDirectives) ?? disableDirectiveForMessage(message, directives);
2208
2243
  if (directive) {
2209
2244
  suppressed.push({
2210
2245
  ...message,
@@ -2220,6 +2255,68 @@ function applyDisableDirectives(messages, sourceCode) {
2220
2255
  return { messages: kept, suppressedMessages: suppressed };
2221
2256
  }
2222
2257
 
2258
+ function utlintDirectiveFromComment(comment, sourceCode) {
2259
+ const match = String(comment?.value ?? "").trim().match(/^(utlint-ignore(?:-all|-start|-end)?)\b\s*(.*)$/u);
2260
+ if (!match) {
2261
+ return null;
2262
+ }
2263
+
2264
+ const [, type, tail] = match;
2265
+ const colon = tail.indexOf(":");
2266
+ const rawRuleId = (colon === -1 ? tail : tail.slice(0, colon)).trim();
2267
+ const justification = colon === -1 ? "" : tail.slice(colon + 1).trim();
2268
+ const nextToken = sourceCode.getAllTokens().find((token) => token.range?.[0] >= comment.range?.[1]);
2269
+ return {
2270
+ type,
2271
+ node: comment,
2272
+ ruleId: rawRuleId || null,
2273
+ justification,
2274
+ nextCodeLine: nextToken?.loc?.start?.line ?? null,
2275
+ topLevel: sourceCode.getAllTokens().every((token) => token.range?.[0] >= comment.range?.[0])
2276
+ };
2277
+ }
2278
+
2279
+ function utlintDirectiveForMessage(message, directives) {
2280
+ let allRulesRangeDepth = 0;
2281
+ let namedRuleRangeDepth = 0;
2282
+ let allRulesRangeDirective = null;
2283
+ let namedRuleRangeDirective = null;
2284
+
2285
+ for (const directive of directives) {
2286
+ if (directive.node.loc.start.line > message.line) {
2287
+ break;
2288
+ }
2289
+ if (directive.type === "utlint-ignore-start") {
2290
+ if (directive.ruleId == null) {
2291
+ allRulesRangeDirective ??= directive;
2292
+ allRulesRangeDepth += 1;
2293
+ } else if (directive.ruleId === message.ruleId) {
2294
+ namedRuleRangeDirective ??= directive;
2295
+ namedRuleRangeDepth += 1;
2296
+ }
2297
+ continue;
2298
+ }
2299
+ if (directive.type === "utlint-ignore-end") {
2300
+ if (directive.ruleId == null && allRulesRangeDepth > 0) {
2301
+ allRulesRangeDepth -= 1;
2302
+ if (allRulesRangeDepth === 0) allRulesRangeDirective = null;
2303
+ } else if (directive.ruleId === message.ruleId && namedRuleRangeDepth > 0) {
2304
+ namedRuleRangeDepth -= 1;
2305
+ if (namedRuleRangeDepth === 0) namedRuleRangeDirective = null;
2306
+ }
2307
+ continue;
2308
+ }
2309
+ if (directive.type === "utlint-ignore-all" && directive.topLevel && disableDirectiveMatchesRule(directive, message.ruleId)) {
2310
+ return directive;
2311
+ }
2312
+ if (directive.type === "utlint-ignore" && directive.nextCodeLine === message.line && disableDirectiveMatchesRule(directive, message.ruleId)) {
2313
+ return directive;
2314
+ }
2315
+ }
2316
+
2317
+ return namedRuleRangeDirective ?? allRulesRangeDirective;
2318
+ }
2319
+
2223
2320
  function disableDirectiveForMessage(message, directives) {
2224
2321
  const active = {
2225
2322
  all: null,
@@ -3695,6 +3792,18 @@ function reportToESLintResults(report, textOptions = {}) {
3695
3792
  byFile.get(filePath).messages.push(diagnosticToESLintMessage(diagnostic, ruleSeverities));
3696
3793
  }
3697
3794
 
3795
+ for (const diagnostic of report.suppressedDiagnostics ?? []) {
3796
+ const filePath = textOptions.filePath ?? normalizeESLintFilePath(diagnostic.filePath, textOptions.cwd);
3797
+ if (!byFile.has(filePath)) {
3798
+ byFile.set(filePath, emptyESLintResult(filePath, textOptions.source));
3799
+ }
3800
+ const ruleSeverities = textOptions.ruleSeverityForFile?.(filePath) ?? textOptions.ruleSeverities;
3801
+ if (diagnostic.ruleId && ruleSeverities?.get(diagnostic.ruleId) === 0) {
3802
+ continue;
3803
+ }
3804
+ byFile.get(filePath).suppressedMessages.push(suppressedDiagnosticToESLintMessage(diagnostic, ruleSeverities));
3805
+ }
3806
+
3698
3807
  for (const fixed of report.outputs ?? []) {
3699
3808
  const filePath = textOptions.filePath ?? normalizeESLintFilePath(fixed.filePath, textOptions.cwd);
3700
3809
  if (!byFile.has(filePath)) {
@@ -4398,6 +4507,16 @@ function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
4398
4507
  return message;
4399
4508
  }
4400
4509
 
4510
+ function suppressedDiagnosticToESLintMessage(diagnostic, ruleSeverities) {
4511
+ return {
4512
+ ...diagnosticToESLintMessage(diagnostic, ruleSeverities),
4513
+ suppressions: [{
4514
+ kind: diagnostic.suppression?.kind ?? "directive",
4515
+ justification: diagnostic.suppression?.justification ?? ""
4516
+ }]
4517
+ };
4518
+ }
4519
+
4401
4520
  function finalizeESLintResult(result) {
4402
4521
  result.errorCount = 0;
4403
4522
  result.warningCount = 0;
package/index.d.ts CHANGED
@@ -9,10 +9,17 @@ export interface LintMessage {
9
9
  nodeType?: string | null;
10
10
  fix?: LintFix | LintFix[];
11
11
  suggestions?: LintSuggestion[];
12
+ suppressions?: LintSuppression[];
12
13
  }
13
14
 
14
- export interface LintDiagnostic extends Omit<LintMessage, "severity"> {
15
+ export interface LintSuppression {
16
+ kind: "directive";
17
+ justification: string;
18
+ }
19
+
20
+ export interface LintDiagnostic extends Omit<LintMessage, "severity" | "suppressions"> {
15
21
  severity: "warning" | "error";
22
+ suppression?: LintSuppression;
16
23
  }
17
24
 
18
25
  export interface LintFix {
@@ -42,6 +49,7 @@ export interface LintReport {
42
49
  files: number;
43
50
  filePaths: string[];
44
51
  diagnostics: LintDiagnostic[];
52
+ suppressedDiagnostics: LintDiagnostic[];
45
53
  outputs?: Array<{ filePath: string; output: string }>;
46
54
  exitCode: number;
47
55
  stderr?: string;
package/index.js CHANGED
@@ -308,6 +308,11 @@ const BUILTIN_RULE_IDS = [
308
308
  "import/no-named-as-default-member",
309
309
  "import/no-unresolved",
310
310
  "import/no-self-import",
311
+ "promise/no-promise-in-callback",
312
+ "promise/no-return-in-finally",
313
+ "promise/no-return-wrap",
314
+ "promise/param-names",
315
+ "promise/valid-params",
311
316
  "jsx-a11y/alt-text",
312
317
  "jsx-a11y/anchor-has-content",
313
318
  "jsx-a11y/aria-props",
@@ -321,6 +326,11 @@ const BUILTIN_RULE_IDS = [
321
326
  "jsx-a11y/role-has-required-aria-props",
322
327
  "jsx-a11y/role-supports-aria-props",
323
328
  "jsx-a11y/scope",
329
+ "promise/always-return",
330
+ "promise/catch-or-return",
331
+ "promise/no-callback-in-promise",
332
+ "promise/no-nesting",
333
+ "promise/no-new-statics",
324
334
  "react/button-has-type",
325
335
  "react/default-props-match-prop-types",
326
336
  "react/display-name",
@@ -361,7 +371,9 @@ const BUILTIN_RULE_IDS = [
361
371
  "react/self-closing-comp",
362
372
  "react/style-prop-object",
363
373
  "react/void-dom-elements-no-children",
374
+ "react-hooks/exhaustive-deps",
364
375
  "react-hooks/rules-of-hooks",
376
+ "unused-imports/no-unused-imports",
365
377
  "@typescript-eslint/adjacent-overload-signatures",
366
378
  "@typescript-eslint/array-type",
367
379
  "@typescript-eslint/ban-ts-comment",
@@ -379,6 +391,7 @@ const BUILTIN_RULE_IDS = [
379
391
  "@typescript-eslint/no-dupe-class-members",
380
392
  "@typescript-eslint/no-empty-function",
381
393
  "@typescript-eslint/no-empty-interface",
394
+ "@typescript-eslint/no-empty-object-type",
382
395
  "@typescript-eslint/no-duplicate-enum-values",
383
396
  "@typescript-eslint/no-extra-semi",
384
397
  "@typescript-eslint/no-extra-non-null-assertion",
@@ -394,6 +407,7 @@ const BUILTIN_RULE_IDS = [
394
407
  "@typescript-eslint/no-shadow",
395
408
  "@typescript-eslint/no-this-alias",
396
409
  "@typescript-eslint/no-unsafe-declaration-merging",
410
+ "@typescript-eslint/no-unsafe-function-type",
397
411
  "@typescript-eslint/triple-slash-reference",
398
412
  "@typescript-eslint/typedef",
399
413
  "@typescript-eslint/unified-signatures",
@@ -437,7 +451,11 @@ const NATIVE_ONLY_RULE_IDS = [
437
451
  "semantic-errors"
438
452
  ];
439
453
  const NATIVE_RULE_IDS = [...BUILTIN_RULE_IDS, ...NATIVE_ONLY_RULE_IDS];
440
- const FIXABLE_BUILTIN_RULE_IDS = new Set(["no-extra-semi", "@typescript-eslint/no-extra-semi"]);
454
+ const FIXABLE_BUILTIN_RULE_IDS = new Set([
455
+ "no-extra-semi",
456
+ "@typescript-eslint/no-extra-semi",
457
+ "unused-imports/no-unused-imports"
458
+ ]);
441
459
  const BUILTIN_RULES = new Map(BUILTIN_RULE_IDS.map((ruleId) => [ruleId, createBuiltinRule(ruleId)]));
442
460
 
443
461
  export class UtooLint {
@@ -517,7 +535,7 @@ export class UtooLint {
517
535
  const results = reportToESLintResults(report, {
518
536
  source: code,
519
537
  filePath,
520
- includeEmptyTextResult: report.files !== 0 || (report.diagnostics?.length ?? 0) > 0,
538
+ includeEmptyTextResult: report.files !== 0 || (report.diagnostics?.length ?? 0) > 0 || (report.suppressedDiagnostics?.length ?? 0) > 0,
521
539
  ruleSeverityForFile: (filePath) => ruleSeverityMapForOptions(nativeOptions, filePath)
522
540
  });
523
541
  if (report.files !== 0) {
@@ -635,7 +653,10 @@ export class Linter {
635
653
  config: calculatedConfig({ cwd: verifyOptions.cwd, noConfig: true, overrideConfig: config }, normalizedFilePath)
636
654
  });
637
655
  const customRuleFilter = applyDisableDirectives(customRuleMessages, sourceCode);
638
- this.suppressedMessages = customRuleFilter.suppressedMessages;
656
+ this.suppressedMessages = [
657
+ ...(report.suppressedDiagnostics ?? []).map((diagnostic) => suppressedDiagnosticToESLintMessage(diagnostic, ruleSeverities)),
658
+ ...customRuleFilter.suppressedMessages
659
+ ];
639
660
  this.times = { passes: [] };
640
661
  this.fixPassCount = 0;
641
662
  return [
@@ -944,14 +965,18 @@ function applyDisableDirectives(messages, sourceCode) {
944
965
  const directives = sourceCode.getDisableDirectives()
945
966
  .filter((directive) => directive.node?.loc?.start?.line)
946
967
  .sort((left, right) => left.node.loc.start.line - right.node.loc.start.line);
947
- if (directives.length === 0) {
968
+ const utlintDirectives = sourceCode.getAllComments()
969
+ .map((comment) => utlintDirectiveFromComment(comment, sourceCode))
970
+ .filter(Boolean)
971
+ .sort((left, right) => left.node.range[0] - right.node.range[0]);
972
+ if (directives.length === 0 && utlintDirectives.length === 0) {
948
973
  return { messages, suppressedMessages: [] };
949
974
  }
950
975
 
951
976
  const kept = [];
952
977
  const suppressed = [];
953
978
  for (const message of messages) {
954
- const directive = disableDirectiveForMessage(message, directives);
979
+ const directive = utlintDirectiveForMessage(message, utlintDirectives) ?? disableDirectiveForMessage(message, directives);
955
980
  if (directive) {
956
981
  suppressed.push({
957
982
  ...message,
@@ -967,6 +992,68 @@ function applyDisableDirectives(messages, sourceCode) {
967
992
  return { messages: kept, suppressedMessages: suppressed };
968
993
  }
969
994
 
995
+ function utlintDirectiveFromComment(comment, sourceCode) {
996
+ const match = String(comment?.value ?? "").trim().match(/^(utlint-ignore(?:-all|-start|-end)?)\b\s*(.*)$/u);
997
+ if (!match) {
998
+ return null;
999
+ }
1000
+
1001
+ const [, type, tail] = match;
1002
+ const colon = tail.indexOf(":");
1003
+ const rawRuleId = (colon === -1 ? tail : tail.slice(0, colon)).trim();
1004
+ const justification = colon === -1 ? "" : tail.slice(colon + 1).trim();
1005
+ const nextToken = sourceCode.getAllTokens().find((token) => token.range?.[0] >= comment.range?.[1]);
1006
+ return {
1007
+ type,
1008
+ node: comment,
1009
+ ruleId: rawRuleId || null,
1010
+ justification,
1011
+ nextCodeLine: nextToken?.loc?.start?.line ?? null,
1012
+ topLevel: sourceCode.getAllTokens().every((token) => token.range?.[0] >= comment.range?.[0])
1013
+ };
1014
+ }
1015
+
1016
+ function utlintDirectiveForMessage(message, directives) {
1017
+ let allRulesRangeDepth = 0;
1018
+ let namedRuleRangeDepth = 0;
1019
+ let allRulesRangeDirective = null;
1020
+ let namedRuleRangeDirective = null;
1021
+
1022
+ for (const directive of directives) {
1023
+ if (directive.node.loc.start.line > message.line) {
1024
+ break;
1025
+ }
1026
+ if (directive.type === "utlint-ignore-start") {
1027
+ if (directive.ruleId == null) {
1028
+ allRulesRangeDirective ??= directive;
1029
+ allRulesRangeDepth += 1;
1030
+ } else if (directive.ruleId === message.ruleId) {
1031
+ namedRuleRangeDirective ??= directive;
1032
+ namedRuleRangeDepth += 1;
1033
+ }
1034
+ continue;
1035
+ }
1036
+ if (directive.type === "utlint-ignore-end") {
1037
+ if (directive.ruleId == null && allRulesRangeDepth > 0) {
1038
+ allRulesRangeDepth -= 1;
1039
+ if (allRulesRangeDepth === 0) allRulesRangeDirective = null;
1040
+ } else if (directive.ruleId === message.ruleId && namedRuleRangeDepth > 0) {
1041
+ namedRuleRangeDepth -= 1;
1042
+ if (namedRuleRangeDepth === 0) namedRuleRangeDirective = null;
1043
+ }
1044
+ continue;
1045
+ }
1046
+ if (directive.type === "utlint-ignore-all" && directive.topLevel && disableDirectiveMatchesRule(directive, message.ruleId)) {
1047
+ return directive;
1048
+ }
1049
+ if (directive.type === "utlint-ignore" && directive.nextCodeLine === message.line && disableDirectiveMatchesRule(directive, message.ruleId)) {
1050
+ return directive;
1051
+ }
1052
+ }
1053
+
1054
+ return namedRuleRangeDirective ?? allRulesRangeDirective;
1055
+ }
1056
+
970
1057
  function disableDirectiveForMessage(message, directives) {
971
1058
  const active = {
972
1059
  all: null,
@@ -2477,7 +2564,7 @@ export class CLIEngine {
2477
2564
  const results = maybeFilterQuietResults(reportToESLintResults(report, {
2478
2565
  source: code,
2479
2566
  filePath: normalizeESLintFilePath(filePath, mergedOptions.cwd),
2480
- includeEmptyTextResult: report.files !== 0 || (report.diagnostics?.length ?? 0) > 0,
2567
+ includeEmptyTextResult: report.files !== 0 || (report.diagnostics?.length ?? 0) > 0 || (report.suppressedDiagnostics?.length ?? 0) > 0,
2481
2568
  ruleSeverityForFile: (filePath) => ruleSeverityMapForOptions(mergedOptions, filePath)
2482
2569
  }), mergedOptions);
2483
2570
  return resultsToCLIEngineReport(results);
@@ -2678,6 +2765,7 @@ function publicCliReport(report) {
2678
2765
  files: report.files ?? 0,
2679
2766
  filePaths: report.filePaths ?? [],
2680
2767
  diagnostics: report.diagnostics ?? [],
2768
+ suppressedDiagnostics: report.suppressedDiagnostics ?? [],
2681
2769
  outputs: report.outputs ?? []
2682
2770
  };
2683
2771
  }
@@ -2976,7 +3064,7 @@ export function lintFiles(paths, options = {}) {
2976
3064
  const ignoredDiagnostics = ignoredLintPathDiagnostics(targets, options);
2977
3065
  const lintPaths = filteredLintPaths(targets, options);
2978
3066
  if (lintPaths.length === 0) {
2979
- return { files: 0, filePaths: [], diagnostics: ignoredDiagnostics, exitCode: exitCodeForDiagnostics(ignoredDiagnostics) };
3067
+ return { files: 0, filePaths: [], diagnostics: ignoredDiagnostics, suppressedDiagnostics: [], exitCode: exitCodeForDiagnostics(ignoredDiagnostics) };
2980
3068
  }
2981
3069
 
2982
3070
  const configRuns = nativeConfigRunsForFiles(expandNativeConfigRunPaths(lintPaths, options), options);
@@ -3041,8 +3129,10 @@ function finalizeLintReport(report, ignoredDiagnostics, options) {
3041
3129
  if (!options.deferDiagnosticConfigFiltering) {
3042
3130
  report.filePaths = normalizeReportFilePaths(report.filePaths, options);
3043
3131
  report.diagnostics = normalizeDiagnosticFilePaths(report.diagnostics, options);
3132
+ report.suppressedDiagnostics = normalizeDiagnosticFilePaths(report.suppressedDiagnostics, options);
3044
3133
  report.outputs = normalizeReportOutputs(report.outputs, options);
3045
3134
  report.diagnostics = normalizeReportDiagnostics(report.diagnostics, options);
3135
+ report.suppressedDiagnostics = normalizeReportDiagnostics(report.suppressedDiagnostics, options);
3046
3136
  report.exitCode = exitCodeForDiagnostics(report.diagnostics);
3047
3137
  }
3048
3138
  return report;
@@ -3139,6 +3229,7 @@ function mergeNativeReports(reports) {
3139
3229
  files: reports.reduce((count, item) => count + (item.files ?? 0), 0),
3140
3230
  filePaths: reports.flatMap((item) => item.filePaths ?? []),
3141
3231
  diagnostics: reports.flatMap((item) => item.diagnostics ?? []),
3232
+ suppressedDiagnostics: reports.flatMap((item) => item.suppressedDiagnostics ?? []),
3142
3233
  outputs: reports.flatMap((item) => item.outputs ?? []),
3143
3234
  exitCode: reports.some((item) => item.exitCode) ? 1 : 0
3144
3235
  };
@@ -3171,6 +3262,7 @@ export function lintText(code, options = {}) {
3171
3262
  files: 0,
3172
3263
  filePaths: [],
3173
3264
  diagnostics,
3265
+ suppressedDiagnostics: [],
3174
3266
  exitCode: exitCodeForDiagnostics(diagnostics)
3175
3267
  };
3176
3268
  }
@@ -3192,7 +3284,12 @@ export function lintText(code, options = {}) {
3192
3284
  ...diagnostic,
3193
3285
  filePath: requestedPath
3194
3286
  }));
3287
+ report.suppressedDiagnostics = (report.suppressedDiagnostics ?? []).map((diagnostic) => ({
3288
+ ...diagnostic,
3289
+ filePath: requestedPath
3290
+ }));
3195
3291
  report.diagnostics = normalizeReportDiagnostics(report.diagnostics, options);
3292
+ report.suppressedDiagnostics = normalizeReportDiagnostics(report.suppressedDiagnostics, options);
3196
3293
  report.exitCode = exitCodeForDiagnostics(report.diagnostics);
3197
3294
  return report;
3198
3295
  } finally {
@@ -3698,6 +3795,18 @@ function reportToESLintResults(report, textOptions = {}) {
3698
3795
  byFile.get(filePath).messages.push(diagnosticToESLintMessage(diagnostic, ruleSeverities));
3699
3796
  }
3700
3797
 
3798
+ for (const diagnostic of report.suppressedDiagnostics ?? []) {
3799
+ const filePath = textOptions.filePath ?? normalizeESLintFilePath(diagnostic.filePath, textOptions.cwd);
3800
+ if (!byFile.has(filePath)) {
3801
+ byFile.set(filePath, emptyESLintResult(filePath, textOptions.source));
3802
+ }
3803
+ const ruleSeverities = textOptions.ruleSeverityForFile?.(filePath) ?? textOptions.ruleSeverities;
3804
+ if (diagnostic.ruleId && ruleSeverities?.get(diagnostic.ruleId) === 0) {
3805
+ continue;
3806
+ }
3807
+ byFile.get(filePath).suppressedMessages.push(suppressedDiagnosticToESLintMessage(diagnostic, ruleSeverities));
3808
+ }
3809
+
3701
3810
  for (const fixed of report.outputs ?? []) {
3702
3811
  const filePath = textOptions.filePath ?? normalizeESLintFilePath(fixed.filePath, textOptions.cwd);
3703
3812
  if (!byFile.has(filePath)) {
@@ -4401,6 +4510,16 @@ function diagnosticToESLintMessage(diagnostic, ruleSeverities) {
4401
4510
  return message;
4402
4511
  }
4403
4512
 
4513
+ function suppressedDiagnosticToESLintMessage(diagnostic, ruleSeverities) {
4514
+ return {
4515
+ ...diagnosticToESLintMessage(diagnostic, ruleSeverities),
4516
+ suppressions: [{
4517
+ kind: diagnostic.suppression?.kind ?? "directive",
4518
+ justification: diagnostic.suppression?.justification ?? ""
4519
+ }]
4520
+ };
4521
+ }
4522
+
4404
4523
  function finalizeESLintResult(result) {
4405
4524
  result.errorCount = 0;
4406
4525
  result.warningCount = 0;
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@utoo/lint",
3
- "version": "0.2.7",
4
- "description": "Experimental JavaScript and TypeScript linter powered by Yuku",
3
+ "version": "0.2.9",
4
+ "description": "High-performance JavaScript and TypeScript linter powered by Yuku",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/fireairforce/utoo-lint.git"
8
+ "url": "git+https://github.com/utooland/utoo-lint.git"
9
9
  },
10
10
  "bugs": {
11
- "url": "https://github.com/fireairforce/utoo-lint/issues"
11
+ "url": "https://github.com/utooland/utoo-lint/issues"
12
12
  },
13
- "homepage": "https://github.com/fireairforce/utoo-lint#readme",
13
+ "homepage": "https://github.com/utooland/utoo-lint#readme",
14
14
  "type": "module",
15
15
  "engines": {
16
- "node": ">=18.0.0"
16
+ "node": ">=20.0.0"
17
17
  },
18
18
  "types": "./index.d.ts",
19
19
  "exports": {
@@ -76,11 +76,11 @@
76
76
  "use-at-your-own-risk.js"
77
77
  ],
78
78
  "optionalDependencies": {
79
- "@utoo/lint-darwin-arm64": "0.2.7",
80
- "@utoo/lint-darwin-x64": "0.2.7",
81
- "@utoo/lint-linux-arm64": "0.2.7",
82
- "@utoo/lint-linux-x64": "0.2.7",
83
- "@utoo/lint-win32-x64": "0.2.7"
79
+ "@utoo/lint-darwin-arm64": "0.2.9",
80
+ "@utoo/lint-darwin-x64": "0.2.9",
81
+ "@utoo/lint-linux-arm64": "0.2.9",
82
+ "@utoo/lint-linux-x64": "0.2.9",
83
+ "@utoo/lint-win32-x64": "0.2.9"
84
84
  },
85
85
  "dependencies": {
86
86
  "jiti": "2.7.0"
package/schema.json CHANGED
@@ -1,50 +1,63 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft-07/schema",
3
- "$id": "https://raw.githubusercontent.com/fireairforce/utoo-lint/main/npm/utoo-lint/schema.json",
3
+ "$id": "https://raw.githubusercontent.com/utooland/utoo-lint/main/npm/utoo-lint/schema.json",
4
4
  "title": "utoo-lint configuration",
5
- "type": "object",
6
- "additionalProperties": true,
7
- "properties": {
8
- "$schema": {
9
- "type": "string"
5
+ "oneOf": [
6
+ {
7
+ "$ref": "#/definitions/configObject"
10
8
  },
11
- "rules": {
12
- "description": "ESLint-compatible rule severity map. Unknown rule names are rejected by utoo-lint.",
13
- "type": "object",
14
- "additionalProperties": {
15
- "$ref": "#/definitions/ruleSeverity"
9
+ {
10
+ "type": "array",
11
+ "items": {
12
+ "$ref": "#/definitions/configObject"
16
13
  }
17
- },
18
- "files": {
19
- "description": "File globs this config entry applies to.",
20
- "oneOf": [
21
- {
14
+ }
15
+ ],
16
+ "definitions": {
17
+ "configObject": {
18
+ "type": "object",
19
+ "additionalProperties": true,
20
+ "properties": {
21
+ "$schema": {
22
22
  "type": "string"
23
23
  },
24
- {
25
- "type": "array",
26
- "items": {
27
- "type": "string"
24
+ "rules": {
25
+ "description": "ESLint-compatible rule severity map. Unknown rule names are rejected by utoo-lint.",
26
+ "type": "object",
27
+ "additionalProperties": {
28
+ "$ref": "#/definitions/ruleSeverity"
28
29
  }
29
- }
30
- ]
31
- },
32
- "ignores": {
33
- "description": "File globs ignored by this config entry.",
34
- "oneOf": [
35
- {
36
- "type": "string"
37
30
  },
38
- {
39
- "type": "array",
40
- "items": {
41
- "type": "string"
42
- }
31
+ "files": {
32
+ "description": "File globs this config entry applies to.",
33
+ "oneOf": [
34
+ {
35
+ "type": "string"
36
+ },
37
+ {
38
+ "type": "array",
39
+ "items": {
40
+ "type": "string"
41
+ }
42
+ }
43
+ ]
44
+ },
45
+ "ignores": {
46
+ "description": "File globs ignored by this config entry.",
47
+ "oneOf": [
48
+ {
49
+ "type": "string"
50
+ },
51
+ {
52
+ "type": "array",
53
+ "items": {
54
+ "type": "string"
55
+ }
56
+ }
57
+ ]
43
58
  }
44
- ]
45
- }
46
- },
47
- "definitions": {
59
+ }
60
+ },
48
61
  "ruleSeverity": {
49
62
  "oneOf": [
50
63
  {