@herb-tools/linter 0.7.4 → 0.8.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 (395) hide show
  1. package/README.md +253 -13
  2. package/dist/herb-lint.js +26087 -3414
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +5783 -1568
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +5749 -1569
  7. package/dist/index.js.map +1 -1
  8. package/dist/loader.cjs +17010 -0
  9. package/dist/loader.cjs.map +1 -0
  10. package/dist/loader.js +16879 -0
  11. package/dist/loader.js.map +1 -0
  12. package/dist/package.json +13 -5
  13. package/dist/src/cli/argument-parser.js +42 -35
  14. package/dist/src/cli/argument-parser.js.map +1 -1
  15. package/dist/src/cli/file-processor.js +124 -23
  16. package/dist/src/cli/file-processor.js.map +1 -1
  17. package/dist/src/cli/formatters/detailed-formatter.js +18 -3
  18. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
  19. package/dist/src/cli/formatters/github-actions-formatter.js +15 -1
  20. package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
  21. package/dist/src/cli/formatters/json-formatter.js +3 -0
  22. package/dist/src/cli/formatters/json-formatter.js.map +1 -1
  23. package/dist/src/cli/formatters/simple-formatter.js +20 -7
  24. package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
  25. package/dist/src/cli/output-manager.js +22 -3
  26. package/dist/src/cli/output-manager.js.map +1 -1
  27. package/dist/src/cli/summary-reporter.js +26 -3
  28. package/dist/src/cli/summary-reporter.js.map +1 -1
  29. package/dist/src/cli.js +109 -43
  30. package/dist/src/cli.js.map +1 -1
  31. package/dist/src/custom-rule-loader.js +139 -0
  32. package/dist/src/custom-rule-loader.js.map +1 -0
  33. package/dist/src/herb-disable-comment-utils.js +129 -0
  34. package/dist/src/herb-disable-comment-utils.js.map +1 -0
  35. package/dist/src/index.js +1 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/linter.js +369 -34
  38. package/dist/src/linter.js.map +1 -1
  39. package/dist/src/loader.js +17 -0
  40. package/dist/src/loader.js.map +1 -0
  41. package/dist/src/rules/erb-comment-syntax.js +48 -0
  42. package/dist/src/rules/erb-comment-syntax.js.map +1 -0
  43. package/dist/src/rules/erb-no-case-node-children.js +52 -0
  44. package/dist/src/rules/erb-no-case-node-children.js.map +1 -0
  45. package/dist/src/rules/erb-no-empty-tags.js +7 -1
  46. package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
  47. package/dist/src/rules/erb-no-extra-newline.js +65 -0
  48. package/dist/src/rules/erb-no-extra-newline.js.map +1 -0
  49. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js +95 -0
  50. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
  51. package/dist/src/rules/erb-no-output-control-flow.js +7 -1
  52. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
  53. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +7 -1
  54. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -1
  55. package/dist/src/rules/erb-prefer-image-tag-helper.js +7 -1
  56. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  57. package/dist/src/rules/erb-require-trailing-newline.js +35 -0
  58. package/dist/src/rules/erb-require-trailing-newline.js.map +1 -0
  59. package/dist/src/rules/erb-require-whitespace-inside-tags.js +70 -20
  60. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  61. package/dist/src/rules/erb-right-trim.js +45 -0
  62. package/dist/src/rules/erb-right-trim.js.map +1 -0
  63. package/dist/src/rules/herb-disable-comment-base.js +51 -0
  64. package/dist/src/rules/herb-disable-comment-base.js.map +1 -0
  65. package/dist/src/rules/herb-disable-comment-malformed.js +51 -0
  66. package/dist/src/rules/herb-disable-comment-malformed.js.map +1 -0
  67. package/dist/src/rules/herb-disable-comment-missing-rules.js +29 -0
  68. package/dist/src/rules/herb-disable-comment-missing-rules.js.map +1 -0
  69. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js +32 -0
  70. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
  71. package/dist/src/rules/herb-disable-comment-no-redundant-all.js +31 -0
  72. package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
  73. package/dist/src/rules/herb-disable-comment-unnecessary.js +65 -0
  74. package/dist/src/rules/herb-disable-comment-unnecessary.js.map +1 -0
  75. package/dist/src/rules/herb-disable-comment-valid-rule-name.js +44 -0
  76. package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
  77. package/dist/src/rules/html-anchor-require-href.js +7 -1
  78. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  79. package/dist/src/rules/html-aria-attribute-must-be-valid.js +7 -1
  80. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  81. package/dist/src/rules/html-aria-label-is-well-formatted.js +9 -3
  82. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -1
  83. package/dist/src/rules/html-aria-level-must-be-valid.js +6 -0
  84. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
  85. package/dist/src/rules/html-aria-role-heading-requires-level.js +7 -1
  86. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  87. package/dist/src/rules/html-aria-role-must-be-valid.js +7 -1
  88. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  89. package/dist/src/rules/html-attribute-double-quotes.js +29 -2
  90. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  91. package/dist/src/rules/html-attribute-equals-spacing.js +18 -2
  92. package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -1
  93. package/dist/src/rules/html-attribute-values-require-quotes.js +39 -3
  94. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  95. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +7 -1
  96. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -1
  97. package/dist/src/rules/html-body-only-elements.js +46 -0
  98. package/dist/src/rules/html-body-only-elements.js.map +1 -0
  99. package/dist/src/rules/html-boolean-attributes-no-value.js +18 -1
  100. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  101. package/dist/src/rules/html-head-only-elements.js +51 -0
  102. package/dist/src/rules/html-head-only-elements.js.map +1 -0
  103. package/dist/src/rules/html-iframe-has-title.js +8 -2
  104. package/dist/src/rules/html-iframe-has-title.js.map +1 -1
  105. package/dist/src/rules/html-img-require-alt.js +7 -1
  106. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  107. package/dist/src/rules/html-input-require-autocomplete.js +70 -0
  108. package/dist/src/rules/html-input-require-autocomplete.js.map +1 -0
  109. package/dist/src/rules/html-navigation-has-label.js +7 -1
  110. package/dist/src/rules/html-navigation-has-label.js.map +1 -1
  111. package/dist/src/rules/html-no-aria-hidden-on-focusable.js +7 -1
  112. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -1
  113. package/dist/src/rules/html-no-block-inside-inline.js +7 -1
  114. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  115. package/dist/src/rules/html-no-duplicate-attributes.js +7 -1
  116. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  117. package/dist/src/rules/html-no-duplicate-ids.js +9 -3
  118. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  119. package/dist/src/rules/html-no-duplicate-meta-names.js +136 -0
  120. package/dist/src/rules/html-no-duplicate-meta-names.js.map +1 -0
  121. package/dist/src/rules/html-no-empty-attributes.js +45 -7
  122. package/dist/src/rules/html-no-empty-attributes.js.map +1 -1
  123. package/dist/src/rules/html-no-empty-headings.js +7 -6
  124. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  125. package/dist/src/rules/html-no-nested-links.js +7 -1
  126. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  127. package/dist/src/rules/html-no-positive-tab-index.js +7 -1
  128. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
  129. package/dist/src/rules/html-no-self-closing.js +48 -3
  130. package/dist/src/rules/html-no-self-closing.js.map +1 -1
  131. package/dist/src/rules/html-no-space-in-tag.js +173 -0
  132. package/dist/src/rules/html-no-space-in-tag.js.map +1 -0
  133. package/dist/src/rules/html-no-title-attribute.js +7 -1
  134. package/dist/src/rules/html-no-title-attribute.js.map +1 -1
  135. package/dist/src/rules/html-no-underscores-in-attribute-names.js +7 -1
  136. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -1
  137. package/dist/src/rules/html-tag-name-lowercase.js +23 -5
  138. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  139. package/dist/src/rules/index.js +20 -2
  140. package/dist/src/rules/index.js.map +1 -1
  141. package/dist/src/rules/parser-no-errors.js +6 -0
  142. package/dist/src/rules/parser-no-errors.js.map +1 -1
  143. package/dist/src/rules/rule-utils.js +211 -31
  144. package/dist/src/rules/rule-utils.js.map +1 -1
  145. package/dist/src/rules/svg-tag-name-capitalization.js +22 -2
  146. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  147. package/dist/src/{default-rules.js → rules.js} +46 -14
  148. package/dist/src/rules.js.map +1 -0
  149. package/dist/src/types.js +34 -1
  150. package/dist/src/types.js.map +1 -1
  151. package/dist/tsconfig.tsbuildinfo +1 -1
  152. package/dist/types/cli/argument-parser.d.ts +8 -2
  153. package/dist/types/cli/file-processor.d.ts +15 -0
  154. package/dist/types/cli/formatters/json-formatter.d.ts +6 -0
  155. package/dist/types/cli/formatters/simple-formatter.d.ts +1 -0
  156. package/dist/types/cli/summary-reporter.d.ts +6 -0
  157. package/dist/types/cli.d.ts +9 -4
  158. package/dist/types/custom-rule-loader.d.ts +62 -0
  159. package/dist/types/herb-disable-comment-utils.d.ts +69 -0
  160. package/dist/types/index.d.ts +1 -0
  161. package/dist/types/linter.d.ts +99 -3
  162. package/dist/types/loader.d.ts +20 -0
  163. package/dist/types/rules/erb-comment-syntax.d.ts +14 -0
  164. package/dist/types/rules/erb-no-case-node-children.d.ts +8 -0
  165. package/dist/types/rules/erb-no-empty-tags.d.ts +3 -2
  166. package/dist/types/rules/erb-no-extra-newline.d.ts +14 -0
  167. package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
  168. package/dist/types/rules/erb-no-output-control-flow.d.ts +3 -2
  169. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
  170. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +3 -2
  171. package/dist/types/rules/erb-require-trailing-newline.d.ts +9 -0
  172. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
  173. package/dist/types/rules/erb-right-trim.d.ts +14 -0
  174. package/dist/types/rules/herb-disable-comment-base.d.ts +37 -0
  175. package/dist/types/rules/herb-disable-comment-malformed.d.ts +8 -0
  176. package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +8 -0
  177. package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
  178. package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
  179. package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +8 -0
  180. package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
  181. package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
  182. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
  183. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +3 -2
  184. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +3 -2
  185. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +3 -2
  186. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +3 -2
  187. package/dist/types/rules/html-attribute-double-quotes.d.ts +13 -5
  188. package/dist/types/rules/html-attribute-equals-spacing.d.ts +12 -5
  189. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +13 -5
  190. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
  191. package/dist/types/rules/html-body-only-elements.d.ts +9 -0
  192. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +12 -5
  193. package/dist/types/rules/html-head-only-elements.d.ts +9 -0
  194. package/dist/types/rules/html-iframe-has-title.d.ts +3 -2
  195. package/dist/types/rules/html-img-require-alt.d.ts +3 -2
  196. package/dist/types/rules/html-input-require-autocomplete.d.ts +8 -0
  197. package/dist/types/rules/html-navigation-has-label.d.ts +3 -2
  198. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
  199. package/dist/types/rules/html-no-block-inside-inline.d.ts +3 -2
  200. package/dist/types/rules/html-no-duplicate-attributes.d.ts +3 -2
  201. package/dist/types/rules/html-no-duplicate-ids.d.ts +3 -2
  202. package/dist/types/rules/html-no-duplicate-meta-names.d.ts +9 -0
  203. package/dist/types/rules/html-no-empty-attributes.d.ts +3 -2
  204. package/dist/types/rules/html-no-empty-headings.d.ts +3 -2
  205. package/dist/types/rules/html-no-nested-links.d.ts +3 -2
  206. package/dist/types/rules/html-no-positive-tab-index.d.ts +3 -2
  207. package/dist/types/rules/html-no-self-closing.d.ts +14 -5
  208. package/dist/types/rules/html-no-space-in-tag.d.ts +16 -0
  209. package/dist/types/rules/html-no-title-attribute.d.ts +3 -2
  210. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
  211. package/dist/types/rules/html-tag-name-lowercase.d.ts +16 -6
  212. package/dist/types/rules/index.d.ts +20 -2
  213. package/dist/types/rules/parser-no-errors.d.ts +2 -1
  214. package/dist/types/rules/rule-utils.d.ts +72 -25
  215. package/dist/types/rules/svg-tag-name-capitalization.d.ts +13 -4
  216. package/dist/types/rules.d.ts +2 -0
  217. package/dist/types/src/cli/argument-parser.d.ts +8 -2
  218. package/dist/types/src/cli/file-processor.d.ts +15 -0
  219. package/dist/types/src/cli/formatters/json-formatter.d.ts +6 -0
  220. package/dist/types/src/cli/formatters/simple-formatter.d.ts +1 -0
  221. package/dist/types/src/cli/summary-reporter.d.ts +6 -0
  222. package/dist/types/src/cli.d.ts +9 -4
  223. package/dist/types/src/custom-rule-loader.d.ts +62 -0
  224. package/dist/types/src/herb-disable-comment-utils.d.ts +69 -0
  225. package/dist/types/src/index.d.ts +1 -0
  226. package/dist/types/src/linter.d.ts +99 -3
  227. package/dist/types/src/loader.d.ts +20 -0
  228. package/dist/types/src/rules/erb-comment-syntax.d.ts +14 -0
  229. package/dist/types/src/rules/erb-no-case-node-children.d.ts +8 -0
  230. package/dist/types/src/rules/erb-no-empty-tags.d.ts +3 -2
  231. package/dist/types/src/rules/erb-no-extra-newline.d.ts +14 -0
  232. package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
  233. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +3 -2
  234. package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
  235. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +3 -2
  236. package/dist/types/src/rules/erb-require-trailing-newline.d.ts +9 -0
  237. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
  238. package/dist/types/src/rules/erb-right-trim.d.ts +14 -0
  239. package/dist/types/src/rules/herb-disable-comment-base.d.ts +37 -0
  240. package/dist/types/src/rules/herb-disable-comment-malformed.d.ts +8 -0
  241. package/dist/types/src/rules/herb-disable-comment-missing-rules.d.ts +8 -0
  242. package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
  243. package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
  244. package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +8 -0
  245. package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
  246. package/dist/types/src/rules/html-anchor-require-href.d.ts +3 -2
  247. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
  248. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +3 -2
  249. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +3 -2
  250. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +3 -2
  251. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +3 -2
  252. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +13 -5
  253. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +12 -5
  254. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +13 -5
  255. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
  256. package/dist/types/src/rules/html-body-only-elements.d.ts +9 -0
  257. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +12 -5
  258. package/dist/types/src/rules/html-head-only-elements.d.ts +9 -0
  259. package/dist/types/src/rules/html-iframe-has-title.d.ts +3 -2
  260. package/dist/types/src/rules/html-img-require-alt.d.ts +3 -2
  261. package/dist/types/src/rules/html-input-require-autocomplete.d.ts +8 -0
  262. package/dist/types/src/rules/html-navigation-has-label.d.ts +3 -2
  263. package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
  264. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +3 -2
  265. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +3 -2
  266. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +3 -2
  267. package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +9 -0
  268. package/dist/types/src/rules/html-no-empty-attributes.d.ts +3 -2
  269. package/dist/types/src/rules/html-no-empty-headings.d.ts +3 -2
  270. package/dist/types/src/rules/html-no-nested-links.d.ts +3 -2
  271. package/dist/types/src/rules/html-no-positive-tab-index.d.ts +3 -2
  272. package/dist/types/src/rules/html-no-self-closing.d.ts +14 -5
  273. package/dist/types/src/rules/html-no-space-in-tag.d.ts +16 -0
  274. package/dist/types/src/rules/html-no-title-attribute.d.ts +3 -2
  275. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
  276. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +16 -6
  277. package/dist/types/src/rules/index.d.ts +20 -2
  278. package/dist/types/src/rules/parser-no-errors.d.ts +2 -1
  279. package/dist/types/src/rules/rule-utils.d.ts +72 -25
  280. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +13 -4
  281. package/dist/types/src/rules.d.ts +2 -0
  282. package/dist/types/src/types.d.ts +102 -11
  283. package/dist/types/types.d.ts +102 -11
  284. package/docs/rules/README.md +19 -3
  285. package/docs/rules/erb-comment-syntax.md +44 -0
  286. package/docs/rules/erb-no-case-node-children.md +50 -0
  287. package/docs/rules/erb-no-extra-newline.md +74 -0
  288. package/docs/rules/erb-no-extra-whitespace-inside-tags.md +39 -0
  289. package/docs/rules/{erb-requires-trailing-newline.md → erb-require-trailing-newline.md} +1 -1
  290. package/docs/rules/erb-right-trim.md +52 -0
  291. package/docs/rules/herb-disable-comment-malformed.md +45 -0
  292. package/docs/rules/herb-disable-comment-missing-rules.md +60 -0
  293. package/docs/rules/herb-disable-comment-no-duplicate-rules.md +49 -0
  294. package/docs/rules/herb-disable-comment-no-redundant-all.md +53 -0
  295. package/docs/rules/herb-disable-comment-unnecessary.md +44 -0
  296. package/docs/rules/herb-disable-comment-valid-rule-name.md +41 -0
  297. package/docs/rules/html-aria-attribute-must-be-valid.md +2 -5
  298. package/docs/rules/html-aria-label-is-well-formatted.md +1 -1
  299. package/docs/rules/html-attribute-double-quotes.md +2 -2
  300. package/docs/rules/html-attribute-equals-spacing.md +2 -2
  301. package/docs/rules/html-attribute-values-require-quotes.md +3 -3
  302. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +2 -2
  303. package/docs/rules/html-body-only-elements.md +99 -0
  304. package/docs/rules/html-boolean-attributes-no-value.md +2 -2
  305. package/docs/rules/html-head-only-elements.md +81 -0
  306. package/docs/rules/html-input-require-autocomplete.md +64 -0
  307. package/docs/rules/html-no-aria-hidden-on-focusable.md +2 -2
  308. package/docs/rules/html-no-duplicate-attributes.md +2 -2
  309. package/docs/rules/html-no-duplicate-meta-names.md +64 -0
  310. package/docs/rules/html-no-empty-attributes.md +3 -3
  311. package/docs/rules/html-no-empty-headings.md +4 -26
  312. package/docs/rules/html-no-positive-tab-index.md +1 -2
  313. package/docs/rules/html-no-self-closing.md +17 -2
  314. package/docs/rules/html-no-space-in-tag.md +66 -0
  315. package/docs/rules/html-no-title-attribute.md +2 -2
  316. package/docs/rules/html-no-underscores-in-attribute-names.md +2 -2
  317. package/docs/rules/html-tag-name-lowercase.md +2 -2
  318. package/package.json +13 -5
  319. package/src/cli/argument-parser.ts +50 -39
  320. package/src/cli/file-processor.ts +159 -28
  321. package/src/cli/formatters/detailed-formatter.ts +21 -3
  322. package/src/cli/formatters/github-actions-formatter.ts +17 -1
  323. package/src/cli/formatters/json-formatter.ts +9 -0
  324. package/src/cli/formatters/simple-formatter.ts +24 -8
  325. package/src/cli/output-manager.ts +23 -3
  326. package/src/cli/summary-reporter.ts +40 -3
  327. package/src/cli.ts +137 -52
  328. package/src/custom-rule-loader.ts +189 -0
  329. package/src/herb-disable-comment-utils.ts +175 -0
  330. package/src/index.ts +2 -0
  331. package/src/linter.ts +501 -36
  332. package/src/loader.ts +30 -0
  333. package/src/rules/erb-comment-syntax.ts +73 -0
  334. package/src/rules/erb-no-case-node-children.ts +68 -0
  335. package/src/rules/erb-no-empty-tags.ts +9 -3
  336. package/src/rules/erb-no-extra-newline.ts +91 -0
  337. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +147 -0
  338. package/src/rules/erb-no-output-control-flow.ts +9 -3
  339. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +9 -3
  340. package/src/rules/erb-prefer-image-tag-helper.ts +9 -3
  341. package/src/rules/erb-require-trailing-newline.ts +47 -0
  342. package/src/rules/erb-require-whitespace-inside-tags.ts +96 -26
  343. package/src/rules/erb-right-trim.ts +67 -0
  344. package/src/rules/herb-disable-comment-base.ts +76 -0
  345. package/src/rules/herb-disable-comment-malformed.ts +66 -0
  346. package/src/rules/herb-disable-comment-missing-rules.ts +41 -0
  347. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +46 -0
  348. package/src/rules/herb-disable-comment-no-redundant-all.ts +40 -0
  349. package/src/rules/herb-disable-comment-unnecessary.ts +103 -0
  350. package/src/rules/herb-disable-comment-valid-rule-name.ts +62 -0
  351. package/src/rules/html-anchor-require-href.ts +9 -3
  352. package/src/rules/html-aria-attribute-must-be-valid.ts +9 -3
  353. package/src/rules/html-aria-label-is-well-formatted.ts +9 -5
  354. package/src/rules/html-aria-level-must-be-valid.ts +9 -2
  355. package/src/rules/html-aria-role-heading-requires-level.ts +9 -3
  356. package/src/rules/html-aria-role-must-be-valid.ts +9 -3
  357. package/src/rules/html-attribute-double-quotes.ts +42 -8
  358. package/src/rules/html-attribute-equals-spacing.ts +31 -7
  359. package/src/rules/html-attribute-values-require-quotes.ts +56 -10
  360. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +9 -3
  361. package/src/rules/html-body-only-elements.ts +60 -0
  362. package/src/rules/html-boolean-attributes-no-value.ts +31 -6
  363. package/src/rules/html-head-only-elements.ts +65 -0
  364. package/src/rules/html-iframe-has-title.ts +9 -4
  365. package/src/rules/html-img-require-alt.ts +10 -4
  366. package/src/rules/html-input-require-autocomplete.ts +85 -0
  367. package/src/rules/html-navigation-has-label.ts +9 -3
  368. package/src/rules/html-no-aria-hidden-on-focusable.ts +9 -3
  369. package/src/rules/html-no-block-inside-inline.ts +9 -3
  370. package/src/rules/html-no-duplicate-attributes.ts +9 -3
  371. package/src/rules/html-no-duplicate-ids.ts +11 -7
  372. package/src/rules/html-no-duplicate-meta-names.ts +188 -0
  373. package/src/rules/html-no-empty-attributes.ts +58 -10
  374. package/src/rules/html-no-empty-headings.ts +10 -8
  375. package/src/rules/html-no-nested-links.ts +10 -4
  376. package/src/rules/html-no-positive-tab-index.ts +9 -3
  377. package/src/rules/html-no-self-closing.ts +69 -9
  378. package/src/rules/html-no-space-in-tag.ts +221 -0
  379. package/src/rules/html-no-title-attribute.ts +9 -3
  380. package/src/rules/html-no-underscores-in-attribute-names.ts +12 -4
  381. package/src/rules/html-tag-name-lowercase.ts +41 -10
  382. package/src/rules/index.ts +24 -2
  383. package/src/rules/parser-no-errors.ts +8 -1
  384. package/src/rules/rule-utils.ts +250 -44
  385. package/src/rules/svg-tag-name-capitalization.ts +39 -6
  386. package/src/{default-rules.ts → rules.ts} +53 -13
  387. package/src/types.ts +133 -15
  388. package/dist/src/default-rules.js.map +0 -1
  389. package/dist/src/rules/erb-requires-trailing-newline.js +0 -22
  390. package/dist/src/rules/erb-requires-trailing-newline.js.map +0 -1
  391. package/dist/types/default-rules.d.ts +0 -2
  392. package/dist/types/rules/erb-requires-trailing-newline.d.ts +0 -6
  393. package/dist/types/src/default-rules.d.ts +0 -2
  394. package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +0 -6
  395. package/src/rules/erb-requires-trailing-newline.ts +0 -29
