@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
@@ -29,7 +29,15 @@ export class DetailedFormatter extends BaseFormatter {
29
29
 
30
30
  if (isSingleFile) {
31
31
  const { filename, content } = allOffenses[0]
32
- const diagnostics = allOffenses.map(item => item.offense)
32
+ const diagnostics = allOffenses.map(item => {
33
+ if (item.autocorrectable && item.offense.code) {
34
+ return {
35
+ ...item.offense,
36
+ message: `${item.offense.message} ${colorize(colorize("[Correctable]", "green"), "bold")}`
37
+ }
38
+ }
39
+ return item.offense
40
+ })
33
41
 
34
42
  const highlighted = this.highlighter.highlight(filename, content, {
35
43
  diagnostics: diagnostics,
@@ -44,8 +52,18 @@ export class DetailedFormatter extends BaseFormatter {
44
52
  const totalMessageCount = allOffenses.length
45
53
 
46
54
  for (let i = 0; i < allOffenses.length; i++) {
47
- const { filename, offense, content } = allOffenses[i]
48
- const formatted = this.highlighter.highlightDiagnostic(filename, offense, content, {
55
+ const { filename, offense, content, autocorrectable } = allOffenses[i]
56
+
57
+ let modifiedOffense = offense
58
+
59
+ if (autocorrectable && offense.code) {
60
+ modifiedOffense = {
61
+ ...offense,
62
+ message: `${offense.message} ${colorize(colorize("[Correctable]", "green"), "bold")}`
63
+ }
64
+ }
65
+
66
+ const formatted = this.highlighter.highlightDiagnostic(filename, modifiedOffense, content, {
49
67
  contextLines: 2,
50
68
  wrapLines: this.wrapLines,
51
69
  truncateLines: this.truncateLines
@@ -79,7 +79,23 @@ export class GitHubActionsFormatter extends BaseFormatter {
79
79
  // ::{level} file={file},line={line},col={col},title={title}::{message}
80
80
  //
81
81
  private formatDiagnostic(filename: string, diagnostic: Diagnostic, codePreview: string = ""): void {
82
- const level = diagnostic.severity === "error" ? "error" : "warning"
82
+ let level: string
83
+
84
+ switch (diagnostic.severity) {
85
+ case "error":
86
+ level = "error"
87
+ break
88
+ case "warning":
89
+ level = "warning"
90
+ break
91
+ case "info":
92
+ case "hint":
93
+ level = "notice"
94
+ break
95
+ default:
96
+ level = "warning"
97
+ }
98
+
83
99
  const { line, column } = diagnostic.location.start
84
100
 
85
101
  const escapedFilename = this.escapeParam(filename)
@@ -12,6 +12,9 @@ interface JSONSummary {
12
12
  filesWithOffenses: number
13
13
  totalErrors: number
14
14
  totalWarnings: number
15
+ totalInfo: number
16
+ totalHints: number
17
+ totalIgnored: number
15
18
  totalOffenses: number
16
19
  ruleCount: number
17
20
  }
@@ -34,6 +37,9 @@ interface JSONFormatOptions {
34
37
  files: string[]
35
38
  totalErrors: number
36
39
  totalWarnings: number
40
+ totalInfo: number
41
+ totalHints: number
42
+ totalIgnored: number
37
43
  filesWithOffenses: number
38
44
  ruleCount: number
39
45
  startTime: number
@@ -79,6 +85,9 @@ export class JSONFormatter extends BaseFormatter {
79
85
  filesWithOffenses: options.filesWithOffenses,
80
86
  totalErrors: options.totalErrors,
81
87
  totalWarnings: options.totalWarnings,
88
+ totalInfo: options.totalInfo,
89
+ totalHints: options.totalHints,
90
+ totalIgnored: options.totalIgnored,
82
91
  totalOffenses: options.totalErrors + options.totalWarnings,
83
92
  ruleCount: options.ruleCount
84
93
  }
@@ -9,17 +9,17 @@ export class SimpleFormatter extends BaseFormatter {
9
9
  async format(allOffenses: ProcessedFile[]): Promise<void> {
10
10
  if (allOffenses.length === 0) return
11
11
 
12
- const groupedOffenses = new Map<string, Diagnostic[]>()
12
+ const groupedOffenses = new Map<string, ProcessedFile[]>()
13
13
 
14
- for (const { filename, offense } of allOffenses) {
15
- const offenses = groupedOffenses.get(filename) || []
16
- offenses.push(offense)
17
- groupedOffenses.set(filename, offenses)
14
+ for (const processedFile of allOffenses) {
15
+ const offenses = groupedOffenses.get(processedFile.filename) || []
16
+ offenses.push(processedFile)
17
+ groupedOffenses.set(processedFile.filename, offenses)
18
18
  }
19
19
 
20
- for (const [filename, offenses] of groupedOffenses) {
20
+ for (const [filename, processedFiles] of groupedOffenses) {
21
21
  console.log("")
22
- this.formatFile(filename, offenses)
22
+ this.formatFileProcessed(filename, processedFiles)
23
23
  }
24
24
  }
25
25
 
@@ -31,10 +31,26 @@ export class SimpleFormatter extends BaseFormatter {
31
31
  const severity = isError ? colorize("✗", "brightRed") : colorize("⚠", "brightYellow")
32
32
  const rule = colorize(`(${offense.code})`, "blue")
33
33
  const locationString = `${offense.location.start.line}:${offense.location.start.column}`
34
- const paddedLocation = locationString.padEnd(4) // Pad to 4 characters for alignment
34
+ const paddedLocation = locationString.padEnd(4)
35
35
 
36
36
  console.log(` ${colorize(paddedLocation, "gray")} ${severity} ${offense.message} ${rule}`)
37
37
  }
38
38
  console.log("")
39
39
  }
40
+
41
+ formatFileProcessed(filename: string, processedFiles: ProcessedFile[]): void {
42
+ console.log(`${colorize(filename, "cyan")}:`)
43
+
44
+ for (const { offense, autocorrectable } of processedFiles) {
45
+ const isError = offense.severity === "error"
46
+ const severity = isError ? colorize("✗", "brightRed") : colorize("⚠", "brightYellow")
47
+ const rule = colorize(`(${offense.code})`, "blue")
48
+ const locationString = `${offense.location.start.line}:${offense.location.start.column}`
49
+ const paddedLocation = locationString.padEnd(4)
50
+ const correctable = autocorrectable ? colorize(colorize(" [Correctable]", "green"), "bold") : ""
51
+
52
+ console.log(` ${colorize(paddedLocation, "gray")} ${severity} ${offense.message} ${rule}${correctable}`)
53
+ }
54
+ console.log("")
55
+ }
40
56
  }
@@ -27,7 +27,9 @@ export class OutputManager {
27
27
  * Output successful lint results
28
28
  */
29
29
  async outputResults(results: LintResults, options: OutputOptions): Promise<void> {
30
- const { allOffenses, files, totalErrors, totalWarnings, filesWithOffenses, ruleCount, ruleOffenses } = results
30
+ const { allOffenses, files, totalErrors, totalWarnings, totalInfo, totalHints, totalIgnored, totalWouldBeIgnored, filesWithOffenses, ruleCount, ruleOffenses, context } = results
31
+
32
+ const autofixableCount = allOffenses.filter(offense => offense.autocorrectable).length
31
33
 
32
34
  if (options.useGitHubActions) {
33
35
  const githubFormatter = new GitHubActionsFormatter(options.wrapLines, options.truncateLines)
@@ -45,12 +47,18 @@ export class OutputManager {
45
47
  files,
46
48
  totalErrors,
47
49
  totalWarnings,
50
+ totalInfo,
51
+ totalHints,
52
+ totalIgnored,
53
+ totalWouldBeIgnored,
48
54
  filesWithOffenses,
49
55
  ruleCount,
50
56
  startTime: options.startTime,
51
57
  startDate: options.startDate,
52
58
  showTiming: options.showTiming,
53
- ruleOffenses
59
+ ruleOffenses,
60
+ autofixableCount,
61
+ ignoreDisableComments: context?.ignoreDisableComments,
54
62
  })
55
63
  }
56
64
  } else if (options.formatOption === "json") {
@@ -68,6 +76,9 @@ export class OutputManager {
68
76
  filesWithOffenses,
69
77
  totalErrors,
70
78
  totalWarnings,
79
+ totalInfo,
80
+ totalHints,
81
+ totalIgnored,
71
82
  totalOffenses: totalErrors + totalWarnings,
72
83
  ruleCount
73
84
  },
@@ -96,12 +107,18 @@ export class OutputManager {
96
107
  files,
97
108
  totalErrors,
98
109
  totalWarnings,
110
+ totalInfo,
111
+ totalHints,
112
+ totalIgnored,
113
+ totalWouldBeIgnored,
99
114
  filesWithOffenses,
100
115
  ruleCount,
101
116
  startTime: options.startTime,
102
117
  startDate: options.startDate,
103
118
  showTiming: options.showTiming,
104
- ruleOffenses
119
+ ruleOffenses,
120
+ autofixableCount,
121
+ ignoreDisableComments: context?.ignoreDisableComments,
105
122
  })
106
123
  }
107
124
  }
@@ -120,6 +137,9 @@ export class OutputManager {
120
137
  filesWithOffenses: 0,
121
138
  totalErrors: 0,
122
139
  totalWarnings: 0,
140
+ totalInfo: 0,
141
+ totalHints: 0,
142
+ totalIgnored: 0,
123
143
  totalOffenses: 0,
124
144
  ruleCount: 0
125
145
  },
@@ -4,12 +4,18 @@ export interface SummaryData {
4
4
  files: string[]
5
5
  totalErrors: number
6
6
  totalWarnings: number
7
+ totalInfo?: number
8
+ totalHints?: number
9
+ totalIgnored: number
10
+ totalWouldBeIgnored?: number
7
11
  filesWithOffenses: number
8
12
  ruleCount: number
9
13
  startTime: number
10
14
  startDate: Date
11
15
  showTiming: boolean
12
16
  ruleOffenses: Map<string, { count: number, files: Set<string> }>
17
+ autofixableCount: number
18
+ ignoreDisableComments?: boolean
13
19
  }
14
20
 
15
21
  export class SummaryReporter {
@@ -18,7 +24,7 @@ export class SummaryReporter {
18
24
  }
19
25
 
20
26
  displaySummary(data: SummaryData): void {
21
- const { files, totalErrors, totalWarnings, filesWithOffenses, ruleCount, startTime, startDate, showTiming } = data
27
+ const { files, totalErrors, totalWarnings, totalInfo = 0, totalHints = 0, totalIgnored, totalWouldBeIgnored, filesWithOffenses, ruleCount, startTime, startDate, showTiming, autofixableCount, ignoreDisableComments } = data
22
28
 
23
29
  console.log("\n")
24
30
  console.log(` ${colorize("Summary:", "bold")}`)
@@ -62,6 +68,18 @@ export class SummaryReporter {
62
68
  parts.push(colorize(colorize(`${totalWarnings} ${this.pluralize(totalWarnings, "warning")}`, "green"), "bold"))
63
69
  }
64
70
 
71
+ if (totalInfo > 0) {
72
+ parts.push(colorize(colorize(`${totalInfo} info`, "brightBlue"), "bold"))
73
+ }
74
+
75
+ if (totalHints > 0) {
76
+ parts.push(colorize(colorize(`${totalHints} ${this.pluralize(totalHints, "hint")}`, "gray"), "bold"))
77
+ }
78
+
79
+ if (totalIgnored > 0) {
80
+ parts.push(colorize(colorize(`${totalIgnored} ignored`, "gray"), "bold"))
81
+ }
82
+
65
83
  if (parts.length === 0) {
66
84
  offensesSummary = colorize(colorize("0 offenses", "green"), "bold")
67
85
  } else {
@@ -69,17 +87,36 @@ export class SummaryReporter {
69
87
 
70
88
  let detailText = ""
71
89
 
72
- const totalOffenses = totalErrors + totalWarnings
90
+ const totalOffenses = totalErrors + totalWarnings + totalInfo + totalHints
73
91
 
74
92
  if (filesWithOffenses > 0) {
75
93
  detailText = `${totalOffenses} ${this.pluralize(totalOffenses, "offense")} across ${filesWithOffenses} ${this.pluralize(filesWithOffenses, "file")}`
76
94
  }
77
95
 
78
- offensesSummary += ` ${colorize(colorize(`(${detailText})`, "gray"), "dim")}`
96
+ if (detailText) {
97
+ offensesSummary += ` ${colorize(colorize(`(${detailText})`, "gray"), "dim")}`
98
+ }
79
99
  }
80
100
 
81
101
  console.log(` ${colorize(pad("Offenses"), "gray")} ${offensesSummary}`)
82
102
 
103
+ if (ignoreDisableComments && totalWouldBeIgnored && totalWouldBeIgnored > 0) {
104
+ const message = `${colorize(colorize(`${totalWouldBeIgnored} additional ${this.pluralize(totalWouldBeIgnored, "offense")} reported (would have been ignored)`, "cyan"), "bold")}`
105
+ console.log(` ${colorize(pad("Note"), "gray")} ${message}`)
106
+ }
107
+
108
+ const totalOffenses = totalErrors + totalWarnings + totalInfo + totalHints
109
+
110
+ if (autofixableCount > 0 || totalOffenses > 0) {
111
+ let fixableLine = `${colorize(colorize(`${totalOffenses} ${this.pluralize(totalOffenses, "offense")}`, "brightRed"), "bold")}`
112
+
113
+ if (autofixableCount > 0) {
114
+ fixableLine += ` | ${colorize(colorize(`${autofixableCount} autocorrectable using \`--fix\``, "green"), "bold")}`
115
+ }
116
+
117
+ console.log(` ${colorize(pad("Fixable"), "gray")} ${fixableLine}`)
118
+ }
119
+
83
120
  if (showTiming) {
84
121
  const duration = Date.now() - startTime
85
122
  const timeString = startDate.toTimeString().split(' ')[0]
package/src/cli.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { glob } from "glob"
2
2
  import { Herb } from "@herb-tools/node-wasm"
3
+ import { Config, addHerbExtensionRecommendation, getExtensionsJsonRelativePath } from "@herb-tools/config"
4
+
3
5
  import { existsSync, statSync } from "fs"
4
6
  import { dirname, resolve, relative } from "path"
5
7
 
6
- import { ArgumentParser, type FormatOption } from "./cli/argument-parser.js"
8
+ import { ArgumentParser } from "./cli/argument-parser.js"
7
9
  import { FileProcessor } from "./cli/file-processor.js"
8
10
  import { OutputManager } from "./cli/output-manager.js"
11
+ import { version } from "../package.json"
12
+
13
+ import type { ProcessingContext } from "./cli/file-processor.js"
14
+ import type { FormatOption } from "./cli/argument-parser.js"
9
15
 
10
16
  export * from "./cli/index.js"
11
17
 
@@ -19,44 +25,6 @@ export class CLI {
19
25
  return this.projectPath
20
26
  }
21
27
 
22
- protected findProjectRoot(startPath: string): string {
23
- let currentPath = resolve(startPath)
24
-
25
- if (existsSync(currentPath) && statSync(currentPath).isFile()) {
26
- currentPath = dirname(currentPath)
27
- }
28
-
29
- const projectIndicators = [
30
- 'package.json',
31
- 'Gemfile',
32
- '.git',
33
- 'tsconfig.json',
34
- 'composer.json',
35
- 'pyproject.toml',
36
- 'requirements.txt',
37
- '.herb.yml'
38
- ]
39
-
40
- while (currentPath !== '/') {
41
- for (const indicator of projectIndicators) {
42
- if (existsSync(resolve(currentPath, indicator))) {
43
- return currentPath
44
- }
45
- }
46
-
47
- const parentPath = dirname(currentPath)
48
- if (parentPath === currentPath) {
49
- break
50
- }
51
-
52
- currentPath = parentPath
53
- }
54
-
55
- return existsSync(startPath) && statSync(startPath).isDirectory()
56
- ? startPath
57
- : dirname(startPath)
58
- }
59
-
60
28
  protected exitWithError(message: string, formatOption: FormatOption, exitCode: number = 1) {
61
29
  this.outputManager.outputError(message, {
62
30
  formatOption,
@@ -87,7 +55,8 @@ export class CLI {
87
55
  process.exit(exitCode)
88
56
  }
89
57
 
90
- protected determineProjectPath(pattern: string | undefined): void {
58
+ protected determineProjectPath(patterns: string[]): void {
59
+ const pattern = patterns[0]
91
60
  if (pattern) {
92
61
  const resolvedPattern = resolve(pattern)
93
62
 
@@ -97,15 +66,15 @@ export class CLI {
97
66
  if (stats.isDirectory()) {
98
67
  this.projectPath = resolvedPattern
99
68
  } else {
100
- this.projectPath = this.findProjectRoot(resolvedPattern)
69
+ this.projectPath = dirname(resolvedPattern)
101
70
  }
102
71
  }
103
72
  }
104
73
  }
105
74
 
106
- protected adjustPattern(pattern: string | undefined): string {
75
+ protected adjustPattern(pattern: string | undefined, configGlobPattern: string): string {
107
76
  if (!pattern) {
108
- return '**/*.html.erb'
77
+ return configGlobPattern
109
78
  }
110
79
 
111
80
  const resolvedPattern = resolve(pattern)
@@ -114,7 +83,7 @@ export class CLI {
114
83
  const stats = statSync(resolvedPattern)
115
84
 
116
85
  if (stats.isDirectory()) {
117
- return '**/*.html.erb'
86
+ return configGlobPattern
118
87
  } else if (stats.isFile()) {
119
88
  return relative(this.projectPath, resolvedPattern)
120
89
  }
@@ -123,8 +92,43 @@ export class CLI {
123
92
  return pattern
124
93
  }
125
94
 
95
+ protected async resolvePatternToFiles(pattern: string, config: Config, force: boolean): Promise<{ files: string[], explicitFile: string | undefined }> {
96
+ const resolvedPattern = resolve(pattern)
97
+ const isExplicitFile = existsSync(resolvedPattern) && statSync(resolvedPattern).isFile()
98
+ let explicitFile: string | undefined
99
+
100
+ if (isExplicitFile) {
101
+ explicitFile = pattern
102
+ }
103
+
104
+ const filesConfig = config.getFilesConfigForTool('linter')
105
+ const configGlobPattern = filesConfig.include && filesConfig.include.length > 0
106
+ ? (filesConfig.include.length === 1 ? filesConfig.include[0] : `{${filesConfig.include.join(',')}}`)
107
+ : '**/*.html.erb'
108
+ const adjustedPattern = this.adjustPattern(pattern, configGlobPattern)
109
+
110
+ let files = await glob(adjustedPattern, {
111
+ cwd: this.projectPath,
112
+ ignore: filesConfig.exclude || []
113
+ })
114
+
115
+ if (explicitFile && files.length === 0) {
116
+ if (!force) {
117
+ console.error(`⚠️ File ${explicitFile} is excluded by configuration patterns.`)
118
+ console.error(` Use --force to lint it anyway.\n`)
119
+ process.exit(0)
120
+ } else {
121
+ console.log(`⚠️ Forcing linter on excluded file: ${explicitFile}`)
122
+ console.log()
123
+ files = [adjustedPattern]
124
+ }
125
+ }
126
+
127
+ return { files, explicitFile }
128
+ }
129
+
126
130
  protected async beforeProcess(): Promise<void> {
127
- await Herb.load()
131
+ // Hook for subclasses to add custom output before processing
128
132
  }
129
133
 
130
134
  protected async afterProcess(_results: any, _outputOptions: any): Promise<void> {
@@ -132,14 +136,41 @@ export class CLI {
132
136
  }
133
137
 
134
138
  async run() {
139
+ await Herb.load()
140
+
135
141
  const startTime = Date.now()
136
142
  const startDate = new Date()
137
143
 
138
- let { pattern, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions } = this.argumentParser.parse(process.argv)
144
+ let { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, ignoreDisableComments, force, init, loadCustomRules } = this.argumentParser.parse(process.argv)
145
+
146
+ this.determineProjectPath(patterns)
147
+
148
+ if (init) {
149
+ const configPath = configFile || this.projectPath
150
+
151
+ if (Config.exists(configPath)) {
152
+ const fullPath = configFile || Config.configPathFromProjectPath(this.projectPath)
153
+ console.error(`\n✗ Configuration file already exists at ${fullPath}`)
154
+ console.error(` Use --config-file to specify a different location.\n`)
155
+ process.exit(1)
156
+ }
157
+
158
+ const config = await Config.loadForCLI(configPath, version, true)
159
+ const extensionAdded = addHerbExtensionRecommendation(this.projectPath)
139
160
 
140
- this.determineProjectPath(pattern)
161
+ console.log(`\n✓ Configuration initialized at ${config.path}`)
141
162
 
142
- pattern = this.adjustPattern(pattern)
163
+ if (extensionAdded) {
164
+ console.log(`✓ VSCode extension recommended in ${getExtensionsJsonRelativePath()}`)
165
+ }
166
+
167
+ console.log(` Edit this file to customize linter and formatter settings.\n`)
168
+ process.exit(0)
169
+ }
170
+
171
+ const silent = formatOption === 'json'
172
+ const config = await Config.load(configFile || this.projectPath, { version, exitOnError: true, createIfMissing: false, silent })
173
+ const linterConfig = config.options.linter || {}
143
174
 
144
175
  const outputOptions = {
145
176
  formatOption,
@@ -155,15 +186,69 @@ export class CLI {
155
186
  try {
156
187
  await this.beforeProcess()
157
188
 
158
- const files = await glob(pattern, { cwd: this.projectPath })
189
+ if (linterConfig.enabled === false && !force) {
190
+ this.exitWithInfo("Linter is disabled in .herb.yml configuration. Use --force to lint anyway.", formatOption, 0, { startTime, startDate, showTiming })
191
+ }
192
+
193
+ if (force && linterConfig.enabled === false) {
194
+ console.log("⚠️ Forcing linter run (disabled in .herb.yml)")
195
+ console.log()
196
+ }
197
+
198
+ let files: string[]
199
+ let explicitFiles: string[] = []
200
+
201
+ if (patterns.length === 0) {
202
+ files = await config.findFilesForTool('linter', this.projectPath)
203
+ } else {
204
+ const allFiles: string[] = []
205
+
206
+ for (const pattern of patterns) {
207
+ const { files: patternFiles, explicitFile } = await this.resolvePatternToFiles(pattern, config, force)
208
+
209
+ if (patternFiles.length === 0) {
210
+ console.error(`✗ No files found matching pattern: ${pattern}`)
211
+ process.exit(1)
212
+ }
213
+
214
+ allFiles.push(...patternFiles)
215
+ if (explicitFile) {
216
+ explicitFiles.push(explicitFile)
217
+ }
218
+ }
219
+
220
+ files = [...new Set(allFiles)]
221
+ }
159
222
 
160
223
  if (files.length === 0) {
161
- this.exitWithInfo(`No files found matching pattern: ${pattern}`, formatOption, 0, { startTime, startDate, showTiming })
224
+ this.exitWithInfo(`No files found matching patterns: ${patterns.join(', ') || 'from config'}`, formatOption, 0, { startTime, startDate, showTiming })
225
+ }
226
+
227
+ let processingConfig = config
228
+
229
+ if (force && explicitFiles.length > 0) {
230
+ const modifiedConfig = Object.create(Object.getPrototypeOf(config))
231
+ Object.assign(modifiedConfig, config)
232
+
233
+ modifiedConfig.config = {
234
+ ...config.config,
235
+ linter: {
236
+ ...config.config.linter,
237
+ exclude: []
238
+ }
239
+ }
240
+
241
+ processingConfig = modifiedConfig
162
242
  }
163
243
 
164
- const context = {
244
+ const context: ProcessingContext = {
165
245
  projectPath: this.projectPath,
166
- pattern: pattern
246
+ pattern: patterns.join(' '),
247
+ fix,
248
+ ignoreDisableComments,
249
+ linterConfig,
250
+ config: processingConfig,
251
+ loadCustomRules
167
252
  }
168
253
 
169
254
  const results = await this.fileProcessor.processFiles(files, formatOption, context)