@herb-tools/linter 0.7.4 → 0.8.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 (395) hide show
  1. package/README.md +253 -13
  2. package/dist/herb-lint.js +26087 -3414
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +5783 -1568
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +5749 -1569
  7. package/dist/index.js.map +1 -1
  8. package/dist/loader.cjs +17010 -0
  9. package/dist/loader.cjs.map +1 -0
  10. package/dist/loader.js +16879 -0
  11. package/dist/loader.js.map +1 -0
  12. package/dist/package.json +13 -5
  13. package/dist/src/cli/argument-parser.js +42 -35
  14. package/dist/src/cli/argument-parser.js.map +1 -1
  15. package/dist/src/cli/file-processor.js +124 -23
  16. package/dist/src/cli/file-processor.js.map +1 -1
  17. package/dist/src/cli/formatters/detailed-formatter.js +18 -3
  18. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
  19. package/dist/src/cli/formatters/github-actions-formatter.js +15 -1
  20. package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
  21. package/dist/src/cli/formatters/json-formatter.js +3 -0
  22. package/dist/src/cli/formatters/json-formatter.js.map +1 -1
  23. package/dist/src/cli/formatters/simple-formatter.js +20 -7
  24. package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
  25. package/dist/src/cli/output-manager.js +22 -3
  26. package/dist/src/cli/output-manager.js.map +1 -1
  27. package/dist/src/cli/summary-reporter.js +26 -3
  28. package/dist/src/cli/summary-reporter.js.map +1 -1
  29. package/dist/src/cli.js +109 -43
  30. package/dist/src/cli.js.map +1 -1
  31. package/dist/src/custom-rule-loader.js +139 -0
  32. package/dist/src/custom-rule-loader.js.map +1 -0
  33. package/dist/src/herb-disable-comment-utils.js +129 -0
  34. package/dist/src/herb-disable-comment-utils.js.map +1 -0
  35. package/dist/src/index.js +1 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/linter.js +369 -34
  38. package/dist/src/linter.js.map +1 -1
  39. package/dist/src/loader.js +17 -0
  40. package/dist/src/loader.js.map +1 -0
  41. package/dist/src/rules/erb-comment-syntax.js +48 -0
  42. package/dist/src/rules/erb-comment-syntax.js.map +1 -0
  43. package/dist/src/rules/erb-no-case-node-children.js +52 -0
  44. package/dist/src/rules/erb-no-case-node-children.js.map +1 -0
  45. package/dist/src/rules/erb-no-empty-tags.js +7 -1
  46. package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
  47. package/dist/src/rules/erb-no-extra-newline.js +65 -0
  48. package/dist/src/rules/erb-no-extra-newline.js.map +1 -0
  49. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js +95 -0
  50. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
  51. package/dist/src/rules/erb-no-output-control-flow.js +7 -1
  52. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
  53. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +7 -1
  54. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -1
  55. package/dist/src/rules/erb-prefer-image-tag-helper.js +7 -1
  56. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  57. package/dist/src/rules/erb-require-trailing-newline.js +35 -0
  58. package/dist/src/rules/erb-require-trailing-newline.js.map +1 -0
  59. package/dist/src/rules/erb-require-whitespace-inside-tags.js +70 -20
  60. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  61. package/dist/src/rules/erb-right-trim.js +45 -0
  62. package/dist/src/rules/erb-right-trim.js.map +1 -0
  63. package/dist/src/rules/herb-disable-comment-base.js +51 -0
  64. package/dist/src/rules/herb-disable-comment-base.js.map +1 -0
  65. package/dist/src/rules/herb-disable-comment-malformed.js +51 -0
  66. package/dist/src/rules/herb-disable-comment-malformed.js.map +1 -0
  67. package/dist/src/rules/herb-disable-comment-missing-rules.js +29 -0
  68. package/dist/src/rules/herb-disable-comment-missing-rules.js.map +1 -0
  69. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js +32 -0
  70. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
  71. package/dist/src/rules/herb-disable-comment-no-redundant-all.js +31 -0
  72. package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
  73. package/dist/src/rules/herb-disable-comment-unnecessary.js +65 -0
  74. package/dist/src/rules/herb-disable-comment-unnecessary.js.map +1 -0
  75. package/dist/src/rules/herb-disable-comment-valid-rule-name.js +44 -0
  76. package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
  77. package/dist/src/rules/html-anchor-require-href.js +7 -1
  78. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  79. package/dist/src/rules/html-aria-attribute-must-be-valid.js +7 -1
  80. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  81. package/dist/src/rules/html-aria-label-is-well-formatted.js +9 -3
  82. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -1
  83. package/dist/src/rules/html-aria-level-must-be-valid.js +6 -0
  84. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
  85. package/dist/src/rules/html-aria-role-heading-requires-level.js +7 -1
  86. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  87. package/dist/src/rules/html-aria-role-must-be-valid.js +7 -1
  88. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  89. package/dist/src/rules/html-attribute-double-quotes.js +29 -2
  90. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  91. package/dist/src/rules/html-attribute-equals-spacing.js +18 -2
  92. package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -1
  93. package/dist/src/rules/html-attribute-values-require-quotes.js +39 -3
  94. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  95. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +7 -1
  96. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -1
  97. package/dist/src/rules/html-body-only-elements.js +46 -0
  98. package/dist/src/rules/html-body-only-elements.js.map +1 -0
  99. package/dist/src/rules/html-boolean-attributes-no-value.js +18 -1
  100. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  101. package/dist/src/rules/html-head-only-elements.js +51 -0
  102. package/dist/src/rules/html-head-only-elements.js.map +1 -0
  103. package/dist/src/rules/html-iframe-has-title.js +8 -2
  104. package/dist/src/rules/html-iframe-has-title.js.map +1 -1
  105. package/dist/src/rules/html-img-require-alt.js +7 -1
  106. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  107. package/dist/src/rules/html-input-require-autocomplete.js +70 -0
  108. package/dist/src/rules/html-input-require-autocomplete.js.map +1 -0
  109. package/dist/src/rules/html-navigation-has-label.js +7 -1
  110. package/dist/src/rules/html-navigation-has-label.js.map +1 -1
  111. package/dist/src/rules/html-no-aria-hidden-on-focusable.js +7 -1
  112. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -1
  113. package/dist/src/rules/html-no-block-inside-inline.js +7 -1
  114. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  115. package/dist/src/rules/html-no-duplicate-attributes.js +7 -1
  116. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  117. package/dist/src/rules/html-no-duplicate-ids.js +9 -3
  118. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  119. package/dist/src/rules/html-no-duplicate-meta-names.js +136 -0
  120. package/dist/src/rules/html-no-duplicate-meta-names.js.map +1 -0
  121. package/dist/src/rules/html-no-empty-attributes.js +45 -7
  122. package/dist/src/rules/html-no-empty-attributes.js.map +1 -1
  123. package/dist/src/rules/html-no-empty-headings.js +7 -6
  124. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  125. package/dist/src/rules/html-no-nested-links.js +7 -1
  126. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  127. package/dist/src/rules/html-no-positive-tab-index.js +7 -1
  128. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
  129. package/dist/src/rules/html-no-self-closing.js +48 -3
  130. package/dist/src/rules/html-no-self-closing.js.map +1 -1
  131. package/dist/src/rules/html-no-space-in-tag.js +173 -0
  132. package/dist/src/rules/html-no-space-in-tag.js.map +1 -0
  133. package/dist/src/rules/html-no-title-attribute.js +7 -1
  134. package/dist/src/rules/html-no-title-attribute.js.map +1 -1
  135. package/dist/src/rules/html-no-underscores-in-attribute-names.js +7 -1
  136. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -1
  137. package/dist/src/rules/html-tag-name-lowercase.js +23 -5
  138. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  139. package/dist/src/rules/index.js +20 -2
  140. package/dist/src/rules/index.js.map +1 -1
  141. package/dist/src/rules/parser-no-errors.js +6 -0
  142. package/dist/src/rules/parser-no-errors.js.map +1 -1
  143. package/dist/src/rules/rule-utils.js +211 -31
  144. package/dist/src/rules/rule-utils.js.map +1 -1
  145. package/dist/src/rules/svg-tag-name-capitalization.js +22 -2
  146. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  147. package/dist/src/{default-rules.js → rules.js} +46 -14
  148. package/dist/src/rules.js.map +1 -0
  149. package/dist/src/types.js +34 -1
  150. package/dist/src/types.js.map +1 -1
  151. package/dist/tsconfig.tsbuildinfo +1 -1
  152. package/dist/types/cli/argument-parser.d.ts +8 -2
  153. package/dist/types/cli/file-processor.d.ts +15 -0
  154. package/dist/types/cli/formatters/json-formatter.d.ts +6 -0
  155. package/dist/types/cli/formatters/simple-formatter.d.ts +1 -0
  156. package/dist/types/cli/summary-reporter.d.ts +6 -0
  157. package/dist/types/cli.d.ts +9 -4
  158. package/dist/types/custom-rule-loader.d.ts +62 -0
  159. package/dist/types/herb-disable-comment-utils.d.ts +69 -0
  160. package/dist/types/index.d.ts +1 -0
  161. package/dist/types/linter.d.ts +99 -3
  162. package/dist/types/loader.d.ts +20 -0
  163. package/dist/types/rules/erb-comment-syntax.d.ts +14 -0
  164. package/dist/types/rules/erb-no-case-node-children.d.ts +8 -0
  165. package/dist/types/rules/erb-no-empty-tags.d.ts +3 -2
  166. package/dist/types/rules/erb-no-extra-newline.d.ts +14 -0
  167. package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
  168. package/dist/types/rules/erb-no-output-control-flow.d.ts +3 -2
  169. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
  170. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +3 -2
  171. package/dist/types/rules/erb-require-trailing-newline.d.ts +9 -0
  172. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
  173. package/dist/types/rules/erb-right-trim.d.ts +14 -0
  174. package/dist/types/rules/herb-disable-comment-base.d.ts +37 -0
  175. package/dist/types/rules/herb-disable-comment-malformed.d.ts +8 -0
  176. package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +8 -0
  177. package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
  178. package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
  179. package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +8 -0
  180. package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
  181. package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
  182. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
  183. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +3 -2
  184. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +3 -2
  185. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +3 -2
  186. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +3 -2
  187. package/dist/types/rules/html-attribute-double-quotes.d.ts +13 -5
  188. package/dist/types/rules/html-attribute-equals-spacing.d.ts +12 -5
  189. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +13 -5
  190. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
  191. package/dist/types/rules/html-body-only-elements.d.ts +9 -0
  192. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +12 -5
  193. package/dist/types/rules/html-head-only-elements.d.ts +9 -0
  194. package/dist/types/rules/html-iframe-has-title.d.ts +3 -2
  195. package/dist/types/rules/html-img-require-alt.d.ts +3 -2
  196. package/dist/types/rules/html-input-require-autocomplete.d.ts +8 -0
  197. package/dist/types/rules/html-navigation-has-label.d.ts +3 -2
  198. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
  199. package/dist/types/rules/html-no-block-inside-inline.d.ts +3 -2
  200. package/dist/types/rules/html-no-duplicate-attributes.d.ts +3 -2
  201. package/dist/types/rules/html-no-duplicate-ids.d.ts +3 -2
  202. package/dist/types/rules/html-no-duplicate-meta-names.d.ts +9 -0
  203. package/dist/types/rules/html-no-empty-attributes.d.ts +3 -2
  204. package/dist/types/rules/html-no-empty-headings.d.ts +3 -2
  205. package/dist/types/rules/html-no-nested-links.d.ts +3 -2
  206. package/dist/types/rules/html-no-positive-tab-index.d.ts +3 -2
  207. package/dist/types/rules/html-no-self-closing.d.ts +14 -5
  208. package/dist/types/rules/html-no-space-in-tag.d.ts +16 -0
  209. package/dist/types/rules/html-no-title-attribute.d.ts +3 -2
  210. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
  211. package/dist/types/rules/html-tag-name-lowercase.d.ts +16 -6
  212. package/dist/types/rules/index.d.ts +20 -2
  213. package/dist/types/rules/parser-no-errors.d.ts +2 -1
  214. package/dist/types/rules/rule-utils.d.ts +72 -25
  215. package/dist/types/rules/svg-tag-name-capitalization.d.ts +13 -4
  216. package/dist/types/rules.d.ts +2 -0
  217. package/dist/types/src/cli/argument-parser.d.ts +8 -2
  218. package/dist/types/src/cli/file-processor.d.ts +15 -0
  219. package/dist/types/src/cli/formatters/json-formatter.d.ts +6 -0
  220. package/dist/types/src/cli/formatters/simple-formatter.d.ts +1 -0
  221. package/dist/types/src/cli/summary-reporter.d.ts +6 -0
  222. package/dist/types/src/cli.d.ts +9 -4
  223. package/dist/types/src/custom-rule-loader.d.ts +62 -0
  224. package/dist/types/src/herb-disable-comment-utils.d.ts +69 -0
  225. package/dist/types/src/index.d.ts +1 -0
  226. package/dist/types/src/linter.d.ts +99 -3
  227. package/dist/types/src/loader.d.ts +20 -0
  228. package/dist/types/src/rules/erb-comment-syntax.d.ts +14 -0
  229. package/dist/types/src/rules/erb-no-case-node-children.d.ts +8 -0
  230. package/dist/types/src/rules/erb-no-empty-tags.d.ts +3 -2
  231. package/dist/types/src/rules/erb-no-extra-newline.d.ts +14 -0
  232. package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
  233. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +3 -2
  234. package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
  235. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +3 -2
  236. package/dist/types/src/rules/erb-require-trailing-newline.d.ts +9 -0
  237. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
  238. package/dist/types/src/rules/erb-right-trim.d.ts +14 -0
  239. package/dist/types/src/rules/herb-disable-comment-base.d.ts +37 -0
  240. package/dist/types/src/rules/herb-disable-comment-malformed.d.ts +8 -0
  241. package/dist/types/src/rules/herb-disable-comment-missing-rules.d.ts +8 -0
  242. package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
  243. package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
  244. package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +8 -0
  245. package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
  246. package/dist/types/src/rules/html-anchor-require-href.d.ts +3 -2
  247. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
  248. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +3 -2
  249. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +3 -2
  250. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +3 -2
  251. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +3 -2
  252. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +13 -5
  253. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +12 -5
  254. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +13 -5
  255. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
  256. package/dist/types/src/rules/html-body-only-elements.d.ts +9 -0
  257. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +12 -5
  258. package/dist/types/src/rules/html-head-only-elements.d.ts +9 -0
  259. package/dist/types/src/rules/html-iframe-has-title.d.ts +3 -2
  260. package/dist/types/src/rules/html-img-require-alt.d.ts +3 -2
  261. package/dist/types/src/rules/html-input-require-autocomplete.d.ts +8 -0
  262. package/dist/types/src/rules/html-navigation-has-label.d.ts +3 -2
  263. package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
  264. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +3 -2
  265. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +3 -2
  266. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +3 -2
  267. package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +9 -0
  268. package/dist/types/src/rules/html-no-empty-attributes.d.ts +3 -2
  269. package/dist/types/src/rules/html-no-empty-headings.d.ts +3 -2
  270. package/dist/types/src/rules/html-no-nested-links.d.ts +3 -2
  271. package/dist/types/src/rules/html-no-positive-tab-index.d.ts +3 -2
  272. package/dist/types/src/rules/html-no-self-closing.d.ts +14 -5
  273. package/dist/types/src/rules/html-no-space-in-tag.d.ts +16 -0
  274. package/dist/types/src/rules/html-no-title-attribute.d.ts +3 -2
  275. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
  276. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +16 -6
  277. package/dist/types/src/rules/index.d.ts +20 -2
  278. package/dist/types/src/rules/parser-no-errors.d.ts +2 -1
  279. package/dist/types/src/rules/rule-utils.d.ts +72 -25
  280. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +13 -4
  281. package/dist/types/src/rules.d.ts +2 -0
  282. package/dist/types/src/types.d.ts +102 -11
  283. package/dist/types/types.d.ts +102 -11
  284. package/docs/rules/README.md +19 -3
  285. package/docs/rules/erb-comment-syntax.md +44 -0
  286. package/docs/rules/erb-no-case-node-children.md +50 -0
  287. package/docs/rules/erb-no-extra-newline.md +74 -0
  288. package/docs/rules/erb-no-extra-whitespace-inside-tags.md +39 -0
  289. package/docs/rules/{erb-requires-trailing-newline.md → erb-require-trailing-newline.md} +1 -1
  290. package/docs/rules/erb-right-trim.md +52 -0
  291. package/docs/rules/herb-disable-comment-malformed.md +45 -0
  292. package/docs/rules/herb-disable-comment-missing-rules.md +60 -0
  293. package/docs/rules/herb-disable-comment-no-duplicate-rules.md +49 -0
  294. package/docs/rules/herb-disable-comment-no-redundant-all.md +53 -0
  295. package/docs/rules/herb-disable-comment-unnecessary.md +44 -0
  296. package/docs/rules/herb-disable-comment-valid-rule-name.md +41 -0
  297. package/docs/rules/html-aria-attribute-must-be-valid.md +2 -5
  298. package/docs/rules/html-aria-label-is-well-formatted.md +1 -1
  299. package/docs/rules/html-attribute-double-quotes.md +2 -2
  300. package/docs/rules/html-attribute-equals-spacing.md +2 -2
  301. package/docs/rules/html-attribute-values-require-quotes.md +3 -3
  302. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +2 -2
  303. package/docs/rules/html-body-only-elements.md +99 -0
  304. package/docs/rules/html-boolean-attributes-no-value.md +2 -2
  305. package/docs/rules/html-head-only-elements.md +81 -0
  306. package/docs/rules/html-input-require-autocomplete.md +64 -0
  307. package/docs/rules/html-no-aria-hidden-on-focusable.md +2 -2
  308. package/docs/rules/html-no-duplicate-attributes.md +2 -2
  309. package/docs/rules/html-no-duplicate-meta-names.md +64 -0
  310. package/docs/rules/html-no-empty-attributes.md +3 -3
  311. package/docs/rules/html-no-empty-headings.md +4 -26
  312. package/docs/rules/html-no-positive-tab-index.md +1 -2
  313. package/docs/rules/html-no-self-closing.md +17 -2
  314. package/docs/rules/html-no-space-in-tag.md +66 -0
  315. package/docs/rules/html-no-title-attribute.md +2 -2
  316. package/docs/rules/html-no-underscores-in-attribute-names.md +2 -2
  317. package/docs/rules/html-tag-name-lowercase.md +2 -2
  318. package/package.json +13 -5
  319. package/src/cli/argument-parser.ts +50 -39
  320. package/src/cli/file-processor.ts +159 -28
  321. package/src/cli/formatters/detailed-formatter.ts +21 -3
  322. package/src/cli/formatters/github-actions-formatter.ts +17 -1
  323. package/src/cli/formatters/json-formatter.ts +9 -0
  324. package/src/cli/formatters/simple-formatter.ts +24 -8
  325. package/src/cli/output-manager.ts +23 -3
  326. package/src/cli/summary-reporter.ts +40 -3
  327. package/src/cli.ts +137 -52
  328. package/src/custom-rule-loader.ts +189 -0
  329. package/src/herb-disable-comment-utils.ts +175 -0
  330. package/src/index.ts +2 -0
  331. package/src/linter.ts +501 -36
  332. package/src/loader.ts +30 -0
  333. package/src/rules/erb-comment-syntax.ts +73 -0
  334. package/src/rules/erb-no-case-node-children.ts +68 -0
  335. package/src/rules/erb-no-empty-tags.ts +9 -3
  336. package/src/rules/erb-no-extra-newline.ts +91 -0
  337. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +147 -0
  338. package/src/rules/erb-no-output-control-flow.ts +9 -3
  339. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +9 -3
  340. package/src/rules/erb-prefer-image-tag-helper.ts +9 -3
  341. package/src/rules/erb-require-trailing-newline.ts +47 -0
  342. package/src/rules/erb-require-whitespace-inside-tags.ts +96 -26
  343. package/src/rules/erb-right-trim.ts +67 -0
  344. package/src/rules/herb-disable-comment-base.ts +76 -0
  345. package/src/rules/herb-disable-comment-malformed.ts +66 -0
  346. package/src/rules/herb-disable-comment-missing-rules.ts +41 -0
  347. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +46 -0
  348. package/src/rules/herb-disable-comment-no-redundant-all.ts +40 -0
  349. package/src/rules/herb-disable-comment-unnecessary.ts +103 -0
  350. package/src/rules/herb-disable-comment-valid-rule-name.ts +62 -0
  351. package/src/rules/html-anchor-require-href.ts +9 -3
  352. package/src/rules/html-aria-attribute-must-be-valid.ts +9 -3
  353. package/src/rules/html-aria-label-is-well-formatted.ts +9 -5
  354. package/src/rules/html-aria-level-must-be-valid.ts +9 -2
  355. package/src/rules/html-aria-role-heading-requires-level.ts +9 -3
  356. package/src/rules/html-aria-role-must-be-valid.ts +9 -3
  357. package/src/rules/html-attribute-double-quotes.ts +42 -8
  358. package/src/rules/html-attribute-equals-spacing.ts +31 -7
  359. package/src/rules/html-attribute-values-require-quotes.ts +56 -10
  360. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +9 -3
  361. package/src/rules/html-body-only-elements.ts +60 -0
  362. package/src/rules/html-boolean-attributes-no-value.ts +31 -6
  363. package/src/rules/html-head-only-elements.ts +65 -0
  364. package/src/rules/html-iframe-has-title.ts +9 -4
  365. package/src/rules/html-img-require-alt.ts +10 -4
  366. package/src/rules/html-input-require-autocomplete.ts +85 -0
  367. package/src/rules/html-navigation-has-label.ts +9 -3
  368. package/src/rules/html-no-aria-hidden-on-focusable.ts +9 -3
  369. package/src/rules/html-no-block-inside-inline.ts +9 -3
  370. package/src/rules/html-no-duplicate-attributes.ts +9 -3
  371. package/src/rules/html-no-duplicate-ids.ts +11 -7
  372. package/src/rules/html-no-duplicate-meta-names.ts +188 -0
  373. package/src/rules/html-no-empty-attributes.ts +58 -10
  374. package/src/rules/html-no-empty-headings.ts +10 -8
  375. package/src/rules/html-no-nested-links.ts +10 -4
  376. package/src/rules/html-no-positive-tab-index.ts +9 -3
  377. package/src/rules/html-no-self-closing.ts +69 -9
  378. package/src/rules/html-no-space-in-tag.ts +221 -0
  379. package/src/rules/html-no-title-attribute.ts +9 -3
  380. package/src/rules/html-no-underscores-in-attribute-names.ts +12 -4
  381. package/src/rules/html-tag-name-lowercase.ts +41 -10
  382. package/src/rules/index.ts +24 -2
  383. package/src/rules/parser-no-errors.ts +8 -1
  384. package/src/rules/rule-utils.ts +250 -44
  385. package/src/rules/svg-tag-name-capitalization.ts +39 -6
  386. package/src/{default-rules.ts → rules.ts} +53 -13
  387. package/src/types.ts +133 -15
  388. package/dist/src/default-rules.js.map +0 -1
  389. package/dist/src/rules/erb-requires-trailing-newline.js +0 -22
  390. package/dist/src/rules/erb-requires-trailing-newline.js.map +0 -1
  391. package/dist/types/default-rules.d.ts +0 -2
  392. package/dist/types/rules/erb-requires-trailing-newline.d.ts +0 -6
  393. package/dist/types/src/default-rules.d.ts +0 -2
  394. package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +0 -6
  395. package/src/rules/erb-requires-trailing-newline.ts +0 -29
