@herb-tools/linter 0.8.10 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (587) hide show
  1. package/README.md +5 -5
  2. package/dist/{src/cli → cli}/argument-parser.js +15 -2
  3. package/dist/cli/argument-parser.js.map +1 -0
  4. package/dist/{src/cli → cli}/file-processor.js +155 -9
  5. package/dist/cli/file-processor.js.map +1 -0
  6. package/dist/cli/file-url.js +6 -0
  7. package/dist/cli/file-url.js.map +1 -0
  8. package/dist/cli/formatters/base-formatter.js.map +1 -0
  9. package/dist/{src/cli → cli}/formatters/detailed-formatter.js +16 -19
  10. package/dist/cli/formatters/detailed-formatter.js.map +1 -0
  11. package/dist/cli/formatters/github-actions-formatter.js.map +1 -0
  12. package/dist/cli/formatters/index.js.map +1 -0
  13. package/dist/cli/formatters/json-formatter.js.map +1 -0
  14. package/dist/cli/formatters/simple-formatter.js +54 -0
  15. package/dist/cli/formatters/simple-formatter.js.map +1 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/cli/lint-worker.js +143 -0
  18. package/dist/cli/lint-worker.js.map +1 -0
  19. package/dist/cli/output-manager.js.map +1 -0
  20. package/dist/{src/cli → cli}/summary-reporter.js +13 -16
  21. package/dist/cli/summary-reporter.js.map +1 -0
  22. package/dist/{src/cli.js → cli.js} +5 -3
  23. package/dist/cli.js.map +1 -0
  24. package/dist/{src/custom-rule-loader.js → custom-rule-loader.js} +20 -4
  25. package/dist/custom-rule-loader.js.map +1 -0
  26. package/dist/herb-disable-comment-utils.js.map +1 -0
  27. package/dist/herb-lint.js +61834 -17340
  28. package/dist/herb-lint.js.map +1 -1
  29. package/dist/index.cjs +3109 -956
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +2969 -897
  32. package/dist/index.js.map +1 -1
  33. package/dist/lint-worker.js +72889 -0
  34. package/dist/lint-worker.js.map +1 -0
  35. package/dist/linter-ignore.js.map +1 -0
  36. package/dist/{src/linter.js → linter.js} +89 -74
  37. package/dist/linter.js.map +1 -0
  38. package/dist/loader.cjs +32163 -7842
  39. package/dist/loader.cjs.map +1 -1
  40. package/dist/loader.js +32121 -7828
  41. package/dist/loader.js.map +1 -1
  42. package/dist/parse-cache.js +30 -0
  43. package/dist/parse-cache.js.map +1 -0
  44. package/dist/rules/actionview-no-silent-helper.js +45 -0
  45. package/dist/rules/actionview-no-silent-helper.js.map +1 -0
  46. package/dist/rules/actionview-no-silent-render.js +31 -0
  47. package/dist/rules/actionview-no-silent-render.js.map +1 -0
  48. package/dist/{src/rules → rules}/erb-comment-syntax.js +2 -2
  49. package/dist/rules/erb-comment-syntax.js.map +1 -0
  50. package/dist/{src/rules → rules}/erb-no-case-node-children.js +5 -3
  51. package/dist/rules/erb-no-case-node-children.js.map +1 -0
  52. package/dist/rules/erb-no-conditional-html-element.js +38 -0
  53. package/dist/rules/erb-no-conditional-html-element.js.map +1 -0
  54. package/dist/rules/erb-no-conditional-open-tag.js +24 -0
  55. package/dist/rules/erb-no-conditional-open-tag.js.map +1 -0
  56. package/dist/rules/erb-no-duplicate-branch-elements.js +329 -0
  57. package/dist/rules/erb-no-duplicate-branch-elements.js.map +1 -0
  58. package/dist/rules/erb-no-empty-control-flow.js +190 -0
  59. package/dist/rules/erb-no-empty-control-flow.js.map +1 -0
  60. package/dist/{src/rules → rules}/erb-no-empty-tags.js +2 -2
  61. package/dist/rules/erb-no-empty-tags.js.map +1 -0
  62. package/dist/{src/rules → rules}/erb-no-extra-newline.js +4 -21
  63. package/dist/rules/erb-no-extra-newline.js.map +1 -0
  64. package/dist/{src/rules → rules}/erb-no-extra-whitespace-inside-tags.js +39 -13
  65. package/dist/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
  66. package/dist/rules/erb-no-inline-case-conditions.js +40 -0
  67. package/dist/rules/erb-no-inline-case-conditions.js.map +1 -0
  68. package/dist/rules/erb-no-instance-variables-in-partials.js +67 -0
  69. package/dist/rules/erb-no-instance-variables-in-partials.js.map +1 -0
  70. package/dist/rules/erb-no-interpolated-class-names.js +47 -0
  71. package/dist/rules/erb-no-interpolated-class-names.js.map +1 -0
  72. package/dist/rules/erb-no-javascript-tag-helper.js +34 -0
  73. package/dist/rules/erb-no-javascript-tag-helper.js.map +1 -0
  74. package/dist/{src/rules → rules}/erb-no-output-control-flow.js +9 -12
  75. package/dist/rules/erb-no-output-control-flow.js.map +1 -0
  76. package/dist/rules/erb-no-output-in-attribute-name.js +30 -0
  77. package/dist/rules/erb-no-output-in-attribute-name.js.map +1 -0
  78. package/dist/rules/erb-no-output-in-attribute-position.js +30 -0
  79. package/dist/rules/erb-no-output-in-attribute-position.js.map +1 -0
  80. package/dist/rules/erb-no-raw-output-in-attribute-value.js +35 -0
  81. package/dist/rules/erb-no-raw-output-in-attribute-value.js.map +1 -0
  82. package/dist/rules/erb-no-silent-statement.js +44 -0
  83. package/dist/rules/erb-no-silent-statement.js.map +1 -0
  84. package/dist/{src/rules → rules}/erb-no-silent-tag-in-attribute-name.js +2 -2
  85. package/dist/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
  86. package/dist/rules/erb-no-statement-in-script.js +58 -0
  87. package/dist/rules/erb-no-statement-in-script.js.map +1 -0
  88. package/dist/rules/erb-no-then-in-control-flow.js +45 -0
  89. package/dist/rules/erb-no-then-in-control-flow.js.map +1 -0
  90. package/dist/rules/erb-no-trailing-whitespace.js +138 -0
  91. package/dist/rules/erb-no-trailing-whitespace.js.map +1 -0
  92. package/dist/rules/erb-no-unsafe-js-attribute.js +36 -0
  93. package/dist/rules/erb-no-unsafe-js-attribute.js.map +1 -0
  94. package/dist/rules/erb-no-unsafe-raw.js +63 -0
  95. package/dist/rules/erb-no-unsafe-raw.js.map +1 -0
  96. package/dist/rules/erb-no-unsafe-script-interpolation.js +88 -0
  97. package/dist/rules/erb-no-unsafe-script-interpolation.js.map +1 -0
  98. package/dist/{src/rules → rules}/erb-prefer-image-tag-helper.js +5 -4
  99. package/dist/rules/erb-prefer-image-tag-helper.js.map +1 -0
  100. package/dist/{src/rules → rules}/erb-require-trailing-newline.js +2 -2
  101. package/dist/rules/erb-require-trailing-newline.js.map +1 -0
  102. package/dist/{src/rules → rules}/erb-require-whitespace-inside-tags.js +39 -15
  103. package/dist/rules/erb-require-whitespace-inside-tags.js.map +1 -0
  104. package/dist/{src/rules → rules}/erb-right-trim.js +2 -2
  105. package/dist/rules/erb-right-trim.js.map +1 -0
  106. package/dist/{src/rules → rules}/erb-strict-locals-comment-syntax.js +4 -4
  107. package/dist/rules/erb-strict-locals-comment-syntax.js.map +1 -0
  108. package/dist/{src/rules → rules}/erb-strict-locals-required.js +2 -2
  109. package/dist/rules/erb-strict-locals-required.js.map +1 -0
  110. package/dist/rules/file-utils.js.map +1 -0
  111. package/dist/rules/herb-disable-comment-base.js.map +1 -0
  112. package/dist/{src/rules → rules}/herb-disable-comment-malformed.js +2 -2
  113. package/dist/rules/herb-disable-comment-malformed.js.map +1 -0
  114. package/dist/{src/rules → rules}/herb-disable-comment-missing-rules.js +2 -2
  115. package/dist/rules/herb-disable-comment-missing-rules.js.map +1 -0
  116. package/dist/{src/rules → rules}/herb-disable-comment-no-duplicate-rules.js +2 -2
  117. package/dist/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
  118. package/dist/{src/rules → rules}/herb-disable-comment-no-redundant-all.js +2 -2
  119. package/dist/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
  120. package/dist/{src/rules → rules}/herb-disable-comment-unnecessary.js +2 -2
  121. package/dist/rules/herb-disable-comment-unnecessary.js.map +1 -0
  122. package/dist/{src/rules → rules}/herb-disable-comment-valid-rule-name.js +2 -2
  123. package/dist/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
  124. package/dist/rules/html-allowed-script-type.js +57 -0
  125. package/dist/rules/html-allowed-script-type.js.map +1 -0
  126. package/dist/rules/html-anchor-require-href.js +68 -0
  127. package/dist/rules/html-anchor-require-href.js.map +1 -0
  128. package/dist/{src/rules → rules}/html-aria-attribute-must-be-valid.js +3 -3
  129. package/dist/rules/html-aria-attribute-must-be-valid.js.map +1 -0
  130. package/dist/{src/rules → rules}/html-aria-label-is-well-formatted.js +3 -3
  131. package/dist/rules/html-aria-label-is-well-formatted.js.map +1 -0
  132. package/dist/{src/rules → rules}/html-aria-level-must-be-valid.js +3 -3
  133. package/dist/rules/html-aria-level-must-be-valid.js.map +1 -0
  134. package/dist/{src/rules → rules}/html-aria-role-heading-requires-level.js +5 -4
  135. package/dist/rules/html-aria-role-heading-requires-level.js.map +1 -0
  136. package/dist/{src/rules → rules}/html-aria-role-must-be-valid.js +3 -3
  137. package/dist/rules/html-aria-role-must-be-valid.js.map +1 -0
  138. package/dist/{src/rules → rules}/html-attribute-double-quotes.js +4 -4
  139. package/dist/rules/html-attribute-double-quotes.js.map +1 -0
  140. package/dist/{src/rules → rules}/html-attribute-equals-spacing.js +2 -2
  141. package/dist/rules/html-attribute-equals-spacing.js.map +1 -0
  142. package/dist/{src/rules → rules}/html-attribute-values-require-quotes.js +2 -2
  143. package/dist/rules/html-attribute-values-require-quotes.js.map +1 -0
  144. package/dist/{src/rules → rules}/html-avoid-both-disabled-and-aria-disabled.js +9 -9
  145. package/dist/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
  146. package/dist/{src/rules → rules}/html-body-only-elements.js +5 -4
  147. package/dist/rules/html-body-only-elements.js.map +1 -0
  148. package/dist/{src/rules → rules}/html-boolean-attributes-no-value.js +4 -3
  149. package/dist/rules/html-boolean-attributes-no-value.js.map +1 -0
  150. package/dist/rules/html-details-has-summary.js +52 -0
  151. package/dist/rules/html-details-has-summary.js.map +1 -0
  152. package/dist/{src/rules → rules}/html-head-only-elements.js +6 -5
  153. package/dist/rules/html-head-only-elements.js.map +1 -0
  154. package/dist/{src/rules → rules}/html-iframe-has-title.js +8 -11
  155. package/dist/rules/html-iframe-has-title.js.map +1 -0
  156. package/dist/{src/rules → rules}/html-img-require-alt.js +11 -5
  157. package/dist/rules/html-img-require-alt.js.map +1 -0
  158. package/dist/{src/rules → rules}/html-input-require-autocomplete.js +7 -10
  159. package/dist/rules/html-input-require-autocomplete.js.map +1 -0
  160. package/dist/{src/rules → rules}/html-navigation-has-label.js +6 -5
  161. package/dist/rules/html-navigation-has-label.js.map +1 -0
  162. package/dist/rules/html-no-abstract-roles.js +29 -0
  163. package/dist/rules/html-no-abstract-roles.js.map +1 -0
  164. package/dist/rules/html-no-aria-hidden-on-body.js +42 -0
  165. package/dist/rules/html-no-aria-hidden-on-body.js.map +1 -0
  166. package/dist/{src/rules → rules}/html-no-aria-hidden-on-focusable.js +6 -5
  167. package/dist/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
  168. package/dist/{src/rules → rules}/html-no-block-inside-inline.js +6 -9
  169. package/dist/rules/html-no-block-inside-inline.js.map +1 -0
  170. package/dist/{src/rules → rules}/html-no-duplicate-attributes.js +4 -3
  171. package/dist/rules/html-no-duplicate-attributes.js.map +1 -0
  172. package/dist/{src/rules → rules}/html-no-duplicate-ids.js +14 -11
  173. package/dist/rules/html-no-duplicate-ids.js.map +1 -0
  174. package/dist/{src/rules → rules}/html-no-duplicate-meta-names.js +22 -20
  175. package/dist/rules/html-no-duplicate-meta-names.js.map +1 -0
  176. package/dist/{src/rules → rules}/html-no-empty-attributes.js +2 -2
  177. package/dist/rules/html-no-empty-attributes.js.map +1 -0
  178. package/dist/rules/html-no-empty-headings.js +98 -0
  179. package/dist/rules/html-no-empty-headings.js.map +1 -0
  180. package/dist/{src/rules → rules}/html-no-nested-links.js +23 -15
  181. package/dist/rules/html-no-nested-links.js.map +1 -0
  182. package/dist/{src/rules → rules}/html-no-positive-tab-index.js +3 -3
  183. package/dist/rules/html-no-positive-tab-index.js.map +1 -0
  184. package/dist/{src/rules → rules}/html-no-self-closing.js +4 -4
  185. package/dist/rules/html-no-self-closing.js.map +1 -0
  186. package/dist/{src/rules → rules}/html-no-space-in-tag.js +4 -6
  187. package/dist/rules/html-no-space-in-tag.js.map +1 -0
  188. package/dist/{src/rules → rules}/html-no-title-attribute.js +6 -5
  189. package/dist/rules/html-no-title-attribute.js.map +1 -0
  190. package/dist/{src/rules → rules}/html-no-underscores-in-attribute-names.js +2 -2
  191. package/dist/rules/html-no-underscores-in-attribute-names.js.map +1 -0
  192. package/dist/rules/html-require-closing-tags.js +29 -0
  193. package/dist/rules/html-require-closing-tags.js.map +1 -0
  194. package/dist/{src/rules → rules}/html-tag-name-lowercase.js +13 -9
  195. package/dist/rules/html-tag-name-lowercase.js.map +1 -0
  196. package/dist/{src/rules → rules}/index.js +27 -4
  197. package/dist/rules/index.js.map +1 -0
  198. package/dist/{src/rules → rules}/parser-no-errors.js +3 -3
  199. package/dist/rules/parser-no-errors.js.map +1 -0
  200. package/dist/{src/rules → rules}/rule-utils.js +144 -219
  201. package/dist/rules/rule-utils.js.map +1 -0
  202. package/dist/rules/string-utils.js.map +1 -0
  203. package/dist/{src/rules → rules}/svg-tag-name-capitalization.js +7 -6
  204. package/dist/rules/svg-tag-name-capitalization.js.map +1 -0
  205. package/dist/rules/turbo-permanent-require-id.js +34 -0
  206. package/dist/rules/turbo-permanent-require-id.js.map +1 -0
  207. package/dist/{src/rules.js → rules.js} +62 -10
  208. package/dist/rules.js.map +1 -0
  209. package/dist/types/cli/argument-parser.d.ts +1 -0
  210. package/dist/types/cli/file-processor.d.ts +13 -0
  211. package/dist/types/cli/file-url.d.ts +1 -0
  212. package/dist/types/cli/index.d.ts +1 -0
  213. package/dist/types/cli/lint-worker.d.ts +34 -0
  214. package/dist/types/custom-rule-loader.d.ts +4 -0
  215. package/dist/types/index.d.ts +2 -0
  216. package/dist/types/linter.d.ts +13 -6
  217. package/dist/types/parse-cache.d.ts +9 -0
  218. package/dist/types/{src/rules/html-aria-level-must-be-valid.d.ts → rules/actionview-no-silent-helper.d.ts} +4 -3
  219. package/dist/types/rules/actionview-no-silent-render.d.ts +9 -0
  220. package/dist/types/rules/erb-comment-syntax.d.ts +1 -1
  221. package/dist/types/rules/erb-no-case-node-children.d.ts +1 -1
  222. package/dist/types/{src/rules/herb-disable-comment-malformed.d.ts → rules/erb-no-conditional-html-element.d.ts} +3 -3
  223. package/dist/types/{src/rules/erb-prefer-image-tag-helper.d.ts → rules/erb-no-conditional-open-tag.d.ts} +3 -3
  224. package/dist/types/rules/erb-no-duplicate-branch-elements.d.ts +18 -0
  225. package/dist/types/{src/rules/html-anchor-require-href.d.ts → rules/erb-no-empty-control-flow.d.ts} +2 -2
  226. package/dist/types/rules/erb-no-empty-tags.d.ts +1 -1
  227. package/dist/types/rules/erb-no-extra-newline.d.ts +1 -1
  228. package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +1 -1
  229. package/dist/types/{src/rules/html-no-duplicate-attributes.d.ts → rules/erb-no-inline-case-conditions.d.ts} +4 -3
  230. package/dist/types/rules/erb-no-instance-variables-in-partials.d.ts +10 -0
  231. package/dist/types/{src/rules/html-no-aria-hidden-on-focusable.d.ts → rules/erb-no-interpolated-class-names.d.ts} +2 -2
  232. package/dist/types/{src/rules/html-aria-attribute-must-be-valid.d.ts → rules/erb-no-javascript-tag-helper.d.ts} +2 -2
  233. package/dist/types/rules/erb-no-output-control-flow.d.ts +1 -1
  234. package/dist/types/{src/rules/erb-no-silent-tag-in-attribute-name.d.ts → rules/erb-no-output-in-attribute-name.d.ts} +2 -2
  235. package/dist/types/{src/rules/herb-disable-comment-missing-rules.d.ts → rules/erb-no-output-in-attribute-position.d.ts} +2 -2
  236. package/dist/types/{src/rules/erb-no-empty-tags.d.ts → rules/erb-no-raw-output-in-attribute-value.d.ts} +2 -2
  237. package/dist/types/{src/rules/html-no-title-attribute.d.ts → rules/erb-no-silent-statement.d.ts} +4 -3
  238. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +1 -1
  239. package/dist/types/{src/rules/html-navigation-has-label.d.ts → rules/erb-no-statement-in-script.d.ts} +2 -2
  240. package/dist/types/rules/erb-no-then-in-control-flow.d.ts +9 -0
  241. package/dist/types/rules/erb-no-trailing-whitespace.d.ts +19 -0
  242. package/dist/types/{src/rules/html-no-positive-tab-index.d.ts → rules/erb-no-unsafe-js-attribute.d.ts} +2 -2
  243. package/dist/types/{src/rules/erb-no-case-node-children.d.ts → rules/erb-no-unsafe-raw.d.ts} +2 -2
  244. package/dist/types/rules/erb-no-unsafe-script-interpolation.d.ts +9 -0
  245. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +1 -1
  246. package/dist/types/rules/erb-require-trailing-newline.d.ts +1 -1
  247. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +1 -1
  248. package/dist/types/rules/erb-right-trim.d.ts +1 -1
  249. package/dist/types/rules/erb-strict-locals-comment-syntax.d.ts +1 -1
  250. package/dist/types/rules/erb-strict-locals-required.d.ts +1 -1
  251. package/dist/types/rules/herb-disable-comment-malformed.d.ts +1 -1
  252. package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +1 -1
  253. package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +1 -1
  254. package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +1 -1
  255. package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +1 -1
  256. package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +1 -1
  257. package/dist/types/{src/rules/html-no-empty-attributes.d.ts → rules/html-allowed-script-type.d.ts} +2 -2
  258. package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
  259. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +1 -1
  260. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +1 -1
  261. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +1 -1
  262. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +1 -1
  263. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +1 -1
  264. package/dist/types/rules/html-attribute-double-quotes.d.ts +1 -1
  265. package/dist/types/rules/html-attribute-equals-spacing.d.ts +1 -1
  266. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +1 -1
  267. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +1 -1
  268. package/dist/types/rules/html-body-only-elements.d.ts +1 -1
  269. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +1 -1
  270. package/dist/types/rules/html-details-has-summary.d.ts +9 -0
  271. package/dist/types/rules/html-head-only-elements.d.ts +1 -1
  272. package/dist/types/rules/html-iframe-has-title.d.ts +1 -1
  273. package/dist/types/rules/html-img-require-alt.d.ts +1 -1
  274. package/dist/types/rules/html-input-require-autocomplete.d.ts +1 -1
  275. package/dist/types/rules/html-navigation-has-label.d.ts +1 -1
  276. package/dist/types/{src/rules/html-no-empty-headings.d.ts → rules/html-no-abstract-roles.d.ts} +2 -2
  277. package/dist/types/{src/rules/erb-no-output-control-flow.d.ts → rules/html-no-aria-hidden-on-body.d.ts} +3 -3
  278. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +1 -1
  279. package/dist/types/rules/html-no-block-inside-inline.d.ts +1 -1
  280. package/dist/types/rules/html-no-duplicate-attributes.d.ts +1 -1
  281. package/dist/types/rules/html-no-duplicate-ids.d.ts +1 -1
  282. package/dist/types/rules/html-no-duplicate-meta-names.d.ts +1 -1
  283. package/dist/types/rules/html-no-empty-attributes.d.ts +1 -1
  284. package/dist/types/rules/html-no-empty-headings.d.ts +1 -1
  285. package/dist/types/rules/html-no-nested-links.d.ts +1 -1
  286. package/dist/types/rules/html-no-positive-tab-index.d.ts +1 -1
  287. package/dist/types/rules/html-no-self-closing.d.ts +1 -1
  288. package/dist/types/rules/html-no-space-in-tag.d.ts +1 -1
  289. package/dist/types/rules/html-no-title-attribute.d.ts +1 -1
  290. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +1 -1
  291. package/dist/types/{src/rules/html-body-only-elements.d.ts → rules/html-require-closing-tags.d.ts} +4 -3
  292. package/dist/types/rules/html-tag-name-lowercase.d.ts +1 -1
  293. package/dist/types/rules/index.d.ts +27 -4
  294. package/dist/types/rules/parser-no-errors.d.ts +1 -1
  295. package/dist/types/rules/rule-utils.d.ts +36 -88
  296. package/dist/types/rules/svg-tag-name-capitalization.d.ts +1 -1
  297. package/dist/types/{src/rules/html-aria-role-must-be-valid.d.ts → rules/turbo-permanent-require-id.d.ts} +2 -2
  298. package/dist/types/types.d.ts +26 -7
  299. package/dist/types/urls.d.ts +1 -0
  300. package/dist/{src/types.js → types.js} +56 -0
  301. package/dist/types.js.map +1 -0
  302. package/dist/urls.js +5 -0
  303. package/dist/urls.js.map +1 -0
  304. package/docs/rules/README.md +26 -2
  305. package/docs/rules/actionview-no-silent-helper.md +57 -0
  306. package/docs/rules/actionview-no-silent-render.md +47 -0
  307. package/docs/rules/erb-no-conditional-html-element.md +90 -0
  308. package/docs/rules/erb-no-conditional-open-tag.md +130 -0
  309. package/docs/rules/erb-no-duplicate-branch-elements.md +98 -0
  310. package/docs/rules/erb-no-empty-control-flow.md +83 -0
  311. package/docs/rules/erb-no-inline-case-conditions.md +85 -0
  312. package/docs/rules/erb-no-instance-variables-in-partials.md +43 -0
  313. package/docs/rules/erb-no-interpolated-class-names.md +57 -0
  314. package/docs/rules/erb-no-javascript-tag-helper.md +33 -0
  315. package/docs/rules/erb-no-output-in-attribute-name.md +38 -0
  316. package/docs/rules/erb-no-output-in-attribute-position.md +60 -0
  317. package/docs/rules/erb-no-raw-output-in-attribute-value.md +37 -0
  318. package/docs/rules/erb-no-silent-statement.md +53 -0
  319. package/docs/rules/erb-no-statement-in-script.md +68 -0
  320. package/docs/rules/erb-no-then-in-control-flow.md +86 -0
  321. package/docs/rules/erb-no-trailing-whitespace.md +69 -0
  322. package/docs/rules/erb-no-unsafe-js-attribute.md +41 -0
  323. package/docs/rules/erb-no-unsafe-raw.md +47 -0
  324. package/docs/rules/erb-no-unsafe-script-interpolation.md +140 -0
  325. package/docs/rules/html-allowed-script-type.md +59 -0
  326. package/docs/rules/html-anchor-require-href.md +19 -6
  327. package/docs/rules/html-details-has-summary.md +46 -0
  328. package/docs/rules/html-img-require-alt.md +5 -3
  329. package/docs/rules/html-no-abstract-roles.md +74 -0
  330. package/docs/rules/html-no-aria-hidden-on-body.md +44 -0
  331. package/docs/rules/html-require-closing-tags.md +142 -0
  332. package/docs/rules/parser-no-errors.md +4 -17
  333. package/docs/rules/turbo-permanent-require-id.md +41 -0
  334. package/package.json +11 -10
  335. package/src/cli/argument-parser.ts +20 -2
  336. package/src/cli/file-processor.ts +189 -10
  337. package/src/cli/file-url.ts +6 -0
  338. package/src/cli/formatters/detailed-formatter.ts +19 -21
  339. package/src/cli/formatters/simple-formatter.ts +23 -13
  340. package/src/cli/index.ts +2 -0
  341. package/src/cli/lint-worker.ts +208 -0
  342. package/src/cli/summary-reporter.ts +14 -15
  343. package/src/cli.ts +5 -3
  344. package/src/custom-rule-loader.ts +20 -5
  345. package/src/herb-disable-comment-utils.ts +0 -3
  346. package/src/index.ts +22 -0
  347. package/src/linter.ts +98 -79
  348. package/src/parse-cache.ts +39 -0
  349. package/src/rules/actionview-no-silent-helper.ts +58 -0
  350. package/src/rules/actionview-no-silent-render.ts +44 -0
  351. package/src/rules/erb-comment-syntax.ts +2 -2
  352. package/src/rules/erb-no-case-node-children.ts +5 -3
  353. package/src/rules/erb-no-conditional-html-element.ts +53 -0
  354. package/src/rules/erb-no-conditional-open-tag.ts +37 -0
  355. package/src/rules/erb-no-duplicate-branch-elements.ts +436 -0
  356. package/src/rules/erb-no-empty-control-flow.ts +255 -0
  357. package/src/rules/erb-no-empty-tags.ts +2 -2
  358. package/src/rules/erb-no-extra-newline.ts +5 -25
  359. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +45 -15
  360. package/src/rules/erb-no-inline-case-conditions.ts +54 -0
  361. package/src/rules/erb-no-instance-variables-in-partials.ts +101 -0
  362. package/src/rules/erb-no-interpolated-class-names.ts +65 -0
  363. package/src/rules/erb-no-javascript-tag-helper.ts +47 -0
  364. package/src/rules/erb-no-output-control-flow.ts +10 -10
  365. package/src/rules/erb-no-output-in-attribute-name.ts +39 -0
  366. package/src/rules/erb-no-output-in-attribute-position.ts +39 -0
  367. package/src/rules/erb-no-raw-output-in-attribute-value.ts +47 -0
  368. package/src/rules/erb-no-silent-statement.ts +58 -0
  369. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +2 -2
  370. package/src/rules/erb-no-statement-in-script.ts +82 -0
  371. package/src/rules/erb-no-then-in-control-flow.ts +62 -0
  372. package/src/rules/erb-no-trailing-whitespace.ts +187 -0
  373. package/src/rules/erb-no-unsafe-js-attribute.ts +47 -0
  374. package/src/rules/erb-no-unsafe-raw.ts +83 -0
  375. package/src/rules/erb-no-unsafe-script-interpolation.ts +122 -0
  376. package/src/rules/erb-prefer-image-tag-helper.ts +5 -4
  377. package/src/rules/erb-require-trailing-newline.ts +2 -2
  378. package/src/rules/erb-require-whitespace-inside-tags.ts +42 -18
  379. package/src/rules/erb-right-trim.ts +2 -2
  380. package/src/rules/erb-strict-locals-comment-syntax.ts +4 -4
  381. package/src/rules/erb-strict-locals-required.ts +2 -2
  382. package/src/rules/herb-disable-comment-malformed.ts +2 -2
  383. package/src/rules/herb-disable-comment-missing-rules.ts +2 -2
  384. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +2 -2
  385. package/src/rules/herb-disable-comment-no-redundant-all.ts +2 -2
  386. package/src/rules/herb-disable-comment-unnecessary.ts +2 -2
  387. package/src/rules/herb-disable-comment-valid-rule-name.ts +2 -2
  388. package/src/rules/html-allowed-script-type.ts +84 -0
  389. package/src/rules/html-anchor-require-href.ts +73 -11
  390. package/src/rules/html-aria-attribute-must-be-valid.ts +3 -3
  391. package/src/rules/html-aria-label-is-well-formatted.ts +3 -3
  392. package/src/rules/html-aria-level-must-be-valid.ts +3 -3
  393. package/src/rules/html-aria-role-heading-requires-level.ts +5 -4
  394. package/src/rules/html-aria-role-must-be-valid.ts +3 -3
  395. package/src/rules/html-attribute-double-quotes.ts +4 -4
  396. package/src/rules/html-attribute-equals-spacing.ts +2 -2
  397. package/src/rules/html-attribute-values-require-quotes.ts +2 -2
  398. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +10 -11
  399. package/src/rules/html-body-only-elements.ts +5 -4
  400. package/src/rules/html-boolean-attributes-no-value.ts +4 -3
  401. package/src/rules/html-details-has-summary.ts +69 -0
  402. package/src/rules/html-head-only-elements.ts +6 -5
  403. package/src/rules/html-iframe-has-title.ts +8 -11
  404. package/src/rules/html-img-require-alt.ts +16 -5
  405. package/src/rules/html-input-require-autocomplete.ts +7 -10
  406. package/src/rules/html-navigation-has-label.ts +6 -5
  407. package/src/rules/html-no-abstract-roles.ts +40 -0
  408. package/src/rules/html-no-aria-hidden-on-body.ts +58 -0
  409. package/src/rules/html-no-aria-hidden-on-focusable.ts +6 -5
  410. package/src/rules/html-no-block-inside-inline.ts +7 -13
  411. package/src/rules/html-no-duplicate-attributes.ts +4 -3
  412. package/src/rules/html-no-duplicate-ids.ts +16 -13
  413. package/src/rules/html-no-duplicate-meta-names.ts +20 -19
  414. package/src/rules/html-no-empty-attributes.ts +2 -2
  415. package/src/rules/html-no-empty-headings.ts +44 -58
  416. package/src/rules/html-no-nested-links.ts +25 -16
  417. package/src/rules/html-no-positive-tab-index.ts +3 -3
  418. package/src/rules/html-no-self-closing.ts +5 -5
  419. package/src/rules/html-no-space-in-tag.ts +5 -8
  420. package/src/rules/html-no-title-attribute.ts +6 -5
  421. package/src/rules/html-no-underscores-in-attribute-names.ts +2 -2
  422. package/src/rules/html-require-closing-tags.ts +41 -0
  423. package/src/rules/html-tag-name-lowercase.ts +14 -9
  424. package/src/rules/index.ts +28 -4
  425. package/src/rules/parser-no-errors.ts +3 -3
  426. package/src/rules/rule-utils.ts +166 -279
  427. package/src/rules/svg-tag-name-capitalization.ts +10 -10
  428. package/src/rules/turbo-permanent-require-id.ts +49 -0
  429. package/src/rules.ts +66 -10
  430. package/src/types.ts +80 -7
  431. package/src/urls.ts +5 -0
  432. package/dist/package.json +0 -65
  433. package/dist/src/cli/argument-parser.js.map +0 -1
  434. package/dist/src/cli/file-processor.js.map +0 -1
  435. package/dist/src/cli/formatters/base-formatter.js.map +0 -1
  436. package/dist/src/cli/formatters/detailed-formatter.js.map +0 -1
  437. package/dist/src/cli/formatters/github-actions-formatter.js.map +0 -1
  438. package/dist/src/cli/formatters/index.js.map +0 -1
  439. package/dist/src/cli/formatters/json-formatter.js.map +0 -1
  440. package/dist/src/cli/formatters/simple-formatter.js +0 -44
  441. package/dist/src/cli/formatters/simple-formatter.js.map +0 -1
  442. package/dist/src/cli/index.js.map +0 -1
  443. package/dist/src/cli/output-manager.js.map +0 -1
  444. package/dist/src/cli/summary-reporter.js.map +0 -1
  445. package/dist/src/cli.js.map +0 -1
  446. package/dist/src/custom-rule-loader.js.map +0 -1
  447. package/dist/src/herb-disable-comment-utils.js.map +0 -1
  448. package/dist/src/herb-lint.js +0 -5
  449. package/dist/src/herb-lint.js.map +0 -1
  450. package/dist/src/index.js +0 -5
  451. package/dist/src/index.js.map +0 -1
  452. package/dist/src/linter-ignore.js.map +0 -1
  453. package/dist/src/linter.js.map +0 -1
  454. package/dist/src/loader.js +0 -17
  455. package/dist/src/loader.js.map +0 -1
  456. package/dist/src/rules/erb-comment-syntax.js.map +0 -1
  457. package/dist/src/rules/erb-no-case-node-children.js.map +0 -1
  458. package/dist/src/rules/erb-no-empty-tags.js.map +0 -1
  459. package/dist/src/rules/erb-no-extra-newline.js.map +0 -1
  460. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +0 -1
  461. package/dist/src/rules/erb-no-output-control-flow.js.map +0 -1
  462. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +0 -1
  463. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +0 -1
  464. package/dist/src/rules/erb-require-trailing-newline.js.map +0 -1
  465. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +0 -1
  466. package/dist/src/rules/erb-right-trim.js.map +0 -1
  467. package/dist/src/rules/erb-strict-locals-comment-syntax.js.map +0 -1
  468. package/dist/src/rules/erb-strict-locals-required.js.map +0 -1
  469. package/dist/src/rules/file-utils.js.map +0 -1
  470. package/dist/src/rules/herb-disable-comment-base.js.map +0 -1
  471. package/dist/src/rules/herb-disable-comment-malformed.js.map +0 -1
  472. package/dist/src/rules/herb-disable-comment-missing-rules.js.map +0 -1
  473. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +0 -1
  474. package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +0 -1
  475. package/dist/src/rules/herb-disable-comment-unnecessary.js.map +0 -1
  476. package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +0 -1
  477. package/dist/src/rules/html-anchor-require-href.js +0 -32
  478. package/dist/src/rules/html-anchor-require-href.js.map +0 -1
  479. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +0 -1
  480. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +0 -1
  481. package/dist/src/rules/html-aria-level-must-be-valid.js.map +0 -1
  482. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +0 -1
  483. package/dist/src/rules/html-aria-role-must-be-valid.js.map +0 -1
  484. package/dist/src/rules/html-attribute-double-quotes.js.map +0 -1
  485. package/dist/src/rules/html-attribute-equals-spacing.js.map +0 -1
  486. package/dist/src/rules/html-attribute-values-require-quotes.js.map +0 -1
  487. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +0 -1
  488. package/dist/src/rules/html-body-only-elements.js.map +0 -1
  489. package/dist/src/rules/html-boolean-attributes-no-value.js.map +0 -1
  490. package/dist/src/rules/html-head-only-elements.js.map +0 -1
  491. package/dist/src/rules/html-iframe-has-title.js.map +0 -1
  492. package/dist/src/rules/html-img-require-alt.js.map +0 -1
  493. package/dist/src/rules/html-input-require-autocomplete.js.map +0 -1
  494. package/dist/src/rules/html-navigation-has-label.js.map +0 -1
  495. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +0 -1
  496. package/dist/src/rules/html-no-block-inside-inline.js.map +0 -1
  497. package/dist/src/rules/html-no-duplicate-attributes.js.map +0 -1
  498. package/dist/src/rules/html-no-duplicate-ids.js.map +0 -1
  499. package/dist/src/rules/html-no-duplicate-meta-names.js.map +0 -1
  500. package/dist/src/rules/html-no-empty-attributes.js.map +0 -1
  501. package/dist/src/rules/html-no-empty-headings.js +0 -115
  502. package/dist/src/rules/html-no-empty-headings.js.map +0 -1
  503. package/dist/src/rules/html-no-nested-links.js.map +0 -1
  504. package/dist/src/rules/html-no-positive-tab-index.js.map +0 -1
  505. package/dist/src/rules/html-no-self-closing.js.map +0 -1
  506. package/dist/src/rules/html-no-space-in-tag.js.map +0 -1
  507. package/dist/src/rules/html-no-title-attribute.js.map +0 -1
  508. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +0 -1
  509. package/dist/src/rules/html-tag-name-lowercase.js.map +0 -1
  510. package/dist/src/rules/index.js.map +0 -1
  511. package/dist/src/rules/parser-no-errors.js.map +0 -1
  512. package/dist/src/rules/rule-utils.js.map +0 -1
  513. package/dist/src/rules/string-utils.js.map +0 -1
  514. package/dist/src/rules/svg-tag-name-capitalization.js.map +0 -1
  515. package/dist/src/rules.js.map +0 -1
  516. package/dist/src/types.js.map +0 -1
  517. package/dist/tsconfig.tsbuildinfo +0 -1
  518. package/dist/types/src/cli/argument-parser.d.ts +0 -25
  519. package/dist/types/src/cli/file-processor.d.ts +0 -43
  520. package/dist/types/src/cli/formatters/base-formatter.d.ts +0 -6
  521. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +0 -13
  522. package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +0 -17
  523. package/dist/types/src/cli/formatters/index.d.ts +0 -5
  524. package/dist/types/src/cli/formatters/json-formatter.d.ts +0 -48
  525. package/dist/types/src/cli/formatters/simple-formatter.d.ts +0 -8
  526. package/dist/types/src/cli/index.d.ts +0 -5
  527. package/dist/types/src/cli/output-manager.d.ts +0 -32
  528. package/dist/types/src/cli/summary-reporter.d.ts +0 -28
  529. package/dist/types/src/cli.d.ts +0 -28
  530. package/dist/types/src/custom-rule-loader.d.ts +0 -62
  531. package/dist/types/src/herb-disable-comment-utils.d.ts +0 -69
  532. package/dist/types/src/herb-lint.d.ts +0 -2
  533. package/dist/types/src/index.d.ts +0 -4
  534. package/dist/types/src/linter-ignore.d.ts +0 -12
  535. package/dist/types/src/linter.d.ts +0 -133
  536. package/dist/types/src/loader.d.ts +0 -20
  537. package/dist/types/src/rules/erb-comment-syntax.d.ts +0 -14
  538. package/dist/types/src/rules/erb-no-extra-newline.d.ts +0 -14
  539. package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +0 -18
  540. package/dist/types/src/rules/erb-require-trailing-newline.d.ts +0 -9
  541. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +0 -18
  542. package/dist/types/src/rules/erb-right-trim.d.ts +0 -14
  543. package/dist/types/src/rules/erb-strict-locals-comment-syntax.d.ts +0 -9
  544. package/dist/types/src/rules/erb-strict-locals-required.d.ts +0 -9
  545. package/dist/types/src/rules/file-utils.d.ts +0 -13
  546. package/dist/types/src/rules/herb-disable-comment-base.d.ts +0 -37
  547. package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +0 -8
  548. package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +0 -8
  549. package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +0 -8
  550. package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +0 -8
  551. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +0 -8
  552. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +0 -8
  553. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +0 -15
  554. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +0 -14
  555. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +0 -15
  556. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +0 -8
  557. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +0 -14
  558. package/dist/types/src/rules/html-head-only-elements.d.ts +0 -9
  559. package/dist/types/src/rules/html-iframe-has-title.d.ts +0 -8
  560. package/dist/types/src/rules/html-img-require-alt.d.ts +0 -8
  561. package/dist/types/src/rules/html-input-require-autocomplete.d.ts +0 -8
  562. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +0 -8
  563. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +0 -8
  564. package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +0 -9
  565. package/dist/types/src/rules/html-no-nested-links.d.ts +0 -8
  566. package/dist/types/src/rules/html-no-self-closing.d.ts +0 -16
  567. package/dist/types/src/rules/html-no-space-in-tag.d.ts +0 -16
  568. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +0 -8
  569. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +0 -18
  570. package/dist/types/src/rules/index.d.ts +0 -54
  571. package/dist/types/src/rules/parser-no-errors.d.ts +0 -9
  572. package/dist/types/src/rules/rule-utils.d.ts +0 -351
  573. package/dist/types/src/rules/string-utils.d.ts +0 -15
  574. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +0 -16
  575. package/dist/types/src/rules.d.ts +0 -2
  576. package/dist/types/src/types.d.ts +0 -190
  577. /package/dist/{src/cli → cli}/formatters/base-formatter.js +0 -0
  578. /package/dist/{src/cli → cli}/formatters/github-actions-formatter.js +0 -0
  579. /package/dist/{src/cli → cli}/formatters/index.js +0 -0
  580. /package/dist/{src/cli → cli}/formatters/json-formatter.js +0 -0
  581. /package/dist/{src/cli → cli}/index.js +0 -0
  582. /package/dist/{src/cli → cli}/output-manager.js +0 -0
  583. /package/dist/{src/herb-disable-comment-utils.js → herb-disable-comment-utils.js} +0 -0
  584. /package/dist/{src/linter-ignore.js → linter-ignore.js} +0 -0
  585. /package/dist/{src/rules → rules}/file-utils.js +0 -0
  586. /package/dist/{src/rules → rules}/herb-disable-comment-base.js +0 -0
  587. /package/dist/{src/rules → rules}/string-utils.js +0 -0