@@ -15,56 +15,34 @@ Headings relay the structure of a webpage and provide a meaningful, hierarchical
15
15
  ### ✅ Good
16
16
 
17
17
  ```erb
18
- <h*>Heading Content</h*>
19
- ```
18
+ <h1>Heading Content</h1>
20
19
 
21
- ```erb
22
- <h*><span>Text</span><h*>
23
- ```
20
+ <h1><span>Text</span></h1>
24
21
 
25
- ```erb
26
22
  <div role="heading" aria-level="1">Heading Content</div>
27
- ```
28
23
 
29
- ```erb
30
- <h* aria-hidden="true">Heading Content</h*>
31
- ```
24
+ <h1 aria-hidden="true">Heading Content</h1>
32
25
 
33
- ```erb
34
- <h* hidden>Heading Content</h*>
26
+ <h1 hidden>Heading Content</h1>
35
27
  ```
36
28
 
37
29
  ### 🚫 Bad
38
30
 
39
31
  ```erb
40
32
  <h1></h1>
41
- ```
42
33
 
43
- ```erb
44
34
  <h2></h2>
45
- ```
46
35
 
47
- ```erb
48
36
  <h3></h3>
49
- ```
50
37
 
51
- ```erb
52
38
  <h4></h4>
53
- ```
54
39
 
