markuplint 5.0.0-rc.0 → 5.0.0-rc.2
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/CHANGELOG.md +36 -0
- package/README.md +43 -0
- package/lib/api/ml-engine.js +5 -1
- package/lib/api/types.d.ts +0 -4
- package/lib/cli/bootstrap.d.ts +15 -1
- package/lib/cli/bootstrap.js +19 -0
- package/lib/cli/command.js +152 -14
- package/lib/cli/output.d.ts +19 -0
- package/lib/cli/output.js +63 -0
- package/lib/suppressions/apply-suppressions.d.ts +45 -0
- package/lib/suppressions/apply-suppressions.js +221 -0
- package/lib/suppressions/compute-scope.d.ts +96 -0
- package/lib/suppressions/compute-scope.js +244 -0
- package/lib/suppressions/downgrade-severity.d.ts +51 -0
- package/lib/suppressions/downgrade-severity.js +102 -0
- package/lib/suppressions/generate-suppressions.d.ts +32 -0
- package/lib/suppressions/generate-suppressions.js +55 -0
- package/lib/suppressions/index.d.ts +23 -0
- package/lib/suppressions/index.js +18 -0
- package/lib/suppressions/merge-suppressions.d.ts +11 -0
- package/lib/suppressions/merge-suppressions.js +31 -0
- package/lib/suppressions/prune-suppressions.d.ts +22 -0
- package/lib/suppressions/prune-suppressions.js +64 -0
- package/lib/suppressions/suppressions-file.d.ts +46 -0
- package/lib/suppressions/suppressions-file.js +108 -0
- package/lib/suppressions/types.d.ts +26 -0
- package/lib/suppressions/types.js +1 -0
- package/lib/testing-tool/index.js +4 -4
- package/lib/types.d.ts +2 -20
- package/package.json +25 -16
- package/lib/api/v1.d.ts +0 -57
- package/lib/api/v1.js +0 -41
- package/lib/v1.d.ts +0 -6
- package/lib/v1.js +0 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,42 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [5.0.0-rc.2](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.1...v5.0.0-rc.2) (2026-04-15)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **markuplint:** add default export condition and re-export isFatalError ([55a990a](https://github.com/markuplint/markuplint/commit/55a990affeec62c23a96cf15b42327fcb867e809))
|
|
11
|
+
- **markuplint:** add missing status property to github-reporter test data ([bb7ba62](https://github.com/markuplint/markuplint/commit/bb7ba62e9350c173987f76de2741aaeb3ba0997b))
|
|
12
|
+
|
|
13
|
+
- build!: remove ESLint and replace with oxlint ([1e0a337](https://github.com/markuplint/markuplint/commit/1e0a337707f76b903b16beeeb8c4d4fc0d8fc9e4))
|
|
14
|
+
- feat(markuplint)!: remove deprecated autoLoad option and MLResultInfo_v1 interface ([4eb1d05](https://github.com/markuplint/markuplint/commit/4eb1d05eb2829019cd4073afa153a512b1c4c8fa))
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- **config-presets:** add document uniqueness rules to html-standard preset ([6ed848b](https://github.com/markuplint/markuplint/commit/6ed848bd800416d1220b9de95ece7a3d752d881f))
|
|
19
|
+
- **markuplint:** add CLI summary output ([4743ba0](https://github.com/markuplint/markuplint/commit/4743ba0be7311288ea2b28fb9345567cf97c1a23))
|
|
20
|
+
- **markuplint:** add suppressions subpath export and editor severity downgrade ([362adef](https://github.com/markuplint/markuplint/commit/362adef1a040c66fec36c01bd8d8fcbe9a66c453))
|
|
21
|
+
- **vscode:** add suppressed message prefix and blame parser tests ([cd80a80](https://github.com/markuplint/markuplint/commit/cd80a802c3bad29a8a1d510d2160a21a6f0a2682))
|
|
22
|
+
|
|
23
|
+
### BREAKING CHANGES
|
|
24
|
+
|
|
25
|
+
- ESLint is no longer used. Use oxlint instead.
|
|
26
|
+
- The autoLoad option has been removed from APIOptions.
|
|
27
|
+
Rules are now always auto-loaded unconditionally.
|
|
28
|
+
The MLResultInfo_v1 interface has also been removed.
|
|
29
|
+
|
|
30
|
+
# [5.0.0-rc.1](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.0...v5.0.0-rc.1) (2026-03-27)
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
- add isFatalError guard to MLEngine.exec and fix accname Deno crash ([c4b20de](https://github.com/markuplint/markuplint/commit/c4b20de128b2cfee582b3588e5004cb90065825b))
|
|
35
|
+
- **markuplint:** use platform-native paths in suppressions round-trip test ([df4b6a5](https://github.com/markuplint/markuplint/commit/df4b6a5f83b0fed3f74afba72cccd7bc2dbf8606))
|
|
36
|
+
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
- **markuplint:** add experimental bulk suppressions ([bd3ab72](https://github.com/markuplint/markuplint/commit/bd3ab7204870dd6061e8e4ccbeaacd751d069a73)), closes [#3503](https://github.com/markuplint/markuplint/issues/3503)
|
|
40
|
+
- **markuplint:** add selector scope (LCA) to bulk suppressions ([84cf73d](https://github.com/markuplint/markuplint/commit/84cf73dd92db49f1f93ff6a5c9b71c7807ce31e9)), closes [#3509](https://github.com/markuplint/markuplint/issues/3509)
|
|
41
|
+
|
|
6
42
|
# [5.0.0-rc.0](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.3...v5.0.0-rc.0) (2026-03-12)
|
|
7
43
|
|
|
8
44
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -89,6 +89,12 @@ Options
|
|
|
89
89
|
--severity-parse-error Specifies the severity level of parse errors. Supports "error", "warning", and "off". Default: "error".
|
|
90
90
|
--max-count Limit the number of violations shown. Default: 0 (no limit).
|
|
91
91
|
--max-warnings Number of warnings to trigger nonzero exit code. Default: -1 (no limit).
|
|
92
|
+
--progressive-output Output results immediately after processing each file. Default: false.
|
|
93
|
+
|
|
94
|
+
--suppress [Experimental] Generate/update suppressions file for all current errors.
|
|
95
|
+
--suppress-rule RULE_ID [Experimental] Suppress only the specified rule.
|
|
96
|
+
--prune-suppressions [Experimental] Remove stale entries from the suppressions file.
|
|
97
|
+
--suppressions-location PATH [Experimental] Custom path for the suppressions file. Default: "markuplint-suppressions.json".
|
|
92
98
|
|
|
93
99
|
--init Initialize settings interactively.
|
|
94
100
|
--search Search lines of codes that include the target element by selectors.
|
|
@@ -102,6 +108,43 @@ Examples
|
|
|
102
108
|
$ cat verifyee.html | markuplint
|
|
103
109
|
```
|
|
104
110
|
|
|
111
|
+
### Bulk Suppressions (Experimental)
|
|
112
|
+
|
|
113
|
+
> **This feature is experimental and may change in future releases.**
|
|
114
|
+
|
|
115
|
+
When introducing new rules to an existing project, you can suppress current violations and enforce rules only on new code.
|
|
116
|
+
|
|
117
|
+
**Typical workflow:**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# 1. Enable new rules in your config, then suppress all current errors
|
|
121
|
+
$ markuplint --suppress "src/**/*.html"
|
|
122
|
+
|
|
123
|
+
# 2. Commit the generated suppressions file to your repository
|
|
124
|
+
$ git add markuplint-suppressions.json
|
|
125
|
+
|
|
126
|
+
# 3. From now on, only new violations are reported
|
|
127
|
+
$ markuplint "src/**/*.html"
|
|
128
|
+
|
|
129
|
+
# 4. As you fix existing violations, clean up stale entries
|
|
130
|
+
$ markuplint --prune-suppressions "src/**/*.html"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- Only `error`-severity violations are suppressed; `warning` and `info` always pass through.
|
|
134
|
+
- If the number of violations for a file+rule pair **exceeds** the suppressed count, **all** violations for that pair are reported.
|
|
135
|
+
- `--suppress` always exits with code 0 (success).
|
|
136
|
+
- When a DOM tree is available, each suppression entry includes a **scope selector** (computed via LCA) to narrow suppression to a specific subtree. Entries without `scope` apply to the entire file.
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"src/index.html": {
|
|
141
|
+
"attr-duplication": { "count": 3, "scope": "#main-nav > ul" }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
See [ESLint's Bulk Suppressions](https://eslint.org/docs/latest/use/suppressions) for the reference design. Tracking issues: [#3503](https://github.com/markuplint/markuplint/issues/3503), [#3509](https://github.com/markuplint/markuplint/issues/3509).
|
|
147
|
+
|
|
105
148
|
## Documentation
|
|
106
149
|
|
|
107
150
|
- [Getting Started](https://markuplint.dev/getting-started)
|
package/lib/api/ml-engine.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ConfigProvider, resolveFiles, resolveParser, resolvePretenders, resolveRules, resolveSpecs, } from '@markuplint/file-resolver';
|
|
2
2
|
import { mergeConfig } from '@markuplint/ml-config';
|
|
3
3
|
import { MLCore, convertRuleset } from '@markuplint/ml-core';
|
|
4
|
+
import { isFatalError } from '@markuplint/shared';
|
|
4
5
|
import { FSWatcher } from 'chokidar';
|
|
5
6
|
import { Emitter } from 'strict-event-emitter';
|
|
6
7
|
import { log as coreLog, verbosely } from '../debug.js';
|
|
@@ -96,6 +97,9 @@ export class MLEngine extends Emitter {
|
|
|
96
97
|
return null;
|
|
97
98
|
}
|
|
98
99
|
const verifyResult = await core.verify({ fix: this.#options?.fix ?? false }).catch(error => {
|
|
100
|
+
if (isFatalError(error)) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
99
103
|
if (error instanceof Error) {
|
|
100
104
|
return error;
|
|
101
105
|
}
|
|
@@ -337,7 +341,7 @@ export class MLEngine extends Emitter {
|
|
|
337
341
|
return pretenders;
|
|
338
342
|
}
|
|
339
343
|
async #resolveRules(plugins, ruleset) {
|
|
340
|
-
const rules = await resolveRules(plugins, ruleset, this.#options?.importPresetRules ?? true
|
|
344
|
+
const rules = await resolveRules(plugins, ruleset, this.#options?.importPresetRules ?? true);
|
|
341
345
|
if (this.#options?.rules) {
|
|
342
346
|
rules.push(...this.#options.rules);
|
|
343
347
|
}
|
package/lib/api/types.d.ts
CHANGED
|
@@ -18,10 +18,6 @@ export type APIOptions = {
|
|
|
18
18
|
readonly rules?: readonly Readonly<AnyMLRule>[];
|
|
19
19
|
readonly importPresetRules?: boolean;
|
|
20
20
|
readonly severity?: SeverityOptions;
|
|
21
|
-
/**
|
|
22
|
-
* @deprecated
|
|
23
|
-
*/
|
|
24
|
-
readonly autoLoad?: boolean;
|
|
25
21
|
};
|
|
26
22
|
/**
|
|
27
23
|
* Event map for the {@link MLEngine}, defining all emitted events and their payload types.
|
package/lib/cli/bootstrap.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ReadonlyDeep } from 'type-fest';
|
|
|
3
3
|
* Help text displayed when the CLI is invoked with `--help` or without arguments.
|
|
4
4
|
* Documents all available options, flags, and usage examples.
|
|
5
5
|
*/
|
|
6
|
-
export declare const help = "\nUsage\n\t$ markuplint <HTML file paths (glob format)>\n\t$ <stdout> | markuplint\n\nOptions\n\t--config, -c FILE_PATH A configuration file path.\n\t--fix, Fix HTML.\n\t--fix-dry-run Show what --fix would change without writing files.\n\t--format, -f FORMAT Output format. Support \"JSON\", \"Simple\", \"GitHub\" and \"Standard\". Default: \"Standard\".\n\t--no-search-config No search a configure file automatically.\n\t--ignore-ext Evaluate files that are received even though the type of extension.\n\t--no-import-preset-rules No import preset rules.\n\t--locale Locale of the message of violation. Default is an OS setting.\n\t--no-color, Output no color.\n\t--problem-only, -p Output only problems, without passeds.\n\t--no-allow-warnings Return status code 1 even if there are warnings.\n\t--allow-empty-input Return status code 1 even if there are no input files.\n\t--show-config Output computed configuration of the target file. Supports \"details\" and empty. Default: empty.\n\t--verbose Output with detailed information.\n\t--include-node-modules Include files in node_modules directory. Default: false.\n\t--severity-parse-error Specifies the severity level of parse errors. Supports \"error\", \"warning\", and \"off\". Default: \"error\".\n\t--max-count Limit the number of violations shown. Default: 0 (no limit).\n\t--max-warnings Number of warnings to trigger nonzero exit code. Default: -1 (no limit).\n\t--progressive-output Output results immediately after processing each file. Default: false.\n\n\t--init Initialize settings interactively.\n\t--search Search lines of codes that include the target element by selectors.\n\n\t--help, -h Show help.\n\t--version, -v Show version.\n\nExamples\n\t$ markuplint verifyee.html --config path/to/.markuplintrc\n\t$ cat verifyee.html | markuplint\n";
|
|
6
|
+
export declare const help = "\nUsage\n\t$ markuplint <HTML file paths (glob format)>\n\t$ <stdout> | markuplint\n\nOptions\n\t--config, -c FILE_PATH A configuration file path.\n\t--fix, Fix HTML.\n\t--fix-dry-run Show what --fix would change without writing files.\n\t--format, -f FORMAT Output format. Support \"JSON\", \"Simple\", \"GitHub\" and \"Standard\". Default: \"Standard\".\n\t--no-search-config No search a configure file automatically.\n\t--ignore-ext Evaluate files that are received even though the type of extension.\n\t--no-import-preset-rules No import preset rules.\n\t--locale Locale of the message of violation. Default is an OS setting.\n\t--no-color, Output no color.\n\t--problem-only, -p Output only problems, without passeds.\n\t--no-allow-warnings Return status code 1 even if there are warnings.\n\t--allow-empty-input Return status code 1 even if there are no input files.\n\t--show-config Output computed configuration of the target file. Supports \"details\" and empty. Default: empty.\n\t--verbose Output with detailed information.\n\t--include-node-modules Include files in node_modules directory. Default: false.\n\t--severity-parse-error Specifies the severity level of parse errors. Supports \"error\", \"warning\", and \"off\". Default: \"error\".\n\t--max-count Limit the number of violations shown. Default: 0 (no limit).\n\t--max-warnings Number of warnings to trigger nonzero exit code. Default: -1 (no limit).\n\t--progressive-output Output results immediately after processing each file. Default: false.\n\n\t--suppress [Experimental] Generate/update suppressions file for all current errors.\n\t--suppress-rule RULE_ID [Experimental] Suppress only the specified rule.\n\t--prune-suppressions [Experimental] Remove stale entries from the suppressions file.\n\t--suppressions-location PATH [Experimental] Custom path for the suppressions file. Default: \"markuplint-suppressions.json\".\n\n\t--init Initialize settings interactively.\n\t--search Search lines of codes that include the target element by selectors.\n\n\t--help, -h Show help.\n\t--version, -v Show version.\n\nExamples\n\t$ markuplint verifyee.html --config path/to/.markuplintrc\n\t$ cat verifyee.html | markuplint\n";
|
|
7
7
|
/**
|
|
8
8
|
* The parsed CLI instance created by `meow`, providing access to
|
|
9
9
|
* positional arguments (`cli.input`) and parsed flags (`cli.flags`).
|
|
@@ -95,6 +95,20 @@ export declare const cli: import("meow").Result<{
|
|
|
95
95
|
type: "boolean";
|
|
96
96
|
default: false;
|
|
97
97
|
};
|
|
98
|
+
suppress: {
|
|
99
|
+
type: "boolean";
|
|
100
|
+
default: false;
|
|
101
|
+
};
|
|
102
|
+
suppressRule: {
|
|
103
|
+
type: "string";
|
|
104
|
+
};
|
|
105
|
+
pruneSuppressions: {
|
|
106
|
+
type: "boolean";
|
|
107
|
+
default: false;
|
|
108
|
+
};
|
|
109
|
+
suppressionsLocation: {
|
|
110
|
+
type: "string";
|
|
111
|
+
};
|
|
98
112
|
}>;
|
|
99
113
|
/**
|
|
100
114
|
* Deeply read-only type representing the parsed CLI flags.
|
package/lib/cli/bootstrap.js
CHANGED
|
@@ -29,6 +29,11 @@ Options
|
|
|
29
29
|
--max-warnings Number of warnings to trigger nonzero exit code. Default: -1 (no limit).
|
|
30
30
|
--progressive-output Output results immediately after processing each file. Default: false.
|
|
31
31
|
|
|
32
|
+
--suppress [Experimental] Generate/update suppressions file for all current errors.
|
|
33
|
+
--suppress-rule RULE_ID [Experimental] Suppress only the specified rule.
|
|
34
|
+
--prune-suppressions [Experimental] Remove stale entries from the suppressions file.
|
|
35
|
+
--suppressions-location PATH [Experimental] Custom path for the suppressions file. Default: "markuplint-suppressions.json".
|
|
36
|
+
|
|
32
37
|
--init Initialize settings interactively.
|
|
33
38
|
--search Search lines of codes that include the target element by selectors.
|
|
34
39
|
|
|
@@ -133,5 +138,19 @@ export const cli = meow(help, {
|
|
|
133
138
|
// TODO: It will be changed to `true` in the next major version.
|
|
134
139
|
default: false,
|
|
135
140
|
},
|
|
141
|
+
suppress: {
|
|
142
|
+
type: 'boolean',
|
|
143
|
+
default: false,
|
|
144
|
+
},
|
|
145
|
+
suppressRule: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
},
|
|
148
|
+
pruneSuppressions: {
|
|
149
|
+
type: 'boolean',
|
|
150
|
+
default: false,
|
|
151
|
+
},
|
|
152
|
+
suppressionsLocation: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
},
|
|
136
155
|
},
|
|
137
156
|
});
|
package/lib/cli/command.js
CHANGED
|
@@ -2,10 +2,12 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { resolveFiles } from '@markuplint/file-resolver';
|
|
4
4
|
import { ViolationCollector } from '@markuplint/ml-core';
|
|
5
|
+
import { isFatalError } from '@markuplint/shared';
|
|
5
6
|
import { MLEngine } from '../api/index.js';
|
|
6
7
|
import { log } from '../debug.js';
|
|
8
|
+
import { applySuppressions, generateSuppressions, mergeSuppressions, pruneSuppressions, readSuppressionsFile, resolveSuppressionsPath, writeSuppressionsFile, } from '../suppressions/index.js';
|
|
7
9
|
import { outputDryRunDiff } from './dry-run-output.js';
|
|
8
|
-
import { output } from './output.js';
|
|
10
|
+
import { output, outputSummary } from './output.js';
|
|
9
11
|
/**
|
|
10
12
|
* Executes the markuplint linting command against the given files.
|
|
11
13
|
*
|
|
@@ -25,6 +27,16 @@ export async function command(files, options, apiOptions) {
|
|
|
25
27
|
process.stderr.write('Warning: --fix-dry-run takes precedence over --fix. Files will not be modified.\n');
|
|
26
28
|
}
|
|
27
29
|
const fix = options.fix || fixDryRun;
|
|
30
|
+
// Mutual exclusion checks for suppressions flags
|
|
31
|
+
const isSuppressMode = options.suppress || options.suppressRule != null;
|
|
32
|
+
const isPruneMode = options.pruneSuppressions;
|
|
33
|
+
if (isSuppressMode && fix) {
|
|
34
|
+
process.stderr.write('Warning: --suppress counts violations from the original code, not from the fixed result. Consider running --fix first, then --suppress.\n');
|
|
35
|
+
}
|
|
36
|
+
if (isSuppressMode && isPruneMode) {
|
|
37
|
+
process.stderr.write('Error: --suppress/--suppress-rule and --prune-suppressions cannot be used together.\n');
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
28
40
|
const configFile = options.config &&
|
|
29
41
|
(path.isAbsolute(options.config) ? options.config : path.resolve(process.cwd(), options.config));
|
|
30
42
|
const locale = options.locale;
|
|
@@ -51,6 +63,7 @@ export async function command(files, options, apiOptions) {
|
|
|
51
63
|
const processedFiles = [];
|
|
52
64
|
const skippedFiles = [];
|
|
53
65
|
const filesContent = new Map();
|
|
66
|
+
const engines = new Map();
|
|
54
67
|
const severityParseError = options.severityParseError.toLowerCase();
|
|
55
68
|
const severity = {
|
|
56
69
|
parseError: ['error', 'warning', 'off'].includes(severityParseError)
|
|
@@ -101,6 +114,13 @@ export async function command(files, options, apiOptions) {
|
|
|
101
114
|
if (!result) {
|
|
102
115
|
continue;
|
|
103
116
|
}
|
|
117
|
+
processedFiles.push(result.filePath);
|
|
118
|
+
filesContent.set(result.filePath, {
|
|
119
|
+
sourceCode: result.sourceCode,
|
|
120
|
+
fixedCode: result.fixedCode,
|
|
121
|
+
});
|
|
122
|
+
// Store engine for scope computation in suppressions
|
|
123
|
+
engines.set(result.filePath, engine);
|
|
104
124
|
// Progressive出力が有効でJSON形式でない場合
|
|
105
125
|
if (options.progressiveOutput && format !== 'json') {
|
|
106
126
|
// 即座に出力
|
|
@@ -112,14 +132,6 @@ export async function command(files, options, apiOptions) {
|
|
|
112
132
|
status: 'processed',
|
|
113
133
|
}, options);
|
|
114
134
|
}
|
|
115
|
-
else {
|
|
116
|
-
// 従来の動作:メモリに蓄積
|
|
117
|
-
processedFiles.push(result.filePath);
|
|
118
|
-
filesContent.set(result.filePath, {
|
|
119
|
-
sourceCode: result.sourceCode,
|
|
120
|
-
fixedCode: result.fixedCode,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
135
|
// Add violations to collector
|
|
124
136
|
collector.pushWithFile(result.filePath, ...result.violations);
|
|
125
137
|
const errorCount = result.violations.filter(v => v.severity === 'error').length;
|
|
@@ -137,19 +149,111 @@ export async function command(files, options, apiOptions) {
|
|
|
137
149
|
outputDryRunDiff(result.filePath, result.sourceCode, result.fixedCode);
|
|
138
150
|
}
|
|
139
151
|
}
|
|
152
|
+
// --- Suppressions handling ---
|
|
153
|
+
const suppressionsFilePath = resolveSuppressionsPath(options.suppressionsLocation);
|
|
154
|
+
const collectedViolationsByFile = collector.groupByFile();
|
|
155
|
+
// Build nodeLists map from engines for scope computation
|
|
156
|
+
const nodeLists = new Map();
|
|
157
|
+
for (const [filePath, engine] of engines) {
|
|
158
|
+
const doc = engine.document;
|
|
159
|
+
if (doc) {
|
|
160
|
+
// MLNode structurally satisfies PositionedNode (startLine, startCol, localName,
|
|
161
|
+
// id, classList, parentElement, children are all present). The double cast is
|
|
162
|
+
// needed because TypeScript can't verify structural compatibility between the
|
|
163
|
+
// generic MLNode<T,O> and the plain PositionedNode interface at compile time.
|
|
164
|
+
nodeLists.set(filePath, doc.nodeList);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (isSuppressMode) {
|
|
168
|
+
// Suppress mode: generate/update suppressions file
|
|
169
|
+
const existing = await readSuppressionsFile(suppressionsFilePath);
|
|
170
|
+
const generated = generateSuppressions(collectedViolationsByFile, suppressionsFilePath, {
|
|
171
|
+
filterRule: options.suppressRule,
|
|
172
|
+
nodeLists,
|
|
173
|
+
});
|
|
174
|
+
const merged = mergeSuppressions(existing, generated);
|
|
175
|
+
await writeSuppressionsFile(suppressionsFilePath, merged);
|
|
176
|
+
let totalSuppressed = 0;
|
|
177
|
+
let totalRules = 0;
|
|
178
|
+
for (const rules of Object.values(generated)) {
|
|
179
|
+
for (const entry of Object.values(rules)) {
|
|
180
|
+
totalSuppressed += entry.count;
|
|
181
|
+
totalRules++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
process.stderr.write(`[Experimental] ${totalSuppressed} violation(s) for ${totalRules} rule(s) suppressed in ${path.relative(process.cwd(), suppressionsFilePath)}\n`);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (isPruneMode) {
|
|
188
|
+
// Prune mode: remove stale entries
|
|
189
|
+
const existing = await readSuppressionsFile(suppressionsFilePath);
|
|
190
|
+
if (Object.keys(existing).length === 0) {
|
|
191
|
+
process.stderr.write('No suppressions file found. Nothing to prune.\n');
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
const pruned = pruneSuppressions(collectedViolationsByFile, existing, suppressionsFilePath);
|
|
195
|
+
await writeSuppressionsFile(suppressionsFilePath, pruned);
|
|
196
|
+
const existingCount = Object.values(existing).reduce((sum, rules) => sum + Object.keys(rules).length, 0);
|
|
197
|
+
const prunedCount = Object.values(pruned).reduce((sum, rules) => sum + Object.keys(rules).length, 0);
|
|
198
|
+
const removedCount = existingCount - prunedCount;
|
|
199
|
+
process.stderr.write(`[Experimental] Suppressions pruned: ${removedCount} entry/entries removed, ${prunedCount} remaining.\n`);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
// Normal lint mode: apply suppressions if file exists
|
|
203
|
+
let outputViolationsByFile = collectedViolationsByFile;
|
|
204
|
+
let suppressionsApplied = false;
|
|
205
|
+
try {
|
|
206
|
+
await fs.access(suppressionsFilePath);
|
|
207
|
+
const suppressionsData = await readSuppressionsFile(suppressionsFilePath);
|
|
208
|
+
if (Object.keys(suppressionsData).length > 0) {
|
|
209
|
+
const { filtered, unusedEntries } = applySuppressions(collectedViolationsByFile, suppressionsData, suppressionsFilePath, { nodeLists });
|
|
210
|
+
outputViolationsByFile = filtered;
|
|
211
|
+
suppressionsApplied = true;
|
|
212
|
+
if (unusedEntries.length > 0) {
|
|
213
|
+
process.stderr.write(`[Experimental] ${unusedEntries.length} unused suppression entry/entries found. Run with --prune-suppressions to clean up.\n`);
|
|
214
|
+
}
|
|
215
|
+
// Recalculate hasError and totalWarningCount from filtered violations
|
|
216
|
+
hasError = false;
|
|
217
|
+
totalWarningCount = 0;
|
|
218
|
+
for (const violations of filtered.values()) {
|
|
219
|
+
const errorCount = violations.filter(v => v.severity === 'error').length;
|
|
220
|
+
const warningCount = violations.filter(v => v.severity === 'warning').length;
|
|
221
|
+
totalWarningCount += warningCount;
|
|
222
|
+
if (errorCount > 0 || (warningCount > 0 && !options.allowWarnings)) {
|
|
223
|
+
hasError = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (isFatalError(error)) {
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
// Suppressions file does not exist or is unreadable, proceed normally
|
|
233
|
+
}
|
|
140
234
|
// Output results
|
|
141
235
|
if (format === 'json') {
|
|
142
|
-
|
|
143
|
-
|
|
236
|
+
if (suppressionsApplied) {
|
|
237
|
+
// Build filtered array with filePath
|
|
238
|
+
const jsonOutput = [];
|
|
239
|
+
for (const [filePath, violations] of outputViolationsByFile) {
|
|
240
|
+
for (const violation of violations) {
|
|
241
|
+
jsonOutput.push({ ...violation, filePath });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const jsonOutput = collector.toArray();
|
|
248
|
+
process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
|
|
249
|
+
}
|
|
144
250
|
return false;
|
|
145
251
|
}
|
|
146
252
|
// Progressive出力が無効の場合のみループ後に出力
|
|
147
253
|
if (!options.progressiveOutput) {
|
|
148
|
-
// For standard/simple/github output, group violations by file
|
|
149
|
-
const violationsByFile = collector.groupByFile();
|
|
150
254
|
// Output per file - include processed files without violations
|
|
151
255
|
for (const filePath of processedFiles) {
|
|
152
|
-
const violations =
|
|
256
|
+
const violations = outputViolationsByFile.get(filePath) || [];
|
|
153
257
|
const content = filesContent.get(filePath) || { sourceCode: '', fixedCode: '' };
|
|
154
258
|
if (violations.length === 0 && !options.problemOnly) {
|
|
155
259
|
log('Output reports');
|
|
@@ -180,6 +284,40 @@ export async function command(files, options, apiOptions) {
|
|
|
180
284
|
}
|
|
181
285
|
}
|
|
182
286
|
}
|
|
287
|
+
let errorCount = 0;
|
|
288
|
+
let warningCount = 0;
|
|
289
|
+
let noticeCount = 0;
|
|
290
|
+
let failedFileCount = 0;
|
|
291
|
+
for (const filePath of processedFiles) {
|
|
292
|
+
const violations = outputViolationsByFile.get(filePath) || [];
|
|
293
|
+
if (violations.length > 0) {
|
|
294
|
+
failedFileCount++;
|
|
295
|
+
}
|
|
296
|
+
for (const violation of violations) {
|
|
297
|
+
switch (violation.severity) {
|
|
298
|
+
case 'error': {
|
|
299
|
+
errorCount++;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
case 'warning': {
|
|
303
|
+
warningCount++;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
case 'info': {
|
|
307
|
+
noticeCount++;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
outputSummary({
|
|
314
|
+
checkedFileCount: processedFiles.length,
|
|
315
|
+
failedFileCount,
|
|
316
|
+
skippedFileCount: skippedFiles.length,
|
|
317
|
+
errorCount,
|
|
318
|
+
warningCount,
|
|
319
|
+
noticeCount,
|
|
320
|
+
}, options);
|
|
183
321
|
// Check if max-warnings limit is exceeded (ESLint compatible)
|
|
184
322
|
if (!hasError && options.maxWarnings >= 0 && totalWarningCount > options.maxWarnings) {
|
|
185
323
|
hasError = true;
|
package/lib/cli/output.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { CLIOptions } from './bootstrap.js';
|
|
2
2
|
import type { MLResultInfo } from '../types.js';
|
|
3
|
+
export interface SummaryInfo {
|
|
4
|
+
readonly checkedFileCount: number;
|
|
5
|
+
readonly failedFileCount: number;
|
|
6
|
+
readonly skippedFileCount: number;
|
|
7
|
+
readonly errorCount: number;
|
|
8
|
+
readonly warningCount: number;
|
|
9
|
+
readonly noticeCount: number;
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
12
|
* Writes lint results to stdout or stderr using the reporter selected by `--format`.
|
|
5
13
|
*
|
|
@@ -12,3 +20,14 @@ import type { MLResultInfo } from '../types.js';
|
|
|
12
20
|
* @param options - CLI options that control the output format and color.
|
|
13
21
|
*/
|
|
14
22
|
export declare function output(results: MLResultInfo, options: CLIOptions): void;
|
|
23
|
+
/**
|
|
24
|
+
* Writes an aggregated lint summary after all file outputs have been emitted.
|
|
25
|
+
*
|
|
26
|
+
* Summary output is enabled for the standard and simple formats only.
|
|
27
|
+
* JSON and GitHub formats are left unchanged because they are intended for
|
|
28
|
+
* machine-readable output.
|
|
29
|
+
*
|
|
30
|
+
* @param summary - Aggregated counts across all linted files.
|
|
31
|
+
* @param options - CLI options that control the output format and color.
|
|
32
|
+
*/
|
|
33
|
+
export declare function outputSummary(summary: SummaryInfo, options: CLIOptions): void;
|
package/lib/cli/output.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { font, xterm } from '@markuplint/cli-utils';
|
|
1
2
|
import stripAnsi from 'strip-ansi';
|
|
2
3
|
import { simpleReporter, standardReporter, githubReporter } from '../reporter/index.js';
|
|
4
|
+
const loggerError = font.red;
|
|
5
|
+
const loggerWarning = xterm(208);
|
|
3
6
|
/**
|
|
4
7
|
* Writes lint results to stdout or stderr using the reporter selected by `--format`.
|
|
5
8
|
*
|
|
@@ -43,3 +46,63 @@ export function output(results, options) {
|
|
|
43
46
|
}
|
|
44
47
|
process.stdout.write(msg);
|
|
45
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Writes an aggregated lint summary after all file outputs have been emitted.
|
|
51
|
+
*
|
|
52
|
+
* Summary output is enabled for the standard and simple formats only.
|
|
53
|
+
* JSON and GitHub formats are left unchanged because they are intended for
|
|
54
|
+
* machine-readable output.
|
|
55
|
+
*
|
|
56
|
+
* @param summary - Aggregated counts across all linted files.
|
|
57
|
+
* @param options - CLI options that control the output format and color.
|
|
58
|
+
*/
|
|
59
|
+
export function outputSummary(summary, options) {
|
|
60
|
+
const format = options.format?.toLowerCase() ?? 'standard';
|
|
61
|
+
if (format === 'json' || format === 'github') {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (summary.checkedFileCount === 0 && summary.skippedFileCount === 0) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const problemCount = summary.errorCount + summary.warningCount + summary.noticeCount;
|
|
68
|
+
if (options.problemOnly && problemCount === 0) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const out = createSummaryLines(summary);
|
|
72
|
+
if (out.length === 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let msg = `\n${out.join('\n')}\n`;
|
|
76
|
+
msg = options.color ? msg : stripAnsi(msg);
|
|
77
|
+
if (problemCount > 0) {
|
|
78
|
+
process.stderr.write(msg);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
process.stdout.write(msg);
|
|
82
|
+
}
|
|
83
|
+
function createSummaryLines(summary) {
|
|
84
|
+
const out = [];
|
|
85
|
+
const problemCount = summary.errorCount + summary.warningCount + summary.noticeCount;
|
|
86
|
+
const passedFileCount = summary.checkedFileCount - summary.failedFileCount;
|
|
87
|
+
if (problemCount === 0) {
|
|
88
|
+
out.push(`${font.green('✔')} ${pluralize(summary.checkedFileCount, 'file')} checked, 0 problems`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const problemDetail = [
|
|
92
|
+
`${summary.errorCount} error${summary.errorCount === 1 ? '' : 's'}`,
|
|
93
|
+
`${summary.warningCount} warning${summary.warningCount === 1 ? '' : 's'}`,
|
|
94
|
+
summary.noticeCount > 0 ? `${summary.noticeCount} notice${summary.noticeCount === 1 ? '' : 's'}` : null,
|
|
95
|
+
]
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.join(', ');
|
|
98
|
+
const logger = summary.errorCount > 0 ? loggerError : loggerWarning;
|
|
99
|
+
out.push(`${logger('✖')} ${problemCount} problem${problemCount === 1 ? '' : 's'} (${problemDetail}) in ${pluralize(summary.checkedFileCount, 'file')}`, `${pluralize(summary.checkedFileCount, 'file')} checked: ${passedFileCount} passed, ${summary.failedFileCount} failed`);
|
|
100
|
+
}
|
|
101
|
+
if (summary.skippedFileCount > 0) {
|
|
102
|
+
out.push(`${font.yellow('⚠')} ${pluralize(summary.skippedFileCount, 'file')} skipped because --max-count was reached`);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
function pluralize(count, noun) {
|
|
107
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Violation } from '@markuplint/ml-config';
|
|
2
|
+
import type { PositionedNode } from './compute-scope.js';
|
|
3
|
+
import type { SuppressionsData } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* @experimental
|
|
6
|
+
* Result of applying suppressions to violations.
|
|
7
|
+
*/
|
|
8
|
+
export type ApplySuppressionsResult = {
|
|
9
|
+
/** Violations after filtering out suppressed ones. */
|
|
10
|
+
readonly filtered: Map<string, Violation[]>;
|
|
11
|
+
/** List of unused suppression entries (as "filePath:ruleId" strings). */
|
|
12
|
+
readonly unusedEntries: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* @experimental
|
|
16
|
+
* Options for applying suppressions.
|
|
17
|
+
*/
|
|
18
|
+
export type ApplySuppressionsOptions = {
|
|
19
|
+
/**
|
|
20
|
+
* Map of absolute file paths to their document node lists.
|
|
21
|
+
* When provided, scope-aware filtering is used.
|
|
22
|
+
* When absent, scope is ignored (Phase 1 compatible).
|
|
23
|
+
*/
|
|
24
|
+
readonly nodeLists?: ReadonlyMap<string, readonly PositionedNode[]>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* @experimental
|
|
28
|
+
* Applies suppressions to collected violations.
|
|
29
|
+
*
|
|
30
|
+
* For each file+ruleId pair:
|
|
31
|
+
* - If the entry has a `scope`, only violations within that scope subtree are
|
|
32
|
+
* counted AND filtered. Violations outside the scope are always passed through.
|
|
33
|
+
* - If current scoped error count <= suppressed count: scoped error violations are removed.
|
|
34
|
+
* - If current scoped error count > suppressed count: ALL scoped violations are kept.
|
|
35
|
+
*
|
|
36
|
+
* Warning and info violations always pass through unmodified.
|
|
37
|
+
* When `options.nodeLists` is not provided, scope is ignored (Phase 1 compatible).
|
|
38
|
+
*
|
|
39
|
+
* @param violationsByFile - Map of absolute file paths to violations.
|
|
40
|
+
* @param suppressions - The loaded suppressions data.
|
|
41
|
+
* @param suppressionsFilePath - Absolute path to the suppressions file.
|
|
42
|
+
* @param options - Optional options including document node lists for scope-aware filtering.
|
|
43
|
+
* @returns Filtered violations and a list of unused suppression entries.
|
|
44
|
+
*/
|
|
45
|
+
export declare function applySuppressions(violationsByFile: ReadonlyMap<string, readonly Violation[]>, suppressions: SuppressionsData, suppressionsFilePath: string, options?: ApplySuppressionsOptions): ApplySuppressionsResult;
|