@herb-tools/linter 0.8.10 → 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 +60509 -17442
  28. package/dist/herb-lint.js.map +1 -1
  29. package/dist/index.cjs +2620 -933
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +2553 -872
  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 +31203 -7866
  39. package/dist/loader.cjs.map +1 -1
  40. package/dist/loader.js +31165 -7834
  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 +4 -4
  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 +4 -4
  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,65 @@
1
+ import { isLiteralNode, isPureWhitespaceNode, splitLiteralsAtWhitespace, groupNodesByClass } from "@herb-tools/core"
2
+ import { IdentityPrinter } from "@herb-tools/printer"
3
+
4
+ import { ParserRule } from "../types.js"
5
+ import { AttributeVisitorMixin } from "./rule-utils.js"
6
+
7
+ import type { Node } from "@herb-tools/core"
8
+ import type { StaticAttributeDynamicValueParams } from "./rule-utils.js"
9
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
10
+ import type { ParseResult } from "@herb-tools/core"
11
+
12
+ function groupToString(group: Node[]): string {
13
+ return group.map(node => {
14
+ if (isLiteralNode(node)) {
15
+ return node.content
16
+ }
17
+
18
+ return IdentityPrinter.print(node, { ignoreErrors: true })
19
+ }).join("")
20
+ }
21
+
22
+ class ERBNoInterpolatedClassNamesVisitor extends AttributeVisitorMixin {
23
+ protected checkStaticAttributeDynamicValue({ attributeName, valueNodes, attributeNode }: StaticAttributeDynamicValueParams) {
24
+ if (attributeName !== "class") return
25
+
26
+ const splitNodes = splitLiteralsAtWhitespace(valueNodes)
27
+ const groups = groupNodesByClass(splitNodes)
28
+
29
+ for (const group of groups) {
30
+ if (group.every(node => isPureWhitespaceNode(node))) continue
31
+
32
+ const isInterpolated = group.some(node => !isLiteralNode(node))
33
+ if (!isInterpolated) continue
34
+
35
+ const hasAttachedLiteral = group.some(node => isLiteralNode(node) && node.content.trim())
36
+ if (!hasAttachedLiteral) continue
37
+
38
+ const className = groupToString(group)
39
+
40
+ this.addOffense(
41
+ `Avoid ERB interpolation inside class names: \`${className}\`. Use standalone ERB expressions that output complete class names instead.`,
42
+ attributeNode.value!.location,
43
+ )
44
+ }
45
+ }
46
+ }
47
+
48
+ export class ERBNoInterpolatedClassNamesRule extends ParserRule {
49
+ static ruleName = "erb-no-interpolated-class-names"
50
+
51
+ get defaultConfig(): FullRuleConfig {
52
+ return {
53
+ enabled: true,
54
+ severity: "warning"
55
+ }
56
+ }
57
+
58
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
59
+ const visitor = new ERBNoInterpolatedClassNamesVisitor(this.ruleName, context)
60
+
61
+ visitor.visit(result.value)
62
+
63
+ return visitor.offenses
64
+ }
65
+ }
@@ -0,0 +1,47 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+ import { isERBNode, isERBOutputNode } from "@herb-tools/core"
4
+
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { ParseResult, DocumentNode } from "@herb-tools/core"
7
+
8
+ const JAVASCRIPT_TAG_PATTERN = /\bjavascript_tag\b/
9
+
10
+ class ERBNoJavascriptTagHelperVisitor extends BaseRuleVisitor {
11
+ visitDocumentNode(node: DocumentNode): void {
12
+ for (const child of node.children || []) {
13
+ if (!isERBNode(child)) continue
14
+ if (!isERBOutputNode(child)) continue
15
+
16
+ const content = child.content?.value || ""
17
+
18
+ if (JAVASCRIPT_TAG_PATTERN.test(content)) {
19
+ this.addOffense(
20
+ "Avoid `javascript_tag`. Use inline `<script>` tags instead.",
21
+ child.location,
22
+ )
23
+ }
24
+ }
25
+
26
+ super.visitDocumentNode(node)
27
+ }
28
+ }
29
+
30
+ export class ERBNoJavascriptTagHelperRule extends ParserRule {
31
+ static ruleName = "erb-no-javascript-tag-helper"
32
+
33
+ get defaultConfig(): FullRuleConfig {
34
+ return {
35
+ enabled: true,
36
+ severity: "warning"
37
+ }
38
+ }
39
+
40
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
41
+ const visitor = new ERBNoJavascriptTagHelperVisitor(this.ruleName, context)
42
+
43
+ visitor.visit(result.value)
44
+
45
+ return visitor.offenses
46
+ }
47
+ }
@@ -25,6 +25,13 @@ class ERBNoOutputControlFlowRuleVisitor extends BaseRuleVisitor {
25
25
  this.visitChildNodes(node)
26
26
  }