@@ -1,4 +1,6 @@
1
- import { colorize } from "@herb-tools/highlighter"
1
+ import { colorize, hyperlink } from "@herb-tools/highlighter"
2
+
3
+ import { ruleDocumentationUrl } from "../urls.js"
2
4
 
3
5
  export interface SummaryData {
4
6
  files: string[]
@@ -39,20 +41,14 @@ export class SummaryReporter {
39
41
  const filesClean = filesChecked - filesWithOffenses
40
42
 
41
43
  let filesSummary = ""
42
- let shouldDim = false
43
44
 
44
45
  if (filesWithOffenses > 0) {
45
- filesSummary = `${colorize(colorize(`${filesWithOffenses} with offenses`, "brightRed"), "bold")} | ${colorize(colorize(`${filesClean} clean`, "green"), "bold")} ${colorize(colorize(`(${filesChecked} total)`, "gray"), "dim")}`
46
+ filesSummary = `${colorize(colorize(`${filesWithOffenses} with offenses`, "brightRed"), "bold")} | ${colorize(colorize(`${filesClean} clean`, "green"), "bold")} ${colorize(`(${filesChecked} total)`, "gray")}`
46
47
  } else {
47
- filesSummary = `${colorize(colorize(`${filesChecked} clean`, "green"), "bold")} ${colorize(colorize(`(${filesChecked} total)`, "gray"), "dim")}`
48
- shouldDim = true
48
+ filesSummary = `${colorize(colorize(`${filesChecked} clean`, "green"), "bold")} ${colorize(`(${filesChecked} total)`, "gray")}`
49
49
  }
50
50
 
51
- if (shouldDim) {
52
- console.log(colorize(` ${colorize(pad("Files"), "gray")} ${filesSummary}`, "dim"))
53
- } else {
54
- console.log(` ${colorize(pad("Files"), "gray")} ${filesSummary}`)
55
- }
51
+ console.log(` ${colorize(pad("Files"), "gray")} ${filesSummary}`)
56
52
  }
57
53
 
58
54
  let offensesSummary = ""
@@ -69,7 +65,7 @@ export class SummaryReporter {
69
65
  }
70
66
 
71
67
  if (totalInfo > 0) {
72
- parts.push(colorize(colorize(`${totalInfo} info`, "brightBlue"), "bold"))
68
+ parts.push(colorize(colorize(`${totalInfo} info`, "cyan"), "bold"))
73
69
  }
74
70
 
75
71
  if (totalHints > 0) {
@@ -94,7 +90,7 @@ export class SummaryReporter {
94
90
  }
95
91
 
96
92
  if (detailText) {
97
- offensesSummary += ` ${colorize(colorize(`(${detailText})`, "gray"), "dim")}`
93
+ offensesSummary += ` ${colorize(`(${detailText})`, "gray")}`
98
94
  }
99
95
  }
100
96
 
@@ -122,7 +118,7 @@ export class SummaryReporter {
122
118
  const timeString = startDate.toTimeString().split(' ')[0]
123
119
 
124
120
  console.log(` ${colorize(pad("Start at"), "gray")} ${colorize(timeString, "cyan")}`)
125
- console.log(` ${colorize(pad("Duration"), "gray")} ${colorize(`${duration}ms`, "cyan")} ${colorize(colorize(`(${ruleCount} ${this.pluralize(ruleCount, "rule")})`, "gray"), "dim")}`)
121
+ console.log(` ${colorize(pad("Duration"), "gray")} ${colorize(`${duration}ms`, "cyan")} ${colorize(`(${ruleCount} ${this.pluralize(ruleCount, "rule")})`, "gray")}`)
126
122
  }
127
123
 
128
124
  if (filesWithOffenses === 0 && files.length > 1) {
@@ -139,18 +135,21 @@ export class SummaryReporter {
139
135
  const remainingRules = allRules.slice(limit)
140
136
 
141
137
  const title = ruleOffenses.size <= limit ? "Rule offenses:" : "Most frequent rule offenses:"
138
+ console.log("\n")
142
139
  console.log(` ${colorize(title, "bold")}`)
143
140
 
144
141
  for (const [rule, data] of displayedRules) {
145
142
  const fileCount = data.files.size
146
143
  const countText = `(${data.count} ${this.pluralize(data.count, "offense")} in ${fileCount} ${this.pluralize(fileCount, "file")})`
147
- console.log(` ${colorize(rule, "gray")} ${colorize(colorize(countText, "gray"), "dim")}`)
144
+ const ruleText = colorize(rule, "white")
145
+ const ruleLink = hyperlink(ruleText, ruleDocumentationUrl(rule))
146
+ console.log(` ${ruleLink} ${colorize(countText, "gray")}`)
148
147
  }
149
148
 
150
149
  if (remainingRules.length > 0) {
151
150
  const remainingOffenseCount = remainingRules.reduce((sum, [_, data]) => sum + data.count, 0)
152
151
  const remainingRuleCount = remainingRules.length
153
- console.log(colorize(colorize(`\n ...and ${remainingRuleCount} more ${this.pluralize(remainingRuleCount, "rule")} with ${remainingOffenseCount} ${this.pluralize(remainingOffenseCount, "offense")}`, "gray"), "dim"))
152
+ console.log(colorize(`\n ...and ${remainingRuleCount} more ${this.pluralize(remainingRuleCount, "rule")} with ${remainingOffenseCount} ${this.pluralize(remainingOffenseCount, "offense")}`, "gray"))
154
153
  }
155
154
  }
156
155
  }
package/src/cli.ts CHANGED
@@ -144,7 +144,7 @@ export class CLI {
144
144
  const startTime = Date.now()
145
145
  const startDate = new Date()
146
146
 
147
- let { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel } = this.argumentParser.parse(process.argv)
147
+ const { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel, jobs } = this.argumentParser.parse(process.argv)
148
148
 
149
149
  this.determineProjectPath(patterns)
150
150
 
@@ -199,7 +199,7 @@ export class CLI {
199
199
  }
200
200
 
201
201
  let files: string[]
202
- let explicitFiles: string[] = []
202
+ const explicitFiles: string[] = []
203
203
 
204
204
  if (patterns.length === 0) {
205
205
  files = await config.findFilesForTool('linter', this.projectPath)
@@ -246,13 +246,15 @@ export class CLI {
246
246
 
247
247
  const context: ProcessingContext = {
248
248
  projectPath: this.projectPath,
249
+ configPath: configFile,
249
250
  pattern: patterns.join(' '),
250
251
  fix,
251
252
  fixUnsafe,
252
253
  ignoreDisableComments,
253
254
  linterConfig,
254
255
  config: processingConfig,
255
- loadCustomRules
256
+ loadCustomRules,
257
+ jobs
256
258
  }
257
259
 
258
260
  const results = await this.fileProcessor.processFiles(files, formatOption, context)
@@ -75,10 +75,16 @@ export class CustomRuleLoader {
75
75
  const cacheBustedUrl = `${fileUrl}?t=${Date.now()}`
76
76
  const module = await import(cacheBustedUrl)
77
77
 
78
- if (module.default && this.isValidRuleClass(module.default)) {
78
+ if (this.isValidRuleClass(module.default)) {
79
79
  return [module.default]
80
80
  }
81
81
 
82
+ if (this.hasDeprecatedNameProperty(module.default)) {
83
+ const name = new module.default().name
84
+ console.error(`Error: Custom rule in "${filePath}" sets 'name' as an instance property, which is no longer supported. Use 'static ruleName = "${name}"' instead.`)
85
+ process.exit(1)
86
+ }
87
+
82
88
  if (!this.silent) {
83
89
  console.warn(`Warning: No valid default export found in "${filePath}". Custom rules must use default export.`)
84
90
  }
@@ -96,12 +102,22 @@ export class CustomRuleLoader {
96
102
  * Type guard to check if an export is a valid rule class
97
103
  */
98
104
  private isValidRuleClass(value: any): value is RuleClass {
105
+ if (!value) return false
99
106
  if (typeof value !== 'function') return false
100
107
  if (!value.prototype) return false
101
108
 
102
- const instance = new value()
109
+ return typeof value.ruleName === 'string' && typeof value.prototype.check === 'function'
110
+ }
103
111
 
104
- return typeof instance.check === 'function' && typeof instance.name === 'string'
112
+ /**
113
+ * Check for usage of deprecated 'name' property instead of 'static ruleName'
114
+ */
115
+ private hasDeprecatedNameProperty(value: any): boolean {
116
+ if (!value) return false
117
+ if (typeof value !== 'function') return false
118
+
119
+ const instance = new value()
120
+ return Object.prototype.hasOwnProperty.call(instance, 'name')
105
121
  }
106
122
 
107
123
  /**
@@ -142,8 +158,7 @@ export class CustomRuleLoader {
142
158
  for (const filePath of ruleFiles) {
143
159
  const rules = await this.loadRuleFile(filePath)
144
160
  for (const RuleClass of rules) {
145
- const instance = new RuleClass()
146
- const ruleName = instance.name
161
+ const ruleName = RuleClass.ruleName
147
162
 
148
163
  if (seenNames.has(ruleName)) {
149
164
  const firstPath = seenNames.get(ruleName)!
@@ -2,9 +2,6 @@
2
2
  * Utilities for parsing herb:disable comments
3
3
  */
4
4
 
5
- import { isERBCommentNode } from "@herb-tools/core"
6
- import type { Node } from "@herb-tools/core"
7
-
8
5
  /**
9
6
  * Information about a single rule name in a herb:disable comment
10
7
  */
package/src/index.ts CHANGED
@@ -2,4 +2,26 @@ export * from "./linter.js"
2
2
  export * from "./rules/index.js"
3
3
  export * from "./types.js"
4
4
 
5
+ export { ruleDocumentationUrl } from "./urls.js"
5
6
  export { rules } from "./rules.js"
7
+
8
+ export {
9
+ findAttributeByName,
10
+ getAttribute,
11
+ getAttributeName,
12
+ getAttributes,
13
+ getAttributeValue,
14
+ getAttributeValueNodes,
15
+ getAttributeValueQuoteType,
16
+ getCombinedAttributeNameString,
17
+ getStaticAttributeValue,
18
+ getStaticAttributeValueContent,
19
+ getTagName,
20
+ hasAttribute,
21
+ hasAttributeValue,
22
+ hasDynamicAttributeName,
23
+ hasDynamicAttributeValue,
24
+ hasStaticAttributeValue,
25
+ hasStaticAttributeValueContent,
26
+ isAttributeValueQuoted,
27
+ } from "@herb-tools/core"
package/src/linter.ts CHANGED
@@ -1,16 +1,19 @@
1
- import { Location } from "@herb-tools/core"
2
- import { IdentityPrinter } from "@herb-tools/printer"
3
1
  import picomatch from "picomatch"
4
2
 
3
+ import { Location } from "@herb-tools/core"
4
+ import { IdentityPrinter, IndentPrinter } from "@herb-tools/printer"
5
+
5
6
  import { rules } from "./rules.js"
6
7
  import { findNodeByLocation } from "./rules/rule-utils.js"
7
8
  import { parseHerbDisableLine } from "./herb-disable-comment-utils.js"
8
9
  import { hasLinterIgnoreDirective } from "./linter-ignore.js"
10
+ import { ParseCache } from "./parse-cache.js"
9
11
 
10
12
  import { ParserNoErrorsRule } from "./rules/parser-no-errors.js"
13
+
11
14
  import { DEFAULT_RULE_CONFIG } from "./types.js"
12
15
 
13
- import type { RuleClass, Rule, ParserRule, LexerRule, SourceRule, LintResult, LintOffense, UnboundLintOffense, LintContext, AutofixResult } from "./types.js"
16
+ import type { RuleClass, ParserRuleClass, LexerRuleClass, SourceRuleClass, Rule, ParserRule, LexerRule, SourceRule, LintResult, LintOffense, UnboundLintOffense, LintContext, AutofixResult } from "./types.js"
14
17
  import type { ParseResult, LexResult, HerbBackend } from "@herb-tools/core"
15
18
  import type { RuleConfig, Config } from "@herb-tools/config"
16
19
 
@@ -45,9 +48,10 @@ export interface LinterOptions {
45
48
  }
46
49
 
47
50
  export class Linter {
48
- protected rules: RuleClass[]
51
+ public rules: RuleClass[]
49
52
  protected allAvailableRules: RuleClass[]
50
53
  protected herb: HerbBackend
54
+ protected parseCache: ParseCache
51
55
  protected offenses: LintOffense[]
52
56
  protected config?: Config
53
57
 
@@ -81,6 +85,7 @@ export class Linter {
81
85
  */
82
86
  constructor(herb: HerbBackend, rules?: RuleClass[], config?: Config, allAvailableRules?: RuleClass[]) {
83
87
  this.herb = herb
88
+ this.parseCache = new ParseCache(herb)
84
89
  this.config = config
85
90
  this.rules = rules !== undefined ? rules : this.getDefaultRules()
86
91
  this.allAvailableRules = allAvailableRules !== undefined ? allAvailableRules : this.rules
@@ -104,10 +109,9 @@ export class Linter {
104
109
  ): RuleClass[] {
105
110
  return allRules.filter(ruleClass => {
106
111
  const instance = new ruleClass()
107
- const ruleName = instance.name
108
112
 
109
113
  const defaultEnabled = instance.defaultConfig?.enabled ?? DEFAULT_RULE_CONFIG.enabled
110
- const userRuleConfig = userRulesConfig?.[ruleName]
114
+ const userRuleConfig = userRulesConfig?.[ruleClass.ruleName]
111
115
 
112
116
  if (userRuleConfig !== undefined) {
113
117
  return userRuleConfig.enabled !== false
@@ -156,18 +160,29 @@ export class Linter {
156
160
  return this.rules.length
157
161
  }
158
162
 
163
+ protected findRuleClass(ruleName: string): RuleClass | undefined {
164
+ return this.rules.find(ruleClass => ruleClass.ruleName === ruleName)
165
+ }
166
+
167
+ /**
168
+ * Type guard to check if a rule class is a LexerRule class
169
+ */
170
+ protected isLexerRuleClass(ruleClass: RuleClass): ruleClass is LexerRuleClass {
171
+ return ruleClass.type === "lexer"
172
+ }
173
+
159
174
  /**
160
- * Type guard to check if a rule is a LexerRule
175
+ * Type guard to check if a rule class is a SourceRule class
161
176
  */
162
- protected isLexerRule(rule: Rule): rule is LexerRule {
163
- return (rule.constructor as any).type === "lexer"
177
+ protected isSourceRuleClass(ruleClass: RuleClass): ruleClass is SourceRuleClass {
178
+ return ruleClass.type === "source"
164
179
  }
165
180
 
166
181
  /**
167
- * Type guard to check if a rule is a SourceRule
182
+ * Type guard to check if a rule class is a ParserRule class
168
183
  */
169
- protected isSourceRule(rule: Rule): rule is SourceRule {
170
- return (rule.constructor as any).type === "source"
184
+ protected isParserRuleClass(ruleClass: RuleClass): ruleClass is ParserRuleClass {
185
+ return ruleClass.type === "parser" || ruleClass.type === undefined
171
186
  }
172
187
 
173
188
  /**
@@ -175,19 +190,22 @@ export class Linter {
175
190
  * Handles rule type checking (Lexer/Parser/Source) and isEnabled checks.
176
191
  */
177
192
  private executeRule(
193
+ ruleClass: RuleClass,
178
194
  rule: Rule,
179
195
  parseResult: ParseResult,
180
196
  lexResult: LexResult,
181
197
  source: string,
182
198
  context?: Partial<LintContext>
183
199
  ): UnboundLintOffense[] {
200
+ const ruleName = rule.ruleName
201
+
184
202
  if (this.config && context?.fileName) {
185
- if (!this.config.isRuleEnabledForPath(rule.name, context.fileName)) {
203
+ if (!this.config.isRuleEnabledForPath(ruleName, context.fileName)) {
186
204
  return []
187
205
  }
188
206
  }
189
207
 
190
- if (context?.fileName && !this.config?.linter?.rules?.[rule.name]?.exclude) {
208
+ if (context?.fileName && !this.config?.linter?.rules?.[ruleName]?.exclude) {
191
209
  const defaultExclude = rule.defaultConfig?.exclude ?? DEFAULT_RULE_CONFIG.exclude
192
210
 
193
211
  if (defaultExclude && defaultExclude.length > 0) {
@@ -202,34 +220,40 @@ export class Linter {
202
220
  let isEnabled = true
203
221
  let ruleOffenses: UnboundLintOffense[]
204
222
 
205
- if (this.isLexerRule(rule)) {
206
- if (rule.isEnabled) {
207
- isEnabled = rule.isEnabled(lexResult, context)
223
+ if (this.isLexerRuleClass(ruleClass)) {
224
+ const lexerRule = rule as LexerRule
225
+
226
+ if (lexerRule.isEnabled) {
227
+ isEnabled = lexerRule.isEnabled(lexResult, context)
208
228
  }
209
229
 
210
230
  if (isEnabled) {
211
- ruleOffenses = (rule as LexerRule).check(lexResult, context)
231
+ ruleOffenses = lexerRule.check(lexResult, context)
212
232
  } else {
213
233
  ruleOffenses = []
214
234
  }
215
235
 
216
- } else if (this.isSourceRule(rule)) {
217
- if (rule.isEnabled) {
218
- isEnabled = rule.isEnabled(source, context)
236
+ } else if (this.isSourceRuleClass(ruleClass)) {
237
+ const sourceRule = rule as SourceRule
238
+
239
+ if (sourceRule.isEnabled) {
240
+ isEnabled = sourceRule.isEnabled(source, context)
219
241
  }
220
242
 
221
243
  if (isEnabled) {
222
- ruleOffenses = (rule as SourceRule).check(source, context)
244
+ ruleOffenses = sourceRule.check(source, context)
223
245
  } else {
224
246
  ruleOffenses = []
225
247
  }
226
248
  } else {
227
- if (rule.isEnabled) {
228
- isEnabled = rule.isEnabled(parseResult, context)
249
+ const parserRule = rule as ParserRule
250
+
251
+ if (parserRule.isEnabled) {
252
+ isEnabled = parserRule.isEnabled(parseResult, context)
229
253
  }
230
254
 
231
255
  if (isEnabled) {
232
- ruleOffenses = (rule as ParserRule).check(parseResult, context)
256
+ ruleOffenses = parserRule.check(parseResult, context)
233
257
  } else {
234
258
  ruleOffenses = []
235
259
  }
@@ -303,7 +327,7 @@ export class Linter {
303
327
  let ignoredCount = 0
304
328
  let wouldBeIgnoredCount = 0
305
329
 
306
- const parseResult = this.herb.parse(source, { track_whitespace: true })
330
+ const parseResult = this.parseCache.get(source)
307
331
 
308
332
  // Check for file-level ignore directive using visitor
309
333
  if (hasLinterIgnoreDirective(parseResult)) {
@@ -323,22 +347,13 @@ export class Linter {
323
347
  const herbDisableCache = new Map<number, string[]>()
324
348
 
325
349
  if (hasParserErrors) {
326
- const hasParserRule = this.rules.find(RuleClass => (new RuleClass()).name === "parser-no-errors")
350
+ const hasParserRule = this.findRuleClass("parser-no-errors")
327
351
 
328
352
  if (hasParserRule) {
329
353
  const rule = new ParserNoErrorsRule()
330
354
  const offenses = rule.check(parseResult)
331
355
  this.offenses.push(...offenses)
332
356
  }
333
-
334
- return {
335
- offenses: this.offenses,
336
- errors: this.offenses.filter(o => o.severity === "error").length,
337
- warnings: this.offenses.filter(o => o.severity === "warning").length,
338
- info: this.offenses.filter(o => o.severity === "info").length,
339
- hints: this.offenses.filter(o => o.severity === "hint").length,
340
- ignored: 0
341
- }
342
357
  }
343
358
 
344
359
  for (let i = 0; i < sourceLines.length; i++) {
@@ -352,24 +367,31 @@ export class Linter {
352
367
 
353
368
  context = {
354
369
  ...context,
355
- validRuleNames: this.getAvailableRules().map(RuleClass => new RuleClass().name),
370
+ validRuleNames: this.getAvailableRules().map(ruleClass => ruleClass.ruleName),
356
371
  ignoredOffensesByLine
357
372
  }
358
373
 
359
- const regularRules = this.rules.filter(RuleClass => {
360
- const rule = new RuleClass()
374
+ const regularRules = this.rules.filter(ruleClass => ruleClass.ruleName !== "herb-disable-comment-unnecessary")
361
375
 
362
- return rule.name !== "herb-disable-comment-unnecessary"
363
- })
376
+ for (const ruleClass of regularRules) {
377
+ const rule = new ruleClass()
378
+ const parserOptions = this.isParserRuleClass(ruleClass) ? (rule as ParserRule).parserOptions : {}
379
+ const parseResult = this.parseCache.get(source, parserOptions)
380
+
381
+ // Skip parser rules whose parse result has errors (parser-no-errors handled above)
382
+ // Skip lexer/source rules when the default parse has errors
383
+ if (this.isParserRuleClass(ruleClass)) {
384
+ if (parseResult.recursiveErrors().length > 0) continue
385
+ } else if (hasParserErrors) {
386
+ continue
387
+ }
364
388
 
365
- for (const RuleClass of regularRules) {
366
- const rule = new RuleClass()
367
- const unboundOffenses = this.executeRule(rule, parseResult, lexResult, source, context)
368
- const boundOffenses = this.bindSeverity(unboundOffenses, rule.name)
389
+ const unboundOffenses = this.executeRule(ruleClass, rule, parseResult, lexResult, source, context)
390
+ const boundOffenses = this.bindSeverity(unboundOffenses, ruleClass.ruleName)
369
391
 
370
392
  const { kept, ignored, wouldBeIgnored } = this.filterOffenses(
371
393
  boundOffenses,
372
- rule.name,
394
+ ruleClass.ruleName,
373
395
  ignoredOffensesByLine,
374
396
  herbDisableCache,
375
397
  context?.ignoreDisableComments
@@ -380,16 +402,13 @@ export class Linter {
380
402
  this.offenses.push(...kept)
381
403
  }
382
404
 
383
- const unnecessaryRuleClass = this.rules.find(RuleClass => {
384
- const rule = new RuleClass()
385
-
386
- return rule.name === "herb-disable-comment-unnecessary"
387
- })
405
+ const unnecessaryRuleClass = this.findRuleClass("herb-disable-comment-unnecessary")
388
406
 
389
407
  if (unnecessaryRuleClass) {
390
408
  const unnecessaryRule = new unnecessaryRuleClass() as ParserRule
409
+ const parseResult = this.parseCache.get(source, unnecessaryRule.parserOptions)
391
410
  const unboundOffenses = unnecessaryRule.check(parseResult, context)
392
- const boundOffenses = this.bindSeverity(unboundOffenses, unnecessaryRule.name)
411
+ const boundOffenses = this.bindSeverity(unboundOffenses, unnecessaryRuleClass.ruleName)
393
412
 
394
413
  this.offenses.push(...boundOffenses)
395
414
  }
@@ -429,19 +448,16 @@ export class Linter {
429
448
  * @returns Array of offenses with severity bound
430
449
  */
431
450
  protected bindSeverity(unboundOffenses: UnboundLintOffense[], ruleName: string): LintOffense[] {
432
- const RuleClass = this.rules.find(rule => {
433
- const instance = new rule()
434
- return instance.name === ruleName
435
- })
451
+ const ruleClass = this.findRuleClass(ruleName)
436
452
 
437
- if (!RuleClass) {
453
+ if (!ruleClass) {
438
454
  return unboundOffenses.map(offense => ({
439
455
  ...offense,
440
456
  severity: "error" as const
441
457
  }))
442
458
  }
443
459
 
444
- const ruleInstance = new RuleClass()
460
+ const ruleInstance = new ruleClass()
445
461
  const defaultSeverity = ruleInstance.defaultConfig?.severity ?? DEFAULT_RULE_CONFIG.severity
446
462
 
447
463
  const userRuleConfig = this.config?.linter?.rules?.[ruleName]
@@ -449,7 +465,7 @@ export class Linter {
449
465
 
450
466
  return unboundOffenses.map(offense => ({
451
467
  ...offense,
452
- severity
468
+ severity: offense.severity ?? severity
453
469
  }))
454
470
  }
455
471
 
@@ -472,17 +488,13 @@ export class Linter {
472
488
  const sourceOffenses: LintOffense[] = []
473
489
 
474
490
  for (const offense of lintResult.offenses) {
475
- const RuleClass = this.rules.find(rule => {
476
- const instance = new rule()
477
-
478
- return instance.name === offense.rule
479
- })
491
+ const ruleClass = this.findRuleClass(offense.rule)
480
492
 
481
- if (!RuleClass) continue
493
+ if (!ruleClass) continue
482
494
 
483
- if ((RuleClass as any).type === "lexer") {
495
+ if (this.isLexerRuleClass(ruleClass)) {
484
496
  lexerOffenses.push(offense)
485
- } else if ((RuleClass as any).type === "source") {
497
+ } else if (this.isSourceRuleClass(ruleClass)) {
486
498
  sourceOffenses.push(offense)
487
499
  } else {
488
500
  parserOffenses.push(offense)
@@ -494,19 +506,20 @@ export class Linter {
494
506
  const unfixed: LintOffense[] = []
495
507
 
496
508
  if (parserOffenses.length > 0) {
497
- const parseResult = this.herb.parse(currentSource, { track_whitespace: true })
509
+ const parseResult = this.parseCache.get(currentSource)
510
+ let needsReindent = false
498
511
 
499
512
  for (const offense of parserOffenses) {
500
- const RuleClass = this.rules.find(rule => new rule().name === offense.rule)
513
+ const ruleClass = this.findRuleClass(offense.rule)
501
514
 
502
- if (!RuleClass) {
515
+ if (!ruleClass) {
503
516
  unfixed.push(offense)
504
517
 
505
518
  continue
506
519
  }
507
520
 
508
- const rule = new RuleClass() as ParserRule
509
- const isUnsafe = (RuleClass as any).unsafeAutocorrectable === true
521
+ const rule = new ruleClass() as ParserRule
522
+ const isUnsafe = (ruleClass as any).unsafeAutocorrectable === true || offense.autofixContext?.unsafe === true
510
523
 
511
524
  if (!rule.autofix) {
512
525
  unfixed.push(offense)
@@ -543,14 +556,21 @@ export class Linter {
543
556
 
544
557
  if (fixedResult) {
545
558
  fixed.push(offense)
559
+
560
+ if (this.isParserRuleClass(ruleClass) && ruleClass.reindentAfterAutofix === true) {
561
+ needsReindent = true
562
+ }
546
563
  } else {
547
564
  unfixed.push(offense)
548
565
  }
549
566
  }
550
567
 
551
568
  if (fixed.length > 0) {
552
- const printer = new IdentityPrinter()
553
- currentSource = printer.print(parseResult.value)
569
+ if (needsReindent) {
570
+ currentSource = new IndentPrinter().print(parseResult.value)
571
+ } else {
572
+ currentSource = new IdentityPrinter().print(parseResult.value)
573
+ }
554
574
  }
555
575
  }
556
576
 
@@ -564,15 +584,15 @@ export class Linter {
564
584
  })
565
585
 
566
586
  for (const offense of sortedSourceOffenses) {
567
- const RuleClass = this.rules.find(rule => new rule().name === offense.rule)
587
+ const ruleClass = this.findRuleClass(offense.rule)
568
588
 
569
- if (!RuleClass) {
589
+ if (!ruleClass) {
570
590
  unfixed.push(offense)
571
591
  continue
572
592
  }
573
593
 
574
- const rule = new RuleClass() as SourceRule
575
- const isUnsafe = (RuleClass as any).unsafeAutocorrectable === true
594
+ const rule = new ruleClass() as SourceRule
595
+ const isUnsafe = (ruleClass as any).unsafeAutocorrectable === true || offense.autofixContext?.unsafe === true
576
596
 
577
597
  if (!rule.autofix) {
578
598
  unfixed.push(offense)
@@ -602,4 +622,3 @@ export class Linter {
602
622
  }
603
623
  }
604
624
  }
605
-
@@ -0,0 +1,39 @@
1
+ import { DEFAULT_PARSER_OPTIONS } from "@herb-tools/core"
2
+ import { DEFAULT_LINTER_PARSER_OPTIONS } from "./types.js"
3
+
4
+ import type { ParseResult, HerbBackend, ParserOptions } from "@herb-tools/core"
5
+
6
+ export class ParseCache {
7
+ private herb: HerbBackend
8
+ private cache = new Map<string, ParseResult>()
9
+
10
+ constructor(herb: HerbBackend) {
11
+ this.herb = herb
12
+ }
13
+
14
+ get(source: string, parserOptions: Partial<ParserOptions> = {}): ParseResult {
15
+ const effectiveOptions = this.resolveOptions(parserOptions)
16
+ const key = source + JSON.stringify(effectiveOptions)
17
+
18
+ let result = this.cache.get(key)
19
+
20
+ if (!result) {
21
+ result = this.herb.parse(source, effectiveOptions)
22
+ this.cache.set(key, result)
23
+ }
24
+
25
+ return result
26
+ }
27
+
28
+ clear(): void {
29
+ this.cache.clear()
30
+ }
31
+
32
+ resolveOptions(parserOptions: Partial<ParserOptions>): ParserOptions {
33
+ return {
34
+ ...DEFAULT_PARSER_OPTIONS,
35
+ ...DEFAULT_LINTER_PARSER_OPTIONS,
36
+ ...parserOptions
37
+ }
38
+ }
39
+ }