@herb-tools/linter 0.8.9 → 0.9.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 (573) hide show
  1. package/README.md +5 -5
  2. package/dist/{src/cli → cli}/argument-parser.js +15 -2
  3. package/dist/cli/argument-parser.js.map +1 -0
  4. package/dist/{src/cli → cli}/file-processor.js +155 -9
  5. package/dist/cli/file-processor.js.map +1 -0
  6. package/dist/cli/file-url.js +6 -0
  7. package/dist/cli/file-url.js.map +1 -0
  8. package/dist/cli/formatters/base-formatter.js.map +1 -0
  9. package/dist/{src/cli → cli}/formatters/detailed-formatter.js +16 -19
  10. package/dist/cli/formatters/detailed-formatter.js.map +1 -0
  11. package/dist/cli/formatters/github-actions-formatter.js.map +1 -0
  12. package/dist/cli/formatters/index.js.map +1 -0
  13. package/dist/cli/formatters/json-formatter.js.map +1 -0
  14. package/dist/cli/formatters/simple-formatter.js +54 -0
  15. package/dist/cli/formatters/simple-formatter.js.map +1 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/cli/lint-worker.js +143 -0
  18. package/dist/cli/lint-worker.js.map +1 -0
  19. package/dist/cli/output-manager.js.map +1 -0
  20. package/dist/{src/cli → cli}/summary-reporter.js +13 -16
  21. package/dist/cli/summary-reporter.js.map +1 -0
  22. package/dist/{src/cli.js → cli.js} +5 -3
  23. package/dist/cli.js.map +1 -0
  24. package/dist/{src/custom-rule-loader.js → custom-rule-loader.js} +20 -4
  25. package/dist/custom-rule-loader.js.map +1 -0
  26. package/dist/herb-disable-comment-utils.js.map +1 -0
  27. package/dist/herb-lint.js +60648 -17513
  28. package/dist/herb-lint.js.map +1 -1
  29. package/dist/index.cjs +2621 -934
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +2554 -873
  32. package/dist/index.js.map +1 -1
  33. package/dist/lint-worker.js +71462 -0
  34. package/dist/lint-worker.js.map +1 -0
  35. package/dist/linter-ignore.js.map +1 -0
  36. package/dist/{src/linter.js → linter.js} +89 -74
  37. package/dist/linter.js.map +1 -0
  38. package/dist/loader.cjs +31206 -7834
  39. package/dist/loader.cjs.map +1 -1
  40. package/dist/loader.js +31168 -7802
  41. package/dist/loader.js.map +1 -1
  42. package/dist/parse-cache.js +30 -0
  43. package/dist/parse-cache.js.map +1 -0
  44. package/dist/rules/actionview-no-silent-helper.js +45 -0
  45. package/dist/rules/actionview-no-silent-helper.js.map +1 -0
  46. package/dist/{src/rules → rules}/erb-comment-syntax.js +2 -2
  47. package/dist/rules/erb-comment-syntax.js.map +1 -0
  48. package/dist/{src/rules → rules}/erb-no-case-node-children.js +2 -2
  49. package/dist/rules/erb-no-case-node-children.js.map +1 -0
  50. package/dist/rules/erb-no-conditional-html-element.js +38 -0
  51. package/dist/rules/erb-no-conditional-html-element.js.map +1 -0
  52. package/dist/rules/erb-no-conditional-open-tag.js +24 -0
  53. package/dist/rules/erb-no-conditional-open-tag.js.map +1 -0
  54. package/dist/rules/erb-no-duplicate-branch-elements.js +245 -0
  55. package/dist/rules/erb-no-duplicate-branch-elements.js.map +1 -0
  56. package/dist/{src/rules → rules}/erb-no-empty-tags.js +2 -2
  57. package/dist/rules/erb-no-empty-tags.js.map +1 -0
  58. package/dist/{src/rules → rules}/erb-no-extra-newline.js +4 -21
  59. package/dist/rules/erb-no-extra-newline.js.map +1 -0
  60. package/dist/{src/rules → rules}/erb-no-extra-whitespace-inside-tags.js +39 -13
  61. package/dist/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
  62. package/dist/rules/erb-no-inline-case-conditions.js +40 -0
  63. package/dist/rules/erb-no-inline-case-conditions.js.map +1 -0
  64. package/dist/rules/erb-no-instance-variables-in-partials.js +67 -0
  65. package/dist/rules/erb-no-instance-variables-in-partials.js.map +1 -0
  66. package/dist/rules/erb-no-interpolated-class-names.js +47 -0
  67. package/dist/rules/erb-no-interpolated-class-names.js.map +1 -0
  68. package/dist/rules/erb-no-javascript-tag-helper.js +34 -0
  69. package/dist/rules/erb-no-javascript-tag-helper.js.map +1 -0
  70. package/dist/{src/rules → rules}/erb-no-output-control-flow.js +9 -12
  71. package/dist/rules/erb-no-output-control-flow.js.map +1 -0
  72. package/dist/rules/erb-no-output-in-attribute-name.js +30 -0
  73. package/dist/rules/erb-no-output-in-attribute-name.js.map +1 -0
  74. package/dist/rules/erb-no-output-in-attribute-position.js +30 -0
  75. package/dist/rules/erb-no-output-in-attribute-position.js.map +1 -0
  76. package/dist/rules/erb-no-raw-output-in-attribute-value.js +35 -0
  77. package/dist/rules/erb-no-raw-output-in-attribute-value.js.map +1 -0
  78. package/dist/{src/rules → rules}/erb-no-silent-tag-in-attribute-name.js +2 -2
  79. package/dist/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
  80. package/dist/rules/erb-no-statement-in-script.js +58 -0
  81. package/dist/rules/erb-no-statement-in-script.js.map +1 -0
  82. package/dist/rules/erb-no-then-in-control-flow.js +45 -0
  83. package/dist/rules/erb-no-then-in-control-flow.js.map +1 -0
  84. package/dist/rules/erb-no-trailing-whitespace.js +138 -0
  85. package/dist/rules/erb-no-trailing-whitespace.js.map +1 -0
  86. package/dist/rules/erb-no-unsafe-js-attribute.js +36 -0
  87. package/dist/rules/erb-no-unsafe-js-attribute.js.map +1 -0
  88. package/dist/rules/erb-no-unsafe-raw.js +63 -0
  89. package/dist/rules/erb-no-unsafe-raw.js.map +1 -0
  90. package/dist/rules/erb-no-unsafe-script-interpolation.js +54 -0
  91. package/dist/rules/erb-no-unsafe-script-interpolation.js.map +1 -0
  92. package/dist/{src/rules → rules}/erb-prefer-image-tag-helper.js +5 -4
  93. package/dist/rules/erb-prefer-image-tag-helper.js.map +1 -0
  94. package/dist/{src/rules → rules}/erb-require-trailing-newline.js +2 -2
  95. package/dist/rules/erb-require-trailing-newline.js.map +1 -0
  96. package/dist/{src/rules → rules}/erb-require-whitespace-inside-tags.js +39 -15
  97. package/dist/rules/erb-require-whitespace-inside-tags.js.map +1 -0
  98. package/dist/{src/rules → rules}/erb-right-trim.js +2 -2
  99. package/dist/rules/erb-right-trim.js.map +1 -0
  100. package/dist/{src/rules → rules}/erb-strict-locals-comment-syntax.js +5 -5
  101. package/dist/rules/erb-strict-locals-comment-syntax.js.map +1 -0
  102. package/dist/{src/rules → rules}/erb-strict-locals-required.js +2 -2
  103. package/dist/rules/erb-strict-locals-required.js.map +1 -0
  104. package/dist/rules/file-utils.js.map +1 -0
  105. package/dist/rules/herb-disable-comment-base.js.map +1 -0
  106. package/dist/{src/rules → rules}/herb-disable-comment-malformed.js +2 -2
  107. package/dist/rules/herb-disable-comment-malformed.js.map +1 -0
  108. package/dist/{src/rules → rules}/herb-disable-comment-missing-rules.js +2 -2
  109. package/dist/rules/herb-disable-comment-missing-rules.js.map +1 -0
  110. package/dist/{src/rules → rules}/herb-disable-comment-no-duplicate-rules.js +2 -2
  111. package/dist/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
  112. package/dist/{src/rules → rules}/herb-disable-comment-no-redundant-all.js +2 -2
  113. package/dist/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
  114. package/dist/{src/rules → rules}/herb-disable-comment-unnecessary.js +2 -2
  115. package/dist/rules/herb-disable-comment-unnecessary.js.map +1 -0
  116. package/dist/{src/rules → rules}/herb-disable-comment-valid-rule-name.js +2 -2
  117. package/dist/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
  118. package/dist/rules/html-allowed-script-type.js +57 -0
  119. package/dist/rules/html-allowed-script-type.js.map +1 -0
  120. package/dist/rules/html-anchor-require-href.js +68 -0
  121. package/dist/rules/html-anchor-require-href.js.map +1 -0
  122. package/dist/{src/rules → rules}/html-aria-attribute-must-be-valid.js +3 -3
  123. package/dist/rules/html-aria-attribute-must-be-valid.js.map +1 -0
  124. package/dist/{src/rules → rules}/html-aria-label-is-well-formatted.js +3 -3
  125. package/dist/rules/html-aria-label-is-well-formatted.js.map +1 -0
  126. package/dist/{src/rules → rules}/html-aria-level-must-be-valid.js +3 -3
  127. package/dist/rules/html-aria-level-must-be-valid.js.map +1 -0
  128. package/dist/{src/rules → rules}/html-aria-role-heading-requires-level.js +5 -4
  129. package/dist/rules/html-aria-role-heading-requires-level.js.map +1 -0
  130. package/dist/{src/rules → rules}/html-aria-role-must-be-valid.js +3 -3
  131. package/dist/rules/html-aria-role-must-be-valid.js.map +1 -0
  132. package/dist/{src/rules → rules}/html-attribute-double-quotes.js +4 -4
  133. package/dist/rules/html-attribute-double-quotes.js.map +1 -0
  134. package/dist/{src/rules → rules}/html-attribute-equals-spacing.js +2 -2
  135. package/dist/rules/html-attribute-equals-spacing.js.map +1 -0
  136. package/dist/{src/rules → rules}/html-attribute-values-require-quotes.js +2 -2
  137. package/dist/rules/html-attribute-values-require-quotes.js.map +1 -0
  138. package/dist/{src/rules → rules}/html-avoid-both-disabled-and-aria-disabled.js +9 -9
  139. package/dist/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
  140. package/dist/{src/rules → rules}/html-body-only-elements.js +5 -4
  141. package/dist/rules/html-body-only-elements.js.map +1 -0
  142. package/dist/{src/rules → rules}/html-boolean-attributes-no-value.js +4 -3
  143. package/dist/rules/html-boolean-attributes-no-value.js.map +1 -0
  144. package/dist/rules/html-details-has-summary.js +52 -0
  145. package/dist/rules/html-details-has-summary.js.map +1 -0
  146. package/dist/{src/rules → rules}/html-head-only-elements.js +6 -5
  147. package/dist/rules/html-head-only-elements.js.map +1 -0
  148. package/dist/{src/rules → rules}/html-iframe-has-title.js +8 -11
  149. package/dist/rules/html-iframe-has-title.js.map +1 -0
  150. package/dist/{src/rules → rules}/html-img-require-alt.js +11 -5
  151. package/dist/rules/html-img-require-alt.js.map +1 -0
  152. package/dist/{src/rules → rules}/html-input-require-autocomplete.js +7 -10
  153. package/dist/rules/html-input-require-autocomplete.js.map +1 -0
  154. package/dist/{src/rules → rules}/html-navigation-has-label.js +6 -5
  155. package/dist/rules/html-navigation-has-label.js.map +1 -0
  156. package/dist/rules/html-no-abstract-roles.js +29 -0
  157. package/dist/rules/html-no-abstract-roles.js.map +1 -0
  158. package/dist/rules/html-no-aria-hidden-on-body.js +42 -0
  159. package/dist/rules/html-no-aria-hidden-on-body.js.map +1 -0
  160. package/dist/{src/rules → rules}/html-no-aria-hidden-on-focusable.js +6 -5
  161. package/dist/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
  162. package/dist/{src/rules → rules}/html-no-block-inside-inline.js +6 -9
  163. package/dist/rules/html-no-block-inside-inline.js.map +1 -0
  164. package/dist/{src/rules → rules}/html-no-duplicate-attributes.js +4 -3
  165. package/dist/rules/html-no-duplicate-attributes.js.map +1 -0
  166. package/dist/{src/rules → rules}/html-no-duplicate-ids.js +14 -11
  167. package/dist/rules/html-no-duplicate-ids.js.map +1 -0
  168. package/dist/{src/rules → rules}/html-no-duplicate-meta-names.js +22 -20
  169. package/dist/rules/html-no-duplicate-meta-names.js.map +1 -0
  170. package/dist/{src/rules → rules}/html-no-empty-attributes.js +2 -2
  171. package/dist/rules/html-no-empty-attributes.js.map +1 -0
  172. package/dist/rules/html-no-empty-headings.js +98 -0
  173. package/dist/rules/html-no-empty-headings.js.map +1 -0
  174. package/dist/{src/rules → rules}/html-no-nested-links.js +23 -15
  175. package/dist/rules/html-no-nested-links.js.map +1 -0
  176. package/dist/{src/rules → rules}/html-no-positive-tab-index.js +3 -3
  177. package/dist/rules/html-no-positive-tab-index.js.map +1 -0
  178. package/dist/{src/rules → rules}/html-no-self-closing.js +4 -4
  179. package/dist/rules/html-no-self-closing.js.map +1 -0
  180. package/dist/{src/rules → rules}/html-no-space-in-tag.js +4 -6
  181. package/dist/rules/html-no-space-in-tag.js.map +1 -0
  182. package/dist/{src/rules → rules}/html-no-title-attribute.js +6 -5
  183. package/dist/rules/html-no-title-attribute.js.map +1 -0
  184. package/dist/{src/rules → rules}/html-no-underscores-in-attribute-names.js +2 -2
  185. package/dist/rules/html-no-underscores-in-attribute-names.js.map +1 -0
  186. package/dist/rules/html-require-closing-tags.js +29 -0
  187. package/dist/rules/html-require-closing-tags.js.map +1 -0
  188. package/dist/{src/rules → rules}/html-tag-name-lowercase.js +13 -9
  189. package/dist/rules/html-tag-name-lowercase.js.map +1 -0
  190. package/dist/{src/rules → rules}/index.js +19 -0
  191. package/dist/rules/index.js.map +1 -0
  192. package/dist/{src/rules → rules}/parser-no-errors.js +3 -3
  193. package/dist/rules/parser-no-errors.js.map +1 -0
  194. package/dist/{src/rules → rules}/rule-utils.js +141 -219
  195. package/dist/rules/rule-utils.js.map +1 -0
  196. package/dist/rules/string-utils.js.map +1 -0
  197. package/dist/{src/rules → rules}/svg-tag-name-capitalization.js +7 -6
  198. package/dist/rules/svg-tag-name-capitalization.js.map +1 -0
  199. package/dist/rules/turbo-permanent-require-id.js +34 -0
  200. package/dist/rules/turbo-permanent-require-id.js.map +1 -0
  201. package/dist/{src/rules.js → rules.js} +56 -10
  202. package/dist/rules.js.map +1 -0
  203. package/dist/types/cli/argument-parser.d.ts +1 -0
  204. package/dist/types/cli/file-processor.d.ts +13 -0
  205. package/dist/types/cli/file-url.d.ts +1 -0
  206. package/dist/types/cli/index.d.ts +1 -0
  207. package/dist/types/cli/lint-worker.d.ts +34 -0
  208. package/dist/types/custom-rule-loader.d.ts +4 -0
  209. package/dist/types/index.d.ts +1 -0
  210. package/dist/types/linter.d.ts +13 -6
  211. package/dist/types/parse-cache.d.ts +9 -0
  212. package/dist/types/{src/rules/html-aria-level-must-be-valid.d.ts → rules/actionview-no-silent-helper.d.ts} +4 -3
  213. package/dist/types/rules/erb-comment-syntax.d.ts +1 -1
  214. package/dist/types/rules/erb-no-case-node-children.d.ts +1 -1
  215. package/dist/types/{src/rules/herb-disable-comment-malformed.d.ts → rules/erb-no-conditional-html-element.d.ts} +3 -3
  216. package/dist/types/{src/rules/erb-prefer-image-tag-helper.d.ts → rules/erb-no-conditional-open-tag.d.ts} +3 -3
  217. package/dist/types/rules/erb-no-duplicate-branch-elements.d.ts +17 -0
  218. package/dist/types/rules/erb-no-empty-tags.d.ts +1 -1
  219. package/dist/types/rules/erb-no-extra-newline.d.ts +1 -1
  220. package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +1 -1
  221. package/dist/types/{src/rules/html-no-duplicate-attributes.d.ts → rules/erb-no-inline-case-conditions.d.ts} +4 -3
  222. package/dist/types/rules/erb-no-instance-variables-in-partials.d.ts +10 -0
  223. package/dist/types/{src/rules/html-no-aria-hidden-on-focusable.d.ts → rules/erb-no-interpolated-class-names.d.ts} +2 -2
  224. package/dist/types/{src/rules/html-aria-attribute-must-be-valid.d.ts → rules/erb-no-javascript-tag-helper.d.ts} +2 -2
  225. package/dist/types/rules/erb-no-output-control-flow.d.ts +1 -1
  226. package/dist/types/{src/rules/erb-no-silent-tag-in-attribute-name.d.ts → rules/erb-no-output-in-attribute-name.d.ts} +2 -2
  227. package/dist/types/{src/rules/herb-disable-comment-missing-rules.d.ts → rules/erb-no-output-in-attribute-position.d.ts} +2 -2
  228. package/dist/types/{src/rules/erb-no-empty-tags.d.ts → rules/erb-no-raw-output-in-attribute-value.d.ts} +2 -2
  229. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +1 -1
  230. package/dist/types/{src/rules/html-navigation-has-label.d.ts → rules/erb-no-statement-in-script.d.ts} +2 -2
  231. package/dist/types/rules/erb-no-then-in-control-flow.d.ts +9 -0
  232. package/dist/types/rules/erb-no-trailing-whitespace.d.ts +19 -0
  233. package/dist/types/{src/rules/html-no-positive-tab-index.d.ts → rules/erb-no-unsafe-js-attribute.d.ts} +2 -2
  234. package/dist/types/{src/rules/erb-no-case-node-children.d.ts → rules/erb-no-unsafe-raw.d.ts} +2 -2
  235. package/dist/types/rules/erb-no-unsafe-script-interpolation.d.ts +8 -0
  236. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +1 -1
  237. package/dist/types/rules/erb-require-trailing-newline.d.ts +1 -1
  238. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +1 -1
  239. package/dist/types/rules/erb-right-trim.d.ts +1 -1
  240. package/dist/types/rules/erb-strict-locals-comment-syntax.d.ts +1 -1
  241. package/dist/types/rules/erb-strict-locals-required.d.ts +1 -1
  242. package/dist/types/rules/herb-disable-comment-malformed.d.ts +1 -1
  243. package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +1 -1
  244. package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +1 -1
  245. package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +1 -1
  246. package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +1 -1
  247. package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +1 -1
  248. package/dist/types/{src/rules/html-anchor-require-href.d.ts → rules/html-allowed-script-type.d.ts} +2 -2
  249. package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
  250. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +1 -1
  251. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +1 -1
  252. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +1 -1
  253. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +1 -1
  254. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +1 -1
  255. package/dist/types/rules/html-attribute-double-quotes.d.ts +1 -1
  256. package/dist/types/rules/html-attribute-equals-spacing.d.ts +1 -1
  257. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +1 -1
  258. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +1 -1
  259. package/dist/types/rules/html-body-only-elements.d.ts +1 -1
  260. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +1 -1
  261. package/dist/types/{src/rules/html-no-empty-attributes.d.ts → rules/html-details-has-summary.d.ts} +4 -3
  262. package/dist/types/rules/html-head-only-elements.d.ts +1 -1
  263. package/dist/types/rules/html-iframe-has-title.d.ts +1 -1
  264. package/dist/types/rules/html-img-require-alt.d.ts +1 -1
  265. package/dist/types/rules/html-input-require-autocomplete.d.ts +1 -1
  266. package/dist/types/rules/html-navigation-has-label.d.ts +1 -1
  267. package/dist/types/{src/rules/html-no-empty-headings.d.ts → rules/html-no-abstract-roles.d.ts} +2 -2
  268. package/dist/types/{src/rules/erb-no-output-control-flow.d.ts → rules/html-no-aria-hidden-on-body.d.ts} +3 -3
  269. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +1 -1
  270. package/dist/types/rules/html-no-block-inside-inline.d.ts +1 -1
  271. package/dist/types/rules/html-no-duplicate-attributes.d.ts +1 -1
  272. package/dist/types/rules/html-no-duplicate-ids.d.ts +1 -1
  273. package/dist/types/rules/html-no-duplicate-meta-names.d.ts +1 -1
  274. package/dist/types/rules/html-no-empty-attributes.d.ts +1 -1
  275. package/dist/types/rules/html-no-empty-headings.d.ts +1 -1
  276. package/dist/types/rules/html-no-nested-links.d.ts +1 -1
  277. package/dist/types/rules/html-no-positive-tab-index.d.ts +1 -1
  278. package/dist/types/rules/html-no-self-closing.d.ts +1 -1
  279. package/dist/types/rules/html-no-space-in-tag.d.ts +1 -1
  280. package/dist/types/rules/html-no-title-attribute.d.ts +1 -1
  281. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +1 -1
  282. package/dist/types/{src/rules/html-body-only-elements.d.ts → rules/html-require-closing-tags.d.ts} +4 -3
  283. package/dist/types/rules/html-tag-name-lowercase.d.ts +1 -1
  284. package/dist/types/rules/index.d.ts +19 -0
  285. package/dist/types/rules/parser-no-errors.d.ts +1 -1
  286. package/dist/types/rules/rule-utils.d.ts +35 -88
  287. package/dist/types/rules/svg-tag-name-capitalization.d.ts +1 -1
  288. package/dist/types/{src/rules/html-aria-role-must-be-valid.d.ts → rules/turbo-permanent-require-id.d.ts} +2 -2
  289. package/dist/types/types.d.ts +25 -7
  290. package/dist/types/urls.d.ts +1 -0
  291. package/dist/{src/types.js → types.js} +53 -0
  292. package/dist/types.js.map +1 -0
  293. package/dist/urls.js +5 -0
  294. package/dist/urls.js.map +1 -0
  295. package/docs/rules/README.md +23 -2
  296. package/docs/rules/actionview-no-silent-helper.md +57 -0
  297. package/docs/rules/erb-no-conditional-html-element.md +90 -0
  298. package/docs/rules/erb-no-conditional-open-tag.md +130 -0
  299. package/docs/rules/erb-no-duplicate-branch-elements.md +98 -0
  300. package/docs/rules/erb-no-inline-case-conditions.md +85 -0
  301. package/docs/rules/erb-no-instance-variables-in-partials.md +43 -0
  302. package/docs/rules/erb-no-interpolated-class-names.md +57 -0
  303. package/docs/rules/erb-no-javascript-tag-helper.md +33 -0
  304. package/docs/rules/erb-no-output-in-attribute-name.md +38 -0
  305. package/docs/rules/erb-no-output-in-attribute-position.md +60 -0
  306. package/docs/rules/erb-no-raw-output-in-attribute-value.md +37 -0
  307. package/docs/rules/erb-no-statement-in-script.md +68 -0
  308. package/docs/rules/erb-no-then-in-control-flow.md +86 -0
  309. package/docs/rules/erb-no-trailing-whitespace.md +69 -0
  310. package/docs/rules/erb-no-unsafe-js-attribute.md +41 -0
  311. package/docs/rules/erb-no-unsafe-raw.md +47 -0
  312. package/docs/rules/erb-no-unsafe-script-interpolation.md +73 -0
  313. package/docs/rules/html-allowed-script-type.md +59 -0
  314. package/docs/rules/html-anchor-require-href.md +19 -6
  315. package/docs/rules/html-details-has-summary.md +46 -0
  316. package/docs/rules/html-img-require-alt.md +5 -3
  317. package/docs/rules/html-no-abstract-roles.md +74 -0
  318. package/docs/rules/html-no-aria-hidden-on-body.md +44 -0
  319. package/docs/rules/html-require-closing-tags.md +142 -0
  320. package/docs/rules/parser-no-errors.md +4 -17
  321. package/docs/rules/turbo-permanent-require-id.md +41 -0
  322. package/package.json +12 -11
  323. package/src/cli/argument-parser.ts +20 -2
  324. package/src/cli/file-processor.ts +189 -10
  325. package/src/cli/file-url.ts +6 -0
  326. package/src/cli/formatters/detailed-formatter.ts +19 -21
  327. package/src/cli/formatters/simple-formatter.ts +23 -13
  328. package/src/cli/index.ts +2 -0
  329. package/src/cli/lint-worker.ts +208 -0
  330. package/src/cli/summary-reporter.ts +14 -15
  331. package/src/cli.ts +5 -3
  332. package/src/custom-rule-loader.ts +20 -5
  333. package/src/herb-disable-comment-utils.ts +0 -3
  334. package/src/index.ts +1 -0
  335. package/src/linter.ts +98 -79
  336. package/src/parse-cache.ts +39 -0
  337. package/src/rules/actionview-no-silent-helper.ts +58 -0
  338. package/src/rules/erb-comment-syntax.ts +2 -2
  339. package/src/rules/erb-no-case-node-children.ts +2 -2
  340. package/src/rules/erb-no-conditional-html-element.ts +53 -0
  341. package/src/rules/erb-no-conditional-open-tag.ts +37 -0
  342. package/src/rules/erb-no-duplicate-branch-elements.ts +320 -0
  343. package/src/rules/erb-no-empty-tags.ts +2 -2
  344. package/src/rules/erb-no-extra-newline.ts +5 -25
  345. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +45 -15
  346. package/src/rules/erb-no-inline-case-conditions.ts +54 -0
  347. package/src/rules/erb-no-instance-variables-in-partials.ts +101 -0
  348. package/src/rules/erb-no-interpolated-class-names.ts +65 -0
  349. package/src/rules/erb-no-javascript-tag-helper.ts +47 -0
  350. package/src/rules/erb-no-output-control-flow.ts +10 -10
  351. package/src/rules/erb-no-output-in-attribute-name.ts +39 -0
  352. package/src/rules/erb-no-output-in-attribute-position.ts +39 -0
  353. package/src/rules/erb-no-raw-output-in-attribute-value.ts +47 -0
  354. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +2 -2
  355. package/src/rules/erb-no-statement-in-script.ts +82 -0
  356. package/src/rules/erb-no-then-in-control-flow.ts +62 -0
  357. package/src/rules/erb-no-trailing-whitespace.ts +187 -0
  358. package/src/rules/erb-no-unsafe-js-attribute.ts +47 -0
  359. package/src/rules/erb-no-unsafe-raw.ts +83 -0
  360. package/src/rules/erb-no-unsafe-script-interpolation.ts +76 -0
  361. package/src/rules/erb-prefer-image-tag-helper.ts +5 -4
  362. package/src/rules/erb-require-trailing-newline.ts +2 -2
  363. package/src/rules/erb-require-whitespace-inside-tags.ts +42 -18
  364. package/src/rules/erb-right-trim.ts +2 -2
  365. package/src/rules/erb-strict-locals-comment-syntax.ts +5 -5
  366. package/src/rules/erb-strict-locals-required.ts +2 -2
  367. package/src/rules/herb-disable-comment-malformed.ts +2 -2
  368. package/src/rules/herb-disable-comment-missing-rules.ts +2 -2
  369. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +2 -2
  370. package/src/rules/herb-disable-comment-no-redundant-all.ts +2 -2
  371. package/src/rules/herb-disable-comment-unnecessary.ts +2 -2
  372. package/src/rules/herb-disable-comment-valid-rule-name.ts +2 -2
  373. package/src/rules/html-allowed-script-type.ts +84 -0
  374. package/src/rules/html-anchor-require-href.ts +73 -11
  375. package/src/rules/html-aria-attribute-must-be-valid.ts +3 -3
  376. package/src/rules/html-aria-label-is-well-formatted.ts +3 -3
  377. package/src/rules/html-aria-level-must-be-valid.ts +3 -3
  378. package/src/rules/html-aria-role-heading-requires-level.ts +5 -4
  379. package/src/rules/html-aria-role-must-be-valid.ts +3 -3
  380. package/src/rules/html-attribute-double-quotes.ts +4 -4
  381. package/src/rules/html-attribute-equals-spacing.ts +2 -2
  382. package/src/rules/html-attribute-values-require-quotes.ts +2 -2
  383. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +10 -11
  384. package/src/rules/html-body-only-elements.ts +5 -4
  385. package/src/rules/html-boolean-attributes-no-value.ts +4 -3
  386. package/src/rules/html-details-has-summary.ts +69 -0
  387. package/src/rules/html-head-only-elements.ts +6 -5
  388. package/src/rules/html-iframe-has-title.ts +8 -11
  389. package/src/rules/html-img-require-alt.ts +16 -5
  390. package/src/rules/html-input-require-autocomplete.ts +7 -10
  391. package/src/rules/html-navigation-has-label.ts +6 -5
  392. package/src/rules/html-no-abstract-roles.ts +40 -0
  393. package/src/rules/html-no-aria-hidden-on-body.ts +58 -0
  394. package/src/rules/html-no-aria-hidden-on-focusable.ts +6 -5
  395. package/src/rules/html-no-block-inside-inline.ts +7 -13
  396. package/src/rules/html-no-duplicate-attributes.ts +4 -3
  397. package/src/rules/html-no-duplicate-ids.ts +16 -13
  398. package/src/rules/html-no-duplicate-meta-names.ts +20 -19
  399. package/src/rules/html-no-empty-attributes.ts +2 -2
  400. package/src/rules/html-no-empty-headings.ts +44 -58
  401. package/src/rules/html-no-nested-links.ts +25 -16
  402. package/src/rules/html-no-positive-tab-index.ts +3 -3
  403. package/src/rules/html-no-self-closing.ts +5 -5
  404. package/src/rules/html-no-space-in-tag.ts +5 -8
  405. package/src/rules/html-no-title-attribute.ts +6 -5
  406. package/src/rules/html-no-underscores-in-attribute-names.ts +2 -2
  407. package/src/rules/html-require-closing-tags.ts +41 -0
  408. package/src/rules/html-tag-name-lowercase.ts +14 -9
  409. package/src/rules/index.ts +19 -0
  410. package/src/rules/parser-no-errors.ts +3 -3
  411. package/src/rules/rule-utils.ts +162 -279
  412. package/src/rules/svg-tag-name-capitalization.ts +10 -10
  413. package/src/rules/turbo-permanent-require-id.ts +49 -0
  414. package/src/rules.ts +60 -10
  415. package/src/types.ts +76 -7
  416. package/src/urls.ts +5 -0
  417. package/dist/package.json +0 -65
  418. package/dist/src/cli/argument-parser.js.map +0 -1
  419. package/dist/src/cli/file-processor.js.map +0 -1
  420. package/dist/src/cli/formatters/base-formatter.js.map +0 -1
  421. package/dist/src/cli/formatters/detailed-formatter.js.map +0 -1
  422. package/dist/src/cli/formatters/github-actions-formatter.js.map +0 -1
  423. package/dist/src/cli/formatters/index.js.map +0 -1
  424. package/dist/src/cli/formatters/json-formatter.js.map +0 -1
  425. package/dist/src/cli/formatters/simple-formatter.js +0 -44
  426. package/dist/src/cli/formatters/simple-formatter.js.map +0 -1
  427. package/dist/src/cli/index.js.map +0 -1
  428. package/dist/src/cli/output-manager.js.map +0 -1
  429. package/dist/src/cli/summary-reporter.js.map +0 -1
  430. package/dist/src/cli.js.map +0 -1
  431. package/dist/src/custom-rule-loader.js.map +0 -1
  432. package/dist/src/herb-disable-comment-utils.js.map +0 -1
  433. package/dist/src/herb-lint.js +0 -5
  434. package/dist/src/herb-lint.js.map +0 -1
  435. package/dist/src/index.js +0 -5
  436. package/dist/src/index.js.map +0 -1
  437. package/dist/src/linter-ignore.js.map +0 -1
  438. package/dist/src/linter.js.map +0 -1
  439. package/dist/src/loader.js +0 -17
  440. package/dist/src/loader.js.map +0 -1
  441. package/dist/src/rules/erb-comment-syntax.js.map +0 -1
  442. package/dist/src/rules/erb-no-case-node-children.js.map +0 -1
  443. package/dist/src/rules/erb-no-empty-tags.js.map +0 -1
  444. package/dist/src/rules/erb-no-extra-newline.js.map +0 -1
  445. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +0 -1
  446. package/dist/src/rules/erb-no-output-control-flow.js.map +0 -1
  447. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +0 -1
  448. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +0 -1
  449. package/dist/src/rules/erb-require-trailing-newline.js.map +0 -1
  450. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +0 -1
  451. package/dist/src/rules/erb-right-trim.js.map +0 -1
  452. package/dist/src/rules/erb-strict-locals-comment-syntax.js.map +0 -1
  453. package/dist/src/rules/erb-strict-locals-required.js.map +0 -1
  454. package/dist/src/rules/file-utils.js.map +0 -1
  455. package/dist/src/rules/herb-disable-comment-base.js.map +0 -1
  456. package/dist/src/rules/herb-disable-comment-malformed.js.map +0 -1
  457. package/dist/src/rules/herb-disable-comment-missing-rules.js.map +0 -1
  458. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +0 -1
  459. package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +0 -1
  460. package/dist/src/rules/herb-disable-comment-unnecessary.js.map +0 -1
  461. package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +0 -1
  462. package/dist/src/rules/html-anchor-require-href.js +0 -32
  463. package/dist/src/rules/html-anchor-require-href.js.map +0 -1
  464. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +0 -1
  465. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +0 -1
  466. package/dist/src/rules/html-aria-level-must-be-valid.js.map +0 -1
  467. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +0 -1
  468. package/dist/src/rules/html-aria-role-must-be-valid.js.map +0 -1
  469. package/dist/src/rules/html-attribute-double-quotes.js.map +0 -1
  470. package/dist/src/rules/html-attribute-equals-spacing.js.map +0 -1
  471. package/dist/src/rules/html-attribute-values-require-quotes.js.map +0 -1
  472. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +0 -1
  473. package/dist/src/rules/html-body-only-elements.js.map +0 -1
  474. package/dist/src/rules/html-boolean-attributes-no-value.js.map +0 -1
  475. package/dist/src/rules/html-head-only-elements.js.map +0 -1
  476. package/dist/src/rules/html-iframe-has-title.js.map +0 -1
  477. package/dist/src/rules/html-img-require-alt.js.map +0 -1
  478. package/dist/src/rules/html-input-require-autocomplete.js.map +0 -1
  479. package/dist/src/rules/html-navigation-has-label.js.map +0 -1
  480. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +0 -1
  481. package/dist/src/rules/html-no-block-inside-inline.js.map +0 -1
  482. package/dist/src/rules/html-no-duplicate-attributes.js.map +0 -1
  483. package/dist/src/rules/html-no-duplicate-ids.js.map +0 -1
  484. package/dist/src/rules/html-no-duplicate-meta-names.js.map +0 -1
  485. package/dist/src/rules/html-no-empty-attributes.js.map +0 -1
  486. package/dist/src/rules/html-no-empty-headings.js +0 -115
  487. package/dist/src/rules/html-no-empty-headings.js.map +0 -1
  488. package/dist/src/rules/html-no-nested-links.js.map +0 -1
  489. package/dist/src/rules/html-no-positive-tab-index.js.map +0 -1
  490. package/dist/src/rules/html-no-self-closing.js.map +0 -1
  491. package/dist/src/rules/html-no-space-in-tag.js.map +0 -1
  492. package/dist/src/rules/html-no-title-attribute.js.map +0 -1
  493. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +0 -1
  494. package/dist/src/rules/html-tag-name-lowercase.js.map +0 -1
  495. package/dist/src/rules/index.js.map +0 -1
  496. package/dist/src/rules/parser-no-errors.js.map +0 -1
  497. package/dist/src/rules/rule-utils.js.map +0 -1
  498. package/dist/src/rules/string-utils.js.map +0 -1
  499. package/dist/src/rules/svg-tag-name-capitalization.js.map +0 -1
  500. package/dist/src/rules.js.map +0 -1
  501. package/dist/src/types.js.map +0 -1
  502. package/dist/tsconfig.tsbuildinfo +0 -1
  503. package/dist/types/src/cli/argument-parser.d.ts +0 -25
  504. package/dist/types/src/cli/file-processor.d.ts +0 -43
  505. package/dist/types/src/cli/formatters/base-formatter.d.ts +0 -6
  506. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +0 -13
  507. package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +0 -17
  508. package/dist/types/src/cli/formatters/index.d.ts +0 -5
  509. package/dist/types/src/cli/formatters/json-formatter.d.ts +0 -48
  510. package/dist/types/src/cli/formatters/simple-formatter.d.ts +0 -8
  511. package/dist/types/src/cli/index.d.ts +0 -5
  512. package/dist/types/src/cli/output-manager.d.ts +0 -32
  513. package/dist/types/src/cli/summary-reporter.d.ts +0 -28
  514. package/dist/types/src/cli.d.ts +0 -28
  515. package/dist/types/src/custom-rule-loader.d.ts +0 -62
  516. package/dist/types/src/herb-disable-comment-utils.d.ts +0 -69
  517. package/dist/types/src/herb-lint.d.ts +0 -2
  518. package/dist/types/src/index.d.ts +0 -4
  519. package/dist/types/src/linter-ignore.d.ts +0 -12
  520. package/dist/types/src/linter.d.ts +0 -133
  521. package/dist/types/src/loader.d.ts +0 -20
  522. package/dist/types/src/rules/erb-comment-syntax.d.ts +0 -14
  523. package/dist/types/src/rules/erb-no-extra-newline.d.ts +0 -14
  524. package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +0 -18
  525. package/dist/types/src/rules/erb-require-trailing-newline.d.ts +0 -9
  526. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +0 -18
  527. package/dist/types/src/rules/erb-right-trim.d.ts +0 -14
  528. package/dist/types/src/rules/erb-strict-locals-comment-syntax.d.ts +0 -9
  529. package/dist/types/src/rules/erb-strict-locals-required.d.ts +0 -9
  530. package/dist/types/src/rules/file-utils.d.ts +0 -13
  531. package/dist/types/src/rules/herb-disable-comment-base.d.ts +0 -37
  532. package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +0 -8
  533. package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +0 -8
  534. package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +0 -8
  535. package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +0 -8
  536. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +0 -8
  537. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +0 -8
  538. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +0 -15
  539. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +0 -14
  540. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +0 -15
  541. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +0 -8
  542. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +0 -14
  543. package/dist/types/src/rules/html-head-only-elements.d.ts +0 -9
  544. package/dist/types/src/rules/html-iframe-has-title.d.ts +0 -8
  545. package/dist/types/src/rules/html-img-require-alt.d.ts +0 -8
  546. package/dist/types/src/rules/html-input-require-autocomplete.d.ts +0 -8
  547. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +0 -8
  548. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +0 -8
  549. package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +0 -9
  550. package/dist/types/src/rules/html-no-nested-links.d.ts +0 -8
  551. package/dist/types/src/rules/html-no-self-closing.d.ts +0 -16
  552. package/dist/types/src/rules/html-no-space-in-tag.d.ts +0 -16
  553. package/dist/types/src/rules/html-no-title-attribute.d.ts +0 -8
  554. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +0 -8
  555. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +0 -18
  556. package/dist/types/src/rules/index.d.ts +0 -54
  557. package/dist/types/src/rules/parser-no-errors.d.ts +0 -9
  558. package/dist/types/src/rules/rule-utils.d.ts +0 -351
  559. package/dist/types/src/rules/string-utils.d.ts +0 -15
  560. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +0 -16
  561. package/dist/types/src/rules.d.ts +0 -2
  562. package/dist/types/src/types.d.ts +0 -190
  563. /package/dist/{src/cli → cli}/formatters/base-formatter.js +0 -0
  564. /package/dist/{src/cli → cli}/formatters/github-actions-formatter.js +0 -0
  565. /package/dist/{src/cli → cli}/formatters/index.js +0 -0
  566. /package/dist/{src/cli → cli}/formatters/json-formatter.js +0 -0
  567. /package/dist/{src/cli → cli}/index.js +0 -0
  568. /package/dist/{src/cli → cli}/output-manager.js +0 -0
  569. /package/dist/{src/herb-disable-comment-utils.js → herb-disable-comment-utils.js} +0 -0
  570. /package/dist/{src/linter-ignore.js → linter-ignore.js} +0 -0
  571. /package/dist/{src/rules → rules}/file-utils.js +0 -0
  572. /package/dist/{src/rules → rules}/herb-disable-comment-base.js +0 -0
  573. /package/dist/{src/rules → rules}/string-utils.js +0 -0
