@herb-tools/linter 0.7.5 → 0.8.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.
- package/README.md +253 -13
- package/dist/herb-lint.js +26041 -3435
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +5437 -1254
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5405 -1255
- package/dist/index.js.map +1 -1
- package/dist/loader.cjs +17017 -0
- package/dist/loader.cjs.map +1 -0
- package/dist/loader.js +16886 -0
- package/dist/loader.js.map +1 -0
- package/dist/package.json +13 -5
- package/dist/src/cli/argument-parser.js +38 -33
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +124 -23
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli/formatters/detailed-formatter.js +18 -3
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
- package/dist/src/cli/formatters/github-actions-formatter.js +15 -1
- package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
- package/dist/src/cli/formatters/json-formatter.js +3 -0
- package/dist/src/cli/formatters/json-formatter.js.map +1 -1
- package/dist/src/cli/formatters/simple-formatter.js +20 -7
- package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
- package/dist/src/cli/output-manager.js +22 -3
- package/dist/src/cli/output-manager.js.map +1 -1
- package/dist/src/cli/summary-reporter.js +26 -3
- package/dist/src/cli/summary-reporter.js.map +1 -1
- package/dist/src/cli.js +107 -42
- package/dist/src/cli.js.map +1 -1
- package/dist/src/custom-rule-loader.js +139 -0
- package/dist/src/custom-rule-loader.js.map +1 -0
- package/dist/src/herb-disable-comment-utils.js +129 -0
- package/dist/src/herb-disable-comment-utils.js.map +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/linter.js +369 -34
- package/dist/src/linter.js.map +1 -1
- package/dist/src/loader.js +17 -0
- package/dist/src/loader.js.map +1 -0
- package/dist/src/rules/erb-comment-syntax.js +31 -2
- package/dist/src/rules/erb-comment-syntax.js.map +1 -1
- package/dist/src/rules/erb-no-case-node-children.js +52 -0
- package/dist/src/rules/erb-no-case-node-children.js.map +1 -0
- package/dist/src/rules/erb-no-empty-tags.js +7 -1
- package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
- package/dist/src/rules/erb-no-extra-newline.js +65 -0
- package/dist/src/rules/erb-no-extra-newline.js.map +1 -0
- package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js +95 -0
- package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
- package/dist/src/rules/erb-no-output-control-flow.js +7 -1
- package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
- package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +7 -1
- package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js +7 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
- package/dist/src/rules/erb-require-trailing-newline.js +35 -0
- package/dist/src/rules/erb-require-trailing-newline.js.map +1 -0
- package/dist/src/rules/erb-require-whitespace-inside-tags.js +69 -11
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
- package/dist/src/rules/erb-right-trim.js +26 -9
- package/dist/src/rules/erb-right-trim.js.map +1 -1
- package/dist/src/rules/herb-disable-comment-base.js +51 -0
- package/dist/src/rules/herb-disable-comment-base.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-malformed.js +51 -0
- package/dist/src/rules/herb-disable-comment-malformed.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-missing-rules.js +29 -0
- package/dist/src/rules/herb-disable-comment-missing-rules.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js +32 -0
- package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-no-redundant-all.js +31 -0
- package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-unnecessary.js +65 -0
- package/dist/src/rules/herb-disable-comment-unnecessary.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-valid-rule-name.js +44 -0
- package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
- package/dist/src/rules/html-anchor-require-href.js +7 -1
- package/dist/src/rules/html-anchor-require-href.js.map +1 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js +7 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-aria-label-is-well-formatted.js +9 -3
- package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -1
- package/dist/src/rules/html-aria-level-must-be-valid.js +6 -0
- package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-aria-role-heading-requires-level.js +7 -1
- package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
- package/dist/src/rules/html-aria-role-must-be-valid.js +7 -1
- package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-attribute-double-quotes.js +29 -2
- package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
- package/dist/src/rules/html-attribute-equals-spacing.js +18 -2
- package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -1
- package/dist/src/rules/html-attribute-values-require-quotes.js +39 -3
- package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
- package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +7 -1
- package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -1
- package/dist/src/rules/html-body-only-elements.js +46 -0
- package/dist/src/rules/html-body-only-elements.js.map +1 -0
- package/dist/src/rules/html-boolean-attributes-no-value.js +18 -1
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-head-only-elements.js +56 -0
- package/dist/src/rules/html-head-only-elements.js.map +1 -0
- package/dist/src/rules/html-iframe-has-title.js +8 -2
- package/dist/src/rules/html-iframe-has-title.js.map +1 -1
- package/dist/src/rules/html-img-require-alt.js +7 -1
- package/dist/src/rules/html-img-require-alt.js.map +1 -1
- package/dist/src/rules/html-input-require-autocomplete.js +70 -0
- package/dist/src/rules/html-input-require-autocomplete.js.map +1 -0
- package/dist/src/rules/html-navigation-has-label.js +7 -1
- package/dist/src/rules/html-navigation-has-label.js.map +1 -1
- package/dist/src/rules/html-no-aria-hidden-on-focusable.js +7 -1
- package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -1
- package/dist/src/rules/html-no-block-inside-inline.js +7 -1
- package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-attributes.js +7 -1
- package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-ids.js +9 -3
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-meta-names.js +136 -0
- package/dist/src/rules/html-no-duplicate-meta-names.js.map +1 -0
- package/dist/src/rules/html-no-empty-attributes.js +45 -7
- package/dist/src/rules/html-no-empty-attributes.js.map +1 -1
- package/dist/src/rules/html-no-empty-headings.js +7 -6
- package/dist/src/rules/html-no-empty-headings.js.map +1 -1
- package/dist/src/rules/html-no-nested-links.js +7 -1
- package/dist/src/rules/html-no-nested-links.js.map +1 -1
- package/dist/src/rules/html-no-positive-tab-index.js +7 -1
- package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
- package/dist/src/rules/html-no-self-closing.js +48 -3
- package/dist/src/rules/html-no-self-closing.js.map +1 -1
- package/dist/src/rules/html-no-space-in-tag.js +173 -0
- package/dist/src/rules/html-no-space-in-tag.js.map +1 -0
- package/dist/src/rules/html-no-title-attribute.js +7 -1
- package/dist/src/rules/html-no-title-attribute.js.map +1 -1
- package/dist/src/rules/html-no-underscores-in-attribute-names.js +7 -1
- package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -1
- package/dist/src/rules/html-tag-name-lowercase.js +23 -5
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
- package/dist/src/rules/index.js +19 -3
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/parser-no-errors.js +6 -0
- package/dist/src/rules/parser-no-errors.js.map +1 -1
- package/dist/src/rules/rule-utils.js +213 -31
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +22 -2
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
- package/dist/src/{default-rules.js → rules.js} +44 -16
- package/dist/src/rules.js.map +1 -0
- package/dist/src/types.js +34 -1
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/cli/argument-parser.d.ts +8 -2
- package/dist/types/cli/file-processor.d.ts +15 -0
- package/dist/types/cli/formatters/json-formatter.d.ts +6 -0
- package/dist/types/cli/formatters/simple-formatter.d.ts +1 -0
- package/dist/types/cli/summary-reporter.d.ts +6 -0
- package/dist/types/cli.d.ts +9 -4
- package/dist/types/custom-rule-loader.d.ts +62 -0
- package/dist/types/herb-disable-comment-utils.d.ts +69 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/linter.d.ts +99 -3
- package/dist/types/loader.d.ts +20 -0
- package/dist/types/rules/erb-comment-syntax.d.ts +12 -5
- package/dist/types/rules/erb-no-case-node-children.d.ts +8 -0
- package/dist/types/rules/erb-no-empty-tags.d.ts +3 -2
- package/dist/types/rules/erb-no-extra-newline.d.ts +14 -0
- package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
- package/dist/types/rules/erb-no-output-control-flow.d.ts +3 -2
- package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
- package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +3 -2
- package/dist/types/rules/erb-require-trailing-newline.d.ts +9 -0
- package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
- package/dist/types/rules/erb-right-trim.d.ts +12 -5
- package/dist/types/rules/herb-disable-comment-base.d.ts +37 -0
- package/dist/types/rules/herb-disable-comment-malformed.d.ts +8 -0
- package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +8 -0
- package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
- package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
- package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +8 -0
- package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
- package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
- package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
- package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +3 -2
- package/dist/types/rules/html-aria-level-must-be-valid.d.ts +3 -2
- package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +3 -2
- package/dist/types/rules/html-aria-role-must-be-valid.d.ts +3 -2
- package/dist/types/rules/html-attribute-double-quotes.d.ts +13 -5
- package/dist/types/rules/html-attribute-equals-spacing.d.ts +12 -5
- package/dist/types/rules/html-attribute-values-require-quotes.d.ts +13 -5
- package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
- package/dist/types/rules/html-body-only-elements.d.ts +9 -0
- package/dist/types/rules/html-boolean-attributes-no-value.d.ts +12 -5
- package/dist/types/rules/html-head-only-elements.d.ts +9 -0
- package/dist/types/rules/html-iframe-has-title.d.ts +3 -2
- package/dist/types/rules/html-img-require-alt.d.ts +3 -2
- package/dist/types/rules/html-input-require-autocomplete.d.ts +8 -0
- package/dist/types/rules/html-navigation-has-label.d.ts +3 -2
- package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
- package/dist/types/rules/html-no-block-inside-inline.d.ts +3 -2
- package/dist/types/rules/html-no-duplicate-attributes.d.ts +3 -2
- package/dist/types/rules/html-no-duplicate-ids.d.ts +3 -2
- package/dist/types/rules/html-no-duplicate-meta-names.d.ts +9 -0
- package/dist/types/rules/html-no-empty-attributes.d.ts +3 -2
- package/dist/types/rules/html-no-empty-headings.d.ts +3 -2
- package/dist/types/rules/html-no-nested-links.d.ts +3 -2
- package/dist/types/rules/html-no-positive-tab-index.d.ts +3 -2
- package/dist/types/rules/html-no-self-closing.d.ts +14 -5
- package/dist/types/rules/html-no-space-in-tag.d.ts +16 -0
- package/dist/types/rules/html-no-title-attribute.d.ts +3 -2
- package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
- package/dist/types/rules/html-tag-name-lowercase.d.ts +16 -6
- package/dist/types/rules/index.d.ts +19 -3
- package/dist/types/rules/parser-no-errors.d.ts +2 -1
- package/dist/types/rules/rule-utils.d.ts +73 -26
- package/dist/types/rules/svg-tag-name-capitalization.d.ts +13 -4
- package/dist/types/rules.d.ts +2 -0
- package/dist/types/src/cli/argument-parser.d.ts +8 -2
- package/dist/types/src/cli/file-processor.d.ts +15 -0
- package/dist/types/src/cli/formatters/json-formatter.d.ts +6 -0
- package/dist/types/src/cli/formatters/simple-formatter.d.ts +1 -0
- package/dist/types/src/cli/summary-reporter.d.ts +6 -0
- package/dist/types/src/cli.d.ts +9 -4
- package/dist/types/src/custom-rule-loader.d.ts +62 -0
- package/dist/types/src/herb-disable-comment-utils.d.ts +69 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/linter.d.ts +99 -3
- package/dist/types/src/loader.d.ts +20 -0
- package/dist/types/src/rules/erb-comment-syntax.d.ts +12 -5
- package/dist/types/src/rules/erb-no-case-node-children.d.ts +8 -0
- package/dist/types/src/rules/erb-no-empty-tags.d.ts +3 -2
- package/dist/types/src/rules/erb-no-extra-newline.d.ts +14 -0
- package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
- package/dist/types/src/rules/erb-no-output-control-flow.d.ts +3 -2
- package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
- package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +3 -2
- package/dist/types/src/rules/erb-require-trailing-newline.d.ts +9 -0
- package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
- package/dist/types/src/rules/erb-right-trim.d.ts +12 -5
- package/dist/types/src/rules/herb-disable-comment-base.d.ts +37 -0
- package/dist/types/src/rules/herb-disable-comment-malformed.d.ts +8 -0
- package/dist/types/src/rules/herb-disable-comment-missing-rules.d.ts +8 -0
- package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
- package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
- package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +8 -0
- package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
- package/dist/types/src/rules/html-anchor-require-href.d.ts +3 -2
- package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
- package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +3 -2
- package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +3 -2
- package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +3 -2
- package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +3 -2
- package/dist/types/src/rules/html-attribute-double-quotes.d.ts +13 -5
- package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +12 -5
- package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +13 -5
- package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
- package/dist/types/src/rules/html-body-only-elements.d.ts +9 -0
- package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +12 -5
- package/dist/types/src/rules/html-head-only-elements.d.ts +9 -0
- package/dist/types/src/rules/html-iframe-has-title.d.ts +3 -2
- package/dist/types/src/rules/html-img-require-alt.d.ts +3 -2
- package/dist/types/src/rules/html-input-require-autocomplete.d.ts +8 -0
- package/dist/types/src/rules/html-navigation-has-label.d.ts +3 -2
- package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
- package/dist/types/src/rules/html-no-block-inside-inline.d.ts +3 -2
- package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +3 -2
- package/dist/types/src/rules/html-no-duplicate-ids.d.ts +3 -2
- package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +9 -0
- package/dist/types/src/rules/html-no-empty-attributes.d.ts +3 -2
- package/dist/types/src/rules/html-no-empty-headings.d.ts +3 -2
- package/dist/types/src/rules/html-no-nested-links.d.ts +3 -2
- package/dist/types/src/rules/html-no-positive-tab-index.d.ts +3 -2
- package/dist/types/src/rules/html-no-self-closing.d.ts +14 -5
- package/dist/types/src/rules/html-no-space-in-tag.d.ts +16 -0
- package/dist/types/src/rules/html-no-title-attribute.d.ts +3 -2
- package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
- package/dist/types/src/rules/html-tag-name-lowercase.d.ts +16 -6
- package/dist/types/src/rules/index.d.ts +19 -3
- package/dist/types/src/rules/parser-no-errors.d.ts +2 -1
- package/dist/types/src/rules/rule-utils.d.ts +73 -26
- package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +13 -4
- package/dist/types/src/rules.d.ts +2 -0
- package/dist/types/src/types.d.ts +102 -11
- package/dist/types/types.d.ts +102 -11
- package/docs/rules/README.md +16 -2
- package/docs/rules/erb-no-case-node-children.md +50 -0
- package/docs/rules/erb-no-extra-newline.md +74 -0
- package/docs/rules/erb-no-extra-whitespace-inside-tags.md +39 -0
- package/docs/rules/{erb-requires-trailing-newline.md → erb-require-trailing-newline.md} +1 -1
- package/docs/rules/erb-right-trim.md +5 -10
- package/docs/rules/herb-disable-comment-malformed.md +45 -0
- package/docs/rules/herb-disable-comment-missing-rules.md +60 -0
- package/docs/rules/herb-disable-comment-no-duplicate-rules.md +49 -0
- package/docs/rules/herb-disable-comment-no-redundant-all.md +53 -0
- package/docs/rules/herb-disable-comment-unnecessary.md +44 -0
- package/docs/rules/herb-disable-comment-valid-rule-name.md +41 -0
- package/docs/rules/html-aria-attribute-must-be-valid.md +2 -5
- package/docs/rules/html-aria-label-is-well-formatted.md +1 -1
- package/docs/rules/html-attribute-double-quotes.md +2 -2
- package/docs/rules/html-attribute-equals-spacing.md +2 -2
- package/docs/rules/html-attribute-values-require-quotes.md +3 -3
- package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +2 -2
- package/docs/rules/html-body-only-elements.md +99 -0
- package/docs/rules/html-boolean-attributes-no-value.md +2 -2
- package/docs/rules/html-head-only-elements.md +103 -0
- package/docs/rules/html-input-require-autocomplete.md +64 -0
- package/docs/rules/html-no-aria-hidden-on-focusable.md +2 -2
- package/docs/rules/html-no-duplicate-attributes.md +2 -2
- package/docs/rules/html-no-duplicate-meta-names.md +64 -0
- package/docs/rules/html-no-empty-attributes.md +3 -3
- package/docs/rules/html-no-empty-headings.md +4 -26
- package/docs/rules/html-no-positive-tab-index.md +1 -2
- package/docs/rules/html-no-self-closing.md +17 -2
- package/docs/rules/html-no-space-in-tag.md +66 -0
- package/docs/rules/html-no-title-attribute.md +2 -2
- package/docs/rules/html-no-underscores-in-attribute-names.md +2 -2
- package/docs/rules/html-tag-name-lowercase.md +2 -2
- package/package.json +13 -5
- package/src/cli/argument-parser.ts +46 -37
- package/src/cli/file-processor.ts +159 -28
- package/src/cli/formatters/detailed-formatter.ts +21 -3
- package/src/cli/formatters/github-actions-formatter.ts +17 -1
- package/src/cli/formatters/json-formatter.ts +9 -0
- package/src/cli/formatters/simple-formatter.ts +24 -8
- package/src/cli/output-manager.ts +23 -3
- package/src/cli/summary-reporter.ts +40 -3
- package/src/cli.ts +134 -51
- package/src/custom-rule-loader.ts +189 -0
- package/src/herb-disable-comment-utils.ts +175 -0
- package/src/index.ts +2 -0
- package/src/linter.ts +501 -36
- package/src/loader.ts +30 -0
- package/src/rules/erb-comment-syntax.ts +53 -10
- package/src/rules/erb-no-case-node-children.ts +68 -0
- package/src/rules/erb-no-empty-tags.ts +9 -3
- package/src/rules/erb-no-extra-newline.ts +91 -0
- package/src/rules/erb-no-extra-whitespace-inside-tags.ts +147 -0
- package/src/rules/erb-no-output-control-flow.ts +9 -3
- package/src/rules/erb-no-silent-tag-in-attribute-name.ts +9 -3
- package/src/rules/erb-prefer-image-tag-helper.ts +9 -3
- package/src/rules/erb-require-trailing-newline.ts +47 -0
- package/src/rules/erb-require-whitespace-inside-tags.ts +94 -16
- package/src/rules/erb-right-trim.ts +45 -22
- package/src/rules/herb-disable-comment-base.ts +76 -0
- package/src/rules/herb-disable-comment-malformed.ts +66 -0
- package/src/rules/herb-disable-comment-missing-rules.ts +41 -0
- package/src/rules/herb-disable-comment-no-duplicate-rules.ts +46 -0
- package/src/rules/herb-disable-comment-no-redundant-all.ts +40 -0
- package/src/rules/herb-disable-comment-unnecessary.ts +103 -0
- package/src/rules/herb-disable-comment-valid-rule-name.ts +62 -0
- package/src/rules/html-anchor-require-href.ts +9 -3
- package/src/rules/html-aria-attribute-must-be-valid.ts +9 -3
- package/src/rules/html-aria-label-is-well-formatted.ts +9 -5
- package/src/rules/html-aria-level-must-be-valid.ts +9 -2
- package/src/rules/html-aria-role-heading-requires-level.ts +9 -3
- package/src/rules/html-aria-role-must-be-valid.ts +9 -3
- package/src/rules/html-attribute-double-quotes.ts +42 -8
- package/src/rules/html-attribute-equals-spacing.ts +31 -7
- package/src/rules/html-attribute-values-require-quotes.ts +56 -10
- package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +9 -3
- package/src/rules/html-body-only-elements.ts +60 -0
- package/src/rules/html-boolean-attributes-no-value.ts +31 -6
- package/src/rules/html-head-only-elements.ts +70 -0
- package/src/rules/html-iframe-has-title.ts +9 -4
- package/src/rules/html-img-require-alt.ts +10 -4
- package/src/rules/html-input-require-autocomplete.ts +85 -0
- package/src/rules/html-navigation-has-label.ts +9 -3
- package/src/rules/html-no-aria-hidden-on-focusable.ts +9 -3
- package/src/rules/html-no-block-inside-inline.ts +9 -3
- package/src/rules/html-no-duplicate-attributes.ts +9 -3
- package/src/rules/html-no-duplicate-ids.ts +11 -7
- package/src/rules/html-no-duplicate-meta-names.ts +188 -0
- package/src/rules/html-no-empty-attributes.ts +58 -10
- package/src/rules/html-no-empty-headings.ts +10 -8
- package/src/rules/html-no-nested-links.ts +10 -4
- package/src/rules/html-no-positive-tab-index.ts +9 -3
- package/src/rules/html-no-self-closing.ts +69 -9
- package/src/rules/html-no-space-in-tag.ts +221 -0
- package/src/rules/html-no-title-attribute.ts +9 -3
- package/src/rules/html-no-underscores-in-attribute-names.ts +12 -4
- package/src/rules/html-tag-name-lowercase.ts +41 -10
- package/src/rules/index.ts +23 -3
- package/src/rules/parser-no-errors.ts +8 -1
- package/src/rules/rule-utils.ts +251 -43
- package/src/rules/svg-tag-name-capitalization.ts +39 -6
- package/src/{default-rules.ts → rules.ts} +51 -15
- package/src/types.ts +133 -15
- package/dist/src/default-rules.js.map +0 -1
- package/dist/src/rules/erb-requires-trailing-newline.js +0 -22
- package/dist/src/rules/erb-requires-trailing-newline.js.map +0 -1
- package/dist/types/default-rules.d.ts +0 -2
- package/dist/types/rules/erb-requires-trailing-newline.d.ts +0 -6
- package/dist/types/src/default-rules.d.ts +0 -2
- package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +0 -6
- package/src/rules/erb-requires-trailing-newline.ts +0 -29
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { ParserRule } from "../types.js"
|
|
1
|
+
import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
|
|
2
2
|
import { BaseRuleVisitor } from "./rule-utils.js"
|
|
3
|
+
import { isNode, getTagName, HTMLOpenTagNode } from "@herb-tools/core"
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
|
|
6
|
+
import type { HTMLElementNode, HTMLCloseTagNode, ParseResult, XMLDeclarationNode, Node } from "@herb-tools/core"
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
interface TagNameAutofixContext extends BaseAutofixContext {
|
|
9
|
+
node: Mutable<HTMLOpenTagNode | HTMLCloseTagNode>
|
|
10
|
+
tagName: string
|
|
11
|
+
correctedTagName: string
|
|
12
|
+
}
|
|
7
13
|
|
|
8
14
|
class XMLDeclarationChecker extends BaseRuleVisitor {
|
|
9
15
|
hasXMLDeclaration: boolean = false
|
|
@@ -18,7 +24,7 @@ class XMLDeclarationChecker extends BaseRuleVisitor {
|
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
27
|
+
class TagNameLowercaseVisitor extends BaseRuleVisitor<TagNameAutofixContext> {
|
|
22
28
|
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
23
29
|
if (getTagName(node).toLowerCase() === "svg") {
|
|
24
30
|
this.checkTagName(node.open_tag)
|
|
@@ -52,28 +58,53 @@ class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
|
52
58
|
this.addOffense(
|
|
53
59
|
`${type} tag name \`${open}${tagName}>\` should be lowercase. Use \`${open}${lowercaseTagName}>\` instead.`,
|
|
54
60
|
node.tag_name!.location,
|
|
55
|
-
|
|
61
|
+
{
|
|
62
|
+
node,
|
|
63
|
+
tagName,
|
|
64
|
+
correctedTagName: lowercaseTagName
|
|
65
|
+
}
|
|
56
66
|
)
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
export class HTMLTagNameLowercaseRule extends ParserRule {
|
|
71
|
+
export class HTMLTagNameLowercaseRule extends ParserRule<TagNameAutofixContext> {
|
|
72
|
+
static autocorrectable = true
|
|
62
73
|
name = "html-tag-name-lowercase"
|
|
63
74
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
get defaultConfig(): FullRuleConfig {
|
|
76
|
+
return {
|
|
77
|
+
enabled: true,
|
|
78
|
+
severity: "error",
|
|
79
|
+
exclude: ["**/*.xml", "**/*.xml.erb"]
|
|
67
80
|
}
|
|
81
|
+
}
|
|
68
82
|
|
|
83
|
+
isEnabled(result: ParseResult, _context?: Partial<LintContext>): boolean {
|
|
69
84
|
const checker = new XMLDeclarationChecker(this.name)
|
|
85
|
+
|
|
70
86
|
checker.visit(result.value)
|
|
87
|
+
|
|
71
88
|
return !checker.hasXMLDeclaration
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
check(result: ParseResult, context?: Partial<LintContext>):
|
|
91
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<TagNameAutofixContext>[] {
|
|
75
92
|
const visitor = new TagNameLowercaseVisitor(this.name, context)
|
|
93
|
+
|
|
76
94
|
visitor.visit(result.value)
|
|
95
|
+
|
|
77
96
|
return visitor.offenses
|
|
78
97
|
}
|
|
98
|
+
|
|
99
|
+
autofix(offense: LintOffense<TagNameAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
|
|
100
|
+
if (!offense.autofixContext) return null
|
|
101
|
+
|
|
102
|
+
const { node: { tag_name }, correctedTagName } = offense.autofixContext
|
|
103
|
+
|
|
104
|
+
if (!tag_name) return null
|
|
105
|
+
|
|
106
|
+
tag_name.value = correctedTagName
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
79
110
|
}
|
package/src/rules/index.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
export * from "./rule-utils.js"
|
|
2
|
+
export * from "./herb-disable-comment-base.js"
|
|
3
|
+
|
|
2
4
|
export * from "./erb-comment-syntax.js"
|
|
5
|
+
export * from "./erb-no-case-node-children.js"
|
|
3
6
|
export * from "./erb-no-empty-tags.js"
|
|
7
|
+
export * from "./erb-no-extra-newline.js"
|
|
8
|
+
export * from "./erb-no-extra-whitespace-inside-tags.js"
|
|
4
9
|
export * from "./erb-no-output-control-flow.js"
|
|
5
10
|
export * from "./erb-no-silent-tag-in-attribute-name.js"
|
|
6
11
|
export * from "./erb-prefer-image-tag-helper.js"
|
|
7
|
-
export * from "./erb-
|
|
12
|
+
export * from "./erb-require-trailing-newline.js"
|
|
13
|
+
export * from "./erb-require-whitespace-inside-tags.js"
|
|
14
|
+
export * from "./erb-right-trim.js"
|
|
15
|
+
|
|
16
|
+
export * from "./herb-disable-comment-valid-rule-name.js"
|
|
17
|
+
export * from "./herb-disable-comment-no-redundant-all.js"
|
|
18
|
+
export * from "./herb-disable-comment-no-duplicate-rules.js"
|
|
19
|
+
export * from "./herb-disable-comment-missing-rules.js"
|
|
20
|
+
export * from "./herb-disable-comment-malformed.js"
|
|
21
|
+
export * from "./herb-disable-comment-unnecessary.js"
|
|
22
|
+
|
|
8
23
|
export * from "./html-anchor-require-href.js"
|
|
9
24
|
export * from "./html-aria-label-is-well-formatted.js"
|
|
10
25
|
export * from "./html-aria-level-must-be-valid.js"
|
|
@@ -14,21 +29,26 @@ export * from "./html-attribute-double-quotes.js"
|
|
|
14
29
|
export * from "./html-attribute-equals-spacing.js"
|
|
15
30
|
export * from "./html-attribute-values-require-quotes.js"
|
|
16
31
|
export * from "./html-avoid-both-disabled-and-aria-disabled.js"
|
|
32
|
+
export * from "./html-body-only-elements.js"
|
|
17
33
|
export * from "./html-boolean-attributes-no-value.js"
|
|
34
|
+
export * from "./html-head-only-elements.js"
|
|
18
35
|
export * from "./html-iframe-has-title.js"
|
|
19
36
|
export * from "./html-img-require-alt.js"
|
|
37
|
+
export * from "./html-input-require-autocomplete.js"
|
|
20
38
|
export * from "./html-navigation-has-label.js"
|
|
21
39
|
export * from "./html-no-aria-hidden-on-focusable.js"
|
|
22
40
|
export * from "./html-no-block-inside-inline.js"
|
|
23
41
|
export * from "./html-no-duplicate-attributes.js"
|
|
24
42
|
export * from "./html-no-duplicate-ids.js"
|
|
43
|
+
export * from "./html-no-duplicate-meta-names.js"
|
|
25
44
|
export * from "./html-no-empty-attributes.js"
|
|
26
45
|
export * from "./html-no-empty-headings.js"
|
|
27
46
|
export * from "./html-no-nested-links.js"
|
|
28
47
|
export * from "./html-no-positive-tab-index.js"
|
|
29
48
|
export * from "./html-no-self-closing.js"
|
|
49
|
+
export * from "./html-no-space-in-tag.js"
|
|
30
50
|
export * from "./html-no-title-attribute.js"
|
|
51
|
+
export * from "./html-no-underscores-in-attribute-names.js"
|
|
31
52
|
export * from "./html-tag-name-lowercase.js"
|
|
53
|
+
|
|
32
54
|
export * from "./svg-tag-name-capitalization.js"
|
|
33
|
-
export * from "./html-no-underscores-in-attribute-names.js"
|
|
34
|
-
export * from "./erb-right-trim.js"
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { ParserRule } from "../types.js"
|
|
2
2
|
|
|
3
|
-
import type { LintOffense } from "../types.js"
|
|
3
|
+
import type { LintOffense, FullRuleConfig } from "../types.js"
|
|
4
4
|
import type { ParseResult, HerbError } from "@herb-tools/core"
|
|
5
5
|
|
|
6
6
|
export class ParserNoErrorsRule extends ParserRule {
|
|
7
7
|
name = "parser-no-errors"
|
|
8
8
|
|
|
9
|
+
get defaultConfig(): FullRuleConfig {
|
|
10
|
+
return {
|
|
11
|
+
enabled: true,
|
|
12
|
+
severity: "error"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
check(result: ParseResult): LintOffense[] {
|
|
10
17
|
return result.recursiveErrors().map(error =>
|
|
11
18
|
this.herbErrorToLintOffense(error)
|
package/src/rules/rule-utils.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Visitor,
|
|
3
|
-
Position,
|
|
4
3
|
Location,
|
|
5
4
|
getStaticAttributeName,
|
|
6
5
|
hasDynamicAttributeName as hasNodeDynamicAttributeName,
|
|
@@ -17,6 +16,7 @@ import type {
|
|
|
17
16
|
HTMLAttributeNameNode,
|
|
18
17
|
HTMLAttributeNode,
|
|
19
18
|
HTMLAttributeValueNode,
|
|
19
|
+
HTMLElementNode,
|
|
20
20
|
HTMLOpenTagNode,
|
|
21
21
|
LiteralNode,
|
|
22
22
|
LexResult,
|
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
import { DEFAULT_LINT_CONTEXT } from "../types.js"
|
|
28
28
|
|
|
29
29
|
import type * as Nodes from "@herb-tools/core"
|
|
30
|
-
import type {
|
|
30
|
+
import type { UnboundLintOffense, LintContext, BaseAutofixContext } from "../types.js"
|
|
31
31
|
|
|
32
32
|
export enum ControlFlowType {
|
|
33
33
|
CONDITIONAL,
|
|
@@ -37,8 +37,8 @@ export enum ControlFlowType {
|
|
|
37
37
|
/**
|
|
38
38
|
* Base visitor class that provides common functionality for rule visitors
|
|
39
39
|
*/
|
|
40
|
-
export abstract class BaseRuleVisitor extends Visitor {
|
|
41
|
-
public readonly offenses:
|
|
40
|
+
export abstract class BaseRuleVisitor<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> extends Visitor {
|
|
41
|
+
public readonly offenses: UnboundLintOffense<TAutofixContext>[] = []
|
|
42
42
|
protected ruleName: string
|
|
43
43
|
protected context: LintContext
|
|
44
44
|
|
|
@@ -50,24 +50,25 @@ export abstract class BaseRuleVisitor extends Visitor {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Helper method to create
|
|
53
|
+
* Helper method to create an unbound lint offense (without severity).
|
|
54
|
+
* The Linter will bind severity based on the rule's config.
|
|
54
55
|
*/
|
|
55
|
-
protected createOffense(message: string, location: Location,
|
|
56
|
+
protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
|
|
56
57
|
return {
|
|
57
58
|
rule: this.ruleName,
|
|
58
59
|
code: this.ruleName,
|
|
59
60
|
source: "Herb Linter",
|
|
60
61
|
message,
|
|
61
62
|
location,
|
|
62
|
-
|
|
63
|
+
autofixContext,
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
68
|
* Helper method to add an offense to the offenses array
|
|
68
69
|
*/
|
|
69
|
-
protected addOffense(message: string, location: Location,
|
|
70
|
-
this.offenses.push(this.createOffense(message, location,
|
|
70
|
+
protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
|
|
71
|
+
this.offenses.push(this.createOffense(message, location, autofixContext))
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -76,17 +77,18 @@ export abstract class BaseRuleVisitor extends Visitor {
|
|
|
76
77
|
* This allows rules to track state across different control flow structures
|
|
77
78
|
* like if/else branches, loops, etc.
|
|
78
79
|
*
|
|
80
|
+
* @template TAutofixContext - Type for autofix context (node + custom data)
|
|
79
81
|
* @template TControlFlowState - Type for state passed between onEnterControlFlow and onExitControlFlow
|
|
80
82
|
* @template TBranchState - Type for state passed between onEnterBranch and onExitBranch
|
|
81
83
|
*/
|
|
82
|
-
export abstract class ControlFlowTrackingVisitor<TControlFlowState = any, TBranchState = any> extends BaseRuleVisitor {
|
|
84
|
+
export abstract class ControlFlowTrackingVisitor<TAutofixContext extends BaseAutofixContext = BaseAutofixContext, TControlFlowState = any, TBranchState = any> extends BaseRuleVisitor<TAutofixContext> {
|
|
83
85
|
protected isInControlFlow: boolean = false
|
|
84
86
|
protected currentControlFlowType: ControlFlowType | null = null
|
|
85
87
|
|
|
86
88
|
/**
|
|
87
89
|
* Handle visiting a control flow node with proper scope management
|
|
88
90
|
*/
|
|
89
|
-
protected handleControlFlowNode(
|
|
91
|
+
protected handleControlFlowNode(_node: Node, controlFlowType: ControlFlowType, visitChildren: () => void): void {
|
|
90
92
|
const wasInControlFlow = this.isInControlFlow
|
|
91
93
|
const previousControlFlowType = this.currentControlFlowType
|
|
92
94
|
|
|
@@ -170,7 +172,9 @@ export function getAttributes(node: HTMLOpenTagNode): HTMLAttributeNode[] {
|
|
|
170
172
|
/**
|
|
171
173
|
* Gets the tag name from an HTML tag node (lowercased)
|
|
172
174
|
*/
|
|
173
|
-
export function getTagName(node: HTMLOpenTagNode): string | null {
|
|
175
|
+
export function getTagName(node: HTMLElementNode | HTMLOpenTagNode | null | undefined): string | null {
|
|
176
|
+
if (!node) return null
|
|
177
|
+
|
|
174
178
|
return node.tag_name?.value.toLowerCase() || null
|
|
175
179
|
}
|
|
176
180
|
|
|
@@ -374,7 +378,9 @@ export function findAttributeByName(attributes: Node[], attributeName: string):
|
|
|
374
378
|
/**
|
|
375
379
|
* Checks if a tag has a specific attribute
|
|
376
380
|
*/
|
|
377
|
-
export function hasAttribute(node: HTMLOpenTagNode, attributeName: string): boolean {
|
|
381
|
+
export function hasAttribute(node: HTMLOpenTagNode | null | undefined, attributeName: string): boolean {
|
|
382
|
+
if (!node) return false
|
|
383
|
+
|
|
378
384
|
return getAttribute(node, attributeName) !== null
|
|
379
385
|
}
|
|
380
386
|
|
|
@@ -577,10 +583,8 @@ export function createEndOfFileLocation(source: string): Location {
|
|
|
577
583
|
const lastColumnNumber = lastLine.length
|
|
578
584
|
|
|
579
585
|
const startColumn = lastColumnNumber > 0 ? lastColumnNumber - 1 : 0
|
|
580
|
-
const start = new Position(lastLineNumber, startColumn)
|
|
581
|
-
const end = new Position(lastLineNumber, lastColumnNumber)
|
|
582
586
|
|
|
583
|
-
return
|
|
587
|
+
return Location.from(lastLineNumber, startColumn, lastLineNumber, lastColumnNumber)
|
|
584
588
|
}
|
|
585
589
|
|
|
586
590
|
/**
|
|
@@ -621,7 +625,7 @@ export function isBooleanAttribute(attributeName: string): boolean {
|
|
|
621
625
|
* - checkDynamicAttributeStaticValue() - name="data-<%= key %>" value="foo"
|
|
622
626
|
* - checkDynamicAttributeDynamicValue() - name="data-<%= key %>" value="<%= value %>"
|
|
623
627
|
*/
|
|
624
|
-
export abstract class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
628
|
+
export abstract class AttributeVisitorMixin<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> extends BaseRuleVisitor<TAutofixContext> {
|
|
625
629
|
constructor(ruleName: string, context?: Partial<LintContext>) {
|
|
626
630
|
super(ruleName, context)
|
|
627
631
|
}
|
|
@@ -735,8 +739,8 @@ export function forEachAttribute(
|
|
|
735
739
|
/**
|
|
736
740
|
* Base lexer visitor class that provides common functionality for lexer-based rule visitors
|
|
737
741
|
*/
|
|
738
|
-
export abstract class BaseLexerRuleVisitor {
|
|
739
|
-
public readonly offenses:
|
|
742
|
+
export abstract class BaseLexerRuleVisitor<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> {
|
|
743
|
+
public readonly offenses: UnboundLintOffense<TAutofixContext>[] = []
|
|
740
744
|
protected ruleName: string
|
|
741
745
|
protected context: LintContext
|
|
742
746
|
|
|
@@ -746,24 +750,25 @@ export abstract class BaseLexerRuleVisitor {
|
|
|
746
750
|
}
|
|
747
751
|
|
|
748
752
|
/**
|
|
749
|
-
* Helper method to create
|
|
753
|
+
* Helper method to create an unbound lint offense (without severity).
|
|
754
|
+
* The Linter will bind severity based on the rule's config.
|
|
750
755
|
*/
|
|
751
|
-
protected createOffense(message: string, location: Location,
|
|
756
|
+
protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
|
|
752
757
|
return {
|
|
753
758
|
rule: this.ruleName,
|
|
754
759
|
code: this.ruleName,
|
|
755
760
|
source: "Herb Linter",
|
|
756
761
|
message,
|
|
757
762
|
location,
|
|
758
|
-
|
|
763
|
+
autofixContext,
|
|
759
764
|
}
|
|
760
765
|
}
|
|
761
766
|
|
|
762
767
|
/**
|
|
763
768
|
* Helper method to add an offense to the offenses array
|
|
764
769
|
*/
|
|
765
|
-
protected addOffense(message: string, location: Location,
|
|
766
|
-
this.offenses.push(this.createOffense(message, location,
|
|
770
|
+
protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
|
|
771
|
+
this.offenses.push(this.createOffense(message, location, autofixContext))
|
|
767
772
|
}
|
|
768
773
|
|
|
769
774
|
/**
|
|
@@ -791,14 +796,13 @@ export abstract class BaseLexerRuleVisitor {
|
|
|
791
796
|
protected visitToken(_token: Token): void {
|
|
792
797
|
// Default implementation does nothing
|
|
793
798
|
}
|
|
794
|
-
|
|
795
799
|
}
|
|
796
800
|
|
|
797
801
|
/**
|
|
798
802
|
* Base source visitor class that provides common functionality for source-based rule visitors
|
|
799
803
|
*/
|
|
800
|
-
export abstract class BaseSourceRuleVisitor {
|
|
801
|
-
public readonly offenses:
|
|
804
|
+
export abstract class BaseSourceRuleVisitor<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> {
|
|
805
|
+
public readonly offenses: UnboundLintOffense<TAutofixContext>[] = []
|
|
802
806
|
protected ruleName: string
|
|
803
807
|
protected context: LintContext
|
|
804
808
|
|
|
@@ -808,24 +812,25 @@ export abstract class BaseSourceRuleVisitor {
|
|
|
808
812
|
}
|
|
809
813
|
|
|
810
814
|
/**
|
|
811
|
-
* Helper method to create
|
|
815
|
+
* Helper method to create an unbound lint offense (without severity).
|
|
816
|
+
* The Linter will bind severity based on the rule's config.
|
|
812
817
|
*/
|
|
813
|
-
protected createOffense(message: string, location: Location,
|
|
818
|
+
protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
|
|
814
819
|
return {
|
|
815
820
|
rule: this.ruleName,
|
|
816
821
|
code: this.ruleName,
|
|
817
822
|
source: "Herb Linter",
|
|
818
823
|
message,
|
|
819
824
|
location,
|
|
820
|
-
|
|
825
|
+
autofixContext,
|
|
821
826
|
}
|
|
822
827
|
}
|
|
823
828
|
|
|
824
829
|
/**
|
|
825
830
|
* Helper method to add an offense to the offenses array
|
|
826
831
|
*/
|
|
827
|
-
protected addOffense(message: string, location: Location,
|
|
828
|
-
this.offenses.push(this.createOffense(message, location,
|
|
832
|
+
protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
|
|
833
|
+
this.offenses.push(this.createOffense(message, location, autofixContext))
|
|
829
834
|
}
|
|
830
835
|
|
|
831
836
|
/**
|
|
@@ -841,19 +846,222 @@ export abstract class BaseSourceRuleVisitor {
|
|
|
841
846
|
* Override this method to implement source-level checks
|
|
842
847
|
*/
|
|
843
848
|
protected abstract visitSource(source: string): void
|
|
849
|
+
}
|
|
844
850
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
851
|
+
/**
|
|
852
|
+
* Autofix utilities for applying string replacements
|
|
853
|
+
*/
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Checks if two locations are equal
|
|
857
|
+
* @param a - First location
|
|
858
|
+
* @param b - Second location
|
|
859
|
+
* @returns true if locations are equal
|
|
860
|
+
*/
|
|
861
|
+
export function locationsEqual(a: Location, b: Location): boolean {
|
|
862
|
+
return a.start.line === b.start.line &&
|
|
863
|
+
a.start.column === b.start.column &&
|
|
864
|
+
a.end.line === b.end.line &&
|
|
865
|
+
a.end.column === b.end.column
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Finds a node in the AST that has a specific location
|
|
870
|
+
* Uses direct recursive traversal for reliability
|
|
871
|
+
* @param root - The root node to search from
|
|
872
|
+
* @param location - The location to match
|
|
873
|
+
* @param predicate - Optional predicate function to filter nodes (e.g., isERBNode)
|
|
874
|
+
* @returns The matching node or null if not found
|
|
875
|
+
*/
|
|
876
|
+
export function findNodeByLocation(root: Node, location: Location, predicate?: (node: Node) => boolean): any {
|
|
877
|
+
const visited = new Set<any>()
|
|
878
|
+
|
|
879
|
+
function search(node: any): any {
|
|
880
|
+
if (!node || visited.has(node)) return null
|
|
881
|
+
visited.add(node)
|
|
853
882
|
|
|
854
|
-
|
|
855
|
-
|
|
883
|
+
if (node.location && locationsEqual(node.location, location)) {
|
|
884
|
+
if (!predicate || predicate(node)) {
|
|
885
|
+
return node
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const propsToCheck = ['tag_opening', 'tag_closing', 'tag_name', 'name', 'equals', 'value', 'content']
|
|
890
|
+
for (const prop of propsToCheck) {
|
|
891
|
+
if (node[prop]?.location && locationsEqual(node[prop].location, location)) {
|
|
892
|
+
if (!predicate || predicate(node)) {
|
|
893
|
+
return node
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
856
897
|
|
|
857
|
-
|
|
898
|
+
if (typeof node.compactChildNodes === 'function') {
|
|
899
|
+
for (const child of node.compactChildNodes()) {
|
|
900
|
+
const found = search(child)
|
|
901
|
+
if (found) return found
|
|
902
|
+
}
|
|
903
|
+
} else {
|
|
904
|
+
if (node.children && Array.isArray(node.children)) {
|
|
905
|
+
for (const child of node.children) {
|
|
906
|
+
const found = search(child)
|
|
907
|
+
if (found) return found
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (node.body && Array.isArray(node.body)) {
|
|
912
|
+
for (const child of node.body) {
|
|
913
|
+
const found = search(child)
|
|
914
|
+
if (found) return found
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return null
|
|
858
920
|
}
|
|
921
|
+
|
|
922
|
+
return search(root)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* AST Navigation Utilities
|
|
927
|
+
* These utilities help navigate the AST tree for complex autofix operations
|
|
928
|
+
*/
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Finds the parent node of a given child node in the AST
|
|
932
|
+
* @param root - The root node to search from (typically the document node)
|
|
933
|
+
* @param target - The child node to find the parent of
|
|
934
|
+
* @returns The parent node, or null if not found
|
|
935
|
+
*
|
|
936
|
+
* @example
|
|
937
|
+
* const parent = findParent(result.value, offense.autofixContext.node)
|
|
938
|
+
* if (parent?.type === "AST_HTML_ELEMENT_NODE") {
|
|
939
|
+
* // Modify parent...
|
|
940
|
+
* }
|
|
941
|
+
*/
|
|
942
|
+
export function findParent(root: Node, target: Node): Node | null {
|
|
943
|
+
let parentNode: Node | null = null
|
|
944
|
+
|
|
945
|
+
const search = (node: Node, _parent: Node | null = null): void => {
|
|
946
|
+
if (parentNode) return
|
|
947
|
+
|
|
948
|
+
const nodeAny = node as any
|
|
949
|
+
|
|
950
|
+
if (nodeAny.children) {
|
|
951
|
+
for (const child of nodeAny.children) {
|
|
952
|
+
if (child === target) {
|
|
953
|
+
parentNode = node
|
|
954
|
+
return
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const propsToCheck = ['open_tag', 'close_tag', 'body', 'name', 'value']
|
|
960
|
+
|
|
961
|
+
for (const prop of propsToCheck) {
|
|
962
|
+
const value = (node as any)[prop]
|
|
963
|
+
if (value === target) {
|
|
964
|
+
parentNode = node
|
|
965
|
+
return
|
|
966
|
+
}
|
|
967
|
+
if (Array.isArray(value) && value.includes(target)) {
|
|
968
|
+
parentNode = node
|
|
969
|
+
return
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (nodeAny.children) {
|
|
974
|
+
for (const child of nodeAny.children) {
|
|
975
|
+
search(child, node)
|
|
976
|
+
if (parentNode) return
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
for (const prop of propsToCheck) {
|
|
981
|
+
const value = (node as any)[prop]
|
|
982
|
+
if (Array.isArray(value)) {
|
|
983
|
+
for (const item of value) {
|
|
984
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
985
|
+
search(item, node)
|
|
986
|
+
if (parentNode) return
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
} else if (value && typeof value === 'object' && 'type' in value) {
|
|
990
|
+
search(value, node)
|
|
991
|
+
if (parentNode) return
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
search(root)
|
|
997
|
+
|
|
998
|
+
return parentNode
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export const DOCUMENT_ONLY_TAG_NAMES = new Set<string>([
|
|
1002
|
+
"html"
|
|
1003
|
+
])
|
|
1004
|
+
|
|
1005
|
+
export const HTML_ONLY_TAG_NAMES = new Set<string>([
|
|
1006
|
+
"head", "body"
|
|
1007
|
+
])
|
|
1008
|
+
|
|
1009
|
+
export const HEAD_ONLY_TAG_NAMES = new Set<string>([
|
|
1010
|
+
"base",
|
|
1011
|
+
"title",
|
|
1012
|
+
"style",
|
|
1013
|
+
"meta",
|
|
1014
|
+
"link",
|
|
1015
|
+
])
|
|
1016
|
+
|
|
1017
|
+
export const HEAD_AND_BODY_TAG_NAMES = new Set<string>([
|
|
1018
|
+
"script",
|
|
1019
|
+
"noscript",
|
|
1020
|
+
"template",
|
|
1021
|
+
])
|
|
1022
|
+
|
|
1023
|
+
export function isDocumentOnlyTag(tagName: string): boolean {
|
|
1024
|
+
return DOCUMENT_ONLY_TAG_NAMES.has(tagName.toLowerCase())
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
export function isHtmlOnlyTag(tagName: string): boolean {
|
|
1028
|
+
return HTML_ONLY_TAG_NAMES.has(tagName.toLowerCase())
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export function isHeadOnlyTag(tagName: string): boolean {
|
|
1032
|
+
return HEAD_ONLY_TAG_NAMES.has(tagName.toLowerCase())
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
export function isHeadAndBodyTag(tagName: string): boolean {
|
|
1036
|
+
return HEAD_AND_BODY_TAG_NAMES.has(tagName.toLowerCase())
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
export function isBodyOnlyTag(tagName: string): boolean {
|
|
1040
|
+
const tag = tagName.toLowerCase()
|
|
1041
|
+
|
|
1042
|
+
return (
|
|
1043
|
+
!isDocumentOnlyTag(tag) &&
|
|
1044
|
+
!isHtmlOnlyTag(tag) &&
|
|
1045
|
+
!isHeadOnlyTag(tag) &&
|
|
1046
|
+
!isHeadAndBodyTag(tag)
|
|
1047
|
+
)
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export function isBodyTag(tagName: string): boolean {
|
|
1051
|
+
const tag = tagName.toLowerCase()
|
|
1052
|
+
return (
|
|
1053
|
+
!isDocumentOnlyTag(tag) &&
|
|
1054
|
+
!isHtmlOnlyTag(tag) &&
|
|
1055
|
+
(isBodyOnlyTag(tag) || isHeadAndBodyTag(tag))
|
|
1056
|
+
)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
export function isHeadTag(tagName: string): boolean {
|
|
1060
|
+
const tag = tagName.toLowerCase()
|
|
1061
|
+
|
|
1062
|
+
return (
|
|
1063
|
+
!isDocumentOnlyTag(tag) &&
|
|
1064
|
+
!isHtmlOnlyTag(tag) &&
|
|
1065
|
+
(isHeadOnlyTag(tag) || isHeadAndBodyTag(tag))
|
|
1066
|
+
)
|
|
859
1067
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
1
2
|
import { BaseRuleVisitor, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE } from "./rule-utils.js"
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
-
import type { LintOffense, LintContext } from "../types.js"
|
|
4
|
+
import type { UnboundLintOffense, LintOffense, LintContext, BaseAutofixContext, Mutable, FullRuleConfig } from "../types.js"
|
|
5
5
|
import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
interface SVGTagNameCapitalizationAutofixContext extends BaseAutofixContext {
|
|
8
|
+
node: Mutable<HTMLOpenTagNode | HTMLCloseTagNode>
|
|
9
|
+
currentTagName: string
|
|
10
|
+
correctCamelCase: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor<SVGTagNameCapitalizationAutofixContext> {
|
|
8
14
|
private insideSVG = false
|
|
9
15
|
|
|
10
16
|
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
@@ -22,6 +28,7 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
|
|
|
22
28
|
if (node.open_tag) {
|
|
23
29
|
this.checkTagName(node.open_tag as HTMLOpenTagNode)
|
|
24
30
|
}
|
|
31
|
+
|
|
25
32
|
if (node.close_tag) {
|
|
26
33
|
this.checkTagName(node.close_tag as HTMLCloseTagNode)
|
|
27
34
|
}
|
|
@@ -50,18 +57,44 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
|
|
|
50
57
|
this.addOffense(
|
|
51
58
|
`${type} SVG tag name \`${tagName}\` should use proper capitalization. Use \`${correctCamelCase}\` instead.`,
|
|
52
59
|
node.tag_name!.location,
|
|
53
|
-
|
|
60
|
+
{
|
|
61
|
+
node,
|
|
62
|
+
currentTagName: tagName,
|
|
63
|
+
correctCamelCase
|
|
64
|
+
}
|
|
54
65
|
)
|
|
55
66
|
}
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
export class SVGTagNameCapitalizationRule extends ParserRule {
|
|
70
|
+
export class SVGTagNameCapitalizationRule extends ParserRule<SVGTagNameCapitalizationAutofixContext> {
|
|
71
|
+
static autocorrectable = true
|
|
60
72
|
name = "svg-tag-name-capitalization"
|
|
61
73
|
|
|
62
|
-
|
|
74
|
+
get defaultConfig(): FullRuleConfig {
|
|
75
|
+
return {
|
|
76
|
+
enabled: true,
|
|
77
|
+
severity: "error"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<SVGTagNameCapitalizationAutofixContext>[] {
|
|
63
82
|
const visitor = new SVGTagNameCapitalizationVisitor(this.name, context)
|
|
83
|
+
|
|
64
84
|
visitor.visit(result.value)
|
|
85
|
+
|
|
65
86
|
return visitor.offenses
|
|
66
87
|
}
|
|
88
|
+
|
|
89
|
+
autofix(offense: LintOffense<SVGTagNameCapitalizationAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
|
|
90
|
+
if (!offense.autofixContext) return null
|
|
91
|
+
|
|
92
|
+
const { node: { tag_name }, correctCamelCase } = offense.autofixContext
|
|
93
|
+
|
|
94
|
+
if (!tag_name) return null
|
|
95
|
+
|
|
96
|
+
tag_name.value = correctCamelCase
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
67
100
|
}
|