@@ -0,0 +1,68 @@
1
+ import { BaseRuleVisitor } from "./rule-utils.js"
2
+ import { ParserRule } from "../types.js"
3
+ import { isWhitespaceNode, isLiteralNode, isHTMLTextNode, isCommentNode, isERBNode } from "@herb-tools/core"
4
+ import { IdentityPrinter } from "@herb-tools/printer"
5
+
6
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
7
+ import type { ParseResult, ERBCaseNode, ERBCaseMatchNode, Node } from "@herb-tools/core"
8
+
9
+ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
10
+ visitERBCaseNode(node: ERBCaseNode): void {
11
+ this.checkCaseNodeChildren(node, "when")
12
+ this.visitChildNodes(node)
13
+ }
14
+
15
+ visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
16
+ this.checkCaseNodeChildren(node, "in")
17
+ this.visitChildNodes(node)
18
+ }
19
+
20
+ private checkCaseNodeChildren(node: ERBCaseNode | ERBCaseMatchNode, type: string): void {
21
+ if (!node.children || node.children.length === 0) return
22
+
23
+ const caseCode = IdentityPrinter.printERBNode(node)
24
+ const firstCondition = node.conditions?.[0]
25
+ const conditionCode = firstCondition && isERBNode(firstCondition) ? IdentityPrinter.printERBNode(firstCondition) : `<% ${type} ... %>`
26
+
27
+ for (const child of node.children) {
28
+ if (!this.isAllowedContent(child)) {
29
+ const childCode = IdentityPrinter.print(child).trim()
30
+
31
+ this.addOffense(
32
+ `Do not place \`${childCode}\` between \`${caseCode}\` and \`${conditionCode}\`. Content here is not part of any branch and will not be rendered.`,
33
+ child.location,
34
+ )
35
+ }
36
+ }
37
+ }
38
+
39
+ private isAllowedContent(node: Node): boolean {
40
+ if (isWhitespaceNode(node)) return true
41
+ if (isCommentNode(node)) return true
42
+
43
+ if (isLiteralNode(node) || isHTMLTextNode(node)) {
44
+ return /^\s*$/.test(node.content)
45
+ }
46
+
47
+ return false
48
+ }
49
+ }
50
+
51
+ export class ERBNoCaseNodeChildrenRule extends ParserRule {
52
+ name = "erb-no-case-node-children"
53
+
54
+ get defaultConfig(): FullRuleConfig {
55
+ return {
56
+ enabled: true,
57
+ severity: "error"
58
+ }
59
+ }
60
+
61
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
62
+ const visitor = new ERBNoCaseNodeChildrenVisitor(this.name, context)
63
+
64
+ visitor.visit(result.value)
65
+
66
+ return visitor.offenses
67
+ }
68
+ }
@@ -1,7 +1,7 @@
1
1
  import { BaseRuleVisitor } from "./rule-utils.js"
