@herb-tools/linter 0.4.3 → 0.6.0
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 +216 -19
- package/dist/herb-lint.js +5559 -1860
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +722 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +714 -189
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -4
- package/dist/src/cli/argument-parser.js +28 -22
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +19 -13
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli/formatters/detailed-formatter.js +9 -9
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
- package/dist/src/cli/formatters/github-actions-formatter.js +50 -0
- package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -0
- package/dist/src/cli/formatters/index.js +2 -0
- package/dist/src/cli/formatters/index.js.map +1 -1
- package/dist/src/cli/formatters/json-formatter.js +58 -0
- package/dist/src/cli/formatters/json-formatter.js.map +1 -0
- package/dist/src/cli/formatters/simple-formatter.js +15 -15
- package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
- package/dist/src/cli/output-manager.js +120 -0
- package/dist/src/cli/output-manager.js.map +1 -0
- package/dist/src/cli/summary-reporter.js +22 -22
- package/dist/src/cli/summary-reporter.js.map +1 -1
- package/dist/src/cli.js +41 -26
- package/dist/src/cli.js.map +1 -1
- package/dist/src/default-rules.js +22 -0
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/linter.js +29 -4
- package/dist/src/linter.js.map +1 -1
- package/dist/src/rules/erb-no-empty-tags.js +2 -2
- package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
- package/dist/src/rules/erb-no-output-control-flow.js +2 -2
- package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
- package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +26 -0
- package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
- package/dist/src/rules/erb-prefer-image-tag-helper.js +2 -6
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
- package/dist/src/rules/erb-require-whitespace-inside-tags.js +2 -2
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
- package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -1
- package/dist/src/rules/html-anchor-require-href.js +2 -2
- package/dist/src/rules/html-anchor-require-href.js.map +1 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js +13 -12
- package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-aria-label-is-well-formatted.js +33 -0
- package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -0
- package/dist/src/rules/html-aria-level-must-be-valid.js +28 -6
- package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-aria-role-heading-requires-level.js +9 -15
- package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
- package/dist/src/rules/html-aria-role-must-be-valid.js +5 -5
- package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-attribute-double-quotes.js +16 -6
- package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
- package/dist/src/rules/html-attribute-equals-spacing.js +24 -0
- package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -0
- package/dist/src/rules/html-attribute-values-require-quotes.js +21 -10
- package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
- package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +47 -0
- package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
- package/dist/src/rules/html-boolean-attributes-no-value.js +11 -4
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-iframe-has-title.js +39 -0
- package/dist/src/rules/html-iframe-has-title.js.map +1 -0
- package/dist/src/rules/html-img-require-alt.js +2 -6
- package/dist/src/rules/html-img-require-alt.js.map +1 -1
- package/dist/src/rules/html-navigation-has-label.js +43 -0
- package/dist/src/rules/html-navigation-has-label.js.map +1 -0
- package/dist/src/rules/html-no-aria-hidden-on-focusable.js +67 -0
- package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
- package/dist/src/rules/html-no-block-inside-inline.js +4 -4
- package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-attributes.js +24 -27
- package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-ids.js +4 -4
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
- package/dist/src/rules/html-no-empty-headings.js +2 -23
- package/dist/src/rules/html-no-empty-headings.js.map +1 -1
- package/dist/src/rules/html-no-nested-links.js +2 -2
- package/dist/src/rules/html-no-nested-links.js.map +1 -1
- package/dist/src/rules/html-no-positive-tab-index.js +21 -0
- package/dist/src/rules/html-no-positive-tab-index.js.map +1 -0
- package/dist/src/rules/html-no-self-closing.js +22 -0
- package/dist/src/rules/html-no-self-closing.js.map +1 -0
- package/dist/src/rules/html-no-title-attribute.js +27 -0
- package/dist/src/rules/html-no-title-attribute.js.map +1 -0
- package/dist/src/rules/html-tag-name-lowercase.js +37 -25
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
- package/dist/src/rules/index.js +10 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/parser-no-errors.js +18 -0
- package/dist/src/rules/parser-no-errors.js.map +1 -0
- package/dist/src/rules/rule-utils.js +176 -22
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +2 -10
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/cli/argument-parser.d.ts +2 -1
- package/dist/types/cli/file-processor.d.ts +6 -5
- package/dist/types/cli/formatters/base-formatter.d.ts +2 -2
- package/dist/types/cli/formatters/detailed-formatter.d.ts +2 -2
- package/dist/types/cli/formatters/github-actions-formatter.d.ts +12 -0
- package/dist/types/cli/formatters/index.d.ts +2 -0
- package/dist/types/cli/formatters/json-formatter.d.ts +42 -0
- package/dist/types/cli/formatters/simple-formatter.d.ts +2 -2
- package/dist/types/cli/index.d.ts +4 -0
- package/dist/types/cli/output-manager.d.ts +31 -0
- package/dist/types/cli/summary-reporter.d.ts +3 -3
- package/dist/types/cli.d.ts +3 -1
- package/dist/types/rules/erb-no-empty-tags.d.ts +2 -2
- package/dist/types/rules/erb-no-output-control-flow.d.ts +2 -2
- package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
- package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +2 -2
- package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
- package/dist/types/rules/html-anchor-require-href.d.ts +2 -2
- package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
- package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +7 -0
- package/dist/types/rules/html-aria-level-must-be-valid.d.ts +2 -2
- package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +2 -2
- package/dist/types/rules/html-aria-role-must-be-valid.d.ts +2 -2
- package/dist/types/rules/html-attribute-double-quotes.d.ts +2 -2
- package/dist/types/rules/html-attribute-equals-spacing.d.ts +7 -0
- package/dist/types/rules/html-attribute-values-require-quotes.d.ts +2 -2
- package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
- package/dist/types/rules/html-boolean-attributes-no-value.d.ts +2 -2
- package/dist/types/rules/html-iframe-has-title.d.ts +7 -0
- package/dist/types/rules/html-img-require-alt.d.ts +2 -2
- package/dist/types/rules/html-navigation-has-label.d.ts +7 -0
- package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
- package/dist/types/rules/html-no-block-inside-inline.d.ts +2 -2
- package/dist/types/rules/html-no-duplicate-attributes.d.ts +2 -2
- package/dist/types/rules/html-no-duplicate-ids.d.ts +2 -2
- package/dist/types/rules/html-no-empty-headings.d.ts +2 -2
- package/dist/types/rules/html-no-nested-links.d.ts +2 -2
- package/dist/types/rules/html-no-positive-tab-index.d.ts +7 -0
- package/dist/types/rules/html-no-self-closing.d.ts +7 -0
- package/dist/types/rules/html-no-title-attribute.d.ts +7 -0
- package/dist/types/rules/html-tag-name-lowercase.d.ts +3 -2
- package/dist/types/rules/index.d.ts +10 -0
- package/dist/types/rules/parser-no-errors.d.ts +8 -0
- package/dist/types/rules/rule-utils.d.ts +107 -13
- package/dist/types/rules/svg-tag-name-capitalization.d.ts +2 -2
- package/dist/types/src/cli/argument-parser.d.ts +2 -1
- package/dist/types/src/cli/file-processor.d.ts +6 -5
- package/dist/types/src/cli/formatters/base-formatter.d.ts +2 -2
- package/dist/types/src/cli/formatters/detailed-formatter.d.ts +2 -2
- package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +12 -0
- package/dist/types/src/cli/formatters/index.d.ts +2 -0
- package/dist/types/src/cli/formatters/json-formatter.d.ts +42 -0
- package/dist/types/src/cli/formatters/simple-formatter.d.ts +2 -2
- package/dist/types/src/cli/output-manager.d.ts +31 -0
- package/dist/types/src/cli/summary-reporter.d.ts +3 -3
- package/dist/types/src/cli.d.ts +3 -1
- package/dist/types/src/rules/erb-no-empty-tags.d.ts +2 -2
- package/dist/types/src/rules/erb-no-output-control-flow.d.ts +2 -2
- package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
- package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +2 -2
- package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
- package/dist/types/src/rules/html-anchor-require-href.d.ts +2 -2
- package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
- package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +7 -0
- package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +2 -2
- package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +2 -2
- package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +2 -2
- package/dist/types/src/rules/html-attribute-double-quotes.d.ts +2 -2
- package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +7 -0
- package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +2 -2
- package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
- package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +2 -2
- package/dist/types/src/rules/html-iframe-has-title.d.ts +7 -0
- package/dist/types/src/rules/html-img-require-alt.d.ts +2 -2
- package/dist/types/src/rules/html-navigation-has-label.d.ts +7 -0
- package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
- package/dist/types/src/rules/html-no-block-inside-inline.d.ts +2 -2
- package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +2 -2
- package/dist/types/src/rules/html-no-duplicate-ids.d.ts +2 -2
- package/dist/types/src/rules/html-no-empty-headings.d.ts +2 -2
- package/dist/types/src/rules/html-no-nested-links.d.ts +2 -2
- package/dist/types/src/rules/html-no-positive-tab-index.d.ts +7 -0
- package/dist/types/src/rules/html-no-self-closing.d.ts +7 -0
- package/dist/types/src/rules/html-no-title-attribute.d.ts +7 -0
- package/dist/types/src/rules/html-tag-name-lowercase.d.ts +3 -2
- package/dist/types/src/rules/index.d.ts +10 -0
- package/dist/types/src/rules/parser-no-errors.d.ts +8 -0
- package/dist/types/src/rules/rule-utils.d.ts +107 -13
- package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +2 -2
- package/dist/types/src/types.d.ts +27 -3
- package/dist/types/types.d.ts +27 -3
- package/docs/rules/README.md +13 -2
- package/docs/rules/erb-no-silent-tag-in-attribute-name.md +34 -0
- package/docs/rules/html-aria-label-is-well-formatted.md +49 -0
- package/docs/rules/html-attribute-equals-spacing.md +35 -0
- package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +48 -0
- package/docs/rules/html-iframe-has-title.md +43 -0
- package/docs/rules/html-navigation-has-label.md +61 -0
- package/docs/rules/html-no-aria-hidden-on-focusable.md +54 -0
- package/docs/rules/html-no-positive-tab-index.md +55 -0
- package/docs/rules/html-no-self-closing.md +65 -0
- package/docs/rules/html-no-title-attribute.md +69 -0
- package/docs/rules/html-tag-name-lowercase.md +16 -3
- package/docs/rules/parser-no-errors.md +84 -0
- package/package.json +4 -4
- package/src/cli/argument-parser.ts +33 -24
- package/src/cli/file-processor.ts +25 -17
- package/src/cli/formatters/base-formatter.ts +2 -2
- package/src/cli/formatters/detailed-formatter.ts +9 -9
- package/src/cli/formatters/github-actions-formatter.ts +70 -0
- package/src/cli/formatters/index.ts +2 -0
- package/src/cli/formatters/json-formatter.ts +107 -0
- package/src/cli/formatters/simple-formatter.ts +15 -15
- package/src/cli/output-manager.ts +143 -0
- package/src/cli/summary-reporter.ts +24 -24
- package/src/cli.ts +48 -31
- package/src/default-rules.ts +22 -0
- package/src/linter.ts +30 -4
- package/src/rules/erb-no-empty-tags.ts +3 -3
- package/src/rules/erb-no-output-control-flow.ts +3 -3
- package/src/rules/erb-no-silent-tag-in-attribute-name.ts +40 -0
- package/src/rules/erb-prefer-image-tag-helper.ts +4 -9
- package/src/rules/erb-require-whitespace-inside-tags.ts +3 -3
- package/src/rules/erb-requires-trailing-newline.ts +2 -0
- package/src/rules/html-anchor-require-href.ts +3 -3
- package/src/rules/html-aria-attribute-must-be-valid.ts +29 -33
- package/src/rules/html-aria-label-is-well-formatted.ts +59 -0
- package/src/rules/html-aria-level-must-be-valid.ts +40 -7
- package/src/rules/html-aria-role-heading-requires-level.ts +18 -30
- package/src/rules/html-aria-role-must-be-valid.ts +7 -7
- package/src/rules/html-attribute-double-quotes.ts +23 -8
- package/src/rules/html-attribute-equals-spacing.ts +41 -0
- package/src/rules/html-attribute-values-require-quotes.ts +32 -12
- package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +66 -0
- package/src/rules/html-boolean-attributes-no-value.ts +19 -6
- package/src/rules/html-iframe-has-title.ts +62 -0
- package/src/rules/html-img-require-alt.ts +4 -9
- package/src/rules/html-navigation-has-label.ts +64 -0
- package/src/rules/html-no-aria-hidden-on-focusable.ts +90 -0
- package/src/rules/html-no-block-inside-inline.ts +5 -5
- package/src/rules/html-no-duplicate-attributes.ts +30 -30
- package/src/rules/html-no-duplicate-ids.ts +6 -5
- package/src/rules/html-no-empty-headings.ts +4 -33
- package/src/rules/html-no-nested-links.ts +3 -3
- package/src/rules/html-no-positive-tab-index.ts +33 -0
- package/src/rules/html-no-self-closing.ts +36 -0
- package/src/rules/html-no-title-attribute.ts +42 -0
- package/src/rules/html-tag-name-lowercase.ts +44 -31
- package/src/rules/index.ts +10 -0
- package/src/rules/parser-no-errors.ts +25 -0
- package/src/rules/rule-utils.ts +260 -39
- package/src/rules/svg-tag-name-capitalization.ts +4 -11
- package/src/types.ts +30 -3
|
@@ -4,32 +4,33 @@ import { Herb } from "@herb-tools/node-wasm"
|
|
|
4
4
|
import { Linter } from "../linter.js"
|
|
5
5
|
import { colorize } from "@herb-tools/highlighter"
|
|
6
6
|
import type { Diagnostic } from "@herb-tools/core"
|
|
7
|
+
import type { FormatOption } from "./argument-parser.js"
|
|
7
8
|
|
|
8
9
|
export interface ProcessedFile {
|
|
9
10
|
filename: string
|
|
10
|
-
|
|
11
|
+
offense: Diagnostic
|
|
11
12
|
content: string
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface ProcessingResult {
|
|
15
16
|
totalErrors: number
|
|
16
17
|
totalWarnings: number
|
|
17
|
-
|
|
18
|
+
filesWithOffenses: number
|
|
18
19
|
ruleCount: number
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
allOffenses: ProcessedFile[]
|
|
21
|
+
ruleOffenses: Map<string, { count: number, files: Set<string> }>
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export class FileProcessor {
|
|
24
25
|
private linter: Linter | null = null
|
|
25
26
|
|
|
26
|
-
async processFiles(files: string[]): Promise<ProcessingResult> {
|
|
27
|
+
async processFiles(files: string[], formatOption: FormatOption = 'detailed'): Promise<ProcessingResult> {
|
|
27
28
|
let totalErrors = 0
|
|
28
29
|
let totalWarnings = 0
|
|
29
|
-
let
|
|
30
|
+
let filesWithOffenses = 0
|
|
30
31
|
let ruleCount = 0
|
|
31
|
-
const
|
|
32
|
-
const
|
|
32
|
+
const allOffenses: ProcessedFile[] = []
|
|
33
|
+
const ruleOffenses = new Map<string, { count: number, files: Set<string> }>()
|
|
33
34
|
|
|
34
35
|
for (const filename of files) {
|
|
35
36
|
const filePath = resolve(filename)
|
|
@@ -38,14 +39,21 @@ export class FileProcessor {
|
|
|
38
39
|
const parseResult = Herb.parse(content)
|
|
39
40
|
|
|
40
41
|
if (parseResult.errors.length > 0) {
|
|
41
|
-
|
|
42
|
+
if (formatOption !== 'json' && formatOption !== 'github') {
|
|
43
|
+
console.error(`${colorize(filename, "cyan")} - ${colorize("Parse errors:", "brightRed")}`)
|
|
42
44
|
|
|
45
|
+
for (const error of parseResult.errors) {
|
|
46
|
+
console.error(` ${colorize("✗", "brightRed")} ${error.message}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add parse errors to offenses for JSON output
|
|
43
51
|
for (const error of parseResult.errors) {
|
|
44
|
-
|
|
52
|
+
allOffenses.push({ filename, offense: error, content })
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
totalErrors++
|
|
48
|
-
|
|
56
|
+
filesWithOffenses++
|
|
49
57
|
continue
|
|
50
58
|
}
|
|
51
59
|
|
|
@@ -60,25 +68,25 @@ export class FileProcessor {
|
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
if (lintResult.offenses.length === 0) {
|
|
63
|
-
if (files.length === 1) {
|
|
71
|
+
if (files.length === 1 && formatOption !== 'json' && formatOption !== 'github') {
|
|
64
72
|
console.log(`${colorize("✓", "brightGreen")} ${colorize(filename, "cyan")} - ${colorize("No issues found", "green")}`)
|
|
65
73
|
}
|
|
66
74
|
} else {
|
|
67
75
|
for (const offense of lintResult.offenses) {
|
|
68
|
-
|
|
76
|
+
allOffenses.push({ filename, offense: offense, content })
|
|
69
77
|
|
|
70
|
-
const ruleData =
|
|
78
|
+
const ruleData = ruleOffenses.get(offense.rule) || { count: 0, files: new Set() }
|
|
71
79
|
ruleData.count++
|
|
72
80
|
ruleData.files.add(filename)
|
|
73
|
-
|
|
81
|
+
ruleOffenses.set(offense.rule, ruleData)
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
totalErrors += lintResult.errors
|
|
77
85
|
totalWarnings += lintResult.warnings
|
|
78
|
-
|
|
86
|
+
filesWithOffenses++
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
return { totalErrors, totalWarnings,
|
|
90
|
+
return { totalErrors, totalWarnings, filesWithOffenses, ruleCount, allOffenses, ruleOffenses }
|
|
83
91
|
}
|
|
84
92
|
}
|
|
@@ -3,9 +3,9 @@ import type { ProcessedFile } from "../file-processor.js"
|
|
|
3
3
|
|
|
4
4
|
export abstract class BaseFormatter {
|
|
5
5
|
abstract format(
|
|
6
|
-
|
|
6
|
+
allOffenses: ProcessedFile[],
|
|
7
7
|
isSingleFile?: boolean
|
|
8
8
|
): Promise<void>
|
|
9
9
|
|
|
10
|
-
abstract formatFile(filename: string,
|
|
10
|
+
abstract formatFile(filename: string, offenses: Diagnostic[]): void
|
|
11
11
|
}
|
|
@@ -18,8 +18,8 @@ export class DetailedFormatter extends BaseFormatter {
|
|
|
18
18
|
this.truncateLines = truncateLines
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
async format(
|
|
22
|
-
if (
|
|
21
|
+
async format(allOffenses: ProcessedFile[], isSingleFile: boolean = false): Promise<void> {
|
|
22
|
+
if (allOffenses.length === 0) return
|
|
23
23
|
|
|
24
24
|
if (!this.highlighter) {
|
|
25
25
|
this.highlighter = new Highlighter(this.theme)
|
|
@@ -28,8 +28,8 @@ export class DetailedFormatter extends BaseFormatter {
|
|
|
28
28
|
|
|
29
29
|
if (isSingleFile) {
|
|
30
30
|
// For single file, use inline diagnostics with syntax highlighting
|
|
31
|
-
const { filename, content } =
|
|
32
|
-
const diagnostics =
|
|
31
|
+
const { filename, content } = allOffenses[0]
|
|
32
|
+
const diagnostics = allOffenses.map(item => item.offense)
|
|
33
33
|
|
|
34
34
|
const highlighted = this.highlighter.highlight(filename, content, {
|
|
35
35
|
diagnostics: diagnostics,
|
|
@@ -42,11 +42,11 @@ export class DetailedFormatter extends BaseFormatter {
|
|
|
42
42
|
console.log(`\n${highlighted}`)
|
|
43
43
|
} else {
|
|
44
44
|
// For multiple files, show individual diagnostics with syntax highlighting
|
|
45
|
-
const totalMessageCount =
|
|
45
|
+
const totalMessageCount = allOffenses.length
|
|
46
46
|
|
|
47
|
-
for (let i = 0; i <
|
|
48
|
-
const { filename,
|
|
49
|
-
const formatted = this.highlighter.highlightDiagnostic(filename,
|
|
47
|
+
for (let i = 0; i < allOffenses.length; i++) {
|
|
48
|
+
const { filename, offense, content } = allOffenses[i]
|
|
49
|
+
const formatted = this.highlighter.highlightDiagnostic(filename, offense, content, {
|
|
50
50
|
contextLines: 2,
|
|
51
51
|
wrapLines: this.wrapLines,
|
|
52
52
|
truncateLines: this.truncateLines
|
|
@@ -67,7 +67,7 @@ export class DetailedFormatter extends BaseFormatter {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
formatFile(_filename: string,
|
|
70
|
+
formatFile(_filename: string, _offenses: Diagnostic[]): void {
|
|
71
71
|
// Not used in detailed formatter
|
|
72
72
|
throw new Error("formatFile is not implemented for DetailedFormatter")
|
|
73
73
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { BaseFormatter } from "./base-formatter.js"
|
|
2
|
+
|
|
3
|
+
import type { Diagnostic } from "@herb-tools/core"
|
|
4
|
+
import type { ProcessedFile } from "../file-processor.js"
|
|
5
|
+
|
|
6
|
+
// https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands
|
|
7
|
+
export class GitHubActionsFormatter extends BaseFormatter {
|
|
8
|
+
private static readonly MESSAGE_ESCAPE_MAP: Record<string, string> = {
|
|
9
|
+
'%': '%25',
|
|
10
|
+
'\n': '%0A',
|
|
11
|
+
'\r': '%0D'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private static readonly PARAM_ESCAPE_MAP: Record<string, string> = {
|
|
15
|
+
'%': '%25',
|
|
16
|
+
'\n': '%0A',
|
|
17
|
+
'\r': '%0D',
|
|
18
|
+
':': '%3A',
|
|
19
|
+
',': '%2C'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async format(allDiagnostics: ProcessedFile[]): Promise<void> {
|
|
23
|
+
for (const { filename, offense } of allDiagnostics) {
|
|
24
|
+
this.formatDiagnostic(filename, offense)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (allDiagnostics.length > 0) {
|
|
28
|
+
console.log()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
formatFile(filename: string, diagnostics: Diagnostic[]): void {
|
|
33
|
+
for (const diagnostic of diagnostics) {
|
|
34
|
+
this.formatDiagnostic(filename, diagnostic)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// GitHub Actions annotation format:
|
|
39
|
+
// ::{level} file={file},line={line},col={col}::{message}
|
|
40
|
+
//
|
|
41
|
+
private formatDiagnostic(filename: string, diagnostic: Diagnostic): void {
|
|
42
|
+
const level = diagnostic.severity === "error" ? "error" : "warning"
|
|
43
|
+
const { line, column } = diagnostic.location.start
|
|
44
|
+
|
|
45
|
+
const escapedFilename = this.escapeParam(filename)
|
|
46
|
+
const message = this.escapeMessage(diagnostic.message)
|
|
47
|
+
|
|
48
|
+
let fullMessage = message
|
|
49
|
+
|
|
50
|
+
if (diagnostic.code) {
|
|
51
|
+
fullMessage += ` [${diagnostic.code}]`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`\n::${level} file=${escapedFilename},line=${line},col=${column}::${fullMessage}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private escapeMessage(string: string): string {
|
|
58
|
+
return string.replace(
|
|
59
|
+
new RegExp(Object.keys(GitHubActionsFormatter.MESSAGE_ESCAPE_MAP).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g'),
|
|
60
|
+
match => GitHubActionsFormatter.MESSAGE_ESCAPE_MAP[match]
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private escapeParam(string: string): string {
|
|
65
|
+
return string.replace(
|
|
66
|
+
new RegExp(Object.keys(GitHubActionsFormatter.PARAM_ESCAPE_MAP).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g'),
|
|
67
|
+
match => GitHubActionsFormatter.PARAM_ESCAPE_MAP[match]
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { BaseFormatter } from "./base-formatter.js"
|
|
2
2
|
export { SimpleFormatter } from "./simple-formatter.js"
|
|
3
3
|
export { DetailedFormatter } from "./detailed-formatter.js"
|
|
4
|
+
export { JSONFormatter, type JSONOutput } from "./json-formatter.js"
|
|
5
|
+
export { GitHubActionsFormatter } from "./github-actions-formatter.js"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { BaseFormatter } from "./base-formatter.js"
|
|
2
|
+
|
|
3
|
+
import type { Diagnostic, SerializedDiagnostic } from "@herb-tools/core"
|
|
4
|
+
import type { ProcessedFile } from "../file-processor.js"
|
|
5
|
+
|
|
6
|
+
interface JSONOffense extends SerializedDiagnostic {
|
|
7
|
+
filename: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface JSONSummary {
|
|
11
|
+
filesChecked: number
|
|
12
|
+
filesWithOffenses: number
|
|
13
|
+
totalErrors: number
|
|
14
|
+
totalWarnings: number
|
|
15
|
+
totalOffenses: number
|
|
16
|
+
ruleCount: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface JSONTiming {
|
|
20
|
+
startTime: string
|
|
21
|
+
duration: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface JSONOutput {
|
|
25
|
+
offenses: JSONOffense[]
|
|
26
|
+
summary: JSONSummary | null
|
|
27
|
+
timing: JSONTiming | null
|
|
28
|
+
completed: boolean
|
|
29
|
+
clean: boolean | null
|
|
30
|
+
message: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface JSONFormatOptions {
|
|
34
|
+
files: string[]
|
|
35
|
+
totalErrors: number
|
|
36
|
+
totalWarnings: number
|
|
37
|
+
filesWithOffenses: number
|
|
38
|
+
ruleCount: number
|
|
39
|
+
startTime: number
|
|
40
|
+
startDate: Date
|
|
41
|
+
showTiming: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class JSONFormatter extends BaseFormatter {
|
|
45
|
+
async format(allOffenses: ProcessedFile[]): Promise<void> {
|
|
46
|
+
const jsonOffenses: JSONOffense[] = allOffenses.map(({ filename, offense }) => ({
|
|
47
|
+
filename,
|
|
48
|
+
message: offense.message,
|
|
49
|
+
location: offense.location.toJSON(),
|
|
50
|
+
severity: offense.severity,
|
|
51
|
+
code: offense.code,
|
|
52
|
+
source: offense.source
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
const output: JSONOutput = {
|
|
56
|
+
offenses: jsonOffenses,
|
|
57
|
+
summary: null,
|
|
58
|
+
timing: null,
|
|
59
|
+
completed: true,
|
|
60
|
+
clean: jsonOffenses.length === 0,
|
|
61
|
+
message: null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(JSON.stringify(output, null, 2))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async formatWithSummary(allOffenses: ProcessedFile[], options: JSONFormatOptions): Promise<void> {
|
|
68
|
+
const jsonOffenses: JSONOffense[] = allOffenses.map(({ filename, offense }) => ({
|
|
69
|
+
filename,
|
|
70
|
+
message: offense.message,
|
|
71
|
+
location: offense.location.toJSON(),
|
|
72
|
+
severity: offense.severity,
|
|
73
|
+
code: offense.code,
|
|
74
|
+
source: offense.source
|
|
75
|
+
}))
|
|
76
|
+
|
|
77
|
+
const summary: JSONSummary = {
|
|
78
|
+
filesChecked: options.files.length,
|
|
79
|
+
filesWithOffenses: options.filesWithOffenses,
|
|
80
|
+
totalErrors: options.totalErrors,
|
|
81
|
+
totalWarnings: options.totalWarnings,
|
|
82
|
+
totalOffenses: options.totalErrors + options.totalWarnings,
|
|
83
|
+
ruleCount: options.ruleCount
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const output: JSONOutput = {
|
|
87
|
+
offenses: jsonOffenses,
|
|
88
|
+
summary,
|
|
89
|
+
timing: null,
|
|
90
|
+
completed: true,
|
|
91
|
+
clean: options.totalErrors === 0 && options.totalWarnings === 0,
|
|
92
|
+
message: null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const duration = Date.now() - options.startTime
|
|
96
|
+
output.timing = options.showTiming ? {
|
|
97
|
+
startTime: options.startDate.toISOString(),
|
|
98
|
+
duration: duration
|
|
99
|
+
} : null
|
|
100
|
+
|
|
101
|
+
console.log(JSON.stringify(output, null, 2))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
formatFile(_filename: string, _offenses: Diagnostic[]): void {
|
|
105
|
+
// Not used in JSON formatter, everything is handled in format()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -6,34 +6,34 @@ import type { Diagnostic } from "@herb-tools/core"
|
|
|
6
6
|
import type { ProcessedFile } from "../file-processor.js"
|
|
7
7
|
|
|
8
8
|
export class SimpleFormatter extends BaseFormatter {
|
|
9
|
-
async format(
|
|
10
|
-
if (
|
|
9
|
+
async format(allOffenses: ProcessedFile[]): Promise<void> {
|
|
10
|
+
if (allOffenses.length === 0) return
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const groupedOffenses = new Map<string, Diagnostic[]>()
|
|
13
13
|
|
|
14
|
-
for (const { filename,
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
for (const { filename, offense } of allOffenses) {
|
|
15
|
+
const offenses = groupedOffenses.get(filename) || []
|
|
16
|
+
offenses.push(offense)
|
|
17
|
+
groupedOffenses.set(filename, offenses)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
for (const [filename,
|
|
20
|
+
for (const [filename, offenses] of groupedOffenses) {
|
|
21
21
|
console.log("")
|
|
22
|
-
this.formatFile(filename,
|
|
22
|
+
this.formatFile(filename, offenses)
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
formatFile(filename: string,
|
|
26
|
+
formatFile(filename: string, offenses: Diagnostic[]): void {
|
|
27
27
|
console.log(`${colorize(filename, "cyan")}:`)
|
|
28
28
|
|
|
29
|
-
for (const
|
|
30
|
-
const isError =
|
|
29
|
+
for (const offense of offenses) {
|
|
30
|
+
const isError = offense.severity === "error"
|
|
31
31
|
const severity = isError ? colorize("✗", "brightRed") : colorize("⚠", "brightYellow")
|
|
32
|
-
const rule = colorize(`(${
|
|
33
|
-
const locationString = `${
|
|
32
|
+
const rule = colorize(`(${offense.code})`, "blue")
|
|
33
|
+
const locationString = `${offense.location.start.line}:${offense.location.start.column}`
|
|
34
34
|
const paddedLocation = locationString.padEnd(4) // Pad to 4 characters for alignment
|
|
35
35
|
|
|
36
|
-
console.log(` ${colorize(paddedLocation, "gray")} ${severity} ${
|
|
36
|
+
console.log(` ${colorize(paddedLocation, "gray")} ${severity} ${offense.message} ${rule}`)
|
|
37
37
|
}
|
|
38
38
|
console.log("")
|
|
39
39
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { SummaryReporter } from "./summary-reporter.js"
|
|
2
|
+
import { SimpleFormatter, DetailedFormatter, GitHubActionsFormatter, type JSONOutput } from "./formatters/index.js"
|
|
3
|
+
|
|
4
|
+
import type { ThemeInput } from "@herb-tools/highlighter"
|
|
5
|
+
import type { FormatOption } from "./argument-parser.js"
|
|
6
|
+
import type { ProcessingResult } from "./file-processor.js"
|
|
7
|
+
|
|
8
|
+
interface OutputOptions {
|
|
9
|
+
formatOption: FormatOption
|
|
10
|
+
theme: ThemeInput
|
|
11
|
+
wrapLines: boolean
|
|
12
|
+
truncateLines: boolean
|
|
13
|
+
showTiming: boolean
|
|
14
|
+
startTime: number
|
|
15
|
+
startDate: Date
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface LintResults extends ProcessingResult {
|
|
19
|
+
files: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class OutputManager {
|
|
23
|
+
private summaryReporter = new SummaryReporter()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Output successful lint results
|
|
27
|
+
*/
|
|
28
|
+
async outputResults(results: LintResults, options: OutputOptions): Promise<void> {
|
|
29
|
+
const { allOffenses, files, totalErrors, totalWarnings, filesWithOffenses, ruleCount, ruleOffenses } = results
|
|
30
|
+
|
|
31
|
+
if (options.formatOption === "github") {
|
|
32
|
+
const formatter = new GitHubActionsFormatter()
|
|
33
|
+
await formatter.format(allOffenses)
|
|
34
|
+
} else if (options.formatOption === "json") {
|
|
35
|
+
const output: JSONOutput = {
|
|
36
|
+
offenses: allOffenses.map(({ filename, offense }) => ({
|
|
37
|
+
filename,
|
|
38
|
+
message: offense.message,
|
|
39
|
+
location: offense.location.toJSON(),
|
|
40
|
+
severity: offense.severity,
|
|
41
|
+
code: offense.code,
|
|
42
|
+
source: offense.source
|
|
43
|
+
})),
|
|
44
|
+
summary: {
|
|
45
|
+
filesChecked: files.length,
|
|
46
|
+
filesWithOffenses,
|
|
47
|
+
totalErrors,
|
|
48
|
+
totalWarnings,
|
|
49
|
+
totalOffenses: totalErrors + totalWarnings,
|
|
50
|
+
ruleCount
|
|
51
|
+
},
|
|
52
|
+
timing: null,
|
|
53
|
+
completed: true,
|
|
54
|
+
clean: totalErrors === 0 && totalWarnings === 0,
|
|
55
|
+
message: null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const duration = Date.now() - options.startTime
|
|
59
|
+
output.timing = options.showTiming ? {
|
|
60
|
+
startTime: options.startDate.toISOString(),
|
|
61
|
+
duration: duration
|
|
62
|
+
} : null
|
|
63
|
+
|
|
64
|
+
console.log(JSON.stringify(output, null, 2))
|
|
65
|
+
} else {
|
|
66
|
+
const formatter = options.formatOption === "simple"
|
|
67
|
+
? new SimpleFormatter()
|
|
68
|
+
: new DetailedFormatter(options.theme, options.wrapLines, options.truncateLines)
|
|
69
|
+
|
|
70
|
+
await formatter.format(allOffenses, files.length === 1)
|
|
71
|
+
|
|
72
|
+
this.summaryReporter.displayMostViolatedRules(ruleOffenses)
|
|
73
|
+
this.summaryReporter.displaySummary({
|
|
74
|
+
files,
|
|
75
|
+
totalErrors,
|
|
76
|
+
totalWarnings,
|
|
77
|
+
filesWithOffenses,
|
|
78
|
+
ruleCount,
|
|
79
|
+
startTime: options.startTime,
|
|
80
|
+
startDate: options.startDate,
|
|
81
|
+
showTiming: options.showTiming,
|
|
82
|
+
ruleOffenses
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Output informational message (like "no files found")
|
|
89
|
+
*/
|
|
90
|
+
outputInfo(message: string, options: OutputOptions): void {
|
|
91
|
+
if (options.formatOption === "github") {
|
|
92
|
+
// GitHub Actions format doesn't output anything for info messages
|
|
93
|
+
} else if (options.formatOption === "json") {
|
|
94
|
+
const output: JSONOutput = {
|
|
95
|
+
offenses: [],
|
|
96
|
+
summary: {
|
|
97
|
+
filesChecked: 0,
|
|
98
|
+
filesWithOffenses: 0,
|
|
99
|
+
totalErrors: 0,
|
|
100
|
+
totalWarnings: 0,
|
|
101
|
+
totalOffenses: 0,
|
|
102
|
+
ruleCount: 0
|
|
103
|
+
},
|
|
104
|
+
timing: null,
|
|
105
|
+
completed: false,
|
|
106
|
+
clean: null,
|
|
107
|
+
message
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const duration = Date.now() - options.startTime
|
|
111
|
+
output.timing = options.showTiming ? {
|
|
112
|
+
startTime: options.startDate.toISOString(),
|
|
113
|
+
duration: duration
|
|
114
|
+
} : null
|
|
115
|
+
|
|
116
|
+
console.log(JSON.stringify(output, null, 2))
|
|
117
|
+
} else {
|
|
118
|
+
console.log(message)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Output error message
|
|
124
|
+
*/
|
|
125
|
+
outputError(message: string, options: OutputOptions): void {
|
|
126
|
+
if (options.formatOption === "github") {
|
|
127
|
+
console.log(`::error::${message}`)
|
|
128
|
+
} else if (options.formatOption === "json") {
|
|
129
|
+
const output: JSONOutput = {
|
|
130
|
+
offenses: [],
|
|
131
|
+
summary: null,
|
|
132
|
+
timing: null,
|
|
133
|
+
completed: false,
|
|
134
|
+
clean: null,
|
|
135
|
+
message
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(JSON.stringify(output, null, 2))
|
|
139
|
+
} else {
|
|
140
|
+
console.error(message)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -4,12 +4,12 @@ export interface SummaryData {
|
|
|
4
4
|
files: string[]
|
|
5
5
|
totalErrors: number
|
|
6
6
|
totalWarnings: number
|
|
7
|
-
|
|
7
|
+
filesWithOffenses: number
|
|
8
8
|
ruleCount: number
|
|
9
9
|
startTime: number
|
|
10
10
|
startDate: Date
|
|
11
11
|
showTiming: boolean
|
|
12
|
-
|
|
12
|
+
ruleOffenses: Map<string, { count: number, files: Set<string> }>
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export class SummaryReporter {
|
|
@@ -18,13 +18,13 @@ export class SummaryReporter {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
displaySummary(data: SummaryData): void {
|
|
21
|
-
const { files, totalErrors, totalWarnings,
|
|
21
|
+
const { files, totalErrors, totalWarnings, filesWithOffenses, ruleCount, startTime, startDate, showTiming } = data
|
|
22
22
|
|
|
23
23
|
console.log("\n")
|
|
24
24
|
console.log(` ${colorize("Summary:", "bold")}`)
|
|
25
25
|
|
|
26
26
|
// Calculate padding for alignment
|
|
27
|
-
const labelWidth = 12 // Width for the longest label "
|
|
27
|
+
const labelWidth = 12 // Width for the longest label "Offenses"
|
|
28
28
|
const pad = (label: string) => label.padEnd(labelWidth)
|
|
29
29
|
|
|
30
30
|
// Checked summary
|
|
@@ -33,13 +33,13 @@ export class SummaryReporter {
|
|
|
33
33
|
// Files summary (for multiple files)
|
|
34
34
|
if (files.length > 1) {
|
|
35
35
|
const filesChecked = files.length
|
|
36
|
-
const filesClean = filesChecked -
|
|
36
|
+
const filesClean = filesChecked - filesWithOffenses
|
|
37
37
|
|
|
38
38
|
let filesSummary = ""
|
|
39
39
|
let shouldDim = false
|
|
40
40
|
|
|
41
|
-
if (
|
|
42
|
-
filesSummary = `${colorize(colorize(`${
|
|
41
|
+
if (filesWithOffenses > 0) {
|
|
42
|
+
filesSummary = `${colorize(colorize(`${filesWithOffenses} with offenses`, "brightRed"), "bold")} | ${colorize(colorize(`${filesClean} clean`, "green"), "bold")} ${colorize(colorize(`(${filesChecked} total)`, "gray"), "dim")}`
|
|
43
43
|
} else {
|
|
44
44
|
filesSummary = `${colorize(colorize(`${filesChecked} clean`, "green"), "bold")} ${colorize(colorize(`(${filesChecked} total)`, "gray"), "dim")}`
|
|
45
45
|
shouldDim = true
|
|
@@ -52,8 +52,8 @@ export class SummaryReporter {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
//
|
|
56
|
-
let
|
|
55
|
+
// Offenses summary with file count
|
|
56
|
+
let offensesSummary = ""
|
|
57
57
|
const parts = []
|
|
58
58
|
|
|
59
59
|
// Build the main part with errors and warnings
|
|
@@ -69,22 +69,22 @@ export class SummaryReporter {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
if (parts.length === 0) {
|
|
72
|
-
|
|
72
|
+
offensesSummary = colorize(colorize("0 offenses", "green"), "bold")
|
|
73
73
|
} else {
|
|
74
|
-
|
|
74
|
+
offensesSummary = parts.join(" | ")
|
|
75
75
|
// Add total count and file count
|
|
76
76
|
let detailText = ""
|
|
77
77
|
|
|
78
|
-
const
|
|
78
|
+
const totalOffenses = totalErrors + totalWarnings
|
|
79
79
|
|
|
80
|
-
if (
|
|
81
|
-
detailText = `${
|
|
80
|
+
if (filesWithOffenses > 0) {
|
|
81
|
+
detailText = `${totalOffenses} ${this.pluralize(totalOffenses, "offense")} across ${filesWithOffenses} ${this.pluralize(filesWithOffenses, "file")}`
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
offensesSummary += ` ${colorize(colorize(`(${detailText})`, "gray"), "dim")}`
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
console.log(` ${colorize(pad("
|
|
87
|
+
console.log(` ${colorize(pad("Offenses"), "gray")} ${offensesSummary}`)
|
|
88
88
|
|
|
89
89
|
// Timing information (if enabled)
|
|
90
90
|
if (showTiming) {
|
|
@@ -96,32 +96,32 @@ export class SummaryReporter {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// Success message for all files clean
|
|
99
|
-
if (
|
|
99
|
+
if (filesWithOffenses === 0 && files.length > 1) {
|
|
100
100
|
console.log("")
|
|
101
101
|
console.log(` ${colorize("✓", "brightGreen")} ${colorize("All files are clean!", "green")}`)
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
displayMostViolatedRules(
|
|
106
|
-
if (
|
|
105
|
+
displayMostViolatedRules(ruleOffenses: Map<string, { count: number, files: Set<string> }>, limit: number = 5): void {
|
|
106
|
+
if (ruleOffenses.size === 0) return
|
|
107
107
|
|
|
108
|
-
const allRules = Array.from(
|
|
108
|
+
const allRules = Array.from(ruleOffenses.entries()).sort((a, b) => b[1].count - a[1].count)
|
|
109
109
|
const displayedRules = allRules.slice(0, limit)
|
|
110
110
|
const remainingRules = allRules.slice(limit)
|
|
111
111
|
|
|
112
|
-
const title =
|
|
112
|
+
const title = ruleOffenses.size <= limit ? "Rule offenses:" : "Most frequent rule offenses:"
|
|
113
113
|
console.log(` ${colorize(title, "bold")}`)
|
|
114
114
|
|
|
115
115
|
for (const [rule, data] of displayedRules) {
|
|
116
116
|
const fileCount = data.files.size
|
|
117
|
-
const countText = `(${data.count} ${this.pluralize(data.count, "
|
|
117
|
+
const countText = `(${data.count} ${this.pluralize(data.count, "offense")} in ${fileCount} ${this.pluralize(fileCount, "file")})`
|
|
118
118
|
console.log(` ${colorize(rule, "gray")} ${colorize(colorize(countText, "gray"), "dim")}`)
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
if (remainingRules.length > 0) {
|
|
122
|
-
const
|
|
122
|
+
const remainingOffenseCount = remainingRules.reduce((sum, [_, data]) => sum + data.count, 0)
|
|
123
123
|
const remainingRuleCount = remainingRules.length
|
|
124
|
-
console.log(colorize(colorize(`\n ...and ${remainingRuleCount} more ${this.pluralize(remainingRuleCount, "rule")} with ${
|
|
124
|
+
console.log(colorize(colorize(`\n ...and ${remainingRuleCount} more ${this.pluralize(remainingRuleCount, "rule")} with ${remainingOffenseCount} ${this.pluralize(remainingOffenseCount, "offense")}`, "gray"), "dim"))
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
}
|