@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.
Files changed (254) hide show
  1. package/README.md +216 -19
  2. package/dist/herb-lint.js +5559 -1860
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +722 -187
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +714 -189
  7. package/dist/index.js.map +1 -1
  8. package/dist/package.json +4 -4
  9. package/dist/src/cli/argument-parser.js +28 -22
  10. package/dist/src/cli/argument-parser.js.map +1 -1
  11. package/dist/src/cli/file-processor.js +19 -13
  12. package/dist/src/cli/file-processor.js.map +1 -1
  13. package/dist/src/cli/formatters/detailed-formatter.js +9 -9
  14. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
  15. package/dist/src/cli/formatters/github-actions-formatter.js +50 -0
  16. package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -0
  17. package/dist/src/cli/formatters/index.js +2 -0
  18. package/dist/src/cli/formatters/index.js.map +1 -1
  19. package/dist/src/cli/formatters/json-formatter.js +58 -0
  20. package/dist/src/cli/formatters/json-formatter.js.map +1 -0
  21. package/dist/src/cli/formatters/simple-formatter.js +15 -15
  22. package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
  23. package/dist/src/cli/output-manager.js +120 -0
  24. package/dist/src/cli/output-manager.js.map +1 -0
  25. package/dist/src/cli/summary-reporter.js +22 -22
  26. package/dist/src/cli/summary-reporter.js.map +1 -1
  27. package/dist/src/cli.js +41 -26
  28. package/dist/src/cli.js.map +1 -1
  29. package/dist/src/default-rules.js +22 -0
  30. package/dist/src/default-rules.js.map +1 -1
  31. package/dist/src/linter.js +29 -4
  32. package/dist/src/linter.js.map +1 -1
  33. package/dist/src/rules/erb-no-empty-tags.js +2 -2
  34. package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
  35. package/dist/src/rules/erb-no-output-control-flow.js +2 -2
  36. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
  37. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +26 -0
  38. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
  39. package/dist/src/rules/erb-prefer-image-tag-helper.js +2 -6
  40. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  41. package/dist/src/rules/erb-require-whitespace-inside-tags.js +2 -2
  42. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  43. package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -1
  44. package/dist/src/rules/html-anchor-require-href.js +2 -2
  45. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  46. package/dist/src/rules/html-aria-attribute-must-be-valid.js +13 -12
  47. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  48. package/dist/src/rules/html-aria-label-is-well-formatted.js +33 -0
  49. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -0
  50. package/dist/src/rules/html-aria-level-must-be-valid.js +28 -6
  51. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
  52. package/dist/src/rules/html-aria-role-heading-requires-level.js +9 -15
  53. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  54. package/dist/src/rules/html-aria-role-must-be-valid.js +5 -5
  55. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  56. package/dist/src/rules/html-attribute-double-quotes.js +16 -6
  57. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  58. package/dist/src/rules/html-attribute-equals-spacing.js +24 -0
  59. package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -0
  60. package/dist/src/rules/html-attribute-values-require-quotes.js +21 -10
  61. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  62. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +47 -0
  63. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
  64. package/dist/src/rules/html-boolean-attributes-no-value.js +11 -4
  65. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  66. package/dist/src/rules/html-iframe-has-title.js +39 -0
  67. package/dist/src/rules/html-iframe-has-title.js.map +1 -0
  68. package/dist/src/rules/html-img-require-alt.js +2 -6
  69. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  70. package/dist/src/rules/html-navigation-has-label.js +43 -0
  71. package/dist/src/rules/html-navigation-has-label.js.map +1 -0
  72. package/dist/src/rules/html-no-aria-hidden-on-focusable.js +67 -0
  73. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
  74. package/dist/src/rules/html-no-block-inside-inline.js +4 -4
  75. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  76. package/dist/src/rules/html-no-duplicate-attributes.js +24 -27
  77. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  78. package/dist/src/rules/html-no-duplicate-ids.js +4 -4
  79. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  80. package/dist/src/rules/html-no-empty-headings.js +2 -23
  81. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  82. package/dist/src/rules/html-no-nested-links.js +2 -2
  83. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  84. package/dist/src/rules/html-no-positive-tab-index.js +21 -0
  85. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -0
  86. package/dist/src/rules/html-no-self-closing.js +22 -0
  87. package/dist/src/rules/html-no-self-closing.js.map +1 -0
  88. package/dist/src/rules/html-no-title-attribute.js +27 -0
  89. package/dist/src/rules/html-no-title-attribute.js.map +1 -0
  90. package/dist/src/rules/html-tag-name-lowercase.js +37 -25
  91. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  92. package/dist/src/rules/index.js +10 -0
  93. package/dist/src/rules/index.js.map +1 -1
  94. package/dist/src/rules/parser-no-errors.js +18 -0
  95. package/dist/src/rules/parser-no-errors.js.map +1 -0
  96. package/dist/src/rules/rule-utils.js +176 -22
  97. package/dist/src/rules/rule-utils.js.map +1 -1
  98. package/dist/src/rules/svg-tag-name-capitalization.js +2 -10
  99. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  100. package/dist/src/types.js.map +1 -1
  101. package/dist/tsconfig.tsbuildinfo +1 -1
  102. package/dist/types/cli/argument-parser.d.ts +2 -1
  103. package/dist/types/cli/file-processor.d.ts +6 -5
  104. package/dist/types/cli/formatters/base-formatter.d.ts +2 -2
  105. package/dist/types/cli/formatters/detailed-formatter.d.ts +2 -2
  106. package/dist/types/cli/formatters/github-actions-formatter.d.ts +12 -0
  107. package/dist/types/cli/formatters/index.d.ts +2 -0
  108. package/dist/types/cli/formatters/json-formatter.d.ts +42 -0
  109. package/dist/types/cli/formatters/simple-formatter.d.ts +2 -2
  110. package/dist/types/cli/index.d.ts +4 -0
  111. package/dist/types/cli/output-manager.d.ts +31 -0
  112. package/dist/types/cli/summary-reporter.d.ts +3 -3
  113. package/dist/types/cli.d.ts +3 -1
  114. package/dist/types/rules/erb-no-empty-tags.d.ts +2 -2
  115. package/dist/types/rules/erb-no-output-control-flow.d.ts +2 -2
  116. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
  117. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +2 -2
  118. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
  119. package/dist/types/rules/html-anchor-require-href.d.ts +2 -2
  120. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
  121. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +7 -0
  122. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +2 -2
  123. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +2 -2
  124. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +2 -2
  125. package/dist/types/rules/html-attribute-double-quotes.d.ts +2 -2
  126. package/dist/types/rules/html-attribute-equals-spacing.d.ts +7 -0
  127. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +2 -2
  128. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
  129. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +2 -2
  130. package/dist/types/rules/html-iframe-has-title.d.ts +7 -0
  131. package/dist/types/rules/html-img-require-alt.d.ts +2 -2
  132. package/dist/types/rules/html-navigation-has-label.d.ts +7 -0
  133. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
  134. package/dist/types/rules/html-no-block-inside-inline.d.ts +2 -2
  135. package/dist/types/rules/html-no-duplicate-attributes.d.ts +2 -2
  136. package/dist/types/rules/html-no-duplicate-ids.d.ts +2 -2
  137. package/dist/types/rules/html-no-empty-headings.d.ts +2 -2
  138. package/dist/types/rules/html-no-nested-links.d.ts +2 -2
  139. package/dist/types/rules/html-no-positive-tab-index.d.ts +7 -0
  140. package/dist/types/rules/html-no-self-closing.d.ts +7 -0
  141. package/dist/types/rules/html-no-title-attribute.d.ts +7 -0
  142. package/dist/types/rules/html-tag-name-lowercase.d.ts +3 -2
  143. package/dist/types/rules/index.d.ts +10 -0
  144. package/dist/types/rules/parser-no-errors.d.ts +8 -0
  145. package/dist/types/rules/rule-utils.d.ts +107 -13
  146. package/dist/types/rules/svg-tag-name-capitalization.d.ts +2 -2
  147. package/dist/types/src/cli/argument-parser.d.ts +2 -1
  148. package/dist/types/src/cli/file-processor.d.ts +6 -5
  149. package/dist/types/src/cli/formatters/base-formatter.d.ts +2 -2
  150. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +2 -2
  151. package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +12 -0
  152. package/dist/types/src/cli/formatters/index.d.ts +2 -0
  153. package/dist/types/src/cli/formatters/json-formatter.d.ts +42 -0
  154. package/dist/types/src/cli/formatters/simple-formatter.d.ts +2 -2
  155. package/dist/types/src/cli/output-manager.d.ts +31 -0
  156. package/dist/types/src/cli/summary-reporter.d.ts +3 -3
  157. package/dist/types/src/cli.d.ts +3 -1
  158. package/dist/types/src/rules/erb-no-empty-tags.d.ts +2 -2
  159. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +2 -2
  160. package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
  161. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +2 -2
  162. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
  163. package/dist/types/src/rules/html-anchor-require-href.d.ts +2 -2
  164. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
  165. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +7 -0
  166. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +2 -2
  167. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +2 -2
  168. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +2 -2
  169. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +2 -2
  170. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +7 -0
  171. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +2 -2
  172. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
  173. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +2 -2
  174. package/dist/types/src/rules/html-iframe-has-title.d.ts +7 -0
  175. package/dist/types/src/rules/html-img-require-alt.d.ts +2 -2
  176. package/dist/types/src/rules/html-navigation-has-label.d.ts +7 -0
  177. package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
  178. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +2 -2
  179. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +2 -2
  180. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +2 -2
  181. package/dist/types/src/rules/html-no-empty-headings.d.ts +2 -2
  182. package/dist/types/src/rules/html-no-nested-links.d.ts +2 -2
  183. package/dist/types/src/rules/html-no-positive-tab-index.d.ts +7 -0
  184. package/dist/types/src/rules/html-no-self-closing.d.ts +7 -0
  185. package/dist/types/src/rules/html-no-title-attribute.d.ts +7 -0
  186. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +3 -2
  187. package/dist/types/src/rules/index.d.ts +10 -0
  188. package/dist/types/src/rules/parser-no-errors.d.ts +8 -0
  189. package/dist/types/src/rules/rule-utils.d.ts +107 -13
  190. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +2 -2
  191. package/dist/types/src/types.d.ts +27 -3
  192. package/dist/types/types.d.ts +27 -3
  193. package/docs/rules/README.md +13 -2
  194. package/docs/rules/erb-no-silent-tag-in-attribute-name.md +34 -0
  195. package/docs/rules/html-aria-label-is-well-formatted.md +49 -0
  196. package/docs/rules/html-attribute-equals-spacing.md +35 -0
  197. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +48 -0
  198. package/docs/rules/html-iframe-has-title.md +43 -0
  199. package/docs/rules/html-navigation-has-label.md +61 -0
  200. package/docs/rules/html-no-aria-hidden-on-focusable.md +54 -0
  201. package/docs/rules/html-no-positive-tab-index.md +55 -0
  202. package/docs/rules/html-no-self-closing.md +65 -0
  203. package/docs/rules/html-no-title-attribute.md +69 -0
  204. package/docs/rules/html-tag-name-lowercase.md +16 -3
  205. package/docs/rules/parser-no-errors.md +84 -0
  206. package/package.json +4 -4
  207. package/src/cli/argument-parser.ts +33 -24
  208. package/src/cli/file-processor.ts +25 -17
  209. package/src/cli/formatters/base-formatter.ts +2 -2
  210. package/src/cli/formatters/detailed-formatter.ts +9 -9
  211. package/src/cli/formatters/github-actions-formatter.ts +70 -0
  212. package/src/cli/formatters/index.ts +2 -0
  213. package/src/cli/formatters/json-formatter.ts +107 -0
  214. package/src/cli/formatters/simple-formatter.ts +15 -15
  215. package/src/cli/output-manager.ts +143 -0
  216. package/src/cli/summary-reporter.ts +24 -24
  217. package/src/cli.ts +48 -31
  218. package/src/default-rules.ts +22 -0
  219. package/src/linter.ts +30 -4
  220. package/src/rules/erb-no-empty-tags.ts +3 -3
  221. package/src/rules/erb-no-output-control-flow.ts +3 -3
  222. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +40 -0
  223. package/src/rules/erb-prefer-image-tag-helper.ts +4 -9
  224. package/src/rules/erb-require-whitespace-inside-tags.ts +3 -3
  225. package/src/rules/erb-requires-trailing-newline.ts +2 -0
  226. package/src/rules/html-anchor-require-href.ts +3 -3
  227. package/src/rules/html-aria-attribute-must-be-valid.ts +29 -33
  228. package/src/rules/html-aria-label-is-well-formatted.ts +59 -0
  229. package/src/rules/html-aria-level-must-be-valid.ts +40 -7
  230. package/src/rules/html-aria-role-heading-requires-level.ts +18 -30
  231. package/src/rules/html-aria-role-must-be-valid.ts +7 -7
  232. package/src/rules/html-attribute-double-quotes.ts +23 -8
  233. package/src/rules/html-attribute-equals-spacing.ts +41 -0
  234. package/src/rules/html-attribute-values-require-quotes.ts +32 -12
  235. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +66 -0
  236. package/src/rules/html-boolean-attributes-no-value.ts +19 -6
  237. package/src/rules/html-iframe-has-title.ts +62 -0
  238. package/src/rules/html-img-require-alt.ts +4 -9
  239. package/src/rules/html-navigation-has-label.ts +64 -0
  240. package/src/rules/html-no-aria-hidden-on-focusable.ts +90 -0
  241. package/src/rules/html-no-block-inside-inline.ts +5 -5
  242. package/src/rules/html-no-duplicate-attributes.ts +30 -30
  243. package/src/rules/html-no-duplicate-ids.ts +6 -5
  244. package/src/rules/html-no-empty-headings.ts +4 -33
  245. package/src/rules/html-no-nested-links.ts +3 -3
  246. package/src/rules/html-no-positive-tab-index.ts +33 -0
  247. package/src/rules/html-no-self-closing.ts +36 -0
  248. package/src/rules/html-no-title-attribute.ts +42 -0
  249. package/src/rules/html-tag-name-lowercase.ts +44 -31
  250. package/src/rules/index.ts +10 -0
  251. package/src/rules/parser-no-errors.ts +25 -0
  252. package/src/rules/rule-utils.ts +260 -39
  253. package/src/rules/svg-tag-name-capitalization.ts +4 -11
  254. 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
