@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
@@ -0,0 +1,320 @@
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
+ isERBIfNode,
8
+ isERBElseNode,
9
+ isERBUnlessNode,
10
+ isERBCaseNode,
11
+ isERBWhenNode,
12
+ isEquivalentElement,
13
+ isPureWhitespaceNode,
14
+ findParentArray,
15
+ removeNodeFromArray,
16
+ replaceNodeWithBody,
17
+ createLiteral,
18
+ HTMLElementNode,
19
+ Location,
20
+ } from "@herb-tools/core"
21
+
22
+ import type { BaseAutofixContext, UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
23
+ import type { ParseResult, Node, ERBIfNode, ERBUnlessNode, ERBCaseNode, ERBElseNode } from "@herb-tools/core"
24
+ import type { Mutable } from "@herb-tools/rewriter"
25
+
26
+ type ConditionalNode = ERBIfNode | ERBUnlessNode | ERBCaseNode
27
+
28
+ interface DuplicateBranchAutofixContext extends BaseAutofixContext {
29
+ node: Mutable<ConditionalNode>
30
+ }
31
+
32
+ function getSignificantNodes(statements: Node[]): Node[] {
33
+ return statements.filter(node => !isPureWhitespaceNode(node))
34
+ }
35
+
36
+ function allEquivalentElements(nodes: Node[]): nodes is HTMLElementNode[] {
37
+ if (nodes.length < 2) return false
38
+ if (!nodes.every(node => isHTMLElementNode(node))) return false
39
+
40
+ const first = nodes[0]
41
+
42
+ return nodes.slice(1).every(node => isEquivalentElement(first, node as HTMLElementNode))
43
+ }
44
+
45
+ function collectBranchesFromIf(node: ERBIfNode): Node[][] | null {
46
+ const branches: Node[][] = []
47
+ let current: ERBIfNode | ERBElseNode | null = node.subsequent
48
+
49
+ branches.push(node.statements)
50
+
51
+ while (current) {
52
+ if (isERBElseNode(current)) {
53
+ branches.push(current.statements)
54
+ return branches
55
+ }
56
+
57
+ if (isERBIfNode(current)) {
58
+ branches.push(current.statements)
59
+ current = current.subsequent
60
+ } else {
61
+ break
62
+ }
63
+ }
64
+
65
+ return null
66
+ }
67
+
68
+ function collectBranchesFromUnless(node: ERBUnlessNode): Node[][] | null {
69
+ if (!node.else_clause) return null
70
+
71
+ return [node.statements, node.else_clause.statements]
72
+ }
73
+
74
+ function collectBranchesFromCase(node: ERBCaseNode): Node[][] | null {
75
+ if (!node.else_clause) return null
76
+
77
+ const branches: Node[][] = []
78
+
79
+ for (const condition of node.conditions) {
80
+ if (isERBWhenNode(condition)) {
81
+ branches.push(condition.statements)
82
+ }
83
+ }
84
+
85
+ branches.push(node.else_clause.statements)
86
+
87
+ return branches
88
+ }
89
+
90
+ function collectBranches(node: ConditionalNode): Node[][] | null {
91
+ if (isERBIfNode(node)) return collectBranchesFromIf(node)
92
+ if (isERBUnlessNode(node)) return collectBranchesFromUnless(node)
93
+ if (isERBCaseNode(node)) return collectBranchesFromCase(node)
94
+
95
+ return null
96
+ }
97
+
98
+ function findCommonPrefixCount(branches: Node[][], minLength: number): number {
99
+ let count = 0
100
+
101
+ for (let index = 0; index < minLength; index++) {
102
+ const nodesAtIndex = branches.map(branch => branch[index])
103
+
104
+ if (allEquivalentElements(nodesAtIndex)) {
105
+ count++
106
+ } else {
107
+ break
108
+ }
109
+ }
110
+
111
+ return count
112
+ }
113
+
114
+ function findCommonSuffixCount(branches: Node[][], minLength: number, prefixCount: number): number {
115
+ let count = 0
116
+
117
+ for (let offset = 0; offset < minLength - prefixCount; offset++) {
118
+ const nodesAtOffset = branches.map(branch => branch[branch.length - 1 - offset])
119
+
120
+ if (allEquivalentElements(nodesAtOffset)) {
121
+ count++
122
+ } else {
123
+ break
124
+ }
125
+ }
126
+
127
+ return count
128
+ }
129
+
130
+ function createWrapper(template: HTMLElementNode, body: Node[]): HTMLElementNode {
131
+ return new HTMLElementNode({
132
+ type: "AST_HTML_ELEMENT_NODE",
133
+ open_tag: template.open_tag,
134
+ tag_name: template.tag_name,
135
+ body,
136
+ close_tag: template.close_tag,
137
+ is_void: template.is_void,
138
+ element_source: template.element_source,
139
+ location: Location.zero,
140
+ errors: [],
141
+ })
142
+ }
143
+
144
+ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor<DuplicateBranchAutofixContext> {
145
+ private processedIfNodes = new Set<Node>()
146
+
147
+ visitERBIfNode(node: ERBIfNode): void {
148
+ if (this.processedIfNodes.has(node)) {
149
+ this.visitChildNodes(node)
150
+ return
151
+ }
152
+
153
+ this.checkConditionalNode(node)
154
+ this.visitChildNodes(node)
155
+ }
156
+
157
+ visitERBUnlessNode(node: ERBUnlessNode): void {
158
+ this.checkConditionalNode(node)
159
+ this.visitChildNodes(node)
160
+ }
161
+
162
+ visitERBCaseNode(node: ERBCaseNode): void {
163
+ this.checkConditionalNode(node)
164
+ this.visitChildNodes(node)
165
+ }
166
+
167
+ private checkConditionalNode(node: ConditionalNode): void {
168
+ const branches = collectBranches(node)
169
+ if (!branches) return
170
+
171
+ if (isERBIfNode(node)) {
172
+ this.markSubsequentIfNodesAsProcessed(node)
173
+ }
174
+
175
+ const state = { isFirstOffense: true }
176
+ this.checkBranches(branches, node, state)
177
+ }
178
+
179
+ private markSubsequentIfNodesAsProcessed(node: ERBIfNode): void {
180
+ let current: ERBIfNode | ERBElseNode | null = node.subsequent
181
+
182
+ while (current) {
183
+ if (isERBIfNode(current)) {
184
+ this.processedIfNodes.add(current)
185
+ current = current.subsequent
186
+ } else {
187
+ break
188
+ }
189
+ }
190
+ }
191
+
192
+ private checkBranches(branches: Node[][], conditionalNode: ConditionalNode, state: { isFirstOffense: boolean }): void {
193
+ const significantBranches = branches.map(getSignificantNodes)
194
+ if (significantBranches.some(branch => branch.length === 0)) return
195
+
196
+ const minLength = Math.min(...significantBranches.map(branch => branch.length))
197
+ const prefixCount = findCommonPrefixCount(significantBranches, minLength)
198
+ const suffixCount = findCommonSuffixCount(significantBranches, minLength, prefixCount)
199
+
200
+ for (let index = 0; index < prefixCount; index++) {
201
+ const elements = significantBranches.map(branch => branch[index] as HTMLElementNode)
202
+ this.reportAndRecurse(elements, conditionalNode, state)
203
+ }
204
+
205
+ for (let offset = 0; offset < suffixCount; offset++) {
206
+ const elements = significantBranches.map(branch => branch[branch.length - 1 - offset] as HTMLElementNode)
207
+ this.reportAndRecurse(elements, conditionalNode, state)
208
+ }
209
+ }
210
+
211
+ private reportAndRecurse(elements: HTMLElementNode[], conditionalNode: ConditionalNode, state: { isFirstOffense: boolean }): void {
212
+ const bodies = elements.map(element => element.body)
213
+ const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]))
214
+
215
+ for (const element of elements) {
216
+ const printed = IdentityPrinter.print(element.open_tag)
217
+ const autofixContext = state.isFirstOffense
218
+ ? { node: conditionalNode as Mutable<ConditionalNode> }
219
+ : undefined
220
+
221
+ this.addOffense(
222
+ `The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`,
223
+ bodiesMatch ? element.location : (element?.open_tag?.location || element.location),
224
+ autofixContext,
225
+ )
226
+
227
+ state.isFirstOffense = false
228
+ }
229
+
230
+ if (!bodiesMatch && bodies.every(body => body.length > 0)) {
231
+ this.checkBranches(bodies, conditionalNode, state)
232
+ }
233
+ }
234
+ }
235
+
236
+ export class ERBNoDuplicateBranchElementsRule extends ParserRule<DuplicateBranchAutofixContext> {
237
+ static ruleName = "erb-no-duplicate-branch-elements"
238
+ static autocorrectable = true
239
+ static reindentAfterAutofix = true
240
+
241
+ get defaultConfig(): FullRuleConfig {
242
+ return {
243
+ enabled: true,
244
+ severity: "warning",
245
+ }
246
+ }
247
+
248
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<DuplicateBranchAutofixContext>[] {
249
+ const visitor = new ERBNoDuplicateBranchElementsVisitor(this.ruleName, context)
250
+
251
+ visitor.visit(result.value)
252
+
253
+ return visitor.offenses
254
+ }
255
+
256
+ autofix(offense: LintOffense<DuplicateBranchAutofixContext>, result: ParseResult): ParseResult | null {
257
+ if (!offense.autofixContext) return null
258
+
259
+ const conditionalNode = offense.autofixContext.node
260
+ const branches = collectBranches(conditionalNode as ConditionalNode)
261
+ if (!branches) return null
262
+
263
+ const significantBranches = branches.map(getSignificantNodes)
264
+ if (significantBranches.some(branch => branch.length === 0)) return null
265
+
266
+ const minLength = Math.min(...significantBranches.map(branch => branch.length))
267
+ const prefixCount = findCommonPrefixCount(significantBranches, minLength)
268
+ const suffixCount = findCommonSuffixCount(significantBranches, minLength, prefixCount)
269
+
270
+ if (prefixCount === 0 && suffixCount === 0) return null
271
+
272
+ const parentInfo = findParentArray(result.value, conditionalNode as unknown as Node)
273
+ if (!parentInfo) return null
274
+
275
+ let { array: parentArray, index: conditionalIndex } = parentInfo
276
+ let hasWrapped = false
277
+
278
+ const hoistElement = (elements: HTMLElementNode[], position: "before" | "after"): void => {
279
+ const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]))
280
+
281
+ if (bodiesMatch) {
282
+ for (let i = 0; i < branches.length; i++) {
283
+ removeNodeFromArray(branches[i] as Node[], elements[i])
284
+ }
285
+
286
+ if (position === "before") {
287
+ parentArray.splice(conditionalIndex, 0, elements[0])
288
+ conditionalIndex++
289
+ } else {
290
+ parentArray.splice(conditionalIndex + 1, 0, elements[0])
291
+ }
292
+ } else {
293
+ if (hasWrapped) return
294
+
295
+ for (let i = 0; i < branches.length; i++) {
296
+ replaceNodeWithBody(branches[i] as Node[], elements[i])
297
+ }
298
+
299
+ const wrapper = createWrapper(elements[0], [createLiteral("\n"), conditionalNode as unknown as Node, createLiteral("\n")])
300
+
301
+ parentArray[conditionalIndex] = wrapper
302
+ parentArray = wrapper.body as Node[]
303
+ conditionalIndex = 1
304
+ hasWrapped = true
305
+ }
306
+ }
307
+
308
+ for (let index = 0; index < prefixCount; index++) {
309
+ const elements = significantBranches.map(branch => branch[index] as HTMLElementNode)
310
+ hoistElement(elements, "before")
311
+ }
312
+
313
+ for (let offset = 0; offset < suffixCount; offset++) {
314
+ const elements = significantBranches.map(branch => branch[branch.length - 1 - offset] as HTMLElementNode)
315
+ hoistElement(elements, "after")
316
+ }
317
+
318
+ return result
319
+ }
320
+ }
@@ -22,7 +22,7 @@ class ERBNoEmptyTagsVisitor extends BaseRuleVisitor {
22
22
  }
