@herb-tools/linter 0.8.10 → 0.9.1

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 (587) 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 +61834 -17340
  28. package/dist/herb-lint.js.map +1 -1
  29. package/dist/index.cjs +3109 -956
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +2969 -897
  32. package/dist/index.js.map +1 -1
  33. package/dist/lint-worker.js +72889 -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 +32163 -7842
  39. package/dist/loader.cjs.map +1 -1
  40. package/dist/loader.js +32121 -7828
  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/rules/actionview-no-silent-render.js +31 -0
  47. package/dist/rules/actionview-no-silent-render.js.map +1 -0
  48. package/dist/{src/rules → rules}/erb-comment-syntax.js +2 -2
  49. package/dist/rules/erb-comment-syntax.js.map +1 -0
  50. package/dist/{src/rules → rules}/erb-no-case-node-children.js +5 -3
  51. package/dist/rules/erb-no-case-node-children.js.map +1 -0
  52. package/dist/rules/erb-no-conditional-html-element.js +38 -0
  53. package/dist/rules/erb-no-conditional-html-element.js.map +1 -0
  54. package/dist/rules/erb-no-conditional-open-tag.js +24 -0
  55. package/dist/rules/erb-no-conditional-open-tag.js.map +1 -0
  56. package/dist/rules/erb-no-duplicate-branch-elements.js +329 -0
  57. package/dist/rules/erb-no-duplicate-branch-elements.js.map +1 -0
  58. package/dist/rules/erb-no-empty-control-flow.js +190 -0
  59. package/dist/rules/erb-no-empty-control-flow.js.map +1 -0
  60. package/dist/{src/rules → rules}/erb-no-empty-tags.js +2 -2
  61. package/dist/rules/erb-no-empty-tags.js.map +1 -0
  62. package/dist/{src/rules → rules}/erb-no-extra-newline.js +4 -21
  63. package/dist/rules/erb-no-extra-newline.js.map +1 -0
  64. package/dist/{src/rules → rules}/erb-no-extra-whitespace-inside-tags.js +39 -13
  65. package/dist/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
  66. package/dist/rules/erb-no-inline-case-conditions.js +40 -0
  67. package/dist/rules/erb-no-inline-case-conditions.js.map +1 -0
  68. package/dist/rules/erb-no-instance-variables-in-partials.js +67 -0
  69. package/dist/rules/erb-no-instance-variables-in-partials.js.map +1 -0
  70. package/dist/rules/erb-no-interpolated-class-names.js +47 -0
  71. package/dist/rules/erb-no-interpolated-class-names.js.map +1 -0
  72. package/dist/rules/erb-no-javascript-tag-helper.js +34 -0
  73. package/dist/rules/erb-no-javascript-tag-helper.js.map +1 -0
  74. package/dist/{src/rules → rules}/erb-no-output-control-flow.js +9 -12
  75. package/dist/rules/erb-no-output-control-flow.js.map +1 -0
  76. package/dist/rules/erb-no-output-in-attribute-name.js +30 -0
  77. package/dist/rules/erb-no-output-in-attribute-name.js.map +1 -0
  78. package/dist/rules/erb-no-output-in-attribute-position.js +30 -0
  79. package/dist/rules/erb-no-output-in-attribute-position.js.map +1 -0
  80. package/dist/rules/erb-no-raw-output-in-attribute-value.js +35 -0
  81. package/dist/rules/erb-no-raw-output-in-attribute-value.js.map +1 -0
  82. package/dist/rules/erb-no-silent-statement.js +44 -0
  83. package/dist/rules/erb-no-silent-statement.js.map +1 -0
  84. package/dist/{src/rules → rules}/erb-no-silent-tag-in-attribute-name.js +2 -2
  85. package/dist/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
  86. package/dist/rules/erb-no-statement-in-script.js +58 -0
  87. package/dist/rules/erb-no-statement-in-script.js.map +1 -0
  88. package/dist/rules/erb-no-then-in-control-flow.js +45 -0
  89. package/dist/rules/erb-no-then-in-control-flow.js.map +1 -0
  90. package/dist/rules/erb-no-trailing-whitespace.js +138 -0
  91. package/dist/rules/erb-no-trailing-whitespace.js.map +1 -0
  92. package/dist/rules/erb-no-unsafe-js-attribute.js +36 -0
  93. package/dist/rules/erb-no-unsafe-js-attribute.js.map +1 -0
  94. package/dist/rules/erb-no-unsafe-raw.js +63 -0
  95. package/dist/rules/erb-no-unsafe-raw.js.map +1 -0
  96. package/dist/rules/erb-no-unsafe-script-interpolation.js +88 -0
  97. package/dist/rules/erb-no-unsafe-script-interpolation.js.map +1 -0
  98. package/dist/{src/rules → rules}/erb-prefer-image-tag-helper.js +5 -4
  99. package/dist/rules/erb-prefer-image-tag-helper.js.map +1 -0
  100. package/dist/{src/rules → rules}/erb-require-trailing-newline.js +2 -2
  101. package/dist/rules/erb-require-trailing-newline.js.map +1 -0
  102. package/dist/{src/rules → rules}/erb-require-whitespace-inside-tags.js +39 -15
  103. package/dist/rules/erb-require-whitespace-inside-tags.js.map +1 -0
  104. package/dist/{src/rules → rules}/erb-right-trim.js +2 -2
  105. package/dist/rules/erb-right-trim.js.map +1 -0
  106. package/dist/{src/rules → rules}/erb-strict-locals-comment-syntax.js +4 -4
  107. package/dist/rules/erb-strict-locals-comment-syntax.js.map +1 -0
  108. package/dist/{src/rules → rules}/erb-strict-locals-required.js +2 -2
  109. package/dist/rules/erb-strict-locals-required.js.map +1 -0
  110. package/dist/rules/file-utils.js.map +1 -0
  111. package/dist/rules/herb-disable-comment-base.js.map +1 -0
  112. package/dist/{src/rules → rules}/herb-disable-comment-malformed.js +2 -2
  113. package/dist/rules/herb-disable-comment-malformed.js.map +1 -0
  114. package/dist/{src/rules → rules}/herb-disable-comment-missing-rules.js +2 -2
  115. package/dist/rules/herb-disable-comment-missing-rules.js.map +1 -0
  116. package/dist/{src/rules → rules}/herb-disable-comment-no-duplicate-rules.js +2 -2
  117. package/dist/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
  118. package/dist/{src/rules → rules}/herb-disable-comment-no-redundant-all.js +2 -2
  119. package/dist/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
  120. package/dist/{src/rules → rules}/herb-disable-comment-unnecessary.js +2 -2
  121. package/dist/rules/herb-disable-comment-unnecessary.js.map +1 -0
  122. package/dist/{src/rules → rules}/herb-disable-comment-valid-rule-name.js +2 -2
  123. package/dist/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
  124. package/dist/rules/html-allowed-script-type.js +57 -0
  125. package/dist/rules/html-allowed-script-type.js.map +1 -0
  126. package/dist/rules/html-anchor-require-href.js +68 -0
  127. package/dist/rules/html-anchor-require-href.js.map +1 -0
  128. package/dist/{src/rules → rules}/html-aria-attribute-must-be-valid.js +3 -3
  129. package/dist/rules/html-aria-attribute-must-be-valid.js.map +1 -0
  130. package/dist/{src/rules → rules}/html-aria-label-is-well-formatted.js +3 -3
  131. package/dist/rules/html-aria-label-is-well-formatted.js.map +1 -0
  132. package/dist/{src/rules → rules}/html-aria-level-must-be-valid.js +3 -3
  133. package/dist/rules/html-aria-level-must-be-valid.js.map +1 -0
  134. package/dist/{src/rules → rules}/html-aria-role-heading-requires-level.js +5 -4
  135. package/dist/rules/html-aria-role-heading-requires-level.js.map +1 -0
  136. package/dist/{src/rules → rules}/html-aria-role-must-be-valid.js +3 -3
  137. package/dist/rules/html-aria-role-must-be-valid.js.map +1 -0
  138. package/dist/{src/rules → rules}/html-attribute-double-quotes.js +4 -4
  139. package/dist/rules/html-attribute-double-quotes.js.map +1 -0
  140. package/dist/{src/rules → rules}/html-attribute-equals-spacing.js +2 -2
  141. package/dist/rules/html-attribute-equals-spacing.js.map +1 -0
  142. package/dist/{src/rules → rules}/html-attribute-values-require-quotes.js +2 -2
  143. package/dist/rules/html-attribute-values-require-quotes.js.map +1 -0
  144. package/dist/{src/rules → rules}/html-avoid-both-disabled-and-aria-disabled.js +9 -9
  145. package/dist/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
  146. package/dist/{src/rules → rules}/html-body-only-elements.js +5 -4
  147. package/dist/rules/html-body-only-elements.js.map +1 -0
  148. package/dist/{src/rules → rules}/html-boolean-attributes-no-value.js +4 -3
  149. package/dist/rules/html-boolean-attributes-no-value.js.map +1 -0
  150. package/dist/rules/html-details-has-summary.js +52 -0
  151. package/dist/rules/html-details-has-summary.js.map +1 -0
  152. package/dist/{src/rules → rules}/html-head-only-elements.js +6 -5
  153. package/dist/rules/html-head-only-elements.js.map +1 -0
  154. package/dist/{src/rules → rules}/html-iframe-has-title.js +8 -11
  155. package/dist/rules/html-iframe-has-title.js.map +1 -0
  156. package/dist/{src/rules → rules}/html-img-require-alt.js +11 -5
  157. package/dist/rules/html-img-require-alt.js.map +1 -0
  158. package/dist/{src/rules → rules}/html-input-require-autocomplete.js +7 -10
  159. package/dist/rules/html-input-require-autocomplete.js.map +1 -0
  160. package/dist/{src/rules → rules}/html-navigation-has-label.js +6 -5
  161. package/dist/rules/html-navigation-has-label.js.map +1 -0
  162. package/dist/rules/html-no-abstract-roles.js +29 -0
  163. package/dist/rules/html-no-abstract-roles.js.map +1 -0
  164. package/dist/rules/html-no-aria-hidden-on-body.js +42 -0
  165. package/dist/rules/html-no-aria-hidden-on-body.js.map +1 -0
  166. package/dist/{src/rules → rules}/html-no-aria-hidden-on-focusable.js +6 -5
  167. package/dist/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
  168. package/dist/{src/rules → rules}/html-no-block-inside-inline.js +6 -9
  169. package/dist/rules/html-no-block-inside-inline.js.map +1 -0
  170. package/dist/{src/rules → rules}/html-no-duplicate-attributes.js +4 -3
  171. package/dist/rules/html-no-duplicate-attributes.js.map +1 -0
  172. package/dist/{src/rules → rules}/html-no-duplicate-ids.js +14 -11
  173. package/dist/rules/html-no-duplicate-ids.js.map +1 -0
  174. package/dist/{src/rules → rules}/html-no-duplicate-meta-names.js +22 -20
  175. package/dist/rules/html-no-duplicate-meta-names.js.map +1 -0
  176. package/dist/{src/rules → rules}/html-no-empty-attributes.js +2 -2
  177. package/dist/rules/html-no-empty-attributes.js.map +1 -0
  178. package/dist/rules/html-no-empty-headings.js +98 -0
  179. package/dist/rules/html-no-empty-headings.js.map +1 -0
  180. package/dist/{src/rules → rules}/html-no-nested-links.js +23 -15
  181. package/dist/rules/html-no-nested-links.js.map +1 -0
  182. package/dist/{src/rules → rules}/html-no-positive-tab-index.js +3 -3
  183. package/dist/rules/html-no-positive-tab-index.js.map +1 -0
  184. package/dist/{src/rules → rules}/html-no-self-closing.js +4 -4
  185. package/dist/rules/html-no-self-closing.js.map +1 -0
  186. package/dist/{src/rules → rules}/html-no-space-in-tag.js +4 -6
  187. package/dist/rules/html-no-space-in-tag.js.map +1 -0
  188. package/dist/{src/rules → rules}/html-no-title-attribute.js +6 -5
  189. package/dist/rules/html-no-title-attribute.js.map +1 -0
  190. package/dist/{src/rules → rules}/html-no-underscores-in-attribute-names.js +2 -2
  191. package/dist/rules/html-no-underscores-in-attribute-names.js.map +1 -0
  192. package/dist/rules/html-require-closing-tags.js +29 -0
  193. package/dist/rules/html-require-closing-tags.js.map +1 -0
  194. package/dist/{src/rules → rules}/html-tag-name-lowercase.js +13 -9
  195. package/dist/rules/html-tag-name-lowercase.js.map +1 -0
  196. package/dist/{src/rules → rules}/index.js +27 -4
  197. package/dist/rules/index.js.map +1 -0
  198. package/dist/{src/rules → rules}/parser-no-errors.js +3 -3
  199. package/dist/rules/parser-no-errors.js.map +1 -0
  200. package/dist/{src/rules → rules}/rule-utils.js +144 -219
  201. package/dist/rules/rule-utils.js.map +1 -0
  202. package/dist/rules/string-utils.js.map +1 -0
  203. package/dist/{src/rules → rules}/svg-tag-name-capitalization.js +7 -6
  204. package/dist/rules/svg-tag-name-capitalization.js.map +1 -0
  205. package/dist/rules/turbo-permanent-require-id.js +34 -0
  206. package/dist/rules/turbo-permanent-require-id.js.map +1 -0
  207. package/dist/{src/rules.js → rules.js} +62 -10
  208. package/dist/rules.js.map +1 -0
  209. package/dist/types/cli/argument-parser.d.ts +1 -0
  210. package/dist/types/cli/file-processor.d.ts +13 -0
  211. package/dist/types/cli/file-url.d.ts +1 -0
  212. package/dist/types/cli/index.d.ts +1 -0
  213. package/dist/types/cli/lint-worker.d.ts +34 -0
  214. package/dist/types/custom-rule-loader.d.ts +4 -0
  215. package/dist/types/index.d.ts +2 -0
  216. package/dist/types/linter.d.ts +13 -6
  217. package/dist/types/parse-cache.d.ts +9 -0
  218. package/dist/types/{src/rules/html-aria-level-must-be-valid.d.ts → rules/actionview-no-silent-helper.d.ts} +4 -3
  219. package/dist/types/rules/actionview-no-silent-render.d.ts +9 -0
  220. package/dist/types/rules/erb-comment-syntax.d.ts +1 -1
  221. package/dist/types/rules/erb-no-case-node-children.d.ts +1 -1
  222. package/dist/types/{src/rules/herb-disable-comment-malformed.d.ts → rules/erb-no-conditional-html-element.d.ts} +3 -3
  223. package/dist/types/{src/rules/erb-prefer-image-tag-helper.d.ts → rules/erb-no-conditional-open-tag.d.ts} +3 -3
  224. package/dist/types/rules/erb-no-duplicate-branch-elements.d.ts +18 -0
  225. package/dist/types/{src/rules/html-anchor-require-href.d.ts → rules/erb-no-empty-control-flow.d.ts} +2 -2
  226. package/dist/types/rules/erb-no-empty-tags.d.ts +1 -1
  227. package/dist/types/rules/erb-no-extra-newline.d.ts +1 -1
  228. package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +1 -1
  229. package/dist/types/{src/rules/html-no-duplicate-attributes.d.ts → rules/erb-no-inline-case-conditions.d.ts} +4 -3
  230. package/dist/types/rules/erb-no-instance-variables-in-partials.d.ts +10 -0
  231. package/dist/types/{src/rules/html-no-aria-hidden-on-focusable.d.ts → rules/erb-no-interpolated-class-names.d.ts} +2 -2
  232. package/dist/types/{src/rules/html-aria-attribute-must-be-valid.d.ts → rules/erb-no-javascript-tag-helper.d.ts} +2 -2
  233. package/dist/types/rules/erb-no-output-control-flow.d.ts +1 -1
  234. 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
  235. package/dist/types/{src/rules/herb-disable-comment-missing-rules.d.ts → rules/erb-no-output-in-attribute-position.d.ts} +2 -2
  236. package/dist/types/{src/rules/erb-no-empty-tags.d.ts → rules/erb-no-raw-output-in-attribute-value.d.ts} +2 -2
  237. package/dist/types/{src/rules/html-no-title-attribute.d.ts → rules/erb-no-silent-statement.d.ts} +4 -3
  238. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +1 -1
  239. package/dist/types/{src/rules/html-navigation-has-label.d.ts → rules/erb-no-statement-in-script.d.ts} +2 -2
  240. package/dist/types/rules/erb-no-then-in-control-flow.d.ts +9 -0
  241. package/dist/types/rules/erb-no-trailing-whitespace.d.ts +19 -0
  242. package/dist/types/{src/rules/html-no-positive-tab-index.d.ts → rules/erb-no-unsafe-js-attribute.d.ts} +2 -2
  243. package/dist/types/{src/rules/erb-no-case-node-children.d.ts → rules/erb-no-unsafe-raw.d.ts} +2 -2
  244. package/dist/types/rules/erb-no-unsafe-script-interpolation.d.ts +9 -0
  245. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +1 -1
  246. package/dist/types/rules/erb-require-trailing-newline.d.ts +1 -1
  247. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +1 -1
  248. package/dist/types/rules/erb-right-trim.d.ts +1 -1
  249. package/dist/types/rules/erb-strict-locals-comment-syntax.d.ts +1 -1
  250. package/dist/types/rules/erb-strict-locals-required.d.ts +1 -1
  251. package/dist/types/rules/herb-disable-comment-malformed.d.ts +1 -1
  252. package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +1 -1
  253. package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +1 -1
  254. package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +1 -1
  255. package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +1 -1
  256. package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +1 -1
  257. package/dist/types/{src/rules/html-no-empty-attributes.d.ts → rules/html-allowed-script-type.d.ts} +2 -2
  258. package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
  259. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +1 -1
  260. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +1 -1
  261. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +1 -1
  262. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +1 -1
  263. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +1 -1
  264. package/dist/types/rules/html-attribute-double-quotes.d.ts +1 -1
  265. package/dist/types/rules/html-attribute-equals-spacing.d.ts +1 -1
  266. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +1 -1
  267. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +1 -1
  268. package/dist/types/rules/html-body-only-elements.d.ts +1 -1
  269. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +1 -1
  270. package/dist/types/rules/html-details-has-summary.d.ts +9 -0
  271. package/dist/types/rules/html-head-only-elements.d.ts +1 -1
  272. package/dist/types/rules/html-iframe-has-title.d.ts +1 -1
  273. package/dist/types/rules/html-img-require-alt.d.ts +1 -1
  274. package/dist/types/rules/html-input-require-autocomplete.d.ts +1 -1
  275. package/dist/types/rules/html-navigation-has-label.d.ts +1 -1
  276. package/dist/types/{src/rules/html-no-empty-headings.d.ts → rules/html-no-abstract-roles.d.ts} +2 -2
  277. package/dist/types/{src/rules/erb-no-output-control-flow.d.ts → rules/html-no-aria-hidden-on-body.d.ts} +3 -3
  278. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +1 -1
  279. package/dist/types/rules/html-no-block-inside-inline.d.ts +1 -1
  280. package/dist/types/rules/html-no-duplicate-attributes.d.ts +1 -1
  281. package/dist/types/rules/html-no-duplicate-ids.d.ts +1 -1
  282. package/dist/types/rules/html-no-duplicate-meta-names.d.ts +1 -1
  283. package/dist/types/rules/html-no-empty-attributes.d.ts +1 -1
  284. package/dist/types/rules/html-no-empty-headings.d.ts +1 -1
  285. package/dist/types/rules/html-no-nested-links.d.ts +1 -1
  286. package/dist/types/rules/html-no-positive-tab-index.d.ts +1 -1
  287. package/dist/types/rules/html-no-self-closing.d.ts +1 -1
  288. package/dist/types/rules/html-no-space-in-tag.d.ts +1 -1
  289. package/dist/types/rules/html-no-title-attribute.d.ts +1 -1
  290. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +1 -1
  291. package/dist/types/{src/rules/html-body-only-elements.d.ts → rules/html-require-closing-tags.d.ts} +4 -3
  292. package/dist/types/rules/html-tag-name-lowercase.d.ts +1 -1
  293. package/dist/types/rules/index.d.ts +27 -4
  294. package/dist/types/rules/parser-no-errors.d.ts +1 -1
  295. package/dist/types/rules/rule-utils.d.ts +36 -88
  296. package/dist/types/rules/svg-tag-name-capitalization.d.ts +1 -1
  297. package/dist/types/{src/rules/html-aria-role-must-be-valid.d.ts → rules/turbo-permanent-require-id.d.ts} +2 -2
  298. package/dist/types/types.d.ts +26 -7
  299. package/dist/types/urls.d.ts +1 -0
  300. package/dist/{src/types.js → types.js} +56 -0
  301. package/dist/types.js.map +1 -0
  302. package/dist/urls.js +5 -0
  303. package/dist/urls.js.map +1 -0
  304. package/docs/rules/README.md +26 -2
  305. package/docs/rules/actionview-no-silent-helper.md +57 -0
  306. package/docs/rules/actionview-no-silent-render.md +47 -0
  307. package/docs/rules/erb-no-conditional-html-element.md +90 -0
  308. package/docs/rules/erb-no-conditional-open-tag.md +130 -0
  309. package/docs/rules/erb-no-duplicate-branch-elements.md +98 -0
  310. package/docs/rules/erb-no-empty-control-flow.md +83 -0
  311. package/docs/rules/erb-no-inline-case-conditions.md +85 -0
  312. package/docs/rules/erb-no-instance-variables-in-partials.md +43 -0
  313. package/docs/rules/erb-no-interpolated-class-names.md +57 -0
  314. package/docs/rules/erb-no-javascript-tag-helper.md +33 -0
  315. package/docs/rules/erb-no-output-in-attribute-name.md +38 -0
  316. package/docs/rules/erb-no-output-in-attribute-position.md +60 -0
  317. package/docs/rules/erb-no-raw-output-in-attribute-value.md +37 -0
  318. package/docs/rules/erb-no-silent-statement.md +53 -0
  319. package/docs/rules/erb-no-statement-in-script.md +68 -0
  320. package/docs/rules/erb-no-then-in-control-flow.md +86 -0
  321. package/docs/rules/erb-no-trailing-whitespace.md +69 -0
  322. package/docs/rules/erb-no-unsafe-js-attribute.md +41 -0
  323. package/docs/rules/erb-no-unsafe-raw.md +47 -0
  324. package/docs/rules/erb-no-unsafe-script-interpolation.md +140 -0
  325. package/docs/rules/html-allowed-script-type.md +59 -0
  326. package/docs/rules/html-anchor-require-href.md +19 -6
  327. package/docs/rules/html-details-has-summary.md +46 -0
  328. package/docs/rules/html-img-require-alt.md +5 -3
  329. package/docs/rules/html-no-abstract-roles.md +74 -0
  330. package/docs/rules/html-no-aria-hidden-on-body.md +44 -0
  331. package/docs/rules/html-require-closing-tags.md +142 -0
  332. package/docs/rules/parser-no-errors.md +4 -17
  333. package/docs/rules/turbo-permanent-require-id.md +41 -0
  334. package/package.json +11 -10
  335. package/src/cli/argument-parser.ts +20 -2
  336. package/src/cli/file-processor.ts +189 -10
  337. package/src/cli/file-url.ts +6 -0
  338. package/src/cli/formatters/detailed-formatter.ts +19 -21
  339. package/src/cli/formatters/simple-formatter.ts +23 -13
  340. package/src/cli/index.ts +2 -0
  341. package/src/cli/lint-worker.ts +208 -0
  342. package/src/cli/summary-reporter.ts +14 -15
  343. package/src/cli.ts +5 -3
  344. package/src/custom-rule-loader.ts +20 -5
  345. package/src/herb-disable-comment-utils.ts +0 -3
  346. package/src/index.ts +22 -0
  347. package/src/linter.ts +98 -79
  348. package/src/parse-cache.ts +39 -0
  349. package/src/rules/actionview-no-silent-helper.ts +58 -0
  350. package/src/rules/actionview-no-silent-render.ts +44 -0
  351. package/src/rules/erb-comment-syntax.ts +2 -2
  352. package/src/rules/erb-no-case-node-children.ts +5 -3
  353. package/src/rules/erb-no-conditional-html-element.ts +53 -0
  354. package/src/rules/erb-no-conditional-open-tag.ts +37 -0
  355. package/src/rules/erb-no-duplicate-branch-elements.ts +436 -0
  356. package/src/rules/erb-no-empty-control-flow.ts +255 -0
  357. package/src/rules/erb-no-empty-tags.ts +2 -2
  358. package/src/rules/erb-no-extra-newline.ts +5 -25
  359. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +45 -15
  360. package/src/rules/erb-no-inline-case-conditions.ts +54 -0
  361. package/src/rules/erb-no-instance-variables-in-partials.ts +101 -0
  362. package/src/rules/erb-no-interpolated-class-names.ts +65 -0
  363. package/src/rules/erb-no-javascript-tag-helper.ts +47 -0
  364. package/src/rules/erb-no-output-control-flow.ts +10 -10
  365. package/src/rules/erb-no-output-in-attribute-name.ts +39 -0
  366. package/src/rules/erb-no-output-in-attribute-position.ts +39 -0
  367. package/src/rules/erb-no-raw-output-in-attribute-value.ts +47 -0
  368. package/src/rules/erb-no-silent-statement.ts +58 -0
  369. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +2 -2
  370. package/src/rules/erb-no-statement-in-script.ts +82 -0
  371. package/src/rules/erb-no-then-in-control-flow.ts +62 -0
  372. package/src/rules/erb-no-trailing-whitespace.ts +187 -0
  373. package/src/rules/erb-no-unsafe-js-attribute.ts +47 -0
  374. package/src/rules/erb-no-unsafe-raw.ts +83 -0
  375. package/src/rules/erb-no-unsafe-script-interpolation.ts +122 -0
  376. package/src/rules/erb-prefer-image-tag-helper.ts +5 -4
  377. package/src/rules/erb-require-trailing-newline.ts +2 -2
  378. package/src/rules/erb-require-whitespace-inside-tags.ts +42 -18
  379. package/src/rules/erb-right-trim.ts +2 -2
  380. package/src/rules/erb-strict-locals-comment-syntax.ts +4 -4
  381. package/src/rules/erb-strict-locals-required.ts +2 -2
  382. package/src/rules/herb-disable-comment-malformed.ts +2 -2
  383. package/src/rules/herb-disable-comment-missing-rules.ts +2 -2
  384. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +2 -2
  385. package/src/rules/herb-disable-comment-no-redundant-all.ts +2 -2
  386. package/src/rules/herb-disable-comment-unnecessary.ts +2 -2
  387. package/src/rules/herb-disable-comment-valid-rule-name.ts +2 -2
  388. package/src/rules/html-allowed-script-type.ts +84 -0
  389. package/src/rules/html-anchor-require-href.ts +73 -11
  390. package/src/rules/html-aria-attribute-must-be-valid.ts +3 -3
  391. package/src/rules/html-aria-label-is-well-formatted.ts +3 -3
  392. package/src/rules/html-aria-level-must-be-valid.ts +3 -3
  393. package/src/rules/html-aria-role-heading-requires-level.ts +5 -4
  394. package/src/rules/html-aria-role-must-be-valid.ts +3 -3
  395. package/src/rules/html-attribute-double-quotes.ts +4 -4
  396. package/src/rules/html-attribute-equals-spacing.ts +2 -2
  397. package/src/rules/html-attribute-values-require-quotes.ts +2 -2
  398. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +10 -11
  399. package/src/rules/html-body-only-elements.ts +5 -4
  400. package/src/rules/html-boolean-attributes-no-value.ts +4 -3
  401. package/src/rules/html-details-has-summary.ts +69 -0
  402. package/src/rules/html-head-only-elements.ts +6 -5
  403. package/src/rules/html-iframe-has-title.ts +8 -11
  404. package/src/rules/html-img-require-alt.ts +16 -5
  405. package/src/rules/html-input-require-autocomplete.ts +7 -10
  406. package/src/rules/html-navigation-has-label.ts +6 -5
  407. package/src/rules/html-no-abstract-roles.ts +40 -0
  408. package/src/rules/html-no-aria-hidden-on-body.ts +58 -0
  409. package/src/rules/html-no-aria-hidden-on-focusable.ts +6 -5
  410. package/src/rules/html-no-block-inside-inline.ts +7 -13
  411. package/src/rules/html-no-duplicate-attributes.ts +4 -3
  412. package/src/rules/html-no-duplicate-ids.ts +16 -13
  413. package/src/rules/html-no-duplicate-meta-names.ts +20 -19
  414. package/src/rules/html-no-empty-attributes.ts +2 -2
  415. package/src/rules/html-no-empty-headings.ts +44 -58
  416. package/src/rules/html-no-nested-links.ts +25 -16
  417. package/src/rules/html-no-positive-tab-index.ts +3 -3
  418. package/src/rules/html-no-self-closing.ts +5 -5
  419. package/src/rules/html-no-space-in-tag.ts +5 -8
  420. package/src/rules/html-no-title-attribute.ts +6 -5
  421. package/src/rules/html-no-underscores-in-attribute-names.ts +2 -2
  422. package/src/rules/html-require-closing-tags.ts +41 -0
  423. package/src/rules/html-tag-name-lowercase.ts +14 -9
  424. package/src/rules/index.ts +28 -4
  425. package/src/rules/parser-no-errors.ts +3 -3
  426. package/src/rules/rule-utils.ts +166 -279
  427. package/src/rules/svg-tag-name-capitalization.ts +10 -10
  428. package/src/rules/turbo-permanent-require-id.ts +49 -0
  429. package/src/rules.ts +66 -10
  430. package/src/types.ts +80 -7
  431. package/src/urls.ts +5 -0
  432. package/dist/package.json +0 -65
  433. package/dist/src/cli/argument-parser.js.map +0 -1
  434. package/dist/src/cli/file-processor.js.map +0 -1
  435. package/dist/src/cli/formatters/base-formatter.js.map +0 -1
  436. package/dist/src/cli/formatters/detailed-formatter.js.map +0 -1
  437. package/dist/src/cli/formatters/github-actions-formatter.js.map +0 -1
  438. package/dist/src/cli/formatters/index.js.map +0 -1
  439. package/dist/src/cli/formatters/json-formatter.js.map +0 -1
  440. package/dist/src/cli/formatters/simple-formatter.js +0 -44
  441. package/dist/src/cli/formatters/simple-formatter.js.map +0 -1
  442. package/dist/src/cli/index.js.map +0 -1
  443. package/dist/src/cli/output-manager.js.map +0 -1
  444. package/dist/src/cli/summary-reporter.js.map +0 -1
  445. package/dist/src/cli.js.map +0 -1
  446. package/dist/src/custom-rule-loader.js.map +0 -1
  447. package/dist/src/herb-disable-comment-utils.js.map +0 -1
  448. package/dist/src/herb-lint.js +0 -5
  449. package/dist/src/herb-lint.js.map +0 -1
  450. package/dist/src/index.js +0 -5
  451. package/dist/src/index.js.map +0 -1
  452. package/dist/src/linter-ignore.js.map +0 -1
  453. package/dist/src/linter.js.map +0 -1
  454. package/dist/src/loader.js +0 -17
  455. package/dist/src/loader.js.map +0 -1
  456. package/dist/src/rules/erb-comment-syntax.js.map +0 -1
  457. package/dist/src/rules/erb-no-case-node-children.js.map +0 -1
  458. package/dist/src/rules/erb-no-empty-tags.js.map +0 -1
  459. package/dist/src/rules/erb-no-extra-newline.js.map +0 -1
  460. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +0 -1
  461. package/dist/src/rules/erb-no-output-control-flow.js.map +0 -1
  462. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +0 -1
  463. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +0 -1
  464. package/dist/src/rules/erb-require-trailing-newline.js.map +0 -1
  465. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +0 -1
  466. package/dist/src/rules/erb-right-trim.js.map +0 -1
  467. package/dist/src/rules/erb-strict-locals-comment-syntax.js.map +0 -1
  468. package/dist/src/rules/erb-strict-locals-required.js.map +0 -1
  469. package/dist/src/rules/file-utils.js.map +0 -1
  470. package/dist/src/rules/herb-disable-comment-base.js.map +0 -1
  471. package/dist/src/rules/herb-disable-comment-malformed.js.map +0 -1
  472. package/dist/src/rules/herb-disable-comment-missing-rules.js.map +0 -1
  473. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +0 -1
  474. package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +0 -1
  475. package/dist/src/rules/herb-disable-comment-unnecessary.js.map +0 -1
  476. package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +0 -1
  477. package/dist/src/rules/html-anchor-require-href.js +0 -32
  478. package/dist/src/rules/html-anchor-require-href.js.map +0 -1
  479. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +0 -1
  480. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +0 -1
  481. package/dist/src/rules/html-aria-level-must-be-valid.js.map +0 -1
  482. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +0 -1
  483. package/dist/src/rules/html-aria-role-must-be-valid.js.map +0 -1
  484. package/dist/src/rules/html-attribute-double-quotes.js.map +0 -1
  485. package/dist/src/rules/html-attribute-equals-spacing.js.map +0 -1
  486. package/dist/src/rules/html-attribute-values-require-quotes.js.map +0 -1
  487. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +0 -1
  488. package/dist/src/rules/html-body-only-elements.js.map +0 -1
  489. package/dist/src/rules/html-boolean-attributes-no-value.js.map +0 -1
  490. package/dist/src/rules/html-head-only-elements.js.map +0 -1
  491. package/dist/src/rules/html-iframe-has-title.js.map +0 -1
  492. package/dist/src/rules/html-img-require-alt.js.map +0 -1
  493. package/dist/src/rules/html-input-require-autocomplete.js.map +0 -1
  494. package/dist/src/rules/html-navigation-has-label.js.map +0 -1
  495. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +0 -1
  496. package/dist/src/rules/html-no-block-inside-inline.js.map +0 -1
  497. package/dist/src/rules/html-no-duplicate-attributes.js.map +0 -1
  498. package/dist/src/rules/html-no-duplicate-ids.js.map +0 -1
  499. package/dist/src/rules/html-no-duplicate-meta-names.js.map +0 -1
  500. package/dist/src/rules/html-no-empty-attributes.js.map +0 -1
  501. package/dist/src/rules/html-no-empty-headings.js +0 -115
  502. package/dist/src/rules/html-no-empty-headings.js.map +0 -1
  503. package/dist/src/rules/html-no-nested-links.js.map +0 -1
  504. package/dist/src/rules/html-no-positive-tab-index.js.map +0 -1
  505. package/dist/src/rules/html-no-self-closing.js.map +0 -1
  506. package/dist/src/rules/html-no-space-in-tag.js.map +0 -1
  507. package/dist/src/rules/html-no-title-attribute.js.map +0 -1
  508. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +0 -1
  509. package/dist/src/rules/html-tag-name-lowercase.js.map +0 -1
  510. package/dist/src/rules/index.js.map +0 -1
  511. package/dist/src/rules/parser-no-errors.js.map +0 -1
  512. package/dist/src/rules/rule-utils.js.map +0 -1
  513. package/dist/src/rules/string-utils.js.map +0 -1
  514. package/dist/src/rules/svg-tag-name-capitalization.js.map +0 -1
  515. package/dist/src/rules.js.map +0 -1
  516. package/dist/src/types.js.map +0 -1
  517. package/dist/tsconfig.tsbuildinfo +0 -1
  518. package/dist/types/src/cli/argument-parser.d.ts +0 -25
  519. package/dist/types/src/cli/file-processor.d.ts +0 -43
  520. package/dist/types/src/cli/formatters/base-formatter.d.ts +0 -6
  521. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +0 -13
  522. package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +0 -17
  523. package/dist/types/src/cli/formatters/index.d.ts +0 -5
  524. package/dist/types/src/cli/formatters/json-formatter.d.ts +0 -48
  525. package/dist/types/src/cli/formatters/simple-formatter.d.ts +0 -8
  526. package/dist/types/src/cli/index.d.ts +0 -5
  527. package/dist/types/src/cli/output-manager.d.ts +0 -32
  528. package/dist/types/src/cli/summary-reporter.d.ts +0 -28
  529. package/dist/types/src/cli.d.ts +0 -28
  530. package/dist/types/src/custom-rule-loader.d.ts +0 -62
  531. package/dist/types/src/herb-disable-comment-utils.d.ts +0 -69
  532. package/dist/types/src/herb-lint.d.ts +0 -2
  533. package/dist/types/src/index.d.ts +0 -4
  534. package/dist/types/src/linter-ignore.d.ts +0 -12
  535. package/dist/types/src/linter.d.ts +0 -133
  536. package/dist/types/src/loader.d.ts +0 -20
  537. package/dist/types/src/rules/erb-comment-syntax.d.ts +0 -14
  538. package/dist/types/src/rules/erb-no-extra-newline.d.ts +0 -14
  539. package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +0 -18
  540. package/dist/types/src/rules/erb-require-trailing-newline.d.ts +0 -9
  541. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +0 -18
  542. package/dist/types/src/rules/erb-right-trim.d.ts +0 -14
  543. package/dist/types/src/rules/erb-strict-locals-comment-syntax.d.ts +0 -9
  544. package/dist/types/src/rules/erb-strict-locals-required.d.ts +0 -9
  545. package/dist/types/src/rules/file-utils.d.ts +0 -13
  546. package/dist/types/src/rules/herb-disable-comment-base.d.ts +0 -37
  547. package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +0 -8
  548. package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +0 -8
  549. package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +0 -8
  550. package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +0 -8
  551. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +0 -8
  552. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +0 -8
  553. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +0 -15
  554. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +0 -14
  555. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +0 -15
  556. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +0 -8
  557. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +0 -14
  558. package/dist/types/src/rules/html-head-only-elements.d.ts +0 -9
  559. package/dist/types/src/rules/html-iframe-has-title.d.ts +0 -8
  560. package/dist/types/src/rules/html-img-require-alt.d.ts +0 -8
  561. package/dist/types/src/rules/html-input-require-autocomplete.d.ts +0 -8
  562. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +0 -8
  563. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +0 -8
  564. package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +0 -9
  565. package/dist/types/src/rules/html-no-nested-links.d.ts +0 -8
  566. package/dist/types/src/rules/html-no-self-closing.d.ts +0 -16
  567. package/dist/types/src/rules/html-no-space-in-tag.d.ts +0 -16
  568. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +0 -8
  569. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +0 -18
  570. package/dist/types/src/rules/index.d.ts +0 -54
  571. package/dist/types/src/rules/parser-no-errors.d.ts +0 -9
  572. package/dist/types/src/rules/rule-utils.d.ts +0 -351
  573. package/dist/types/src/rules/string-utils.d.ts +0 -15
  574. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +0 -16
  575. package/dist/types/src/rules.d.ts +0 -2
  576. package/dist/types/src/types.d.ts +0 -190
  577. /package/dist/{src/cli → cli}/formatters/base-formatter.js +0 -0
  578. /package/dist/{src/cli → cli}/formatters/github-actions-formatter.js +0 -0
  579. /package/dist/{src/cli → cli}/formatters/index.js +0 -0
  580. /package/dist/{src/cli → cli}/formatters/json-formatter.js +0 -0
  581. /package/dist/{src/cli → cli}/index.js +0 -0
  582. /package/dist/{src/cli → cli}/output-manager.js +0 -0
  583. /package/dist/{src/herb-disable-comment-utils.js → herb-disable-comment-utils.js} +0 -0
  584. /package/dist/{src/linter-ignore.js → linter-ignore.js} +0 -0
  585. /package/dist/{src/rules → rules}/file-utils.js +0 -0
  586. /package/dist/{src/rules → rules}/herb-disable-comment-base.js +0 -0
  587. /package/dist/{src/rules → rules}/string-utils.js +0 -0