- diagnostic: Diagnostic
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
- filesWithIssues: number
18
+ filesWithOffenses: number
18
19
  ruleCount: number
19
- allDiagnostics: ProcessedFile[]
20
- ruleViolations: Map<string, { count: number, files: Set<string> }>
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 filesWithIssues = 0
30
+ let filesWithOffenses = 0
30
31
  let ruleCount = 0
31
- const allDiagnostics: ProcessedFile[] = []
32
- const ruleViolations = new Map<string, { count: number, files: Set<string> }>()
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
- console.error(`${colorize(filename, "cyan")} - ${colorize("Parse errors:", "brightRed")}`)
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
- console.error(` ${colorize("✗", "brightRed")} ${error.message}`)
52
+ allOffenses.push({ filename, offense: error, content })
45
53
  }
46
54
 
47
55
  totalErrors++
48
- filesWithIssues++
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
- allDiagnostics.push({ filename, diagnostic: offense, content })
76
+ allOffenses.push({ filename, offense: offense, content })
69
77
 
70
- const ruleData = ruleViolations.get(offense.rule) || { count: 0, files: new Set() }
78
+ const ruleData = ruleOffenses.get(offense.rule) || { count: 0, files: new Set() }
71
79
  ruleData.count++
72
80
  ruleData.files.add(filename)
