@herb-tools/linter 0.7.5 → 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 (394) hide show
  1. package/README.md +253 -13
  2. package/dist/herb-lint.js +26023 -3424
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +5759 -1583
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +5727 -1584
  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 +38 -33
  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 +107 -42
  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 +31 -2
  42. package/dist/src/rules/erb-comment-syntax.js.map +1 -1
  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 +69 -11
  60. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  61. package/dist/src/rules/erb-right-trim.js +26 -9
  62. package/dist/src/rules/erb-right-trim.js.map +1 -1
  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 +19 -3
  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} +44 -16
  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 +12 -5
  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 +12 -5
  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 +19 -3
  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 +12 -5
  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 +12 -5
  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 +19 -3
  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 +16 -2
  285. package/docs/rules/erb-no-case-node-children.md +50 -0
  286. package/docs/rules/erb-no-extra-newline.md +74 -0
  287. package/docs/rules/erb-no-extra-whitespace-inside-tags.md +39 -0
  288. package/docs/rules/{erb-requires-trailing-newline.md → erb-require-trailing-newline.md} +1 -1
  289. package/docs/rules/erb-right-trim.md +5 -10
  290. package/docs/rules/herb-disable-comment-malformed.md +45 -0
  291. package/docs/rules/herb-disable-comment-missing-rules.md +60 -0
  292. package/docs/rules/herb-disable-comment-no-duplicate-rules.md +49 -0
  293. package/docs/rules/herb-disable-comment-no-redundant-all.md +53 -0
  294. package/docs/rules/herb-disable-comment-unnecessary.md +44 -0
  295. package/docs/rules/herb-disable-comment-valid-rule-name.md +41 -0
  296. package/docs/rules/html-aria-attribute-must-be-valid.md +2 -5
  297. package/docs/rules/html-aria-label-is-well-formatted.md +1 -1
  298. package/docs/rules/html-attribute-double-quotes.md +2 -2
  299. package/docs/rules/html-attribute-equals-spacing.md +2 -2
  300. package/docs/rules/html-attribute-values-require-quotes.md +3 -3
  301. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +2 -2
  302. package/docs/rules/html-body-only-elements.md +99 -0
  303. package/docs/rules/html-boolean-attributes-no-value.md +2 -2
  304. package/docs/rules/html-head-only-elements.md +81 -0
  305. package/docs/rules/html-input-require-autocomplete.md +64 -0
  306. package/docs/rules/html-no-aria-hidden-on-focusable.md +2 -2
  307. package/docs/rules/html-no-duplicate-attributes.md +2 -2
  308. package/docs/rules/html-no-duplicate-meta-names.md +64 -0
  309. package/docs/rules/html-no-empty-attributes.md +3 -3
  310. package/docs/rules/html-no-empty-headings.md +4 -26
  311. package/docs/rules/html-no-positive-tab-index.md +1 -2
  312. package/docs/rules/html-no-self-closing.md +17 -2
  313. package/docs/rules/html-no-space-in-tag.md +66 -0
  314. package/docs/rules/html-no-title-attribute.md +2 -2
  315. package/docs/rules/html-no-underscores-in-attribute-names.md +2 -2
  316. package/docs/rules/html-tag-name-lowercase.md +2 -2
  317. package/package.json +13 -5
  318. package/src/cli/argument-parser.ts +46 -37
  319. package/src/cli/file-processor.ts +159 -28
  320. package/src/cli/formatters/detailed-formatter.ts +21 -3
  321. package/src/cli/formatters/github-actions-formatter.ts +17 -1
  322. package/src/cli/formatters/json-formatter.ts +9 -0
  323. package/src/cli/formatters/simple-formatter.ts +24 -8
  324. package/src/cli/output-manager.ts +23 -3
  325. package/src/cli/summary-reporter.ts +40 -3
  326. package/src/cli.ts +134 -51
  327. package/src/custom-rule-loader.ts +189 -0
  328. package/src/herb-disable-comment-utils.ts +175 -0
  329. package/src/index.ts +2 -0
  330. package/src/linter.ts +501 -36
  331. package/src/loader.ts +30 -0
  332. package/src/rules/erb-comment-syntax.ts +53 -10
  333. package/src/rules/erb-no-case-node-children.ts +68 -0
  334. package/src/rules/erb-no-empty-tags.ts +9 -3
  335. package/src/rules/erb-no-extra-newline.ts +91 -0
  336. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +147 -0
  337. package/src/rules/erb-no-output-control-flow.ts +9 -3
  338. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +9 -3
  339. package/src/rules/erb-prefer-image-tag-helper.ts +9 -3
  340. package/src/rules/erb-require-trailing-newline.ts +47 -0
  341. package/src/rules/erb-require-whitespace-inside-tags.ts +94 -16
  342. package/src/rules/erb-right-trim.ts +45 -22
  343. package/src/rules/herb-disable-comment-base.ts +76 -0
  344. package/src/rules/herb-disable-comment-malformed.ts +66 -0
  345. package/src/rules/herb-disable-comment-missing-rules.ts +41 -0
  346. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +46 -0
  347. package/src/rules/herb-disable-comment-no-redundant-all.ts +40 -0
  348. package/src/rules/herb-disable-comment-unnecessary.ts +103 -0
  349. package/src/rules/herb-disable-comment-valid-rule-name.ts +62 -0
  350. package/src/rules/html-anchor-require-href.ts +9 -3
  351. package/src/rules/html-aria-attribute-must-be-valid.ts +9 -3
  352. package/src/rules/html-aria-label-is-well-formatted.ts +9 -5
  353. package/src/rules/html-aria-level-must-be-valid.ts +9 -2
  354. package/src/rules/html-aria-role-heading-requires-level.ts +9 -3
  355. package/src/rules/html-aria-role-must-be-valid.ts +9 -3
  356. package/src/rules/html-attribute-double-quotes.ts +42 -8
  357. package/src/rules/html-attribute-equals-spacing.ts +31 -7
  358. package/src/rules/html-attribute-values-require-quotes.ts +56 -10
  359. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +9 -3
  360. package/src/rules/html-body-only-elements.ts +60 -0
  361. package/src/rules/html-boolean-attributes-no-value.ts +31 -6
  362. package/src/rules/html-head-only-elements.ts +65 -0
  363. package/src/rules/html-iframe-has-title.ts +9 -4
  364. package/src/rules/html-img-require-alt.ts +10 -4
  365. package/src/rules/html-input-require-autocomplete.ts +85 -0
  366. package/src/rules/html-navigation-has-label.ts +9 -3
  367. package/src/rules/html-no-aria-hidden-on-focusable.ts +9 -3
  368. package/src/rules/html-no-block-inside-inline.ts +9 -3
  369. package/src/rules/html-no-duplicate-attributes.ts +9 -3
  370. package/src/rules/html-no-duplicate-ids.ts +11 -7
  371. package/src/rules/html-no-duplicate-meta-names.ts +188 -0
  372. package/src/rules/html-no-empty-attributes.ts +58 -10
  373. package/src/rules/html-no-empty-headings.ts +10 -8
  374. package/src/rules/html-no-nested-links.ts +10 -4
  375. package/src/rules/html-no-positive-tab-index.ts +9 -3
  376. package/src/rules/html-no-self-closing.ts +69 -9
  377. package/src/rules/html-no-space-in-tag.ts +221 -0
  378. package/src/rules/html-no-title-attribute.ts +9 -3
  379. package/src/rules/html-no-underscores-in-attribute-names.ts +12 -4
  380. package/src/rules/html-tag-name-lowercase.ts +41 -10
  381. package/src/rules/index.ts +23 -3
  382. package/src/rules/parser-no-errors.ts +8 -1
  383. package/src/rules/rule-utils.ts +248 -42
  384. package/src/rules/svg-tag-name-capitalization.ts +39 -6
  385. package/src/{default-rules.ts → rules.ts} +51 -15
  386. package/src/types.ts +133 -15
  387. package/dist/src/default-rules.js.map +0 -1
  388. package/dist/src/rules/erb-requires-trailing-newline.js +0 -22
  389. package/dist/src/rules/erb-requires-trailing-newline.js.map +0 -1
  390. package/dist/types/default-rules.d.ts +0 -2
  391. package/dist/types/rules/erb-requires-trailing-newline.d.ts +0 -6
  392. package/dist/types/src/default-rules.d.ts +0 -2
  393. package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +0 -6
  394. package/src/rules/erb-requires-trailing-newline.ts +0 -29