27
27
 
28
+ private static readonly CONTROL_BLOCK_NAMES: Record<string, string> = {
29
+ "AST_ERB_IF_NODE": "if",
30
+ "AST_ERB_ELSE_NODE": "else",
31
+ "AST_ERB_END_NODE": "end",
32
+ "AST_ERB_UNLESS_NODE": "unless"
33
+ }
34
+
28
35
  private checkOutputControlFlow(controlBlock: ERBIfNode | ERBUnlessNode | ERBElseNode | ERBEndNode): void {
29
36
  const openTag = controlBlock.tag_opening;
30
37
  if (!openTag) {
@@ -32,25 +39,18 @@ class ERBNoOutputControlFlowRuleVisitor extends BaseRuleVisitor {
32
39
  }
33
40
 
34
41
  if (openTag.value === "<%="){
35
- let controlBlockType: string = controlBlock.type
36
-
37
- if (controlBlock.type === "AST_ERB_IF_NODE") controlBlockType = "if"
38
- if (controlBlock.type === "AST_ERB_ELSE_NODE") controlBlockType = "else"
39
- if (controlBlock.type === "AST_ERB_END_NODE") controlBlockType = "end"
40
- if (controlBlock.type === "AST_ERB_UNLESS_NODE") controlBlockType = "unless"
42
+ const controlBlockType = ERBNoOutputControlFlowRuleVisitor.CONTROL_BLOCK_NAMES[controlBlock.type] || controlBlock.type
41
43
 
42
44
  this.addOffense(
43
45
  `Control flow statements like \`${controlBlockType}\` should not be used with output tags. Use \`<% ${controlBlockType} ... %>\` instead.`,
44
46
  openTag.location,
45
47
  )
46
48
  }
47
-
48
- return
49
49
  }
50
50
  }
51
51
 