55
- ```erb
56
40
  <h5></h5>
57
- ```
58
41
 
59
- ```erb
60
42
  <h6></h6>
61
- ```
62
43
 
63
- ```erb
64
44
  <div role="heading" aria-level="1"></div>
65
- ```
66
45
 
67
- ```erb
68
46
  <h1><span aria-hidden="true">Inaccessible text</span></h1>
69
47
  ```
70
48
 
@@ -41,8 +41,7 @@ The recommended approach is to structure your HTML in the correct tab order and
41
41
 
42
42
  <button tabindex="2">Second in tab order</button>
43
43
 
44
-
45
- <input tabindex="5" type="text">
44
+ <input tabindex="5" type="text" autocomplete="off">
46
45
 
47
46
  <button tabindex="10">Submit</button>
48
47
  ```
@@ -22,6 +22,21 @@ Self-closing syntax is an XHTML artifact. In HTML:
22
22
  Removing the slash ensures HTML5-compliant, cleaner markup and avoids mixing
23
23
  XHTML and HTML styles.
24
24
 
25
+ ## Exception for Email Templates
26
+
27
+ ::: info
28
+ This rule is automatically disabled for ActionMailer view files (paths matching `**/views/**/*_mailer/**/*`) because older email clients, particularly legacy versions of Microsoft Outlook, require self-closing syntax for proper rendering of void elements like `<br />`.
29
+ :::
30
+
31
+ If you want to enable this rule for email templates as well, you can override the default exclusion in your `.herb.yml` configuration:
32
+
33
+ ```yaml [.herb.yml]
34
+ linter:
35
+ rules:
36
+ html-no-self-closing:
37
+ exclude: [] # Override to enable for all files including ActionMailer views
38
+ ```
39
+
25
40
  ## Examples
26
41
 
27
42
  ### ✅ Good
@@ -33,7 +48,7 @@ XHTML and HTML styles.
33
48
  <custom-element></custom-element>
34
49
 
35
50
  <img src="/logo.png" alt="Logo">
36
- <input type="text">
51
+ <input type="text" autocomplete="off">
37
52
  <br>
38
53
  <hr>
39
54
  ```