@@ -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
+ }
@@ -0,0 +1,44 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+ import { isERBOutputNode } from "@herb-tools/core"
4
+
5
+ import type { ERBRenderNode, ParseResult, ParserOptions } from "@herb-tools/core"
6
+ import type { FullRuleConfig, LintContext, UnboundLintOffense } from "../types.js"
7
+
8
+ class ActionViewNoSilentRenderVisitor extends BaseRuleVisitor {
9
+ visitERBRenderNode(node: ERBRenderNode): void {
10
+ if (!isERBOutputNode(node)) {
11
+ this.addOffense(
12
+ `Avoid using \`${node.tag_opening?.value} %>\` with \`render\`. Use \`<%= %>\` to ensure the rendered content is output.`,
13
+ node.location,
14
+ )
15
+ }
16
+
17
+ this.visitChildNodes(node)
18
+ }
19
+ }
20
+
21
+ export class ActionViewNoSilentRenderRule extends ParserRule {
22
+ static ruleName = "actionview-no-silent-render"
23
+
24
+ get defaultConfig(): FullRuleConfig {
25
+ return {
26
+ enabled: true,
27
+ severity: "error"
28
+ }
29
+ }
30
+
31
+ get parserOptions(): Partial<ParserOptions> {
32
+ return {
33
+ render_nodes: true,
34
+ }
35
+ }
36
+
37
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
38
+ const visitor = new ActionViewNoSilentRenderVisitor(this.ruleName, context)
39
+
40
+ visitor.visit(result.value)
41
+
42
+ return visitor.offenses
43
+ }
44
+ }
@@ -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
 