52
52
  export class ERBNoOutputControlFlowRule extends ParserRule {
53
- name = "erb-no-output-control-flow"
53
+ static ruleName = "erb-no-output-control-flow"
54
54
 
55
55
  get defaultConfig(): FullRuleConfig {
56
56
  return {
@@ -60,7 +60,7 @@ export class ERBNoOutputControlFlowRule extends ParserRule {
60
60
  }
61
61
 
62
62
  check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
63
- const visitor = new ERBNoOutputControlFlowRuleVisitor(this.name, context)
63
+ const visitor = new ERBNoOutputControlFlowRuleVisitor(this.ruleName, context)
64
64
 
65
65
  visitor.visit(result.value)
66
66
 
@@ -0,0 +1,39 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+ import { isERBNode, isERBOutputNode } from "@herb-tools/core"
4
+
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { ParseResult, HTMLAttributeNameNode } from "@herb-tools/core"
7
+
8
+ class ERBNoOutputInAttributeNameVisitor extends BaseRuleVisitor {
9
+ visitHTMLAttributeNameNode(node: HTMLAttributeNameNode): void {
10
+ for (const child of node.children) {
11
+ if (!isERBNode(child)) continue
12
+ if (!isERBOutputNode(child)) continue
13
+
14
+ this.addOffense(
15
+ "Avoid ERB output in attribute names. Use static attribute names with dynamic values instead.",
16
+ child.location,
17
+ )
18
+ }
19
+
20
+ super.visitHTMLAttributeNameNode(node)
21
+ }
22
+ }
23
+
24
+ export class ERBNoOutputInAttributeNameRule extends ParserRule {
25
+ static ruleName = "erb-no-output-in-attribute-name"
26
+
27
+ get defaultConfig(): FullRuleConfig {
28
+ return {
29
+ enabled: true,
30
+ severity: "error"
31
+ }
32
+ }
33
+
34
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
35
+ const visitor = new ERBNoOutputInAttributeNameVisitor(this.ruleName, context)
36
+ visitor.visit(result.value)
37
+ return visitor.offenses
38
+ }
39
+ }
@@ -0,0 +1,39 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+ import { isERBNode, isERBOutputNode } from "@herb-tools/core"
4
+
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { ParseResult, HTMLOpenTagNode } from "@herb-tools/core"
7
+
8
+ class ERBNoOutputInAttributePositionVisitor extends BaseRuleVisitor {
9
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
10
+ for (const child of node.children) {
11
+ if (!isERBNode(child)) continue
12
+ if (!isERBOutputNode(child)) continue
13
+
14
+ this.addOffense(
15
+ "Avoid `<%= %>` in attribute position. Use `<% if ... %>` with static attributes instead.",
16
+ child.location,
17
+ )
18
+ }
19
+
20
+ super.visitHTMLOpenTagNode(node)
21
+ }
22
+ }
23
+
24
+ export class ERBNoOutputInAttributePositionRule extends ParserRule {
25
+ static ruleName = "erb-no-output-in-attribute-position"
26
+
27
+ get defaultConfig(): FullRuleConfig {
28
+ return {
29
+ enabled: true,
30
+ severity: "error"
31
+ }
32
+ }
33
+
34
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
35
+ const visitor = new ERBNoOutputInAttributePositionVisitor(this.ruleName, context)
36
+ visitor.visit(result.value)
37
+ return visitor.offenses
38
+ }
39
+ }
@@ -0,0 +1,47 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { AttributeVisitorMixin } from "./rule-utils.js"
3
+ import { isERBNode } from "@herb-tools/core"
4
+
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { ParseResult, Node } from "@herb-tools/core"
7
+ import type { StaticAttributeDynamicValueParams, DynamicAttributeDynamicValueParams } from "./rule-utils.js"
8
+
9
+ class ERBNoRawOutputInAttributeValueVisitor extends AttributeVisitorMixin {
10
+ protected checkStaticAttributeDynamicValue({ valueNodes, attributeNode }: StaticAttributeDynamicValueParams): void {
11
+ this.checkValueNodes(valueNodes)
12
+ }
13
+
14
+ protected checkDynamicAttributeDynamicValue({ valueNodes }: DynamicAttributeDynamicValueParams): void {
15
+ this.checkValueNodes(valueNodes)
16
+ }
17
+
18
+ private checkValueNodes(nodes: Node[]): void {
19
+ for (const node of nodes) {
20
+ if (!isERBNode(node)) continue
21
+
22
+ if (node.tag_opening?.value === "<%==") {
23
+ this.addOffense(
24
+ "Avoid `<%==` in attribute values. Use `<%= %>` instead to ensure proper HTML escaping.",
25
+ node.location,
26
+ )
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ export class ERBNoRawOutputInAttributeValueRule extends ParserRule {
33
+ static ruleName = "erb-no-raw-output-in-attribute-value"
34
+
35
+ get defaultConfig(): FullRuleConfig {
36
+ return {
37
+ enabled: true,
38
+ severity: "error"
39
+ }
40
+ }
41
+
42
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
43
+ const visitor = new ERBNoRawOutputInAttributeValueVisitor(this.ruleName, context)
44
+ visitor.visit(result.value)
45
+ return visitor.offenses
46
+ }
47
+ }
@@ -27,7 +27,7 @@ class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
27
27
  }
28
28
 
29
29
  export class ERBNoSilentTagInAttributeNameRule extends ParserRule {
30
- name = "erb-no-silent-tag-in-attribute-name"
30
+ static ruleName = "erb-no-silent-tag-in-attribute-name"
31
31
 
32
32
  get defaultConfig(): FullRuleConfig {
33
33
  return {
@@ -37,7 +37,7 @@ export class ERBNoSilentTagInAttributeNameRule extends ParserRule {
37
37
  }
38
38
 
39
39
  check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
40
- const visitor = new ERBNoSilentTagInAttributeNameVisitor(this.name, context)
40
+ const visitor = new ERBNoSilentTagInAttributeNameVisitor(this.ruleName, context)
41
41
 
42
42
  visitor.visit(result.value)
43
43
 
@@ -0,0 +1,82 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+ import {
4
+ getTagLocalName,
5
+ getAttribute,
6
+ getStaticAttributeValue,
7
+ isERBNode,
8
+ isERBOutputNode,
9
+ isERBCommentNode,
10
+ isHTMLOpenTagNode,
11
+ } from "@herb-tools/core"
12
+
13
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
14
+ import type { ParseResult, HTMLElementNode, Node } from "@herb-tools/core"
15
+
16
+ const END_PATTERN = /^\s*end\b/
17
+
18
+ class ERBNoStatementInScriptVisitor extends BaseRuleVisitor {
19
+ visitHTMLElementNode(node: HTMLElementNode): void {
20
+ if (!isHTMLOpenTagNode(node.open_tag)) {
21
+ super.visitHTMLElementNode(node)
22
+ return
23
+ }
24
+
25
+ if (getTagLocalName(node.open_tag) === "script") {
26
+ this.checkScriptElement(node)
27
+ }
28
+
29
+ super.visitHTMLElementNode(node)
30
+ }
31
+
32
+ private checkScriptElement(node: HTMLElementNode): void {
33
+ if (!isHTMLOpenTagNode(node.open_tag)) return
34
+
35
+ const typeAttribute = getAttribute(node.open_tag, "type")
36
+ const typeValue = typeAttribute ? getStaticAttributeValue(typeAttribute) : null
37
+
38
+ if (typeValue === "text/html") {
39
+ return
40
+ }
41
+
42
+ if (!node.body || node.body.length === 0) {
43
+ return
44
+ }
45
+
46
+ this.checkNodesForStatements(node.body)
47
+ }
48
+
49
+ private checkNodesForStatements(nodes: Node[]): void {
50
+ for (const child of nodes) {
51
+ if (!isERBNode(child)) continue
52
+ if (isERBOutputNode(child)) continue
53
+ if (isERBCommentNode(child)) continue
54
+
55
+ const content = child.content?.value || ""
56
+
57
+ if (END_PATTERN.test(content)) continue
58
+
59
+ this.addOffense(
60
+ "Avoid `<% %>` tags inside `<script>`. Use `<%= %>` to interpolate values into JavaScript.",
61
+ child.location,
62
+ )
63
+ }
64
+ }
65
+ }
66
+
67
+ export class ERBNoStatementInScriptRule extends ParserRule {
68
+ static ruleName = "erb-no-statement-in-script"
69
+
70
+ get defaultConfig(): FullRuleConfig {
71
+ return {
72
+ enabled: true,
73
+ severity: "warning"
74
+ }
75
+ }
76
+
77
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
78
+ const visitor = new ERBNoStatementInScriptVisitor(this.ruleName, context)
79
+ visitor.visit(result.value)
80
+ return visitor.offenses
81
+ }
82
+ }
@@ -0,0 +1,62 @@
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 { ERBIfNode, ERBUnlessNode, ERBWhenNode, ERBInNode, ParseResult, ParserOptions, Location } from "@herb-tools/core"
6
+
7
+ class ERBNoThenInControlFlowVisitor extends BaseRuleVisitor {
8
+ visitERBIfNode(node: ERBIfNode): void {
9
+ const content = node.content?.value?.trim() ?? ""
10
+ const keyword = content.startsWith("elsif") ? "elsif" : "if"
11
+
12
+ this.checkThenKeyword(keyword, node.then_keyword)
13
+ this.visitChildNodes(node)
14
+ }
15
+
16
+ visitERBUnlessNode(node: ERBUnlessNode): void {
17
+ this.checkThenKeyword("unless", node.then_keyword)
18
+ this.visitChildNodes(node)
19
+ }
20
+
21
+ visitERBWhenNode(node: ERBWhenNode): void {
22
+ this.checkThenKeyword("when", node.then_keyword)
23
+ this.visitChildNodes(node)
24
+ }
25
+
26
+ visitERBInNode(node: ERBInNode): void {
27
+ this.checkThenKeyword("in", node.then_keyword)
28
+ this.visitChildNodes(node)
29
+ }
30
+
31
+ private checkThenKeyword(keyword: string, thenKeyword: Location | null): void {
32
+ if (thenKeyword === null) return
33
+
34
+ this.addOffense(
35
+ `Avoid using \`then\` in \`${keyword}\` expressions inside ERB templates. Use the multiline block form instead.`,
36
+ thenKeyword,
37
+ )
38
+ }
39
+ }
40
+
41
+ export class ERBNoThenInControlFlowRule extends ParserRule {
42
+ static ruleName = "erb-no-then-in-control-flow"
43
+
44
+ get defaultConfig(): FullRuleConfig {
45
+ return {
46
+ enabled: true,
47
+ severity: "warning",
48
+ }
49
+ }
50
+
51
+ get parserOptions(): Partial<ParserOptions> {
52
+ return { strict: true }
53
+ }
54
+
55
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
56
+ const visitor = new ERBNoThenInControlFlowVisitor(this.ruleName, context)
57
+
58
+ visitor.visit(result.value)
59
+
60
+ return visitor.offenses
61
+ }
62
+ }
@@ -0,0 +1,187 @@
1
+ import { Location, Visitor } from "@herb-tools/core"
2
+ import { ParserRule, Mutable, BaseAutofixContext } from "../types.js"
3
+
4
+ import { isHTMLOpenTagNode, isHTMLTextNode, isLiteralNode, getTagLocalName } from "@herb-tools/core"
5
+ import { findNodeAtPosition } from "./rule-utils.js"
6
+
7
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
8
+ import type { HTMLElementNode, HTMLTextNode, LiteralNode, ParseResult, DocumentNode, ERBNode } from "@herb-tools/core"
9
+
10
+ const TRAILING_WHITESPACE = /[ \t\r\v\f\u00A0]+$/
11
+ const TRAILING_WHITESPACE_BEFORE_NEWLINE = /[ \t\r\v\f\u00A0]+(?=\n)/g
12
+ const ONLY_WHITESPACE = /^[ \t\r\v\f\u00A0]+$/
13
+
14
+ interface SkipZone {
15
+ startLine: number
16
+ startColumn: number
17
+ endLine: number
18
+ endColumn: number
19
+ }
20
+
21
+ interface TrailingWhitespaceCandidate {
22
+ line: number
23
+ column: number
24
+ length: number
25
+ }
26
+
27
+ interface ERBNoTrailingWhitespaceAutofixContext extends BaseAutofixContext {
28
+ node: Mutable<HTMLTextNode> | Mutable<LiteralNode>
29
+ }
30
+
31
+ class SkipZoneCollector extends Visitor {
32
+ skipZones: SkipZone[] = []
33
+
34
+ SKIP_TAGS = new Set(["pre", "textarea", "script", "style"])
35
+
36
+ visitHTMLElementNode(node: HTMLElementNode): void {
37
+ if (isHTMLOpenTagNode(node.open_tag)) {
38
+ const tagName = getTagLocalName(node.open_tag)
39
+
40
+ if (tagName && this.SKIP_TAGS.has(tagName)) {
41
+ this.skipZones.push({
42
+ startLine: node.location.start.line,
43
+ startColumn: node.location.start.column,
44
+ endLine: node.location.end.line,
45
+ endColumn: node.location.end.column
46
+ })
47
+
48
+ return
49
+ }
50
+ }
51
+
52
+ super.visitHTMLElementNode(node)
53
+ }
54
+
55
+ visitERBNode(node: ERBNode) {
56
+ if (!node.tag_opening) return
57
+ if (!node.tag_closing) return
58
+
59
+ this.skipZones.push({
60
+ startLine: node.tag_opening.location.start.line,
61
+ startColumn: node.tag_opening.location.start.column,
62
+ endLine: node.tag_closing.location.end.line,
63
+ endColumn: node.tag_closing.location.end.column
64
+ })
65
+ }
66
+ }
67
+
68
+ export class ERBNoTrailingWhitespaceRule extends ParserRule<ERBNoTrailingWhitespaceAutofixContext> {
69
+ static autocorrectable = true
70
+ static ruleName = "erb-no-trailing-whitespace"
71
+
72
+ get defaultConfig(): FullRuleConfig {
73
+ return {
74
+ enabled: true,
75
+ severity: "error",
76
+ }
77
+ }
78
+
79
+ check(result: ParseResult, _context?: Partial<LintContext>): UnboundLintOffense<ERBNoTrailingWhitespaceAutofixContext>[] {
80
+ const offenses: UnboundLintOffense<ERBNoTrailingWhitespaceAutofixContext>[] = []
81
+ const lines = result.source.split("\n")
82
+ const candidates = this.findTrailingWhitespaceCandidates(lines)
83
+
84
+ if (candidates.length === 0) return offenses
85
+
86
+ const skipZones = this.collectSkipZones(result.value)
87
+
88
+ for (const candidate of candidates) {
89
+ if (!this.isInSkipZone(candidate, skipZones)) {
90
+ const location = Location.from(candidate.line, candidate.column, candidate.line, candidate.column + candidate.length)
91
+ const node = findNodeAtPosition(result.value, candidate.line, candidate.column, (n) => isHTMLTextNode(n) || isLiteralNode(n)) as HTMLTextNode | LiteralNode | null
92
+
93
+ offenses.push({
94
+ rule: this.ruleName,
95
+ message: "Extra whitespace detected at end of line.",
96
+ location,
97
+ autofixContext: node ? { node } : undefined
98
+ })
99
+ }
100
+ }
101
+
102
+ return offenses
103
+ }
104
+
105
+ private findTrailingWhitespaceCandidates(lines: string[]): TrailingWhitespaceCandidate[] {
106
+ const candidates: TrailingWhitespaceCandidate[] = []
107
+
108
+ for (let i = 0; i < lines.length; i++) {
109
+ const line = lines[i]
110
+ const match = line.match(TRAILING_WHITESPACE)
111
+
112
+ if (match && match.index !== undefined) {
113
+ candidates.push({
114
+ line: i + 1,
115
+ column: match.index,
116
+ length: match[0].length
117
+ })
118
+ }
119
+ }
120
+
121
+ return candidates
122
+ }
123
+
124
+ private collectSkipZones(root: DocumentNode): SkipZone[] {
125
+ const collector = new SkipZoneCollector()
126
+
127
+ collector.visit(root)
128
+
129
+ return collector.skipZones
130
+ }
131
+
132
+ private isInSkipZone(candidate: TrailingWhitespaceCandidate, skipZones: SkipZone[]): boolean {
133
+ for (const zone of skipZones) {
134
+ if (candidate.line < zone.startLine || candidate.line > zone.endLine) continue
135
+ if (candidate.line === zone.endLine && candidate.column >= zone.endColumn) continue
136
+ if (candidate.line === zone.startLine && candidate.column < zone.startColumn) continue
137
+
138
+ return true
139
+ }
140
+
141
+ return false
142
+ }
143
+
144
+ autofix(offense: LintOffense<ERBNoTrailingWhitespaceAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
145
+ if (!offense.autofixContext) return null
146
+
147
+ const { node } = offense.autofixContext
148
+
149
+ if (node.type === "AST_HTML_TEXT_NODE" || node.type === "AST_LITERAL_NODE") {
150
+ let fixedContent = node.content.replace(TRAILING_WHITESPACE_BEFORE_NEWLINE, "")
151
+ const offenseIsAtEndOfContent = this.isOffenseAtEndOfContent(offense, node)
152
+
153
+ if (offenseIsAtEndOfContent) {
154
+ if (this.hasTrailingWhitespaceNotIndentation(fixedContent)) {
155
+ fixedContent = fixedContent.replace(TRAILING_WHITESPACE, "")
156
+ }
157
+
158
+ if (ONLY_WHITESPACE.test(fixedContent) && node.location.start.column !== 0) {
159
+ fixedContent = ""
160
+ }
161
+ }
162
+
163
+ node.content = fixedContent
164
+ }
165
+
166
+ return result
167
+ }
168
+
169
+ private isOffenseAtEndOfContent(offense: LintOffense<ERBNoTrailingWhitespaceAutofixContext>, node: Mutable<HTMLTextNode> | Mutable<LiteralNode>): boolean {
170
+ return offense.location.end.line === node.location.end.line && offense.location.end.column === node.location.end.column
171
+ }
172
+
173
+ private hasTrailingWhitespaceNotIndentation(content: string): boolean {
174
+ if (content.endsWith("\n")) return false
175
+
176
+ const endMatch = content.match(TRAILING_WHITESPACE)
177
+ if (!endMatch) return false
178
+
179
+ const whitespaceStart = content.length - endMatch[0].length
180
+ if (whitespaceStart === 0) return false
181
+
182
+ const characterBefore = content[whitespaceStart - 1]
183
+ if (characterBefore === "\n") return false
184
+
185
+ return true
186
+ }
187
+ }
@@ -0,0 +1,47 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { AttributeVisitorMixin } from "./rule-utils.js"
3
+ import { isERBNode, isERBOutputNode } from "@herb-tools/core"
4
+
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { ParseResult, Node } from "@herb-tools/core"
7
+ import type { StaticAttributeDynamicValueParams } from "./rule-utils.js"
8
+
9
+ const JS_ATTRIBUTE_PATTERN = /^on/i
10
+ const SAFE_PATTERN = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/
11
+
12
+ class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
13
+ protected checkStaticAttributeDynamicValue({ attributeName, valueNodes }: StaticAttributeDynamicValueParams): void {
14
+ if (!JS_ATTRIBUTE_PATTERN.test(attributeName)) return
15
+
16
+ for (const node of valueNodes) {
17
+ if (!isERBNode(node)) continue
18
+ if (!isERBOutputNode(node)) continue
19
+
20
+ const content = node.content?.value?.trim() || ""
21
+
22
+ if (SAFE_PATTERN.test(content)) continue
23
+
24
+ this.addOffense(
25
+ `Unsafe ERB output in \`${attributeName}\` attribute. Use \`.to_json\`, \`j()\`, or \`escape_javascript()\` to safely encode values.`,
26
+ node.location,
27
+ )
28
+ }
29
+ }
30
+ }
31
+
32
+ export class ERBNoUnsafeJSAttributeRule extends ParserRule {
33
+ static ruleName = "erb-no-unsafe-js-attribute"
34
+
35
+ get defaultConfig(): FullRuleConfig {
36
+ return {
37
+ enabled: true,
38
+ severity: "error"
39
+ }
40
+ }
41
+
42
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
43
+ const visitor = new ERBNoUnsafeJSAttributeVisitor(this.ruleName, context)
44
+ visitor.visit(result.value)
45
+ return visitor.offenses
46
+ }
47
+ }