2
2
 
3
3
  import { ParserRule } from "../types.js"
4
- import type { LintOffense, LintContext } from "../types.js"
4
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
5
  import type { ParseResult, ERBContentNode } from "@herb-tools/core"
6
6
 
7
7
  class ERBNoEmptyTagsVisitor extends BaseRuleVisitor {
@@ -17,7 +17,6 @@ class ERBNoEmptyTagsVisitor extends BaseRuleVisitor {
17
17
  this.addOffense(
18
18
  "ERB tag should not be empty. Remove empty ERB tags or add content.",
19
19
  node.location,
20
- "error"
21
20
  )
22
21
  }
23
22
  }
@@ -25,7 +24,14 @@ class ERBNoEmptyTagsVisitor extends BaseRuleVisitor {
25
24
  export class ERBNoEmptyTagsRule extends ParserRule {
26
25
  name = "erb-no-empty-tags"
27
26
 
28
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
27
+ get defaultConfig(): FullRuleConfig {
28
+ return {
29
+ enabled: true,
30
+ severity: "error"
31
+ }
32
+ }
33
+
34
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
29
35
  const visitor = new ERBNoEmptyTagsVisitor(this.name, context)
30
36
 
31
37
  visitor.visit(result.value)
@@ -0,0 +1,91 @@
1
+ import { BaseSourceRuleVisitor } from "./rule-utils.js"
2
+ import { SourceRule } from "../types.js"
3
+ import { Location, Position } from "@herb-tools/core"
4
+
5
+ import type { Node } from "@herb-tools/core"
6
+ import type { UnboundLintOffense, LintOffense, LintContext, BaseAutofixContext, FullRuleConfig } from "../types.js"
7
+
8
+ interface ERBNoExtraNewLineAutofixContext extends BaseAutofixContext {
9
+ startOffset: number
10
+ endOffset: number
11
+ }
12
+
13
+ function positionFromOffset(source: string, offset: number): Position {
14
+ let line = 1
15
+ let column = 0
16
+ let currentOffset = 0
17
+
18
+ for (let i = 0; i < source.length && currentOffset < offset; i++) {
19
+ const char = source[i]
20
+ currentOffset++
21
+ if (char === "\n") {
22
+ line++
23
+ column = 0
24
+ } else {
25
+ column++
26
+ }
27
+ }
28
+
29
+ return new Position(line, column)
30
+ }
31
+
32
+ class ERBNoExtraNewLineVisitor extends BaseSourceRuleVisitor<ERBNoExtraNewLineAutofixContext> {
33
+ protected visitSource(source: string): void {
34
+ if (source.length === 0) return
35
+
36
+ const regex = /\n{4,}/g
37
+
38
+ let match: RegExpExecArray | null
39
+
40
+ while ((match = regex.exec(source)) !== null) {
41
+ const startOffset = match.index + 3
42
+ const endOffset = match.index + match[0].length
43
+ const start = positionFromOffset(source, startOffset)
44
+ const end = positionFromOffset(source, endOffset)
45
+ const location = new Location(start, end)
46
+
47
+ const extraLines = match[0].length - 3
48
+
49
+ this.addOffense(
50
+ `Extra blank line detected. Remove ${extraLines} blank ${extraLines === 1 ? "line" : "lines"} to maintain consistent spacing (max 2 allowed).`,
51
+ location,
52
+ {
53
+ node: null as any as Node,
54
+ startOffset,
55
+ endOffset
56
+ }
57
+ )
58
+ }
59
+ }
60
+ }
61
+
62
+ export class ERBNoExtraNewLineRule extends SourceRule {
63
+ static autocorrectable = true
64
+ name = "erb-no-extra-newline"
65
+
66
+ get defaultConfig(): FullRuleConfig {
67
+ return {
68
+ enabled: true,
69
+ severity: "error"
70
+ }
71
+ }
72
+
73
+ check(source: string, context?: Partial<LintContext>): UnboundLintOffense[] {
74
+ const visitor = new ERBNoExtraNewLineVisitor(this.name, context)
75
+
76
+ visitor.visit(source)
77
+
78
+ return visitor.offenses
79
+ }
80
+
81
+ autofix(offense: LintOffense<ERBNoExtraNewLineAutofixContext>, source: string, _context?: Partial<LintContext>): string | null {
82
+ if (!offense.autofixContext) return null
83
+
84
+ const { startOffset, endOffset } = offense.autofixContext
85
+
86
+ const before = source.substring(0, startOffset)
87
+ const after = source.substring(endOffset)
88
+
89
+ return before + after
90
+ }
91
+ }
@@ -0,0 +1,147 @@
1
+ import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+
4
+ import type { ParseResult, Token, ERBNode } from "@herb-tools/core"
5
+ import { Location } from "@herb-tools/core"
6
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
7
+
8
+ interface ERBNoExtraWhitespaceAutofixContext extends BaseAutofixContext {
9
+ node: Mutable<ERBNode>
10
+ openTag: Token
11
+ closeTag: Token
12
+ content: string
13
+ fixType: "after-open" | "before-close" | "after-comment-equals"
14
+ }
15
+
16
+ class ERBNoExtraWhitespaceInsideTagsVisitor extends BaseRuleVisitor<ERBNoExtraWhitespaceAutofixContext> {
17
+
18
+ visitERBNode(node: ERBNode): void {
19
+ const openTag = node.tag_opening
20
+ const closeTag = node.tag_closing
21
+ const { value } = node.content ?? {}
22
+
23
+ if (!openTag || !closeTag || !value) return
24
+
25
+ if (this.hasExtraLeadingWhitespace(value)) {
26
+ this.reportWhitespace(node, openTag, closeTag, value, "start", 0, `Remove extra whitespace after \`${openTag.value}\`.`, "after-open")
27
+ }
28
+
29
+ if (openTag.value === "<%#" && value.startsWith("=") && value.length > 1) {
30
+ const afterEquals = value.substring(1)
31
+
32
+ if (afterEquals.match(/^\s{2,}/) && !afterEquals.startsWith(" \n") && !afterEquals.startsWith("\n")) {
33
+ this.reportWhitespace(node, openTag, closeTag, value, "start", 1, `Remove extra whitespace after \`<%#=\`.`, "after-comment-equals")
34
+ }
35
+ }
36
+
37
+ if (this.hasExtraTrailingWhitespace(value)) {
38
+ this.reportWhitespace(node, openTag, closeTag, value, "end", 0, `Remove extra whitespace before \`${closeTag.value}\`.`, "before-close")
39
+ }
40
+ }
41
+
42
+ private hasExtraLeadingWhitespace(content: string): boolean {
43
+ return content.startsWith(" ") && !content.startsWith(" \n")
44
+ }
45
+
46
+ private hasExtraTrailingWhitespace(content: string): boolean {
47
+ return !content.includes("\n") && /\s{2,}$/.test(content)
48
+ }
49
+
50
+ private getWhitespaceLocation(node: ERBNode, content: string, position: "start" | "end", offset: number = 0): Location {
51
+ const contentLocation = node.content!.location
52
+
53
+ if (position === "start") {
54
+ const match = content.substring(offset).match(/^\s+/)
55
+ const length = match ? match[0].length : 0
56
+ const startColumn = contentLocation.start.column + offset
57
+
58
+ return Location.from(
59
+ contentLocation.start.line,
60
+ startColumn,
61
+ contentLocation.start.line,
62
+ startColumn + length
63
+ )
64
+ } else {
65
+ const match = content.match(/\s+$/)
66
+ const length = match ? match[0].length : 0
67
+
68
+ return Location.from(
69
+ contentLocation.end.line,
70
+ contentLocation.end.column - length,
71
+ contentLocation.end.line,
72
+ contentLocation.end.column
73
+ )
74
+ }
75
+ }
76
+
77
+ private reportWhitespace(
78
+ node: ERBNode,
79
+ openTag: Token,
80
+ closeTag: Token,
81
+ content: string,
82
+ position: "start" | "end",
83
+ offset: number,
84
+ message: string,
85
+ fixType: "after-open" | "before-close" | "after-comment-equals"
86
+ ): void {
87
+ const location = this.getWhitespaceLocation(node, content, position, offset)
88
+ this.addOffense(message, location, {
89
+ node,
90
+ openTag,
91
+ closeTag,
92
+ content,
93
+ fixType
94
+ })
95
+ }
96
+ }
97
+
98
+ export class ERBNoExtraWhitespaceRule extends ParserRule<ERBNoExtraWhitespaceAutofixContext> {
99
+ static autocorrectable = true
100
+ name = "erb-no-extra-whitespace-inside-tags"
101
+
102
+ get defaultConfig(): FullRuleConfig {
103
+ return {
104
+ enabled: true,
105
+ severity: "error"
106
+ }
107
+ }
108
+
109
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<ERBNoExtraWhitespaceAutofixContext>[] {
110
+ const visitor = new ERBNoExtraWhitespaceInsideTagsVisitor(this.name, context)
111
+
112
+ visitor.visit(result.value)
113
+
114
+ return visitor.offenses
115
+ }
116
+
117
+ autofix(offense: LintOffense<ERBNoExtraWhitespaceAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
118
+ if (!offense.autofixContext) return null
119
+
120
+ const { node, fixType } = offense.autofixContext
121
+ if (!node.content) return null
122
+
123
+ const content = node.content.value
124
+
125
+ switch (fixType) {
126
+ case "before-close":
127
+ node.content.value = content.replace(/\s{2,}$/, " ")
128
+ break
129
+
130
+ case "after-open":
131
+ node.content.value = content.replace(/^\s{2,}/, " ")
132
+ break
133
+
134
+ case "after-comment-equals":
135
+ if (content.startsWith("=")) {
136
+ const afterEquals = content.substring(1)
137
+ node.content.value = "= " + afterEquals.replace(/^\s{2,}/, "")
138
+ }
139
+
140
+ break
141
+ default:
142
+ return null
143
+ }
144
+
145
+ return result
146
+ }
147
+ }
@@ -2,7 +2,7 @@ import { BaseRuleVisitor } from "./rule-utils.js"
2
2
 
3
3
  import type { ParseResult, ERBIfNode, ERBUnlessNode, ERBElseNode, ERBEndNode } from "@herb-tools/core"
4
4
  import { ParserRule } from "../types.js"
5
- import type { LintOffense, LintContext } from "../types.js"
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
6
 
7
7
  class ERBNoOutputControlFlowRuleVisitor extends BaseRuleVisitor {
8
8
  visitERBIfNode(node: ERBIfNode): void {
@@ -42,7 +42,6 @@ class ERBNoOutputControlFlowRuleVisitor extends BaseRuleVisitor {
42
42
  this.addOffense(
43
43
  `Control flow statements like \`${controlBlockType}\` should not be used with output tags. Use \`<% ${controlBlockType} ... %>\` instead.`,
44
44
  openTag.location,
45
- "error"
46
45
  )
47
46
  }
48
47
 
@@ -53,7 +52,14 @@ class ERBNoOutputControlFlowRuleVisitor extends BaseRuleVisitor {
53
52
  export class ERBNoOutputControlFlowRule extends ParserRule {
54
53
  name = "erb-no-output-control-flow"
55
54
 
56
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
55
+ get defaultConfig(): FullRuleConfig {
56
+ return {
57
+ enabled: true,
58
+ severity: "error"
59
+ }
60
+ }
61
+
62
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
57
63
  const visitor = new ERBNoOutputControlFlowRuleVisitor(this.name, context)
58
64
 
59
65
  visitor.visit(result.value)
@@ -2,7 +2,7 @@ import { ParserRule } from "../types.js"
2
2
  import { BaseRuleVisitor } from "./rule-utils.js"
3
3
  import { filterERBContentNodes } from "@herb-tools/core"
4
4
 
5
- import type { LintOffense, LintContext } from "../types.js"
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
6
  import type { ParseResult, HTMLAttributeNameNode, ERBContentNode } from "@herb-tools/core"
7
7
 
8
8
  class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
@@ -14,7 +14,6 @@ class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
14
14
  this.addOffense(
15
15
  `Remove silent ERB tag from HTML attribute name. Silent ERB tags (\`${node.tag_opening?.value}\`) do not output content and should not be used in attribute names.`,
16
16
  node.location,
17
- "error"
18
17
  )
19
18
  }
20
19
  }
@@ -30,7 +29,14 @@ class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
30
29
  export class ERBNoSilentTagInAttributeNameRule extends ParserRule {
31
30
  name = "erb-no-silent-tag-in-attribute-name"
32
31
 
33
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
32
+ get defaultConfig(): FullRuleConfig {
33
+ return {
34
+ enabled: true,
35
+ severity: "error"
36
+ }
37
+ }
38
+
39
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
34
40
  const visitor = new ERBNoSilentTagInAttributeNameVisitor(this.name, context)
35
41
 
36
42
  visitor.visit(result.value)
@@ -4,7 +4,7 @@ import { BaseRuleVisitor, getTagName, findAttributeByName, getAttributes } from
4
4
  import { ERBToRubyStringPrinter } from "@herb-tools/printer"
5
5
  import { filterNodes, ERBContentNode, LiteralNode, isNode } from "@herb-tools/core"
6
6
 
7
- import type { LintOffense, LintContext } from "../types.js"
7
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
8
8
  import type { HTMLOpenTagNode, HTMLAttributeValueNode, ParseResult } from "@herb-tools/core"
9
9
 
10
10
  class ERBPreferImageTagHelperVisitor extends BaseRuleVisitor {
@@ -36,7 +36,6 @@ class ERBPreferImageTagHelperVisitor extends BaseRuleVisitor {
36
36
  this.addOffense(
37
37
  `Prefer \`image_tag\` helper over manual \`<img>\` with dynamic ERB expressions. Use \`<%= image_tag ${suggestedExpression}, alt: "..." %>\` instead.`,
38
38
  srcAttribute.location,
39
- "warning"
40
39
  )
41
40
  }
42
41
  }
@@ -93,7 +92,14 @@ class ERBPreferImageTagHelperVisitor extends BaseRuleVisitor {
93
92
  export class ERBPreferImageTagHelperRule extends ParserRule {
94
93
  name = "erb-prefer-image-tag-helper"
95
94
 
96
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
95
+ get defaultConfig(): FullRuleConfig {
96
+ return {
97
+ enabled: true,
98
+ severity: "warning"
99
+ }
100
+ }
101
+
102
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
97
103
  const visitor = new ERBPreferImageTagHelperVisitor(this.name, context)
98
104
  visitor.visit(result.value)
99
105
  return visitor.offenses
@@ -0,0 +1,47 @@
1
+ import { SourceRule } from "../types.js"
2
+ import { BaseSourceRuleVisitor, createEndOfFileLocation } from "./rule-utils.js"
3
+
4
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
5
+
6
+ class ERBRequireTrailingNewlineVisitor extends BaseSourceRuleVisitor {
7
+ protected visitSource(source: string): void {
8
+ if (source.length === 0) return
9
+ if (!this.context.fileName) return
10
+
11
+ if (!source.endsWith('\n')) {
12
+ this.addOffense(
13
+ "File must end with trailing newline.",
14
+ createEndOfFileLocation(source),
15
+ )
16
+ } else if (source.endsWith('\n\n')) {
17
+ this.addOffense(
18
+ "File must end with exactly one trailing newline.",
19
+ createEndOfFileLocation(source),
20
+ )
21
+ }
22
+ }
23
+ }
24
+
25
+ export class ERBRequireTrailingNewlineRule extends SourceRule {
26
+ static autocorrectable = true
27
+ name = "erb-require-trailing-newline"
28
+
29
+ get defaultConfig(): FullRuleConfig {
30
+ return {
31
+ enabled: true,
32
+ severity: "error"
33
+ }
34
+ }
35
+
36
+ check(source: string, context?: Partial<LintContext>): UnboundLintOffense[] {
37
+ const visitor = new ERBRequireTrailingNewlineVisitor(this.name, context)
38
+
39
+ visitor.visit(source)
40
+
41
+ return visitor.offenses
42
+ }
43
+
44
+ autofix(_offense: LintOffense, source: string, _context?: Partial<LintContext>): string | null {
45
+ return source.trimEnd() + "\n"
46
+ }
47
+ }
@@ -1,20 +1,20 @@
1
- import type { ParseResult, Token, Node } from "@herb-tools/core"
2
- import { isERBNode } from "@herb-tools/core";
3
- import { ParserRule } from "../types.js"
4
- import type { LintOffense, LintContext } from "../types.js"
1
+ import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
5
2
  import { BaseRuleVisitor } from "./rule-utils.js"
6
3
 
7
- class RequireWhitespaceInsideTags extends BaseRuleVisitor {
4
+ import type { ParseResult, Token, ERBNode } from "@herb-tools/core"
5
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
8
6
 
9
- visitChildNodes(node: Node): void {
10
- this.checkWhitespace(node)
11
- super.visitChildNodes(node)
12
- }
7
+ interface ERBRequireWhitespaceAutofixContext extends BaseAutofixContext {
8
+ node: Mutable<ERBNode>
9
+ openTag: Token
10
+ closeTag: Token
11
+ content: string
12
+ fixType: "after-open" | "before-close" | "after-comment-equals"
13
+ }
13
14
 
14
- private checkWhitespace(node: Node): void {
15
- if (!isERBNode(node)) {
16
- return
17
- }
15
+ class RequireWhitespaceInsideTags extends BaseRuleVisitor<ERBRequireWhitespaceAutofixContext> {
16
+
17
+ visitERBNode(node: ERBNode): void {
18
18
  const openTag = node.tag_opening
19
19
  const closeTag = node.tag_closing
20
20
  const content = node.content
@@ -26,25 +26,37 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
26
26
  const value = content.value
27
27
 
28
28
  if (openTag.value === "<%#") {
29
- this.checkCommentTagWhitespace(openTag, closeTag, value)
29
+ this.checkCommentTagWhitespace(node, openTag, closeTag, value)
30
30
  } else {
31
- this.checkOpenTagWhitespace(openTag, value)
32
- this.checkCloseTagWhitespace(closeTag, value)
31
+ this.checkOpenTagWhitespace(node, openTag, closeTag, value)
32
+ this.checkCloseTagWhitespace(node, openTag, closeTag, value)
33
33
  }
34
34
  }
35
35
 
36
- private checkCommentTagWhitespace(openTag: Token, closeTag: Token, content: string): void {
36
+ private checkCommentTagWhitespace(node: ERBNode, openTag: Token, closeTag: Token, content: string): void {
37
37
  if (!content.startsWith(" ") && !content.startsWith("\n") && !content.startsWith("=")) {
38
38
  this.addOffense(
39
39
  `Add whitespace after \`${openTag.value}\`.`,
40
40
  openTag.location,
41
- "error"
41
+ {
42
+ node,
43
+ openTag,
44
+ closeTag,
45
+ content,
46
+ fixType: "after-open"
47
+ }
42
48
  )
43
49
  } else if (content.startsWith("=") && content.length > 1 && !content[1].match(/\s/)) {
44
50
  this.addOffense(
45
51
  `Add whitespace after \`<%#=\`.`,
46
52
  openTag.location,
47
- "error"
53
+ {
54
+ node,
55
+ openTag,
56
+ closeTag,
57
+ content,
58
+ fixType: "after-comment-equals"
59
+ }
48
60
  )
49
61
  }
50
62
 
@@ -52,12 +64,18 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
52
64
  this.addOffense(
53
65
  `Add whitespace before \`${closeTag.value}\`.`,
54
66
  closeTag.location,
55
- "error"
67
+ {
68
+ node,
69
+ openTag,
70
+ closeTag,
71
+ content,
72
+ fixType: "before-close"
73
+ }
56
74
  )
57
75
  }
58
76
  }
59
77
 
60
- private checkOpenTagWhitespace(openTag: Token, content:string):void {
78
+ private checkOpenTagWhitespace(node: ERBNode, openTag: Token, closeTag: Token, content: string):void {
61
79
  if (content.startsWith(" ") || content.startsWith("\n")) {
62
80
  return
63
81
  }
@@ -65,11 +83,17 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
65
83
  this.addOffense(
66
84
  `Add whitespace after \`${openTag.value}\`.`,
67
85
  openTag.location,
68
- "error"
86
+ {
87
+ node,
88
+ openTag,
89
+ closeTag,
90
+ content,
91
+ fixType: "after-open"
92
+ }
69
93
  )
70
94
  }
71
95
 
72
- private checkCloseTagWhitespace(closeTag: Token, content:string):void {
96
+ private checkCloseTagWhitespace(node: ERBNode, openTag: Token, closeTag: Token, content: string):void {
73
97
  if (content.endsWith(" ") || content.endsWith("\n")) {
74
98
  return
75
99
  }
@@ -77,17 +101,63 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
77
101
  this.addOffense(
78
102
  `Add whitespace before \`${closeTag.value}\`.`,
79
103
  closeTag.location,
80
- "error"
104
+ {
105
+ node,
106
+ openTag,
107
+ closeTag,
108
+ content,
109
+ fixType: "before-close"
110
+ }
81
111
  )
82
112
  }
83
113
  }
84
114
 
85
- export class ERBRequireWhitespaceRule extends ParserRule {
115
+ export class ERBRequireWhitespaceRule extends ParserRule<ERBRequireWhitespaceAutofixContext> {
116
+ static autocorrectable = true
86
117
  name = "erb-require-whitespace-inside-tags"
87
118
 
88
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
119
+ get defaultConfig(): FullRuleConfig {
120
+ return {
121
+ enabled: true,
122
+ severity: "error"
123
+ }
124
+ }
125
+
126
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<ERBRequireWhitespaceAutofixContext>[] {
89
127
  const visitor = new RequireWhitespaceInsideTags(this.name, context)
128
+
90
129
  visitor.visit(result.value)
130
+
91
131
  return visitor.offenses
92
132
  }
133
+
134
+ autofix(offense: LintOffense<ERBRequireWhitespaceAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
135
+ if (!offense.autofixContext) return null
136
+
137
+ const { node, fixType } = offense.autofixContext
138
+
139
+ if (!node.content) return null
140
+
141
+ const content = node.content.value
142
+
143
+ if (fixType === "before-close") {
144
+ node.content.value = content + " "
145
+
146
+ return result
147
+ }
148
+
149
+ if (fixType === "after-open") {
150
+ node.content.value = " " + content
151
+
152
+ return result
153
+ }
154
+
155
+ if (fixType === "after-comment-equals" && content.startsWith("=")) {
156
+ node.content.value = "= " + content.substring(1)
157
+
158
+ return result
159
+ }
160
+
161
+ return null
162
+ }
93
163
  }