23
23
 
24
24
  export class ERBNoEmptyTagsRule extends ParserRule {
25
- name = "erb-no-empty-tags"
25
+ static ruleName = "erb-no-empty-tags"
26
26
 
27
27
  get defaultConfig(): FullRuleConfig {
28
28
  return {
@@ -32,7 +32,7 @@ export class ERBNoEmptyTagsRule extends ParserRule {
32
32
  }
33
33
 
34
34
  check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
35
- const visitor = new ERBNoEmptyTagsVisitor(this.name, context)
35
+ const visitor = new ERBNoEmptyTagsVisitor(this.ruleName, context)
36
36
 
37
37
  visitor.visit(result.value)
38
38
 
@@ -1,8 +1,7 @@
1
- import { BaseSourceRuleVisitor } from "./rule-utils.js"
2
- import { SourceRule } from "../types.js"
3
- import { Location, Position } from "@herb-tools/core"
1
+ import { type Node, Location } from "@herb-tools/core"
4
2
 
5
- import type { Node } from "@herb-tools/core"
3
+ import { BaseSourceRuleVisitor, positionFromOffset } from "./rule-utils.js"
4
+ import { SourceRule } from "../types.js"
6
5
  import type { UnboundLintOffense, LintOffense, LintContext, BaseAutofixContext, FullRuleConfig } from "../types.js"
7
6
 
8
7
  interface ERBNoExtraNewLineAutofixContext extends BaseAutofixContext {
@@ -10,25 +9,6 @@ interface ERBNoExtraNewLineAutofixContext extends BaseAutofixContext {
10
9
  endOffset: number
11
10
  }
12
11
 
13
- function positionFromOffset(source: string, offset: number): Position {
14
- let line = 1
15
- let column = 0
16
- let currentOffset = 0
17
-
18
- for (let i = 0; i < source.length && currentOffset < offset; i++) {
19
- const char = source[i]
20
- currentOffset++
21
- if (char === "\n") {
22
- line++
23
- column = 0
24
- } else {
25
- column++
26
- }
27
- }
28
-
29
- return new Position(line, column)
30
- }
31
-
32
12
  class ERBNoExtraNewLineVisitor extends BaseSourceRuleVisitor<ERBNoExtraNewLineAutofixContext> {
33
13
  protected visitSource(source: string): void {
34
14
  if (source.length === 0) return
@@ -61,7 +41,7 @@ class ERBNoExtraNewLineVisitor extends BaseSourceRuleVisitor<ERBNoExtraNewLineAu
61
41
 
62
42
  export class ERBNoExtraNewLineRule extends SourceRule {
63
43
  static autocorrectable = true
64
- name = "erb-no-extra-newline"
44
+ static ruleName = "erb-no-extra-newline"
65
45
 
66
46
  get defaultConfig(): FullRuleConfig {
67
47
  return {
@@ -71,7 +51,7 @@ export class ERBNoExtraNewLineRule extends SourceRule {
71
51
  }
72
52
 
73
53
  check(source: string, context?: Partial<LintContext>): UnboundLintOffense[] {
74
- const visitor = new ERBNoExtraNewLineVisitor(this.name, context)
54
+ const visitor = new ERBNoExtraNewLineVisitor(this.ruleName, context)
75
55
 
76
56
  visitor.visit(source)
77
57
 
@@ -3,7 +3,7 @@ import { BaseRuleVisitor } from "./rule-utils.js"
3
3
 
4
4
  import type { ParseResult, Token, ERBNode } from "@herb-tools/core"
5
5
  import { Location } from "@herb-tools/core"
6
- import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { UnboundLintOffense, LintOffense, LintContext, LintSeverity, FullRuleConfig } from "../types.js"
7
7
 
8
8
  interface ERBNoExtraWhitespaceAutofixContext extends BaseAutofixContext {
9
9
  node: Mutable<ERBNode>
@@ -26,11 +26,24 @@ class ERBNoExtraWhitespaceInsideTagsVisitor extends BaseRuleVisitor<ERBNoExtraWh
26
26
  this.reportWhitespace(node, openTag, closeTag, value, "start", 0, `Remove extra whitespace after \`${openTag.value}\`.`, "after-open")
27
27
  }
28
28
 
29
- if (openTag.value === "<%#" && value.startsWith("=") && value.length > 1) {
30
- const afterEquals = value.substring(1)
31
-
32
- if (afterEquals.match(/^\s{2,}/) && !afterEquals.startsWith(" \n") && !afterEquals.startsWith("\n")) {
33
- this.reportWhitespace(node, openTag, closeTag, value, "start", 1, `Remove extra whitespace after \`<%#=\`.`, "after-comment-equals")
29
+ if (openTag.value === "<%#") {
30
+ const prefix = this.getCommentedTagPrefix(value)
31
+
32
+ if (prefix) {
33
+ const afterPrefix = value.substring(prefix.length)
34
+ const tag = `<%#${prefix}`
35
+ const hasExtraWhitespace = afterPrefix.match(/^\s{2,}/) && !afterPrefix.startsWith(" \n") && !afterPrefix.startsWith("\n")
36
+
37
+ if (hasExtraWhitespace) {
38
+ this.reportWhitespace(node, openTag, closeTag, value, "start", prefix.length, `Remove extra whitespace after \`${tag}\`. This looks like a temporarily commented ERB tag.`, "after-comment-equals", "info")
39
+ } else {
40
+ this.addOffense(
41
+ `\`${tag}\` looks like a temporarily commented ERB tag.`,
42
+ openTag.location,
43
+ { node, openTag, closeTag, content: value, fixType: "after-comment-equals", unsafe: true },
44
+ "info"
45
+ )
46
+ }
34
47
  }
35
48
  }
36
49
 
@@ -39,6 +52,17 @@ class ERBNoExtraWhitespaceInsideTagsVisitor extends BaseRuleVisitor<ERBNoExtraWh
39
52
  }
40
53
  }
41
54
 
55
+ private getCommentedTagPrefix(content: string): string | null {
56
+ if (content.startsWith("graphql")) return "graphql"
57
+ if (content.startsWith("%=")) return "%="
58
+ if (content.startsWith("==")) return "=="
59
+ if (content.startsWith("%")) return "%"
60
+ if (content.startsWith("=")) return "="
61
+ if (content.startsWith("-")) return "-"
62
+
63
+ return null
64
+ }
65
+
42
66
  private hasExtraLeadingWhitespace(content: string): boolean {
43
67
  return content.startsWith(" ") && !content.startsWith(" \n")
44
68
  }
@@ -82,7 +106,9 @@ class ERBNoExtraWhitespaceInsideTagsVisitor extends BaseRuleVisitor<ERBNoExtraWh
82
106
  position: "start" | "end",
83
107
  offset: number,
84
108
  message: string,
85
- fixType: "after-open" | "before-close" | "after-comment-equals"
109
+ fixType: "after-open" | "before-close" | "after-comment-equals",
110
+ severity?: LintSeverity,
111
+ unsafe?: boolean,
86
112
  ): void {
87
113
  const location = this.getWhitespaceLocation(node, content, position, offset)
88
114
  this.addOffense(message, location, {
@@ -90,14 +116,15 @@ class ERBNoExtraWhitespaceInsideTagsVisitor extends BaseRuleVisitor<ERBNoExtraWh
90
116
  openTag,
91
117
  closeTag,
92
118
  content,
93
- fixType
94
- })
119
+ fixType,
120
+ unsafe,
121
+ }, severity)
95
122
  }
96
123
  }
97
124
 
98
125
  export class ERBNoExtraWhitespaceRule extends ParserRule<ERBNoExtraWhitespaceAutofixContext> {
99
126
  static autocorrectable = true
100
- name = "erb-no-extra-whitespace-inside-tags"
127
+ static ruleName = "erb-no-extra-whitespace-inside-tags"
101
128
 
102
129
  get defaultConfig(): FullRuleConfig {
103
130
  return {
@@ -107,7 +134,7 @@ export class ERBNoExtraWhitespaceRule extends ParserRule<ERBNoExtraWhitespaceAut
107
134
  }
108
135
 
109
136
  check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<ERBNoExtraWhitespaceAutofixContext>[] {
110
- const visitor = new ERBNoExtraWhitespaceInsideTagsVisitor(this.name, context)
137
+ const visitor = new ERBNoExtraWhitespaceInsideTagsVisitor(this.ruleName, context)
111
138
 
112
139
  visitor.visit(result.value)
113
140
 
@@ -131,13 +158,16 @@ export class ERBNoExtraWhitespaceRule extends ParserRule<ERBNoExtraWhitespaceAut
131
158
  node.content.value = content.replace(/^\s{2,}/, " ")
132
159
  break
133
160
 
134
- case "after-comment-equals":
135
- if (content.startsWith("=")) {
136
- const afterEquals = content.substring(1)
137
- node.content.value = "= " + afterEquals.replace(/^\s{2,}/, "")
161
+ case "after-comment-equals": {
162
+ const prefix = content.startsWith("graphql") ? "graphql" : content.startsWith("%=") ? "%=" : content.startsWith("==") ? "==" : content.startsWith("%") ? "%" : content.startsWith("=") ? "=" : content.startsWith("-") ? "-" : null
163
+
164
+ if (prefix) {
165
+ const afterPrefix = content.substring(prefix.length)
166
+ node.content.value = prefix + " " + afterPrefix.replace(/^\s{2,}/, "")
138
167
  }
139
168
 
140
169
  break
170
+ }
141
171
  default:
142
172
  return null
143
173
  }
@@ -0,0 +1,54 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+
4
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
+ import type { ERBCaseNode, ERBCaseMatchNode, ERBWhenNode, ERBInNode, ParseResult, ParserOptions } from "@herb-tools/core"
6
+
7
+ class ERBNoInlineCaseConditionsVisitor extends BaseRuleVisitor {
8
+ visitERBCaseNode(node: ERBCaseNode): void {
9
+ this.checkConditions(node, "when")
10
+ this.visitChildNodes(node)
11
+ }
12
+
13
+ visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
14
+ this.checkConditions(node, "in")
15
+ this.visitChildNodes(node)
16
+ }
17
+
18
+ private checkConditions(node: ERBCaseNode | ERBCaseMatchNode, type: string): void {
19
+ if (!node.conditions || node.conditions.length === 0) return
20
+
21
+ for (const condition of node.conditions as (ERBWhenNode | ERBInNode)[]) {
22
+ if (condition.tag_opening === null) {
23
+ this.addOffense(
24
+ `A \`case\` statement with \`${type}\` conditions in a single ERB tag cannot be reliably parsed, compiled, and formatted. Use separate ERB tags for \`case\` and its conditions (e.g., \`<% case x %>\` followed by \`<% ${type} y %>\`).`,
25
+ node.location,
26
+ )
27
+ break
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ export class ERBNoInlineCaseConditionsRule extends ParserRule {
34
+ static ruleName = "erb-no-inline-case-conditions"
35
+
36
+ get defaultConfig(): FullRuleConfig {
37
+ return {
38
+ enabled: true,
39
+ severity: "warning",
40
+ }
41
+ }
42
+
43
+ get parserOptions(): Partial<ParserOptions> {
44
+ return { strict: false }
45
+ }
46
+
47
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
48
+ const visitor = new ERBNoInlineCaseConditionsVisitor(this.ruleName, context)
49
+
50
+ visitor.visit(result.value)
51
+
52
+ return visitor.offenses
53
+ }
54
+ }
@@ -0,0 +1,101 @@
1
+ import { PrismVisitor, PrismNodes } from "@herb-tools/core"
2
+ import { ParserRule } from "../types.js"
3
+
4
+ import { isPartialFile } from "./file-utils.js"
5
+ import { locationFromOffset } from "./rule-utils.js"
6
+
7
+ import type { ParseResult, ParserOptions, PrismLocation } from "@herb-tools/core"
8
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
9
+
10
+ interface InstanceVariableNode {
11
+ name: string
12
+ location: PrismLocation
13
+ }
14
+
15
+ type InstanceVariableUsage = "read" | "write"
16
+
17
+ interface InstanceVariableReference {
18
+ name: string
19
+ usage: InstanceVariableUsage
20
+ startOffset: number
21
+ length: number
22
+ }
23
+
24
+ class InstanceVariableCollector extends PrismVisitor {
25
+ public readonly instanceVariables: InstanceVariableReference[] = []
26
+
27
+ visitInstanceVariableReadNode(node: PrismNodes.InstanceVariableReadNode): void {
28
+ this.collect(node, "read")
29
+ }
30
+
31
+ visitInstanceVariableWriteNode(node: PrismNodes.InstanceVariableWriteNode): void {
32
+ this.collect(node, "write")
33
+ }
34
+
35
+ visitInstanceVariableAndWriteNode(node: PrismNodes.InstanceVariableAndWriteNode): void {
36
+ this.collect(node, "write")
37
+ }
38
+
39
+ visitInstanceVariableOrWriteNode(node: PrismNodes.InstanceVariableOrWriteNode): void {
40
+ this.collect(node, "write")
41
+ }
42
+
43
+ visitInstanceVariableOperatorWriteNode(node: PrismNodes.InstanceVariableOperatorWriteNode): void {
44
+ this.collect(node, "write")
45
+ }
46
+
47
+ visitInstanceVariableTargetNode(node: PrismNodes.InstanceVariableTargetNode): void {
48
+ this.collect(node, "write")
49
+ }
50
+
51
+ private collect(node: InstanceVariableNode, usage: InstanceVariableUsage): void {
52
+ this.instanceVariables.push({
53
+ name: node.name,
54
+ usage,
55
+ startOffset: node.location.startOffset,
56
+ length: node.location.length
57
+ })
58
+ }
59
+ }
60
+
61
+ export class ERBNoInstanceVariablesInPartialsRule extends ParserRule {
62
+ static ruleName = "erb-no-instance-variables-in-partials"
63
+
64
+ get defaultConfig(): FullRuleConfig {
65
+ return {
66
+ enabled: true,
67
+ severity: "error",
68
+ }
69
+ }
70
+
71
+ get parserOptions(): Partial<ParserOptions> {
72
+ return {
73
+ track_whitespace: true,
74
+ prism_program: true,
75
+ }
76
+ }
77
+
78
+ isEnabled(_result: ParseResult, context?: Partial<LintContext>): boolean {
79
+ return isPartialFile(context?.fileName) === true
80
+ }
81
+
82
+ check(result: ParseResult, _context?: Partial<LintContext>): UnboundLintOffense[] {
83
+ const source = result.value.source
84
+ const prismNode = result.value.prismNode
85
+
86
+ if (!prismNode || !source) return []
87
+
88
+ const collector = new InstanceVariableCollector()
89
+
90
+ collector.visit(prismNode)
91
+
92
+ return collector.instanceVariables.map(ivar => {
93
+ const location = locationFromOffset(source, ivar.startOffset, ivar.length)
94
+ const message = ivar.usage === "read"
95
+ ? `Avoid using instance variables in partials. Pass \`${ivar.name}\` as a local variable instead.`
96
+ : `Avoid setting instance variables in partials. Use a local variable instead of \`${ivar.name}\`.`
97
+
98
+ return this.createOffense(message, location)
99
+ })
100
+ }
101
+ }