@@ -1,10 +1,18 @@
1
- import { ParserRule } from "../types.js"
1
+ import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
2
2
  import { BaseRuleVisitor } from "./rule-utils.js"
3
3
 
4
- import type { LintOffense, LintContext } from "../types.js"
5
4
  import type { ParseResult, Token, ERBNode } from "@herb-tools/core"
5
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
6
6
 
7
- class RequireWhitespaceInsideTags extends BaseRuleVisitor {
7
+ interface ERBRequireWhitespaceAutofixContext extends BaseAutofixContext {
8
+ node: Mutable<ERBNode>
9
+ openTag: Token
10
+ closeTag: Token
11
+ content: string
12
+ fixType: "after-open" | "before-close" | "after-comment-equals"
13
+ }
14
+
15
+ class RequireWhitespaceInsideTags extends BaseRuleVisitor<ERBRequireWhitespaceAutofixContext> {
8
16
 
9
17
  visitERBNode(node: ERBNode): void {
10
18
  const openTag = node.tag_opening
@@ -18,25 +26,37 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
18
26
  const value = content.value
19
27
 
20
28
  if (openTag.value === "<%#") {
21
- this.checkCommentTagWhitespace(openTag, closeTag, value)
29
+ this.checkCommentTagWhitespace(node, openTag, closeTag, value)
22
30
  } else {
23
- this.checkOpenTagWhitespace(openTag, value)
24
- this.checkCloseTagWhitespace(closeTag, value)
31
+ this.checkOpenTagWhitespace(node, openTag, closeTag, value)
32
+ this.checkCloseTagWhitespace(node, openTag, closeTag, value)
25
33
  }
26
34
  }
27
35
 
28
- private checkCommentTagWhitespace(openTag: Token, closeTag: Token, content: string): void {
36
+ private checkCommentTagWhitespace(node: ERBNode, openTag: Token, closeTag: Token, content: string): void {
29
37
  if (!content.startsWith(" ") && !content.startsWith("\n") && !content.startsWith("=")) {
30
38
  this.addOffense(
31
39
  `Add whitespace after \`${openTag.value}\`.`,
32
40
  openTag.location,
33
- "error"
41
+ {
42
+ node,
43
+ openTag,
44
+ closeTag,
45
+ content,
46
+ fixType: "after-open"
47
+ }
34
48
  )
35
49
  } else if (content.startsWith("=") && content.length > 1 && !content[1].match(/\s/)) {
36
50
  this.addOffense(
37
51
  `Add whitespace after \`<%#=\`.`,
38
52
  openTag.location,
39
- "error"
53
+ {
54
+ node,
55
+ openTag,
56
+ closeTag,
57
+ content,
58
+ fixType: "after-comment-equals"
59
+ }
40
60
  )
41
61
  }
42
62
 
@@ -44,12 +64,18 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
44
64
  this.addOffense(
45
65
  `Add whitespace before \`${closeTag.value}\`.`,
46
66
  closeTag.location,
47
- "error"
67
+ {
68
+ node,
69
+ openTag,
70
+ closeTag,
71
+ content,
72
+ fixType: "before-close"
73
+ }
48
74
  )
49
75
  }
50
76
  }
51
77
 
52
- private checkOpenTagWhitespace(openTag: Token, content:string):void {
78
+ private checkOpenTagWhitespace(node: ERBNode, openTag: Token, closeTag: Token, content: string):void {
53
79
  if (content.startsWith(" ") || content.startsWith("\n")) {
54
80
  return
55
81
  }
@@ -57,11 +83,17 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
57
83
  this.addOffense(
58
84
  `Add whitespace after \`${openTag.value}\`.`,
59
85
  openTag.location,
60
- "error"
86
+ {
87
+ node,
88
+ openTag,
89
+ closeTag,
90
+ content,
91
+ fixType: "after-open"
92
+ }
61
93
  )
62
94
  }
63
95
 
64
- private checkCloseTagWhitespace(closeTag: Token, content:string):void {
96
+ private checkCloseTagWhitespace(node: ERBNode, openTag: Token, closeTag: Token, content: string):void {
65
97
  if (content.endsWith(" ") || content.endsWith("\n")) {
66
98
  return
67
99
  }
@@ -69,17 +101,63 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
69
101
  this.addOffense(
70
102
  `Add whitespace before \`${closeTag.value}\`.`,
71
103
  closeTag.location,
72
- "error"
104
+ {
105
+ node,
106
+ openTag,
107
+ closeTag,
108
+ content,
109
+ fixType: "before-close"
110
+ }
73
111
  )
74
112
  }
75
113
  }
76
114
 
77
- export class ERBRequireWhitespaceRule extends ParserRule {
115
+ export class ERBRequireWhitespaceRule extends ParserRule<ERBRequireWhitespaceAutofixContext> {
116
+ static autocorrectable = true
78
117
  name = "erb-require-whitespace-inside-tags"
79
118
 
80
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
119
+ get defaultConfig(): FullRuleConfig {
120
+ return {
121
+ enabled: true,
122
+ severity: "error"
123
+ }
124
+ }
125
+
126
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<ERBRequireWhitespaceAutofixContext>[] {
81
127
  const visitor = new RequireWhitespaceInsideTags(this.name, context)
128
+
82
129
  visitor.visit(result.value)
130
+
83
131
  return visitor.offenses
84
132
  }
133
+
134
+ autofix(offense: LintOffense<ERBRequireWhitespaceAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
135
+ if (!offense.autofixContext) return null
136
+
137
+ const { node, fixType } = offense.autofixContext
138
+
139
+ if (!node.content) return null
140
+
141
+ const content = node.content.value
142
+
143
+ if (fixType === "before-close") {
144
+ node.content.value = content + " "
145
+
146
+ return result
147
+ }
148
+
149
+ if (fixType === "after-open") {
150
+ node.content.value = " " + content
151
+
152
+ return result
153
+ }
154
+
155
+ if (fixType === "after-comment-equals" && content.startsWith("=")) {
156
+ node.content.value = "= " + content.substring(1)
157
+
158
+ return result
159
+ }
160
+
161
+ return null
162
+ }
85
163
  }
@@ -1,44 +1,67 @@
1
1
  import { BaseRuleVisitor } from "./rule-utils.js"
2
- import { ParserRule } from "../types.js"
3
- import { isERBOutputNode } from "@herb-tools/core"
2
+ import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
4
3
 
5
- import type { LintOffense, LintContext } from "../types.js"
4
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
6
5
  import type { ERBNode, ParseResult } from "@herb-tools/core"
7
6
 
8
- class ERBRightTrimVisitor extends BaseRuleVisitor {
7
+ interface ERBRightTrimAutofixContext extends BaseAutofixContext {
8
+ node: Mutable<ERBNode>
9
+ }
10
+
11
+ class ERBRightTrimVisitor extends BaseRuleVisitor<ERBRightTrimAutofixContext> {
9
12
  visitERBNode(node: ERBNode): void {
10
13
  if (!node.tag_closing) return
11
14
 
12
15
  const trimClosing = node.tag_closing.value
13
16
 
14
- if (trimClosing !== "=%>" && trimClosing !== "-%>") return
15
-
16
- if (!isERBOutputNode(node)) {
17
- this.addOffense(
18
- `Right-trimming with \`${trimClosing}\` has no effect on non-output ERB tags. Use \`%>\` instead`,
19
- node.tag_closing.location
20
- )
21
-
22
- return
23
- }
17
+ if (trimClosing !== "=%>") return
24
18
 
25
- if (trimClosing === "=%>") {
26
- this.addOffense(
27
- "Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines",
28
- node.tag_closing.location
29
- )
30
- }
19
+ this.addOffense(
20
+ "Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines.",
21
+ node.tag_closing.location,
22
+ { node }
23
+ )
31
24
  }
32
25
  }
33
26
 
34
- export class ERBRightTrimRule extends ParserRule {
27
+ export class ERBRightTrimRule extends ParserRule<ERBRightTrimAutofixContext> {
28
+ static autocorrectable = true
35
29
  name = "erb-right-trim"
36
30
 
37
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
31
+ get defaultConfig(): FullRuleConfig {
32
+ return {
33
+ enabled: true,
34
+ severity: "error"
35
+ }
36
+ }
37
+
38
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<ERBRightTrimAutofixContext>[] {
38
39
  const visitor = new ERBRightTrimVisitor(this.name, context)
39
40
 
40
41
  visitor.visit(result.value)
41
42
 
42
43
  return visitor.offenses
43
44
  }
45
+
46
+ autofix(offense: LintOffense<ERBRightTrimAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
47
+ if (!offense.autofixContext) return null
48
+
49
+ const { node } = offense.autofixContext
50
+
51
+ if (!node.tag_closing) return null
52
+
53
+ const closing = node.tag_closing
54
+
55
+ if (closing.value === "=%>") {
56
+ closing.value = "-%>"
57
+ return result
58
+ }
59
+
60
+ if (closing.value === "-%>") {
61
+ closing.value = "%>"
62
+ return result
63
+ }
64
+
65
+ return null
66
+ }
44
67
  }
@@ -0,0 +1,76 @@
1
+ import { BaseRuleVisitor } from "./rule-utils.js"
2
+ import { ERBContentNode, Location } from "@herb-tools/core"
3
+
4
+ import { parseHerbDisableContent } from "../herb-disable-comment-utils.js"
5
+
6
+ import type { LintContext } from "../types.js"
7
+ import type { HerbDisableComment, HerbDisableRuleName } from "../herb-disable-comment-utils.js"
8
+
9
+ /**
10
+ * Base visitor class for herb:disable comment validation rules.
11
+ * Handles common patterns like checking ERB comments and parsing herb:disable content.
12
+ */
13
+ export abstract class HerbDisableCommentBaseVisitor extends BaseRuleVisitor {
14
+ constructor(ruleName: string, context?: Partial<LintContext>) {
15
+ super(ruleName, context)
16
+ }
17
+
18
+ visitERBContentNode(node: ERBContentNode): void {
19
+ if (node.tag_opening?.value !== "<%#") return
20
+
21
+ const content = node.content?.value
22
+ if (!content) return
23
+
24
+ this.checkHerbDisableComment(node, content)
25
+ }
26
+
27
+ /**
28
+ * Override this method to implement rule-specific logic.
29
+ * This is called for every ERB comment node.
30
+ */
31
+ protected abstract checkHerbDisableComment(node: ERBContentNode, content: string): void
32
+
33
+ /**
34
+ * Helper to create a precise location for a specific rule name within the comment.
35
+ * Returns null if content location is not available.
36
+ */
37
+ protected createRuleNameLocation(node: ERBContentNode, ruleDetail: HerbDisableRuleName): Location | null {
38
+ const contentLocation = node.content?.location
39
+ if (!contentLocation) return null
40
+
41
+ const startLine = contentLocation.start.line
42
+ const startColumn = contentLocation.start.column + ruleDetail.offset
43
+
44
+ return Location.from(
45
+ startLine,
46
+ startColumn,
47
+ startLine,
48
+ startColumn + ruleDetail.length
49
+ )
50
+ }
51
+
52
+ /**
53
+ * Helper to add an offense with a fallback to node location if precise location unavailable.
54
+ */
55
+ protected addOffenseWithFallback(message: string, preciseLocation: Location | null, node: ERBContentNode): void {
56
+ this.addOffense(message, preciseLocation || node.location)
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Base visitor for rules that need to process parsed herb:disable comments.
62
+ * Only calls the abstract method if the content successfully parses as a herb:disable comment.
63
+ */
64
+ export abstract class HerbDisableCommentParsedVisitor extends HerbDisableCommentBaseVisitor {
65
+ protected checkHerbDisableComment(node: ERBContentNode, content: string): void {
66
+ const herbDisable = parseHerbDisableContent(content)
67
+ if (!herbDisable) return
68
+
69
+ this.checkParsedHerbDisable(node, content, herbDisable)
70
+ }
71
+
72
+ /**
73
+ * Override this method to implement rule-specific logic for parsed herb:disable comments.
74
+ */
75
+ protected abstract checkParsedHerbDisable(node: ERBContentNode, content: string, herbDisable: HerbDisableComment): void
76
+ }
@@ -0,0 +1,66 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { HerbDisableCommentBaseVisitor } from "./herb-disable-comment-base.js"
3
+ import { parseHerbDisableContent } from "../herb-disable-comment-utils.js"
4
+
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { ERBContentNode, ParseResult } from "@herb-tools/core"
7
+
8
+ class HerbDisableCommentMalformedVisitor extends HerbDisableCommentBaseVisitor {
9
+ protected checkHerbDisableComment(node: ERBContentNode, content: string): void {
10
+ const trimmed = content.trim()
11
+ const looksLikeHerbDisable = trimmed.startsWith("herb:disable")
12
+ if (!looksLikeHerbDisable) return
13
+
14
+ if (trimmed.length > "herb:disable".length) {
15
+ const charAfterPrefix = trimmed["herb:disable".length]
16
+
17
+ if (charAfterPrefix !== ' ' && charAfterPrefix !== '\t' && charAfterPrefix !== '\n') {
18
+ this.addOffense(
19
+ "`herb:disable` comment is missing a space after `herb:disable`. Add a space before the rule names.",
20
+ node.location,
21
+ )
22
+
23
+ return
24
+ }
25
+ }
26
+
27
+ const afterPrefix = trimmed.substring("herb:disable".length).trim()
28
+ if (afterPrefix.length === 0) return
29
+
30
+ const parsed = parseHerbDisableContent(content)
31
+ if (parsed !== null) return
32
+
33
+ let message = "`herb:disable` comment is malformed."
34
+
35
+ const rulesString = afterPrefix.trim()
36
+
37
+ if (rulesString.endsWith(',')) {
38
+ message = "`herb:disable` comment has a trailing comma. Remove the trailing comma."
39
+ } else if (rulesString.includes(',,') || rulesString.match(/,\s*,/)) {
40
+ message = "`herb:disable` comment has consecutive commas. Remove extra commas."
41
+ } else if (rulesString.startsWith(',')) {
42
+ message = "`herb:disable` comment starts with a comma. Remove the leading comma."
43
+ }
44
+
45
+ this.addOffense(message, node.location)
46
+ }
47
+ }
48
+
49
+ export class HerbDisableCommentMalformedRule extends ParserRule {
50
+ name = "herb-disable-comment-malformed"
51
+
52
+ get defaultConfig(): FullRuleConfig {
53
+ return {
54
+ enabled: true,
55
+ severity: "error"
56
+ }
57
+ }
58
+
59
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
60
+ const visitor = new HerbDisableCommentMalformedVisitor(this.name, context)
61
+
62
+ visitor.visit(result.value)
63
+
64
+ return visitor.offenses
65
+ }
66
+ }
@@ -0,0 +1,41 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { HerbDisableCommentBaseVisitor } from "./herb-disable-comment-base.js"
3
+
4
+ import { parseHerbDisableContent } from "../herb-disable-comment-utils.js"
5
+
6
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
7
+ import type { ERBContentNode, ParseResult } from "@herb-tools/core"
8
+
9
+ class HerbDisableCommentMissingRulesVisitor extends HerbDisableCommentBaseVisitor {
10
+ protected checkHerbDisableComment(node: ERBContentNode, content: string): void {
11
+ const herbDisable = parseHerbDisableContent(content)
12
+ if (herbDisable) return
13
+
14
+ const emptyFormat = /^\s*herb:disable\s*$/
15
+ if (!emptyFormat.test(content)) return
16
+
17
+ this.addOffense(
18
+ `\`herb:disable\` comment is missing rule names. Specify \`all\` or list specific rules to disable.`,
19
+ node.location,
20
+ )
21
+ }
22
+ }
23
+
24
+ export class HerbDisableCommentMissingRulesRule extends ParserRule {
25
+ name = "herb-disable-comment-missing-rules"
26
+
27
+ get defaultConfig(): FullRuleConfig {
28
+ return {
29
+ enabled: true,
30
+ severity: "error"
31
+ }
32
+ }
33
+
34
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
35
+ const visitor = new HerbDisableCommentMissingRulesVisitor(this.name, context)
36
+
37
+ visitor.visit(result.value)
38
+
39
+ return visitor.offenses
40
+ }
41
+ }
@@ -0,0 +1,46 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { HerbDisableCommentParsedVisitor } from "./herb-disable-comment-base.js"
3
+
4
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
+ import type { ERBContentNode, ParseResult } from "@herb-tools/core"
6
+ import type { HerbDisableComment } from "../herb-disable-comment-utils.js"
7
+
8
+ class HerbDisableCommentNoDuplicateRulesVisitor extends HerbDisableCommentParsedVisitor {
9
+ protected checkParsedHerbDisable(node: ERBContentNode, _content: string, herbDisable: HerbDisableComment): void {
10
+ const seenRules = new Map<string, number>()
11
+
12
+ herbDisable.ruleNameDetails.forEach((ruleDetail, index) => {
13
+ const firstIndex = seenRules.get(ruleDetail.name)
14
+
15
+ if (firstIndex !== undefined) {
16
+ const location = this.createRuleNameLocation(node, ruleDetail)
17
+ const message = `Duplicate rule \`${ruleDetail.name}\` in \`herb:disable\` comment. Remove the duplicate.`
18
+
19
+ this.addOffenseWithFallback(message, location, node)
20
+
21
+ return
22
+ }
23
+
24
+ seenRules.set(ruleDetail.name, index)
25
+ })
26
+ }
27
+ }
28
+
29
+ export class HerbDisableCommentNoDuplicateRulesRule extends ParserRule {
30
+ name = "herb-disable-comment-no-duplicate-rules"
31
+
32
+ get defaultConfig(): FullRuleConfig {
33
+ return {
34
+ enabled: true,
35
+ severity: "warning"
36
+ }
37
+ }
38
+
39
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
40
+ const visitor = new HerbDisableCommentNoDuplicateRulesVisitor(this.name, context)
41
+
42
+ visitor.visit(result.value)
43
+
44
+ return visitor.offenses
45
+ }
46
+ }
@@ -0,0 +1,40 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { HerbDisableCommentParsedVisitor } from "./herb-disable-comment-base.js"
3
+
4
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
+ import type { ERBContentNode, ParseResult } from "@herb-tools/core"
6
+ import type { HerbDisableComment } from "../herb-disable-comment-utils.js"
7
+
8
+ class HerbDisableCommentNoRedundantAllVisitor extends HerbDisableCommentParsedVisitor {
9
+ protected checkParsedHerbDisable(node: ERBContentNode, _content: string, herbDisable: HerbDisableComment): void {
10
+ if (!herbDisable.ruleNames.includes("all")) return
11
+ if (herbDisable.ruleNames.length <= 1) return
12
+
13
+ const allDetail = herbDisable.ruleNameDetails.find(detail => detail.name === "all")
14
+ if (!allDetail) return
15
+
16
+ const location = this.createRuleNameLocation(node, allDetail)
17
+ const message = `Using \`all\` with specific rules is redundant. Use \`herb:disable all\` by itself or list only specific rules.`
18
+
19
+ this.addOffenseWithFallback(message, location, node)
20
+ }
21
+ }
22
+
23
+ export class HerbDisableCommentNoRedundantAllRule extends ParserRule {
24
+ name = "herb-disable-comment-no-redundant-all"
25
+
26
+ get defaultConfig(): FullRuleConfig {
27
+ return {
28
+ enabled: true,
29
+ severity: "warning"
30
+ }
31
+ }
32
+
33
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
34
+ const visitor = new HerbDisableCommentNoRedundantAllVisitor(this.name, context)
35
+
36
+ visitor.visit(result.value)
37
+
38
+ return visitor.offenses
39
+ }
40
+ }
@@ -0,0 +1,103 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { HerbDisableCommentParsedVisitor } from "./herb-disable-comment-base.js"
3
+
4
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
+ import type { ERBContentNode, ParseResult } from "@herb-tools/core"
6
+ import type { HerbDisableComment } from "../herb-disable-comment-utils.js"
7
+
8
+ class HerbDisableCommentUnnecessaryVisitor extends HerbDisableCommentParsedVisitor {
9
+ private ignoredOffensesByLine: Map<number, Set<string>>
10
+ private validRuleNames: Set<string>
11
+
12
+ constructor(ruleName: string, ignoredOffensesByLine: Map<number, Set<string>>, validRuleNames: string[], context?: Partial<LintContext>) {
13
+ super(ruleName, context)
14
+
15
+ this.ignoredOffensesByLine = ignoredOffensesByLine
16
+ this.validRuleNames = new Set([...validRuleNames, "all"])
17
+ }
18
+
19
+ protected checkParsedHerbDisable(node: ERBContentNode, _content: string, herbDisable: HerbDisableComment): void {
20
+ const line = node.location.start.line
21
+ const usedRuleNames = this.ignoredOffensesByLine.get(line) || new Set<string>()
22
+
23
+ if (herbDisable.ruleNames.includes("all")) {
24
+ if (herbDisable.ruleNames.length > 1) return
25
+ if (usedRuleNames.size > 0) return
26
+
27
+ this.addOffense(
28
+ `No offenses to disable on this line. Remove the \`herb:disable all\` comment.`,
29
+ node.location,
30
+ )
31
+
32
+ return
33
+ }
34
+
35
+ const unnecessaryRules = herbDisable.ruleNameDetails.filter(
36
+ detail => this.validRuleNames.has(detail.name) && !usedRuleNames.has(detail.name)
37
+ )
38
+
39
+ if (unnecessaryRules.length === 0) return
40
+
41
+ const validRuleCount = herbDisable.ruleNames.filter(name => this.validRuleNames.has(name)).length
42
+
43
+ if (unnecessaryRules.length === validRuleCount) {
44
+ if (unnecessaryRules.length === 1) {
45
+ const ruleName = unnecessaryRules[0].name
46
+
47
+ this.addOffense(
48
+ `No offenses from \`${ruleName}\` on this line. Remove the \`herb:disable\` comment.`,
49
+ node.location,
50
+ )
51
+
52
+ return
53
+ }
54
+
55
+ const unnecessaryRuleNames = unnecessaryRules.map(rule => `\`${rule.name}\``).join(", ")
56
+
57
+ this.addOffense(
58
+ `No offenses from rules ${unnecessaryRuleNames} on this line. Remove them from the \`herb:disable\` comment.`,
59
+ node.location,
60
+ )
61
+
62
+ return
63
+ }
64
+
65
+ for (const unnecessaryRule of unnecessaryRules) {
66
+ const location = this.createRuleNameLocation(node, unnecessaryRule)
67
+ const message = `No offenses from \`${unnecessaryRule.name}\` on this line. Remove it from the \`herb:disable\` comment.`
68
+
69
+ this.addOffenseWithFallback(message, location, node)
70
+ }
71
+ }
72
+ }
73
+
74
+ export class HerbDisableCommentUnnecessaryRule extends ParserRule {
75
+ name = "herb-disable-comment-unnecessary"
76
+
77
+ get defaultConfig(): FullRuleConfig {
78
+ return {
79
+ enabled: true,
80
+ severity: "warning"
81
+ }
82
+ }
83
+
84
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
85
+ const validRuleNames = context?.validRuleNames
86
+ const ignoredOffensesByLine = context?.ignoredOffensesByLine
87
+
88
+ if (!validRuleNames) return []
89
+ if (validRuleNames.length === 0) return []
90
+ if (!ignoredOffensesByLine) return []
91
+
92
+ const visitor = new HerbDisableCommentUnnecessaryVisitor(
93
+ this.name,
94
+ ignoredOffensesByLine,
95
+ validRuleNames,
96
+ context
97
+ )
98
+
99
+ visitor.visit(result.value)
100
+
101
+ return visitor.offenses
102
+ }
103
+ }