package/src/linter.ts CHANGED
@@ -1,16 +1,19 @@
1
- import { Location } from "@herb-tools/core"
2
- import { IdentityPrinter } from "@herb-tools/printer"
3
1
  import picomatch from "picomatch"
4
2
 
3
+ import { Location } from "@herb-tools/core"
4
+ import { IdentityPrinter, IndentPrinter } from "@herb-tools/printer"
5
+
5
6
  import { rules } from "./rules.js"
6
7
  import { findNodeByLocation } from "./rules/rule-utils.js"
7
8
  import { parseHerbDisableLine } from "./herb-disable-comment-utils.js"
8
9
  import { hasLinterIgnoreDirective } from "./linter-ignore.js"
10
+ import { ParseCache } from "./parse-cache.js"
9
11
 
10
12
  import { ParserNoErrorsRule } from "./rules/parser-no-errors.js"
13
+
11
14
  import { DEFAULT_RULE_CONFIG } from "./types.js"
12
15
 
13
- import type { RuleClass, Rule, ParserRule, LexerRule, SourceRule, LintResult, LintOffense, UnboundLintOffense, LintContext, AutofixResult } from "./types.js"
16
+ import type { RuleClass, ParserRuleClass, LexerRuleClass, SourceRuleClass, Rule, ParserRule, LexerRule, SourceRule, LintResult, LintOffense, UnboundLintOffense, LintContext, AutofixResult } from "./types.js"
14
17
  import type { ParseResult, LexResult, HerbBackend } from "@herb-tools/core"