@@ -28,10 +28,12 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
28
28
  if (!this.isAllowedContent(child)) {
29
29
  const childCode = IdentityPrinter.print(child).trim()
30
30
 
31
- this.addOffense(
31
+ const offense = this.createOffense(
32
32
  `Do not place \`${childCode}\` between \`${caseCode}\` and \`${conditionCode}\`. Content here is not part of any branch and will not be rendered.`,
33
33
  child.location,
34
34
  )
35
+ offense.tags = ["unnecessary"]
36
+ this.offenses.push(offense)
35
37
  }
36
38
  }
37
39
  }
@@ -49,7 +51,7 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
49
51
  }
50
52
 
51
53
  export class ERBNoCaseNodeChildrenRule extends ParserRule {
52
- name = "erb-no-case-node-children"
54
+ static ruleName = "erb-no-case-node-children"
53
55
 
54
56
  get defaultConfig(): FullRuleConfig {
55
57
  return {
@@ -59,7 +61,7 @@ export class ERBNoCaseNodeChildrenRule extends ParserRule {
59
61
  }
60
62
 
61
63
  check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
62
- const visitor = new ERBNoCaseNodeChildrenVisitor(this.name, context)
64
+ const visitor = new ERBNoCaseNodeChildrenVisitor(this.ruleName, context)
63
65
 
64
66
  visitor.visit(result.value)
65
67
 
@@ -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
+ }
@@ -0,0 +1,436 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+ import { IdentityPrinter } from "@herb-tools/printer"
4
+
5
+ import {
6
+ isHTMLElementNode,
7
+ isHTMLOpenTagNode,
8
+ isERBIfNode,
9
+ isERBElseNode,
10
+ isERBUnlessNode,
11
+ isERBCaseNode,
12
+ isERBWhenNode,
13
+ isEquivalentElement,
14
+ isPureWhitespaceNode,
15
+ findParentArray,
16
+ removeNodeFromArray,
17
+ replaceNodeWithBody,
18
+ createLiteral,
19
+ HTMLElementNode,
20
+ Location,
21
+ } from "@herb-tools/core"
22
+
23
+ import type { BaseAutofixContext, UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
24
+ import type { ParseResult, Node, ERBIfNode, ERBUnlessNode, ERBCaseNode, ERBElseNode } from "@herb-tools/core"
25
+ import type { Mutable } from "@herb-tools/rewriter"
26
+
27
+ type ConditionalNode = ERBIfNode | ERBUnlessNode | ERBCaseNode
28
+
29
+ interface DuplicateBranchAutofixContext extends BaseAutofixContext {
30
+ node: Mutable<ConditionalNode>
31
+ allIdentical?: boolean
32
+ }
33
+
34
+ function getSignificantNodes(statements: Node[]): Node[] {
35
+ return statements.filter(node => !isPureWhitespaceNode(node))
36
+ }
37
+
38
+ function trimWhitespaceNodes(nodes: Node[]): Node[] {
39
+ let start = 0
40
+ let end = nodes.length
41
+ while (start < end && isPureWhitespaceNode(nodes[start])) start++
42
+ while (end > start && isPureWhitespaceNode(nodes[end - 1])) end--
43
+ return nodes.slice(start, end)
44
+ }
45
+
46
+ function allEquivalentElements(nodes: Node[]): nodes is HTMLElementNode[] {
47
+ if (nodes.length < 2) return false
48
+ if (!nodes.every(node => isHTMLElementNode(node))) return false
49
+
50
+ const first = nodes[0]
51
+
52
+ return nodes.slice(1).every(node => isEquivalentElement(first, node as HTMLElementNode))
53
+ }
54
+
55
+ function collectBranchesFromIf(node: ERBIfNode): Node[][] | null {
56
+ const branches: Node[][] = []
57
+ let current: ERBIfNode | ERBElseNode | null = node.subsequent
58
+
59
+ branches.push(node.statements)
60
+
61
+ while (current) {
62
+ if (isERBElseNode(current)) {
63
+ branches.push(current.statements)
64
+ return branches
65
+ }
66
+
67
+ if (isERBIfNode(current)) {
68
+ branches.push(current.statements)
69
+ current = current.subsequent
70
+ } else {
71
+ break
72
+ }
73
+ }
74
+
75
+ return null
76
+ }
77
+
78
+ function collectBranchesFromUnless(node: ERBUnlessNode): Node[][] | null {
79
+ if (!node.else_clause) return null
80
+
81
+ return [node.statements, node.else_clause.statements]
82
+ }
83
+
84
+ function collectBranchesFromCase(node: ERBCaseNode): Node[][] | null {
85
+ if (!node.else_clause) return null
86
+
87
+ const branches: Node[][] = []
88
+
89
+ for (const condition of node.conditions) {
90
+ if (isERBWhenNode(condition)) {
91
+ branches.push(condition.statements)
92
+ }
93
+ }
94
+
95
+ branches.push(node.else_clause.statements)
96
+
97
+ return branches
98
+ }
99
+
100
+ function collectBranches(node: ConditionalNode): Node[][] | null {
101
+ if (isERBIfNode(node)) return collectBranchesFromIf(node)
102
+ if (isERBUnlessNode(node)) return collectBranchesFromUnless(node)
103
+ if (isERBCaseNode(node)) return collectBranchesFromCase(node)
104
+
105
+ return null
106
+ }
107
+
108
+ function findCommonPrefixCount(branches: Node[][], minLength: number): number {
109
+ let count = 0
110
+
111
+ for (let index = 0; index < minLength; index++) {
112
+ const nodesAtIndex = branches.map(branch => branch[index])
113
+
114
+ if (allEquivalentElements(nodesAtIndex)) {
115
+ count++
116
+ } else {
117
+ break
118
+ }
119
+ }
120
+
121
+ return count
122
+ }
123
+
124
+ function findCommonSuffixCount(branches: Node[][], minLength: number, prefixCount: number): number {
125
+ let count = 0
126
+
127
+ for (let offset = 0; offset < minLength - prefixCount; offset++) {
128
+ const nodesAtOffset = branches.map(branch => branch[branch.length - 1 - offset])
129
+
130
+ if (allEquivalentElements(nodesAtOffset)) {
131
+ count++
132
+ } else {
133
+ break
134
+ }
135
+ }
136
+
137
+ return count
138
+ }
139
+
140
+ function createWrapper(template: HTMLElementNode, body: Node[]): HTMLElementNode {
141
+ return new HTMLElementNode({
142
+ type: "AST_HTML_ELEMENT_NODE",
143
+ open_tag: template.open_tag,
144
+ tag_name: template.tag_name,
145
+ body,
146
+ close_tag: template.close_tag,
147
+ is_void: template.is_void,
148
+ element_source: template.element_source,
149
+ location: Location.zero,
150
+ errors: [],
151
+ })
152
+ }
153
+
154
+ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor<DuplicateBranchAutofixContext> {
155
+ private processedIfNodes = new Set<Node>()
156
+
157
+ visitERBIfNode(node: ERBIfNode): void {
158
+ if (this.processedIfNodes.has(node)) {
159
+ this.visitChildNodes(node)
160
+ return
161
+ }
162
+
163
+ this.checkConditionalNode(node)
164
+ this.visitChildNodes(node)
165
+ }
166
+
167
+ visitERBUnlessNode(node: ERBUnlessNode): void {
168
+ this.checkConditionalNode(node)
169
+ this.visitChildNodes(node)
170
+ }
171
+
172
+ visitERBCaseNode(node: ERBCaseNode): void {
173
+ this.checkConditionalNode(node)
174
+ this.visitChildNodes(node)
175
+ }
176
+
177
+ private checkConditionalNode(node: ConditionalNode): void {
178
+ const branches = collectBranches(node)
179
+ if (!branches) return
180
+
181
+ if (isERBIfNode(node)) {
182
+ this.markSubsequentIfNodesAsProcessed(node)
183
+ }
184
+
185
+ if (this.allBranchesIdentical(branches)) {
186
+ this.addOffense(
187
+ "All branches of this conditional have identical content. The conditional can be removed.",
188
+ node.location,
189
+ { node: node as Mutable<ConditionalNode>, allIdentical: true },
190
+ "warning",
191
+ )
192
+
193
+ return
194
+ }
195
+
196
+ const state = { isFirstOffense: true }
197
+ this.checkBranches(branches, node, state)
198
+ }
199
+
200
+ private allBranchesIdentical(branches: Node[][]): boolean {
201
+ if (branches.length < 2) return false
202
+
203
+ const first = branches[0].map(node => IdentityPrinter.print(node)).join("")
204
+
205
+ return branches.slice(1).every(branch =>
206
+ branch.map(node => IdentityPrinter.print(node)).join("") === first
207
+ )
208
+ }
209
+
210
+ private markSubsequentIfNodesAsProcessed(node: ERBIfNode): void {
211
+ let current: ERBIfNode | ERBElseNode | null = node.subsequent
212
+
213
+ while (current) {
214
+ if (isERBIfNode(current)) {
215
+ this.processedIfNodes.add(current)
216
+ current = current.subsequent
217
+ } else {
218
+ break
219
+ }
220
+ }
221
+ }
222
+
223
+ private checkBranches(branches: Node[][], conditionalNode: ConditionalNode, state: { isFirstOffense: boolean }): void {
224
+ const significantBranches = branches.map(getSignificantNodes)
225
+ if (significantBranches.some(branch => branch.length === 0)) return
226
+
227
+ const minLength = Math.min(...significantBranches.map(branch => branch.length))
228
+ const prefixCount = findCommonPrefixCount(significantBranches, minLength)
229
+ const suffixCount = findCommonSuffixCount(significantBranches, minLength, prefixCount)
230
+
231
+ for (let index = 0; index < prefixCount; index++) {
232
+ const elements = significantBranches.map(branch => branch[index] as HTMLElementNode)
233
+ this.reportAndRecurse(elements, conditionalNode, state)
234
+ }
235
+
236
+ for (let offset = 0; offset < suffixCount; offset++) {
237
+ const elements = significantBranches.map(branch => branch[branch.length - 1 - offset] as HTMLElementNode)
238
+ this.reportAndRecurse(elements, conditionalNode, state)
239
+ }
240
+ }
241
+
242
+ private reportAndRecurse(elements: HTMLElementNode[], conditionalNode: ConditionalNode, state: { isFirstOffense: boolean }): void {
243
+ const bodies = elements.map(element => element.body)
244
+ const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]))
245
+
246
+ for (const element of elements) {
247
+ const printed = IdentityPrinter.print(element.open_tag)
248
+
249
+ if (bodiesMatch) {
250
+ const autofixContext = state.isFirstOffense
251
+ ? { node: conditionalNode as Mutable<ConditionalNode> }
252
+ : undefined
253
+
254
+ this.addOffense(
255
+ `The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`,
256
+ element.location,
257
+ autofixContext,
258
+ )
259
+
260
+ state.isFirstOffense = false
261
+ } else {
262
+ const autofixContext = state.isFirstOffense
263
+ ? { node: conditionalNode as Mutable<ConditionalNode> }
264
+ : undefined
265
+
266
+ const tagNameLocation = isHTMLOpenTagNode(element.open_tag) && element.open_tag.tag_name?.location
267
+ ? element.open_tag.tag_name.location
268
+ : element?.open_tag?.location || element.location
269
+
270
+ this.addOffense(
271
+ `The \`${printed}\` tag is repeated across all branches with different content. Consider extracting the shared tag outside the conditional.`,
272
+ tagNameLocation,
273
+ autofixContext,
274
+ "hint",
275
+ )
276
+
277
+ state.isFirstOffense = false
278
+ }
279
+ }
280
+
281
+ if (!bodiesMatch && bodies.every(body => body.length > 0)) {
282
+ this.checkBranches(bodies, conditionalNode, state)
283
+ }
284
+ }
285
+ }
286
+
287
+ export class ERBNoDuplicateBranchElementsRule extends ParserRule<DuplicateBranchAutofixContext> {
288
+ static ruleName = "erb-no-duplicate-branch-elements"
289
+ static autocorrectable = true
290
+ static reindentAfterAutofix = true
291
+
292
+ get defaultConfig(): FullRuleConfig {
293
+ return {
294
+ enabled: true,
295
+ severity: "warning",
296
+ }
297
+ }
298
+
299
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<DuplicateBranchAutofixContext>[] {
300
+ const visitor = new ERBNoDuplicateBranchElementsVisitor(this.ruleName, context)
301
+
302
+ visitor.visit(result.value)
303
+
304
+ return visitor.offenses
305
+ }
306
+
307
+ autofix(offense: LintOffense<DuplicateBranchAutofixContext>, result: ParseResult): ParseResult | null {
308
+ if (!offense.autofixContext) return null
309
+
310
+ const conditionalNode = offense.autofixContext.node
311
+ const branches = collectBranches(conditionalNode as ConditionalNode)
312
+ if (!branches) return null
313
+
314
+ if (offense.autofixContext.allIdentical) {
315
+ const parentInfo = findParentArray(result.value, conditionalNode as unknown as Node)
316
+ if (!parentInfo) return null
317
+
318
+ const { array: parentArray, index: conditionalIndex } = parentInfo
319
+ const firstBranchContent = trimWhitespaceNodes(branches[0])
320
+
321
+ parentArray.splice(conditionalIndex, 1, ...firstBranchContent)
322
+
323
+ return result
324
+ }
325
+
326
+ const significantBranches = branches.map(getSignificantNodes)
327
+ if (significantBranches.some(branch => branch.length === 0)) return null
328
+
329
+ const minLength = Math.min(...significantBranches.map(branch => branch.length))
330
+ const prefixCount = findCommonPrefixCount(significantBranches, minLength)
331
+ const suffixCount = findCommonSuffixCount(significantBranches, minLength, prefixCount)
332
+
333
+ if (prefixCount === 0 && suffixCount === 0) return null
334
+
335
+ const parentInfo = findParentArray(result.value, conditionalNode as unknown as Node)
336
+ if (!parentInfo) return null
337
+
338
+ let { array: parentArray, index: conditionalIndex } = parentInfo
339
+ let hasWrapped = false
340
+ let didMutate = false
341
+ let failedToHoistPrefix = false
342
+ let hoistedBefore = false
343
+
344
+ const hoistElement = (elements: HTMLElementNode[], position: "before" | "after"): void => {
345
+ const actualPosition = (position === "before" && failedToHoistPrefix) ? "after" : position
346
+ const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]))
347
+
348
+ if (bodiesMatch) {
349
+ if (actualPosition === "after") {
350
+ const currentLengths = branches.map(b => getSignificantNodes(b as Node[]).length)
351
+ if (currentLengths.some(l => l !== currentLengths[0])) return
352
+ }
353
+
354
+ if (actualPosition === "after" && position === "before") {
355
+ const isAtEnd = branches.every((branch, index) => {
356
+ const nodes = getSignificantNodes(branch as Node[])
357
+
358
+ return nodes.length > 0 && nodes[nodes.length - 1] === elements[index]
359
+ })
360
+
361
+ if (!isAtEnd) return
362
+ }
363
+
364
+ for (let i = 0; i < branches.length; i++) {
365
+ removeNodeFromArray(branches[i] as Node[], elements[i])
366
+ }
367
+
368
+ if (actualPosition === "before") {
369
+ parentArray.splice(conditionalIndex, 0, elements[0], createLiteral("\n"))
370
+ conditionalIndex += 2
371
+ hoistedBefore = true
372
+ } else {
373
+ parentArray.splice(conditionalIndex + 1, 0, createLiteral("\n"), elements[0])
374
+ }
375
+
376
+ didMutate = true
377
+ } else {
378
+ if (hasWrapped) return
379
+
380
+ const canWrap = branches.every((branch, index) => {
381
+ const remaining = getSignificantNodes(branch)
382
+
383
+ return remaining.length === 1 && remaining[0] === elements[index]
384
+ })
385
+
386
+ if (!canWrap) {
387
+ if (position === "before") failedToHoistPrefix = true
388
+ return
389
+ }
390
+
391
+ for (let i = 0; i < branches.length; i++) {
392
+ replaceNodeWithBody(branches[i] as Node[], elements[i])
393
+ }
394
+
395
+ const wrapper = createWrapper(elements[0], [createLiteral("\n"), conditionalNode as unknown as Node, createLiteral("\n")])
396
+
397
+ parentArray[conditionalIndex] = wrapper
398
+ parentArray = wrapper.body as Node[]
399
+ conditionalIndex = 1
400
+ hasWrapped = true
401
+ didMutate = true
402
+ }
403
+ }
404
+
405
+ for (let index = 0; index < prefixCount; index++) {
406
+ const elements = significantBranches.map(branch => branch[index] as HTMLElementNode)
407
+ hoistElement(elements, "before")
408
+ }
409
+
410
+ for (let offset = 0; offset < suffixCount; offset++) {
411
+ const elements = significantBranches.map(branch => branch[branch.length - 1 - offset] as HTMLElementNode)
412
+ hoistElement(elements, "after")
413
+ }
414
+
415
+ if (!hasWrapped && hoistedBefore) {
416
+ const remaining = branches.map(branch => getSignificantNodes(branch as Node[]))
417
+
418
+ if (remaining.every(branch => branch.length === 1) && allEquivalentElements(remaining.map(b => b[0]))) {
419
+ const elements = remaining.map(b => b[0] as HTMLElementNode)
420
+ const bodiesMatch = elements.every(el => IdentityPrinter.print(el) === IdentityPrinter.print(elements[0]))
421
+
422
+ if (!bodiesMatch && elements.every(el => el.body.length > 0)) {
423
+ for (let i = 0; i < branches.length; i++) {
424
+ replaceNodeWithBody(branches[i] as Node[], elements[i])
425
+ }
426
+
427
+ const wrapper = createWrapper(elements[0], [createLiteral("\n"), conditionalNode as unknown as Node, createLiteral("\n")])
428
+ parentArray[conditionalIndex] = wrapper
429
+ didMutate = true
430
+ }
431
+ }
432
+ }
433
+
434
+ return didMutate ? result : null
435
+ }
436
+ }