@@ -51,7 +66,7 @@ XHTML and HTML styles.
51
66
 
52
67
  <img src="/logo.png" alt="Logo" />
53
68
 
54
- <input type="text" />
69
+ <input type="text" autocomplete="off" />
55
70
 
56
71
  <br />
57
72
 
@@ -0,0 +1,66 @@
1
+ # Linter Rule: Enforce consistent spacing within HTML tags
2
+
3
+ **Rule:** `html-no-space-in-tag`
4
+
5
+ ## Description
6
+
7
+ Enforce consistent spacing within HTML opening and closing tags. This rule ensures:
8
+ - Exactly one space between tag name and first attribute
9
+ - Exactly one space between attributes
10
+ - No extra spaces before the closing `>` in non-self-closing tags
11
+ - Exactly one space before `/>` in self-closing tags
12
+ - No whitespace in closing tags (e.g., `</div>`)
13
+ - Consistent indentation in multiline tags
14
+
15
+ ## Rationale
16
+
17
+ Consistent spacing within HTML tags improves code readability and maintainability. Extra or missing spaces can make templates harder to scan and can indicate formatting inconsistencies across a codebase. This rule enforces a canonical style that is both readable and machine-parseable.
18
+
19
+ Self-closing tags (`<img />`, `<br />`) should have exactly one space before the `/>` to maintain visual consistency with HTML5 and JSX conventions.
20
+
21
+ ## Examples
22
+
23
+ ### ✅ Good
24
+
25
+ ```erb
26
+ <div class="foo"></div>
27
+
28
+ <img src="/logo.png" alt="Logo">
29
+
30
+ <input class="foo" name="bar">
31
+
32
+ <div class="foo" data-x="bar"></div>
33
+
34
+ <div
35
+ class="foo"
36
+ data-x="bar"
37
+ >
38
+ foo
39
+ </div>
40
+ ```
41
+
42
+ ### 🚫 Bad
43
+
44
+ ```erb
45
+ <div class="foo"></div>
46
+
47
+ <div class="foo" ></div>
48
+
49
+ <img alt="Logo" src="/logo.png">
50
+
51
+ <div class="foo" data-x="bar"></div>
52
+
53
+ <div
54
+ class="foo"
55
+ data-x="bar"
56
+ >
57
+ foo
58
+ </div>
59
+
60
+ <div >
61
+ </ div>
62
+ ```
63
+
64
+ ## References
65
+
66
+ - [Inspiration: ERB Lint `SpaceInHtmlTag` rule](https://github.com/shopify/erb_lint)
@@ -33,7 +33,7 @@ This rule allows `title` on `<iframe>` and `<link>` elements where it serves spe
33
33
  <button aria-label="Close dialog">×</button>
34
34
 
35
35
  <!-- Use aria-describedby for additional context -->
36
- <input type="password" aria-describedby="pwd-help">
36
+ <input type="password" aria-describedby="pwd-help" autocomplete="off">
37
37
  <div id="pwd-help">Password must be at least 8 characters</div>
38
38
 
39
39
  <!-- Exceptions: title allowed on iframe and links -->
@@ -52,7 +52,7 @@ This rule allows `title` on `<iframe>` and `<link>` elements where it serves spe
52
52
  <span title="Required field">*</span>
53
53
 
54
54
  <!-- Don't use title on form elements -->
55
- <input type="text" title="Enter your name">
55
+ <input type="text" title="Enter your name" autocomplete="off">
56
56
 
57
57
  <select title="Choose your country">
58
58
  <option>US</option>
@@ -23,7 +23,7 @@ Underscores in attribute names violate the HTML specification and are not suppor
23
23
  ```html
24
24
  <div data-user-id="123"></div>
25
25
 
26
- <img aria-label="Close">
26
+ <img aria-label="Close" alt="Close">
27
27
 
28
28
  <div data-<%= key %>-attribute="value"></div>
29
29
  ```
@@ -33,7 +33,7 @@ Underscores in attribute names violate the HTML specification and are not suppor
33
33
  ```html
34
34
  <div data_user_id="123"></div>
35
35
 
36
- <img aria_label="Close">
36
+ <img aria_label="Close" alt="Close">
37
37
 
38
38
  <div data-<%= key %>_attribute="value"></div>
39
39
  ```
@@ -33,7 +33,7 @@ This rule does not apply to child elements within `<svg>` tags, as SVG element n
33
33
  ```erb
34
34
  <div class="container"></div>
35
35
 
36
- <input type="text" name="username">
36
+ <input type="text" name="username" autocomplete="off">
37
37
 
38
38
  <span>Label</span>
39
39
 
@@ -45,7 +45,7 @@ This rule does not apply to child elements within `<svg>` tags, as SVG element n
45
45
  ```erb
46
46
  <DIV class="container"></DIV>
47
47
 
48
- <Input type="text" name="username">
48
+ <Input type="text" name="username" autocomplete="off">
49
49
 
50
50
  <Span>Label</Span>
51
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/linter",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
4
4
  "description": "HTML+ERB linter for validating HTML structure and enforcing best practices",
5
5
  "license": "MIT",
6
6
  "homepage": "https://herb-tools.dev",
@@ -36,13 +36,21 @@
36
36
  "types": "./dist/types/src/cli.d.ts",
37
37
  "require": "./dist/src/cli.js",
38
38
  "default": "./dist/src/cli.js"
39
+ },
40
+ "./loader": {
41
+ "types": "./dist/types/loader.d.ts",
42
+ "import": "./dist/loader.js",
43
+ "require": "./dist/loader.cjs",
44
+ "default": "./dist/loader.js"
39
45
  }