15
18
  import type { RuleConfig, Config } from "@herb-tools/config"
16
19
 
@@ -45,9 +48,10 @@ export interface LinterOptions {
45
48
  }
46
49
 
47
50
  export class Linter {
48
- protected rules: RuleClass[]
51
+ public rules: RuleClass[]
49
52
  protected allAvailableRules: RuleClass[]
50
53
  protected herb: HerbBackend
54
+ protected parseCache: ParseCache
51
55
  protected offenses: LintOffense[]
52
56
  protected config?: Config
53
57
 
@@ -81,6 +85,7 @@ export class Linter {
81
85
  */
82
86
  constructor(herb: HerbBackend, rules?: RuleClass[], config?: Config, allAvailableRules?: RuleClass[]) {
83
87
  this.herb = herb
88
+ this.parseCache = new ParseCache(herb)
84
89
  this.config = config
85
90
  this.rules = rules !== undefined ? rules : this.getDefaultRules()
86
91
  this.allAvailableRules = allAvailableRules !== undefined ? allAvailableRules : this.rules
@@ -104,10 +109,9 @@ export class Linter {
104
109
  ): RuleClass[] {
105
110
  return allRules.filter(ruleClass => {
106
111
  const instance = new ruleClass()
107
- const ruleName = instance.name
108
112
 
109
113
  const defaultEnabled = instance.defaultConfig?.enabled ?? DEFAULT_RULE_CONFIG.enabled
110
- const userRuleConfig = userRulesConfig?.[ruleName]
114
+ const userRuleConfig = userRulesConfig?.[ruleClass.ruleName]
111
115
 
112
116
  if (userRuleConfig !== undefined) {
113
117
  return userRuleConfig.enabled !== false
@@ -156,18 +160,29 @@ export class Linter {
156
160
  return this.rules.length
157
161
  }
158
162
 
163
+ protected findRuleClass(ruleName: string): RuleClass | undefined {
164
+ return this.rules.find(ruleClass => ruleClass.ruleName === ruleName)
165
+ }
166
+
167
+ /**
168
+ * Type guard to check if a rule class is a LexerRule class
169
+ */
170
+ protected isLexerRuleClass(ruleClass: RuleClass): ruleClass is LexerRuleClass {
171
+ return ruleClass.type === "lexer"
172
+ }
173
+
159
174
  /**
160
- * Type guard to check if a rule is a LexerRule
175
+ * Type guard to check if a rule class is a SourceRule class
161
176
  */
162
- protected isLexerRule(rule: Rule): rule is LexerRule {
163
- return (rule.constructor as any).type === "lexer"
177
+ protected isSourceRuleClass(ruleClass: RuleClass): ruleClass is SourceRuleClass {
178
+ return ruleClass.type === "source"
164
179
  }
165
180
 
166
181
  /**
167
- * Type guard to check if a rule is a SourceRule
182
+ * Type guard to check if a rule class is a ParserRule class
168
183
  */
169
- protected isSourceRule(rule: Rule): rule is SourceRule {
170
- return (rule.constructor as any).type === "source"
184
+ protected isParserRuleClass(ruleClass: RuleClass): ruleClass is ParserRuleClass {
185
+ return ruleClass.type === "parser" || ruleClass.type === undefined
171
186
  }
172
187
 
173
188
  /**
@@ -175,19 +190,22 @@ export class Linter {
175
190
  * Handles rule type checking (Lexer/Parser/Source) and isEnabled checks.
176
191
  */
177
192
  private executeRule(
193
+ ruleClass: RuleClass,
178
194
  rule: Rule,
179
195
  parseResult: ParseResult,
180
196
  lexResult: LexResult,
181
197
  source: string,
182
198
  context?: Partial<LintContext>
183
199
  ): UnboundLintOffense[] {
200
+ const ruleName = rule.ruleName
201
+
184
202
  if (this.config && context?.fileName) {
185
- if (!this.config.isRuleEnabledForPath(rule.name, context.fileName)) {
203
+ if (!this.config.isRuleEnabledForPath(ruleName, context.fileName)) {
186
204
  return []
187
205
  }
188
206
  }
189
207
 
190
- if (context?.fileName && !this.config?.linter?.rules?.[rule.name]?.exclude) {
208
+ if (context?.fileName && !this.config?.linter?.rules?.[ruleName]?.exclude) {
191
209
  const defaultExclude = rule.defaultConfig?.exclude ?? DEFAULT_RULE_CONFIG.exclude
192
210
 
193
211
  if (defaultExclude && defaultExclude.length > 0) {
@@ -202,34 +220,40 @@ export class Linter {
202
220
  let isEnabled = true
203
221
  let ruleOffenses: UnboundLintOffense[]
204
222
 
205
- if (this.isLexerRule(rule)) {
206
- if (rule.isEnabled) {
207
- isEnabled = rule.isEnabled(lexResult, context)
223
+ if (this.isLexerRuleClass(ruleClass)) {
224
+ const lexerRule = rule as LexerRule
225
+
226
+ if (lexerRule.isEnabled) {
227
+ isEnabled = lexerRule.isEnabled(lexResult, context)
208
228
  }
209
229
 
210
230
  if (isEnabled) {
211
- ruleOffenses = (rule as LexerRule).check(lexResult, context)
231
+ ruleOffenses = lexerRule.check(lexResult, context)
212
232
  } else {
213
233
  ruleOffenses = []
214
234
  }
215
235
 
216
- } else if (this.isSourceRule(rule)) {
217
- if (rule.isEnabled) {
218
- isEnabled = rule.isEnabled(source, context)
236
+ } else if (this.isSourceRuleClass(ruleClass)) {
237
+ const sourceRule = rule as SourceRule
238
+
239
+ if (sourceRule.isEnabled) {
240
+ isEnabled = sourceRule.isEnabled(source, context)
219
241
  }
220
242
 
221
243
  if (isEnabled) {
222
- ruleOffenses = (rule as SourceRule).check(source, context)
244
+ ruleOffenses = sourceRule.check(source, context)
223
245
  } else {
224
246
  ruleOffenses = []
225
247
  }
226
248
  } else {
227
- if (rule.isEnabled) {
228
- isEnabled = rule.isEnabled(parseResult, context)
249
+ const parserRule = rule as ParserRule
250
+
251
+ if (parserRule.isEnabled) {
252
+ isEnabled = parserRule.isEnabled(parseResult, context)
229
253
  }
230
254
 
231
255
  if (isEnabled) {
232
- ruleOffenses = (rule as ParserRule).check(parseResult, context)
256
+ ruleOffenses = parserRule.check(parseResult, context)
233
257
  } else {
234
258
  ruleOffenses = []
235
259
  }
@@ -303,7 +327,7 @@ export class Linter {
303
327
  let ignoredCount = 0
304
328
  let wouldBeIgnoredCount = 0
305
329
 
306
- const parseResult = this.herb.parse(source, { track_whitespace: true })
330
+ const parseResult = this.parseCache.get(source)
307
331
 
308
332
  // Check for file-level ignore directive using visitor
309
333
  if (hasLinterIgnoreDirective(parseResult)) {
@@ -323,22 +347,13 @@ export class Linter {
323
347
  const herbDisableCache = new Map<number, string[]>()
324
348
 
325
349
  if (hasParserErrors) {
326
- const hasParserRule = this.rules.find(RuleClass => (new RuleClass()).name === "parser-no-errors")
350
+ const hasParserRule = this.findRuleClass("parser-no-errors")
327
351
 
328
352
  if (hasParserRule) {
329
353
  const rule = new ParserNoErrorsRule()
330
354
  const offenses = rule.check(parseResult)
331
355
  this.offenses.push(...offenses)
332
356
  }
333
-
334
- return {
335
- offenses: this.offenses,
336
- errors: this.offenses.filter(o => o.severity === "error").length,
337
- warnings: this.offenses.filter(o => o.severity === "warning").length,
338
- info: this.offenses.filter(o => o.severity === "info").length,
339
- hints: this.offenses.filter(o => o.severity === "hint").length,
340
- ignored: 0
341
- }
342
357
  }
343
358
 
344
359
  for (let i = 0; i < sourceLines.length; i++) {
@@ -352,24 +367,31 @@ export class Linter {
352
367
 
353
368
  context = {
354
369
  ...context,
355
- validRuleNames: this.getAvailableRules().map(RuleClass => new RuleClass().name),
370
+ validRuleNames: this.getAvailableRules().map(ruleClass => ruleClass.ruleName),
356
371
  ignoredOffensesByLine
357
372
  }
358
373
 
359
- const regularRules = this.rules.filter(RuleClass => {
360
- const rule = new RuleClass()
374
+ const regularRules = this.rules.filter(ruleClass => ruleClass.ruleName !== "herb-disable-comment-unnecessary")
361
375
 
362
- return rule.name !== "herb-disable-comment-unnecessary"
363
- })
376
+ for (const ruleClass of regularRules) {
377
+ const rule = new ruleClass()
378
+ const parserOptions = this.isParserRuleClass(ruleClass) ? (rule as ParserRule).parserOptions : {}
379
+ const parseResult = this.parseCache.get(source, parserOptions)
380
+
381
+ // Skip parser rules whose parse result has errors (parser-no-errors handled above)
382
+ // Skip lexer/source rules when the default parse has errors
383
+ if (this.isParserRuleClass(ruleClass)) {
384
+ if (parseResult.recursiveErrors().length > 0) continue
385
+ } else if (hasParserErrors) {
386
+ continue
387
+ }
364
388
 
365
- for (const RuleClass of regularRules) {
366
- const rule = new RuleClass()
367
- const unboundOffenses = this.executeRule(rule, parseResult, lexResult, source, context)
368
- const boundOffenses = this.bindSeverity(unboundOffenses, rule.name)
389
+ const unboundOffenses = this.executeRule(ruleClass, rule, parseResult, lexResult, source, context)
390
+ const boundOffenses = this.bindSeverity(unboundOffenses, ruleClass.ruleName)
369
391
 
370
392
  const { kept, ignored, wouldBeIgnored } = this.filterOffenses(
371
393
  boundOffenses,
372
- rule.name,
394
+ ruleClass.ruleName,
373
395
  ignoredOffensesByLine,
374
396
  herbDisableCache,
375
397
  context?.ignoreDisableComments
@@ -380,16 +402,13 @@ export class Linter {
380
402
  this.offenses.push(...kept)
381
403
  }
382
404
 
383
- const unnecessaryRuleClass = this.rules.find(RuleClass => {
384
- const rule = new RuleClass()
385
-
386
- return rule.name === "herb-disable-comment-unnecessary"
387
- })
405
+ const unnecessaryRuleClass = this.findRuleClass("herb-disable-comment-unnecessary")
388
406
 
389
407
  if (unnecessaryRuleClass) {
390
408
  const unnecessaryRule = new unnecessaryRuleClass() as ParserRule
409
+ const parseResult = this.parseCache.get(source, unnecessaryRule.parserOptions)
391
410
  const unboundOffenses = unnecessaryRule.check(parseResult, context)
392
- const boundOffenses = this.bindSeverity(unboundOffenses, unnecessaryRule.name)
411
+ const boundOffenses = this.bindSeverity(unboundOffenses, unnecessaryRuleClass.ruleName)
393
412
 
394
413
  this.offenses.push(...boundOffenses)
395
414
  }
@@ -429,19 +448,16 @@ export class Linter {
429
448
  * @returns Array of offenses with severity bound
430
449
  */
431
450
  protected bindSeverity(unboundOffenses: UnboundLintOffense[], ruleName: string): LintOffense[] {
432
- const RuleClass = this.rules.find(rule => {
433
- const instance = new rule()
434
- return instance.name === ruleName
435
- })
451
+ const ruleClass = this.findRuleClass(ruleName)
436
452
 
437
- if (!RuleClass) {
453
+ if (!ruleClass) {
438
454
  return unboundOffenses.map(offense => ({
439
455
  ...offense,
440
456
  severity: "error" as const
441
457
  }))
442
458
  }
443
459
 
444
- const ruleInstance = new RuleClass()
460
+ const ruleInstance = new ruleClass()
445
461
  const defaultSeverity = ruleInstance.defaultConfig?.severity ?? DEFAULT_RULE_CONFIG.severity
446
462
 
447
463
  const userRuleConfig = this.config?.linter?.rules?.[ruleName]
@@ -449,7 +465,7 @@ export class Linter {
449
465
 
450
466
  return unboundOffenses.map(offense => ({
451
467
  ...offense,
452
- severity
468
+ severity: offense.severity ?? severity
453
469
  }))
454
470
  }
455
471
 
@@ -472,17 +488,13 @@ export class Linter {
472
488
  const sourceOffenses: LintOffense[] = []
473
489
 
474
490
  for (const offense of lintResult.offenses) {
475
- const RuleClass = this.rules.find(rule => {
476
- const instance = new rule()
477
-
478
- return instance.name === offense.rule
479
- })
491
+ const ruleClass = this.findRuleClass(offense.rule)
480
492
 
481
- if (!RuleClass) continue
493
+ if (!ruleClass) continue
482
494
 
483
- if ((RuleClass as any).type === "lexer") {
495
+ if (this.isLexerRuleClass(ruleClass)) {
484
496
  lexerOffenses.push(offense)
485
- } else if ((RuleClass as any).type === "source") {
497
+ } else if (this.isSourceRuleClass(ruleClass)) {
486
498
  sourceOffenses.push(offense)
487
499
  } else {
488
500
  parserOffenses.push(offense)
@@ -494,19 +506,20 @@ export class Linter {
494
506
  const unfixed: LintOffense[] = []
495
507
 
496
508
  if (parserOffenses.length > 0) {
497
- const parseResult = this.herb.parse(currentSource, { track_whitespace: true })
509
+ const parseResult = this.parseCache.get(currentSource)
510
+ let needsReindent = false
498
511
 
499
512
  for (const offense of parserOffenses) {
500
- const RuleClass = this.rules.find(rule => new rule().name === offense.rule)
513
+ const ruleClass = this.findRuleClass(offense.rule)
501
514
 
502
- if (!RuleClass) {
515
+ if (!ruleClass) {
503
516
  unfixed.push(offense)
504
517
 
505
518
  continue
506
519
  }
507
520
 
508
- const rule = new RuleClass() as ParserRule
509
- const isUnsafe = (RuleClass as any).unsafeAutocorrectable === true
521
+ const rule = new ruleClass() as ParserRule
522
+ const isUnsafe = (ruleClass as any).unsafeAutocorrectable === true || offense.autofixContext?.unsafe === true
510
523
 
511
524
  if (!rule.autofix) {
512
525
  unfixed.push(offense)
@@ -543,14 +556,21 @@ export class Linter {
543
556
 
544
557
  if (fixedResult) {
545
558
  fixed.push(offense)
559
+
560
+ if (this.isParserRuleClass(ruleClass) && ruleClass.reindentAfterAutofix === true) {
561
+ needsReindent = true
562
+ }
546
563
  } else {
547
564
  unfixed.push(offense)
548
565
  }
549
566
  }
550
567
 
551
568
  if (fixed.length > 0) {
552
- const printer = new IdentityPrinter()
553
- currentSource = printer.print(parseResult.value)
569
+ if (needsReindent) {
570
+ currentSource = new IndentPrinter().print(parseResult.value)
571
+ } else {
572
+ currentSource = new IdentityPrinter().print(parseResult.value)
573
+ }
554
574
  }
555
575
  }
556
576
 
@@ -564,15 +584,15 @@ export class Linter {
564
584
  })
565
585
 
566
586
  for (const offense of sortedSourceOffenses) {
567
- const RuleClass = this.rules.find(rule => new rule().name === offense.rule)
587
+ const ruleClass = this.findRuleClass(offense.rule)
568
588
 
569
- if (!RuleClass) {
589
+ if (!ruleClass) {
570
590
  unfixed.push(offense)
571
591
  continue
572
592
  }
573
593
 
574
- const rule = new RuleClass() as SourceRule
575
- const isUnsafe = (RuleClass as any).unsafeAutocorrectable === true
594
+ const rule = new ruleClass() as SourceRule
595
+ const isUnsafe = (ruleClass as any).unsafeAutocorrectable === true || offense.autofixContext?.unsafe === true
576
596
 
577
597
  if (!rule.autofix) {
578
598
  unfixed.push(offense)
@@ -602,4 +622,3 @@ export class Linter {
602
622
  }
603
623
  }
604
624
  }
605
-
@@ -0,0 +1,39 @@
1
+ import { DEFAULT_PARSER_OPTIONS } from "@herb-tools/core"
2
+ import { DEFAULT_LINTER_PARSER_OPTIONS } from "./types.js"
3
+
4
+ import type { ParseResult, HerbBackend, ParserOptions } from "@herb-tools/core"
5
+
6
+ export class ParseCache {
7
+ private herb: HerbBackend
8
+ private cache = new Map<string, ParseResult>()
9
+
10
+ constructor(herb: HerbBackend) {
11
+ this.herb = herb
12
+ }
13
+
14
+ get(source: string, parserOptions: Partial<ParserOptions> = {}): ParseResult {
15
+ const effectiveOptions = this.resolveOptions(parserOptions)
16
+ const key = source + JSON.stringify(effectiveOptions)
17
+
18
+ let result = this.cache.get(key)
19
+
20
+ if (!result) {
21
+ result = this.herb.parse(source, effectiveOptions)
22
+ this.cache.set(key, result)
23
+ }
24
+
25
+ return result
26
+ }
27
+
28
+ clear(): void {
29
+ this.cache.clear()
30
+ }
31
+
32
+ resolveOptions(parserOptions: Partial<ParserOptions>): ParserOptions {
33
+ return {
34
+ ...DEFAULT_PARSER_OPTIONS,
35
+ ...DEFAULT_LINTER_PARSER_OPTIONS,
36
+ ...parserOptions
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,58 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+ import { isERBOpenTagNode, isERBOutputNode } from "@herb-tools/core"
4
+
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { ParseResult, HTMLElementNode, ParserOptions } from "@herb-tools/core"
7
+
8
+ class ActionViewNoSilentHelperVisitor extends BaseRuleVisitor {
9
+ visitHTMLElementNode(node: HTMLElementNode): void {
10
+ this.checkActionViewHelper(node)
11
+ super.visitHTMLElementNode(node)
12
+ }
13
+
14
+ private checkActionViewHelper(node: HTMLElementNode): void {
15
+ if (!node.element_source || node.element_source === "HTML") return
16
+ if (!isERBOpenTagNode(node.open_tag)) return
17
+ if (isERBOutputNode(node.open_tag)) return
18
+
19
+ const tagOpening = node.open_tag.tag_opening?.value
20
+
21
+ if (!tagOpening) return
22
+
23
+ const helperName = node.element_source.includes("#")
24
+ ? node.element_source.split("#").pop()
25
+ : node.element_source
26
+
27
+ this.addOffense(
28
+ `Avoid using \`${tagOpening} %>\` with \`${helperName}\`. Use \`<%= %>\` to ensure the helper's output is rendered.`,
29
+ node.open_tag.location,
30
+ )
31
+ }
32
+ }
33
+
34
+ export class ActionViewNoSilentHelperRule extends ParserRule {
35
+ static ruleName = "actionview-no-silent-helper"
36
+
37
+ get defaultConfig(): FullRuleConfig {
38
+ return {
39
+ enabled: true,
40
+ severity: "error"
41
+ }
42
+ }
43
+
44
+ get parserOptions(): Partial<ParserOptions> {
45
+ return {
46
+ track_whitespace: true,
47
+ action_view_helpers: true,
48
+ }
49
+ }
50
+
51
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
52
+ const visitor = new ActionViewNoSilentHelperVisitor(this.ruleName, context)
53
+
54
+ visitor.visit(result.value)
55
+
56
+ return visitor.offenses
57
+ }
58
+ }
@@ -34,7 +34,7 @@ class ERBCommentSyntaxVisitor extends BaseRuleVisitor<ERBCommentSyntaxAutofixCon
34
34
 
35
35
  export class ERBCommentSyntax extends ParserRule<ERBCommentSyntaxAutofixContext> {
36
36
  static autocorrectable = true
37
- name = "erb-comment-syntax"
37
+ static ruleName = "erb-comment-syntax"
38
38
 
39
39
  get defaultConfig(): FullRuleConfig {
40
40
  return {
@@ -44,7 +44,7 @@ export class ERBCommentSyntax extends ParserRule<ERBCommentSyntaxAutofixContext>
44
44
  }
45
45
 
46
46
  check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<ERBCommentSyntaxAutofixContext>[] {
47
- const visitor = new ERBCommentSyntaxVisitor(this.name, context)
47
+ const visitor = new ERBCommentSyntaxVisitor(this.ruleName, context)
48
48
 
49
49
  visitor.visit(result.value)
50
50
 
@@ -49,7 +49,7 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
49
49
  }
50
50
 
51
51
  export class ERBNoCaseNodeChildrenRule extends ParserRule {
52
- name = "erb-no-case-node-children"
52
+ static ruleName = "erb-no-case-node-children"
53
53
 
54
54
  get defaultConfig(): FullRuleConfig {
55
55
  return {
@@ -59,7 +59,7 @@ export class ERBNoCaseNodeChildrenRule extends ParserRule {
59
59
  }
60
60
 
61
61
  check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
62
- const visitor = new ERBNoCaseNodeChildrenVisitor(this.name, context)
62
+ const visitor = new ERBNoCaseNodeChildrenVisitor(this.ruleName, context)
63
63
 
64
64
  visitor.visit(result.value)
65
65
 
@@ -0,0 +1,53 @@
1
+ import dedent from "dedent"
2
+
3
+ import { ParserRule } from "../types.js"
4
+ import { BaseRuleVisitor } from "./rule-utils.js"
5
+
6
+ import type { ParseResult, HTMLConditionalElementNode } from "@herb-tools/core"
7
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
8
+
9
+ class ERBNoConditionalHTMLElementRuleVisitor extends BaseRuleVisitor {
10
+ visitHTMLConditionalElementNode(node: HTMLConditionalElementNode): void {
11
+ const tagName = node.tag_name?.value || "element"
12
+ const condition = node.condition || "condition"
13
+
14
+ const suggestion = dedent`
15
+ Consider using a \`capture\` block instead:
16
+
17
+ <% content = capture do %>
18
+ ... your content here ...
19
+ <% end %>
20
+
21
+ <%= ${condition} ? content_tag(:${tagName}, content) : content %>
22
+ `
23
+
24
+ this.addOffense(
25
+ dedent`
26
+ Avoid opening and closing \`<${tagName}>\` tags in separate conditional blocks with the same condition. \
27
+ This pattern is difficult to read and maintain. ${suggestion}
28
+ `,
29
+ node.location,
30
+ )
31
+
32
+ this.visitChildNodes(node)
33
+ }
34
+ }
35
+
36
+ export class ERBNoConditionalHTMLElementRule extends ParserRule {
37
+ static ruleName = "erb-no-conditional-html-element"
38
+
39
+ get defaultConfig(): FullRuleConfig {
40
+ return {
41
+ enabled: true,
42
+ severity: "error"
43
+ }
44
+ }
45
+
46
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
47
+ const visitor = new ERBNoConditionalHTMLElementRuleVisitor(this.ruleName, context)
48
+
49
+ visitor.visit(result.value)
50
+
51
+ return visitor.offenses
52
+ }
53
+ }
@@ -0,0 +1,37 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+
4
+ import type { ParseResult, HTMLConditionalOpenTagNode } from "@herb-tools/core"
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+
7
+ class ERBNoConditionalOpenTagRuleVisitor extends BaseRuleVisitor {
8
+ visitHTMLConditionalOpenTagNode(node: HTMLConditionalOpenTagNode): void {
9
+ const tagName = node.tag_name?.value || "element"
10
+
11
+ this.addOffense(
12
+ `Avoid using ERB conditionals to split the open and closing tag of \`<${tagName}>\` element.`,
13
+ node.location,
14
+ )
15
+
16
+ this.visitChildNodes(node)
17
+ }
18
+ }
19
+
20
+ export class ERBNoConditionalOpenTagRule extends ParserRule {
21
+ static ruleName = "erb-no-conditional-open-tag"
22
+
23
+ get defaultConfig(): FullRuleConfig {
24
+ return {
25
+ enabled: true,
26
+ severity: "error"
27
+ }
28
+ }
29
+
30
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
31
+ const visitor = new ERBNoConditionalOpenTagRuleVisitor(this.ruleName, context)
32
+
33
+ visitor.visit(result.value)
34
+
35
+ return visitor.offenses
36
+ }
37
+ }