73
- ruleViolations.set(offense.rule, ruleData)
81
+ ruleOffenses.set(offense.rule, ruleData)
74
82
  }
75
83
 
76
84
  totalErrors += lintResult.errors
77
85
  totalWarnings += lintResult.warnings
78
- filesWithIssues++
86
+ filesWithOffenses++
79
87
  }
80
88
  }
81
89
 
82
- return { totalErrors, totalWarnings, filesWithIssues, ruleCount, allDiagnostics, ruleViolations }
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
- allDiagnostics: ProcessedFile[],
6
+ allOffenses: ProcessedFile[],
7
7
  isSingleFile?: boolean
8
8
  ): Promise<void>
9
9
 
10
- abstract formatFile(filename: string, diagnostics: Diagnostic[]): void
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(allDiagnostics: ProcessedFile[], isSingleFile: boolean = false): Promise<void> {
22
- if (allDiagnostics.length === 0) return
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 } = allDiagnostics[0]
32
- const diagnostics = allDiagnostics.map(item => item.diagnostic)
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 = allDiagnostics.length
45
+ const totalMessageCount = allOffenses.length
46
46
 
47
- for (let i = 0; i < allDiagnostics.length; i++) {
48
- const { filename, diagnostic, content } = allDiagnostics[i]
49
- const formatted = this.highlighter.highlightDiagnostic(filename, diagnostic, content, {
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, _diagnostics: Diagnostic[]): void {
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(allDiagnostics: ProcessedFile[]): Promise<void> {
10
- if (allDiagnostics.length === 0) return
9
+ async format(allOffenses: ProcessedFile[]): Promise<void> {
10
+ if (allOffenses.length === 0) return
11
11
 
12
- const groupedDiagnostics = new Map<string, Diagnostic[]>()
12
+ const groupedOffenses = new Map<string, Diagnostic[]>()
13
13
 
14
- for (const { filename, diagnostic } of allDiagnostics) {
15
- const diagnostics = groupedDiagnostics.get(filename) || []
16
- diagnostics.push(diagnostic)
17
- groupedDiagnostics.set(filename, diagnostics)
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, diagnostics] of groupedDiagnostics) {
20
+ for (const [filename, offenses] of groupedOffenses) {
21
21
  console.log("")
22
- this.formatFile(filename, diagnostics)
22
+ this.formatFile(filename, offenses)
23
23
  }
24
24
  }
25
25
 
26
- formatFile(filename: string, diagnostics: Diagnostic[]): void {
26
+ formatFile(filename: string, offenses: Diagnostic[]): void {
27
27
  console.log(`${colorize(filename, "cyan")}:`)
28
28
 
29
- for (const diagnostic of diagnostics) {
30
- const isError = diagnostic.severity === "error"
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(`(${diagnostic.code})`, "blue")
33
- const locationString = `${diagnostic.location.start.line}:${diagnostic.location.start.column}`
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} ${diagnostic.message} ${rule}`)
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
- filesWithViolations: number
7
+ filesWithOffenses: number
8
8
  ruleCount: number
9
9
  startTime: number
10
10
  startDate: Date
11
11
  showTiming: boolean
12
- ruleViolations: Map<string, { count: number, files: Set<string> }>
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, filesWithViolations, ruleCount, startTime, startDate, showTiming } = data
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 "Violations"
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 - filesWithViolations
36
+ const filesClean = filesChecked - filesWithOffenses
37
37
 
38
38
  let filesSummary = ""
39
39
  let shouldDim = false
40
40
 
41
- if (filesWithViolations > 0) {
42
- filesSummary = `${colorize(colorize(`${filesWithViolations} with violations`, "brightRed"), "bold")} | ${colorize(colorize(`${filesClean} clean`, "green"), "bold")} ${colorize(colorize(`(${filesChecked} total)`, "gray"), "dim")}`
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
- // Violations summary with file count
56
- let violationsSummary = ""
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
- violationsSummary = colorize(colorize("0 violations", "green"), "bold")
72
+ offensesSummary = colorize(colorize("0 offenses", "green"), "bold")
73
73
  } else {
74
- violationsSummary = parts.join(" | ")
74
+ offensesSummary = parts.join(" | ")
75
75
  // Add total count and file count
76
76
  let detailText = ""
77
77
 
78
- const totalViolations = totalErrors + totalWarnings
78
+ const totalOffenses = totalErrors + totalWarnings
79
79
 
80
- if (filesWithViolations > 0) {
81
- detailText = `${totalViolations} ${this.pluralize(totalViolations, "violation")} across ${filesWithViolations} ${this.pluralize(filesWithViolations, "file")}`
80
+ if (filesWithOffenses > 0) {
81
+ detailText = `${totalOffenses} ${this.pluralize(totalOffenses, "offense")} across ${filesWithOffenses} ${this.pluralize(filesWithOffenses, "file")}`
82
82
  }
83
83
 
84
- violationsSummary += ` ${colorize(colorize(`(${detailText})`, "gray"), "dim")}`
84
+ offensesSummary += ` ${colorize(colorize(`(${detailText})`, "gray"), "dim")}`
85
85
  }
86
86
 
87
- console.log(` ${colorize(pad("Violations"), "gray")} ${violationsSummary}`)
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 (filesWithViolations === 0 && files.length > 1) {
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(ruleViolations: Map<string, { count: number, files: Set<string> }>, limit: number = 5): void {
106
- if (ruleViolations.size === 0) return
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(ruleViolations.entries()).sort((a, b) => b[1].count - a[1].count)
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 = ruleViolations.size <= limit ? "Rule violations:" : "Most violated rules:"
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, "violation")} in ${fileCount} ${this.pluralize(fileCount, "file")})`
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 remainingViolationCount = remainingRules.reduce((sum, [_, data]) => sum + data.count, 0)
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 ${remainingViolationCount} ${this.pluralize(remainingViolationCount, "violation")}`, "gray"), "dim"))
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
  }