40
46
  },
41
47
  "dependencies": {
42
- "@herb-tools/core": "0.7.4",
43
- "@herb-tools/highlighter": "0.7.4",
44
- "@herb-tools/node-wasm": "0.7.4",
45
- "@herb-tools/printer": "0.7.4",
48
+ "@herb-tools/config": "0.8.0",
49
+ "@herb-tools/core": "0.8.0",
50
+ "@herb-tools/highlighter": "0.8.0",
51
+ "@herb-tools/node-wasm": "0.8.0",
52
+ "@herb-tools/printer": "0.8.0",
53
+ "@herb-tools/rewriter": "0.8.0",
46
54
  "glob": "^11.0.3"
47
55
  },
48
56
  "files": [
@@ -1,50 +1,58 @@
1
1
  import dedent from "dedent"
2
2
 
3
3
  import { parseArgs } from "util"
4
- import { statSync } from "fs"
5
- import { join } from "path"
6
-
7
4
  import { Herb } from "@herb-tools/node-wasm"
8
5
 
9
6
  import { THEME_NAMES, DEFAULT_THEME } from "@herb-tools/highlighter"
10
7
  import type { ThemeInput } from "@herb-tools/highlighter"
11
8
 
12
- import { name, version } from "../../package.json"
9
+ import { name, version, dependencies } from "../../package.json"
13
10
 
14
11
  export type FormatOption = "simple" | "detailed" | "json"
15
12
 
16
13
  export interface ParsedArguments {
17
- pattern: string
14
+ patterns: string[]
15
+ configFile?: string
18
16
  formatOption: FormatOption
19
17
  showTiming: boolean
20
18
  theme: ThemeInput
21
19
  wrapLines: boolean
22
20
  truncateLines: boolean
23
21
  useGitHubActions: boolean
22
+ fix: boolean
23
+ ignoreDisableComments: boolean
24
+ force: boolean
25
+ init: boolean
26
+ loadCustomRules: boolean
24
27
  }
25
28
 
26
29
  export class ArgumentParser {
27
30
  private readonly usage = dedent`
28
- Usage: herb-lint [file|glob-pattern|directory] [options]
31
+ Usage: herb-lint [files|directories|glob-patterns...] [options]
29
32
 
30
33
  Arguments:
31
- file Single file to lint
32
- glob-pattern Files to lint (defaults to **/*.html.erb)
33
- directory Directory to lint (automatically appends **/*.html.erb)
34
+ files Files, directories, or glob patterns to lint (defaults to configured extensions in .herb.yml)
35
+ Multiple arguments are supported (e.g., herb-lint file1.erb file2.erb dir/ "**/*.erb")
34
36
 
35
37
  Options:
36
- -h, --help show help
37
- -v, --version show version
38
- --format output format (simple|detailed|json) [default: detailed]
39
- --simple use simple output format (shortcut for --format simple)
40
- --json use JSON output format (shortcut for --format json)
41
- --github enable GitHub Actions annotations (combines with --format)
42
- --no-github disable GitHub Actions annotations (even in GitHub Actions environment)
43
- --theme syntax highlighting theme (${THEME_NAMES.join("|")}) or path to custom theme file [default: ${DEFAULT_THEME}]
44
- --no-color disable colored output
45
- --no-timing hide timing information
46
- --no-wrap-lines disable line wrapping
47
- --truncate-lines enable line truncation (mutually exclusive with line wrapping)
38
+ -h, --help show help
39
+ -v, --version show version
40
+ --init create a .herb.yml configuration file in the current directory
41
+ -c, --config-file <path> explicitly specify path to .herb.yml config file
42
+ --force force linting even if disabled in .herb.yml
43
+ --fix automatically fix auto-correctable offenses
44
+ --ignore-disable-comments report offenses even when suppressed with <%# herb:disable %> comments
45
+ --format output format (simple|detailed|json) [default: detailed]
46
+ --simple use simple output format (shortcut for --format simple)
47
+ --json use JSON output format (shortcut for --format json)
48
+ --github enable GitHub Actions annotations (combines with --format)
49
+ --no-github disable GitHub Actions annotations (even in GitHub Actions environment)
50
+ --no-custom-rules disable loading custom rules from project (custom rules are loaded by default from .herb/rules/**/*.{mjs,js})
51
+ --theme syntax highlighting theme (${THEME_NAMES.join("|")}) or path to custom theme file [default: ${DEFAULT_THEME}]
52
+ --no-color disable colored output
53
+ --no-timing hide timing information
54
+ --no-wrap-lines disable line wrapping
55
+ --truncate-lines enable line truncation (mutually exclusive with line wrapping)
48
56
  `
49
57
 
50
58
  parse(argv: string[]): ParsedArguments {
@@ -53,6 +61,11 @@ export class ArgumentParser {
53
61
  options: {
54
62
  help: { type: "boolean", short: "h" },
55
63
  version: { type: "boolean", short: "v" },
64
+ init: { type: "boolean" },
65
+ "config-file": { type: "string", short: "c" },
66
+ force: { type: "boolean" },
67
+ fix: { type: "boolean" },
68
+ "ignore-disable-comments": { type: "boolean" },
56
69
  format: { type: "string" },
57
70
  simple: { type: "boolean" },
58
71
  json: { type: "boolean" },
@@ -62,7 +75,8 @@ export class ArgumentParser {
62
75
  "no-color": { type: "boolean" },
63
76
  "no-timing": { type: "boolean" },
64
77
  "no-wrap-lines": { type: "boolean" },
65
- "truncate-lines": { type: "boolean" }
78
+ "truncate-lines": { type: "boolean" },
79
+ "no-custom-rules": { type: "boolean" }
66
80
  },
67
81
  allowPositionals: true
68
82
  })
@@ -74,7 +88,9 @@ export class ArgumentParser {
74
88
 
75
89
  if (values.version) {
76
90
  console.log("Versions:")
77
- console.log(` ${name}@${version}, ${Herb.version}`.split(", ").join("\n "))
91
+ console.log(` ${name}@${version}`)
92
+ console.log(` @herb-tools/printer@${dependencies["@herb-tools/printer"]}`)
93
+ console.log(` ${Herb.version}`.split(", ").join("\n "))
78
94
  process.exit(0)
79
95
  }
80
96
 
@@ -120,23 +136,18 @@ export class ArgumentParser {
120
136
  }
121
137
 
122
138
  const theme = values.theme || DEFAULT_THEME
123
- const pattern = this.getFilePattern(positionals)
124
-
125
- return { pattern, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions }
139
+ const patterns = this.getFilePatterns(positionals)
140
+ const fix = values.fix || false
141
+ const force = !!values.force
142
+ const ignoreDisableComments = values["ignore-disable-comments"] || false
143
+ const configFile = values["config-file"]
144
+ const init = values.init || false
145
+ const loadCustomRules = !values["no-custom-rules"]
146
+
147
+ return { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, ignoreDisableComments, force, init, loadCustomRules }
126
148
  }
127
149
 
128
- private getFilePattern(positionals: string[]): string {
129
- let pattern = positionals.length > 0 ? positionals[0] : "**/*.html.erb"
130
-
131
- try {
132
- const stat = statSync(pattern)
133
- if (stat.isDirectory()) {
134
- pattern = join(pattern, "**/*.html.erb")
135
- }
136
- } catch {
137
- // Not a file/directory, treat as glob pattern
138
- }
139
-
140
- return pattern
150
+ private getFilePatterns(positionals: string[]): string[] {
151
+ return positionals
141
152
  }
142
153
  }
@@ -1,26 +1,42 @@
1
- import { readFileSync } from "fs"
2
- import { resolve } from "path"
3
1
  import { Herb } from "@herb-tools/node-wasm"
4
2
  import { Linter } from "../linter.js"
3
+ import { loadCustomRules } from "../loader.js"
4
+ import { Config } from "@herb-tools/config"
5
+
6
+ import { readFileSync, writeFileSync } from "fs"
7
+ import { resolve } from "path"
5
8
  import { colorize } from "@herb-tools/highlighter"
9
+
6
10
  import type { Diagnostic } from "@herb-tools/core"
7
11
  import type { FormatOption } from "./argument-parser.js"
12
+ import type { HerbConfigOptions } from "@herb-tools/config"
8
13
 
9
14
  export interface ProcessedFile {
10
15
  filename: string
11
16
  offense: Diagnostic
12
17
  content: string
18
+ autocorrectable?: boolean
13
19
  }
14
20
 
15
21
  export interface ProcessingContext {
16
22
  projectPath?: string
17
23
  pattern?: string
24
+ fix?: boolean
25
+ ignoreDisableComments?: boolean
26
+ linterConfig?: HerbConfigOptions['linter']
27
+ config?: Config
28
+ loadCustomRules?: boolean
18
29
  }
19
30
 
20
31
  export interface ProcessingResult {
21
32
  totalErrors: number
22
33
  totalWarnings: number
34
+ totalInfo: number
35
+ totalHints: number
36
+ totalIgnored: number
37
+ totalWouldBeIgnored?: number
23
38
  filesWithOffenses: number
39
+ filesFixed: number
24
40
  ruleCount: number
25
41
  allOffenses: ProcessedFile[]
26
42
  ruleOffenses: Map<string, { count: number, files: Set<string> }>
@@ -29,55 +45,146 @@ export interface ProcessingResult {
29
45
 
30
46
  export class FileProcessor {
31
47
  private linter: Linter | null = null
48
+ private customRulesLoaded: boolean = false
49
+
50
+ private isRuleAutocorrectable(ruleName: string): boolean {
51
+ if (!this.linter) return false
52
+
53
+ const RuleClass = (this.linter as any).rules.find((rule: any) => {
54
+ const instance = new rule()
55
+
56
+ return instance.name === ruleName
57
+ })
58
+
59
+ if (!RuleClass) return false
60
+
61
+ return RuleClass.autocorrectable === true
62
+ }
32
63
 
33
64
  async processFiles(files: string[], formatOption: FormatOption = 'detailed', context?: ProcessingContext): Promise<ProcessingResult> {
34
65
  let totalErrors = 0
35
66
  let totalWarnings = 0
67
+ let totalInfo = 0
68
+ let totalHints = 0
69
+ let totalIgnored = 0
70
+ let totalWouldBeIgnored = 0
36
71
  let filesWithOffenses = 0
72
+ let filesFixed = 0
37
73
  let ruleCount = 0
74
+
38
75
  const allOffenses: ProcessedFile[] = []
39
76
  const ruleOffenses = new Map<string, { count: number, files: Set<string> }>()
40
77
 
41
- for (const filename of files) {
42
- const filePath = context?.projectPath ? resolve(context.projectPath, filename) : resolve(filename)
43
- const content = readFileSync(filePath, "utf-8")
44
- const parseResult = Herb.parse(content)
45
-
46
- if (parseResult.errors.length > 0) {
47
- if (formatOption !== 'json') {
48
- console.error(`${colorize(filename, "cyan")} - ${colorize("Parse errors:", "brightRed")}`)
49
-
50
- for (const error of parseResult.errors) {
51
- console.error(` ${colorize("✗", "brightRed")} ${error.message}`)
78
+ if (!this.linter) {
79
+ let customRules = undefined
80
+ let customRuleInfo: Array<{ name: string, path: string }> = []
81
+ let customRuleWarnings: string[] = []
82
+
83
+ if (context?.loadCustomRules && !this.customRulesLoaded) {
84
+ try {
85
+ const result = await loadCustomRules({
86
+ baseDir: context.projectPath,
87
+ silent: formatOption === 'json'
88
+ })
89
+
90
+ customRules = result.rules
91
+ customRuleInfo = result.ruleInfo
92
+ customRuleWarnings = result.warnings
93
+
94
+ this.customRulesLoaded = true
95
+
96
+ if (customRules.length > 0 && formatOption !== 'json') {
97
+ const ruleText = customRules.length === 1 ? 'rule' : 'rules'
98
+ console.log(colorize(`\nLoaded ${customRules.length} custom ${ruleText}:`, "green"))
99
+
100
+ for (const { name, path } of customRuleInfo) {
101
+ const relativePath = context.projectPath ? path.replace(context.projectPath + '/', '') : path
102
+ console.log(colorize(` • ${name}`, "cyan") + colorize(` (${relativePath})`, "dim"))
103
+ }
104
+
105
+ if (customRuleWarnings.length > 0) {
106
+ console.log()
107
+ for (const warning of customRuleWarnings) {
108
+ console.warn(colorize(` ⚠ ${warning}`, "yellow"))
109
+ }
110
+ }
111
+
112
+ console.log()
113
+ }
114
+ } catch (error) {
115
+ if (formatOption !== 'json') {
116
+ console.warn(colorize(`Warning: Failed to load custom rules: ${error}`, "yellow"))
52
117
  }
53
118
  }
54
-
55
- for (const error of parseResult.errors) {
56
- allOffenses.push({ filename, offense: error, content })
57
- }
58
-
59
- totalErrors++
60
- filesWithOffenses++
61
- continue
62
119
  }
63
120
 
64
- if (!this.linter) {
65
- this.linter = new Linter(Herb)
66
- }
121
+ this.linter = Linter.from(Herb, context?.config, customRules)
122
+ }
67
123
 
68
- const lintResult = this.linter.lint(content, { fileName: filename })
124
+ for (const filename of files) {
125
+ const filePath = context?.projectPath ? resolve(context.projectPath, filename) : resolve(filename)
126
+ let content = readFileSync(filePath, "utf-8")
127
+
128
+ const lintResult = this.linter.lint(content, {
129
+ fileName: filename,
130
+ ignoreDisableComments: context?.ignoreDisableComments
131
+ })
69
132
 
70
133
  if (ruleCount === 0) {
71
134
  ruleCount = this.linter.getRuleCount()
72
135
  }
73
136
 
74
- if (lintResult.offenses.length === 0) {
137
+ if (context?.fix && lintResult.offenses.length > 0) {
138
+ const autofixResult = this.linter.autofix(content, {
139
+ fileName: filename,
140
+ ignoreDisableComments: context?.ignoreDisableComments
141
+ })
142
+
143
+ if (autofixResult.fixed.length > 0) {
144
+ writeFileSync(filePath, autofixResult.source, "utf-8")
145
+
146
+ filesFixed++
147
+
148
+ if (formatOption !== 'json') {
149
+ console.log(`${colorize("✓", "brightGreen")} ${colorize(filename, "cyan")} - ${colorize(`Fixed ${autofixResult.fixed.length} offense(s)`, "green")}`)
150
+ }
151
+ }
152
+
153
+ content = autofixResult.source
154
+
155
+ for (const offense of autofixResult.unfixed) {
156
+ allOffenses.push({
157
+ filename,
158
+ offense: offense,
159
+ content,
160
+ autocorrectable: this.isRuleAutocorrectable(offense.rule)
161
+ })
162
+
163
+ const ruleData = ruleOffenses.get(offense.rule) || { count: 0, files: new Set() }
164
+ ruleData.count++
165
+ ruleData.files.add(filename)
166
+ ruleOffenses.set(offense.rule, ruleData)
167
+ }
168
+
169
+ if (autofixResult.unfixed.length > 0) {
170
+ totalErrors += autofixResult.unfixed.filter(offense => offense.severity === "error").length
171
+ totalWarnings += autofixResult.unfixed.filter(offense => offense.severity === "warning").length
172
+ totalInfo += autofixResult.unfixed.filter(offense => offense.severity === "info").length
173
+ totalHints += autofixResult.unfixed.filter(offense => offense.severity === "hint").length
174
+ filesWithOffenses++
175
+ }
176
+ } else if (lintResult.offenses.length === 0) {
75
177
  if (files.length === 1 && formatOption !== 'json') {
76
178
  console.log(`${colorize("✓", "brightGreen")} ${colorize(filename, "cyan")} - ${colorize("No issues found", "green")}`)
77
179
  }
78
180
  } else {
79
181
  for (const offense of lintResult.offenses) {
80
- allOffenses.push({ filename, offense: offense, content })
182
+ allOffenses.push({
183
+ filename,
184
+ offense: offense,
185
+ content,
186
+ autocorrectable: this.isRuleAutocorrectable(offense.rule)
187
+ })
81
188
 
82
189
  const ruleData = ruleOffenses.get(offense.rule) || { count: 0, files: new Set() }
83
190
  ruleData.count++
@@ -87,10 +194,34 @@ export class FileProcessor {
87
194
 
88
195
  totalErrors += lintResult.errors
89
196
  totalWarnings += lintResult.warnings
197
+ totalInfo += lintResult.offenses.filter(o => o.severity === "info").length
198
+ totalHints += lintResult.offenses.filter(o => o.severity === "hint").length
90
199
  filesWithOffenses++
91
200
  }
201
+ totalIgnored += lintResult.ignored
202
+ if (lintResult.wouldBeIgnored) {
203
+ totalWouldBeIgnored += lintResult.wouldBeIgnored
204
+ }
205
+ }
206
+
207
+ const result: ProcessingResult = {
208
+ totalErrors,
209
+ totalWarnings,
210
+ totalInfo,
211
+ totalHints,
212
+ totalIgnored,
213
+ filesWithOffenses,
214
+ filesFixed,
215
+ ruleCount,
216
+ allOffenses,
217
+ ruleOffenses,
218
+ context
219
+ }
220
+
221
+ if (totalWouldBeIgnored > 0) {
222
+ result.totalWouldBeIgnored = totalWouldBeIgnored
92
223
  }
93
224
 
94
- return { totalErrors, totalWarnings, filesWithOffenses, ruleCount, allOffenses, ruleOffenses, context }
225
+ return result
95
226
  }
96
227
  }