@herb-tools/linter 0.7.5 → 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.
- package/README.md +253 -13
- package/dist/herb-lint.js +26023 -3424
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +5759 -1583
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5727 -1584
- package/dist/index.js.map +1 -1
- package/dist/loader.cjs +17010 -0
- package/dist/loader.cjs.map +1 -0
- package/dist/loader.js +16879 -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 +51 -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 +211 -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 +72 -25
- 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 +72 -25
- 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 +81 -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 +65 -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 +248 -42
- 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
package/src/index.ts
CHANGED
package/src/linter.ts
CHANGED
|
@@ -1,30 +1,154 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Location } from "@herb-tools/core"
|
|
2
|
+
import { IdentityPrinter } from "@herb-tools/printer"
|
|
3
|
+
import { minimatch } from "minimatch"
|
|
2
4
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
+
import { rules } from "./rules.js"
|
|
6
|
+
import { findNodeByLocation } from "./rules/rule-utils.js"
|
|
7
|
+
import { parseHerbDisableLine } from "./herb-disable-comment-utils.js"
|
|
8
|
+
|
|
9
|
+
import { ParserNoErrorsRule } from "./rules/parser-no-errors.js"
|
|
10
|
+
import { DEFAULT_RULE_CONFIG } from "./types.js"
|
|
11
|
+
|
|
12
|
+
import type { RuleClass, Rule, ParserRule, LexerRule, SourceRule, LintResult, LintOffense, UnboundLintOffense, LintContext, AutofixResult } from "./types.js"
|
|
13
|
+
import type { ParseResult, LexResult, HerbBackend } from "@herb-tools/core"
|
|
14
|
+
import type { RuleConfig, Config } from "@herb-tools/config"
|
|
15
|
+
|
|
16
|
+
export interface LinterOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Array of rule classes to use. If not provided, uses default rules.
|
|
19
|
+
*/
|
|
20
|
+
rules?: RuleClass[]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether to load custom rules from the project.
|
|
24
|
+
* Defaults to false for backward compatibility.
|
|
25
|
+
*/
|
|
26
|
+
loadCustomRules?: boolean
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Base directory to search for custom rules.
|
|
30
|
+
* Defaults to current working directory.
|
|
31
|
+
*/
|
|
32
|
+
customRulesBaseDir?: string
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Custom glob patterns to search for rule files.
|
|
36
|
+
*/
|
|
37
|
+
customRulesPatterns?: string[]
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether to suppress custom rule loading errors.
|
|
41
|
+
* Defaults to false.
|
|
42
|
+
*/
|
|
43
|
+
silentCustomRules?: boolean
|
|
44
|
+
}
|
|
5
45
|
|
|
6
46
|
export class Linter {
|
|
7
47
|
protected rules: RuleClass[]
|
|
48
|
+
protected allAvailableRules: RuleClass[]
|
|
8
49
|
protected herb: HerbBackend
|
|
9
50
|
protected offenses: LintOffense[]
|
|
51
|
+
protected config?: Config
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates a new Linter instance with automatic rule filtering based on config.
|
|
55
|
+
*
|
|
56
|
+
* @param herb - The Herb backend instance for parsing and lexing
|
|
57
|
+
* @param config - Optional full Config instance for rule filtering, severity overrides, and path-based filtering
|
|
58
|
+
* @param customRules - Optional array of custom rules to include alongside built-in rules
|
|
59
|
+
* @returns A configured Linter instance
|
|
60
|
+
*/
|
|
61
|
+
static from(herb: HerbBackend, config?: Config, customRules?: RuleClass[]): Linter {
|
|
62
|
+
const allRules = customRules ? [...rules, ...customRules] : rules
|
|
63
|
+
const filteredRules = config?.linter?.rules
|
|
64
|
+
? Linter.filterRulesByConfig(allRules, config.linter.rules)
|
|
65
|
+
: undefined
|
|
66
|
+
|
|
67
|
+
return new Linter(herb, filteredRules, config, allRules)
|
|
68
|
+
}
|
|
10
69
|
|
|
11
70
|
/**
|
|
12
71
|
* Creates a new Linter instance.
|
|
72
|
+
*
|
|
73
|
+
* For most use cases, prefer `Linter.from()` which handles config-based filtering.
|
|
74
|
+
* Use this constructor directly when you need explicit control over rules.
|
|
75
|
+
*
|
|
13
76
|
* @param herb - The Herb backend instance for parsing and lexing
|
|
14
77
|
* @param rules - Array of rule classes (Parser/AST or Lexer) to use. If not provided, uses default rules.
|
|
78
|
+
* @param config - Optional full Config instance for severity overrides and path-based rule filtering
|
|
79
|
+
* @param allAvailableRules - Optional array of ALL available rules (including disabled) for herb:disable validation
|
|
15
80
|
*/
|
|
16
|
-
constructor(herb: HerbBackend, rules?: RuleClass[]) {
|
|
81
|
+
constructor(herb: HerbBackend, rules?: RuleClass[], config?: Config, allAvailableRules?: RuleClass[]) {
|
|
17
82
|
this.herb = herb
|
|
83
|
+
this.config = config
|
|
18
84
|
this.rules = rules !== undefined ? rules : this.getDefaultRules()
|
|
85
|
+
this.allAvailableRules = allAvailableRules !== undefined ? allAvailableRules : this.rules
|
|
19
86
|
this.offenses = []
|
|
20
87
|
}
|
|
21
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Filters rules based on default config and optional user config overrides.
|
|
91
|
+
*
|
|
92
|
+
* Priority:
|
|
93
|
+
* 1. User config override (if rule config exists in userRulesConfig)
|
|
94
|
+
* 2. Default config from rule's defaultConfig getter
|
|
95
|
+
*
|
|
96
|
+
* @param allRules - All available rule classes to filter from
|
|
97
|
+
* @param userRulesConfig - Optional user configuration for rules
|
|
98
|
+
* @returns Filtered array of rule classes that should be enabled
|
|
99
|
+
*/
|
|
100
|
+
static filterRulesByConfig(
|
|
101
|
+
allRules: RuleClass[],
|
|
102
|
+
userRulesConfig?: Record<string, RuleConfig>
|
|
103
|
+
): RuleClass[] {
|
|
104
|
+
return allRules.filter(ruleClass => {
|
|
105
|
+
const instance = new ruleClass()
|
|
106
|
+
const ruleName = instance.name
|
|
107
|
+
|
|
108
|
+
const defaultEnabled = instance.defaultConfig?.enabled ?? DEFAULT_RULE_CONFIG.enabled
|
|
109
|
+
const userRuleConfig = userRulesConfig?.[ruleName]
|
|
110
|
+
|
|
111
|
+
if (userRuleConfig !== undefined) {
|
|
112
|
+
return userRuleConfig.enabled !== false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return defaultEnabled
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
22
119
|
/**
|
|
23
120
|
* Returns the default set of rule classes used by the linter.
|
|
24
|
-
*
|
|
121
|
+
* These are the rules enabled when no custom rules are provided.
|
|
122
|
+
* Filters all available rules to only include those enabled by default.
|
|
123
|
+
* @returns Array of default rule classes
|
|
25
124
|
*/
|
|
26
125
|
protected getDefaultRules(): RuleClass[] {
|
|
27
|
-
return
|
|
126
|
+
return Linter.filterRulesByConfig(rules)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Returns all available rule classes that can be referenced in herb:disable comments.
|
|
131
|
+
* This includes all rules that exist, regardless of whether they're currently enabled.
|
|
132
|
+
* Includes both built-in rules and any loaded custom rules.
|
|
133
|
+
* @returns Array of all available rule classes
|
|
134
|
+
*/
|
|
135
|
+
protected getAvailableRules(): RuleClass[] {
|
|
136
|
+
return this.allAvailableRules
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Meta-linting rules for herb:disable comments cannot be disabled
|
|
141
|
+
* This ensures that invalid herb:disable comments are always caught
|
|
142
|
+
*/
|
|
143
|
+
protected get nonExcludableRules() {
|
|
144
|
+
return [
|
|
145
|
+
"herb-disable-comment-valid-rule-name",
|
|
146
|
+
"herb-disable-comment-no-redundant-all",
|
|
147
|
+
"herb-disable-comment-no-duplicate-rules",
|
|
148
|
+
"herb-disable-comment-malformed",
|
|
149
|
+
"herb-disable-comment-missing-rules",
|
|
150
|
+
"herb-disable-comment-unnecessary"
|
|
151
|
+
]
|
|
28
152
|
}
|
|
29
153
|
|
|
30
154
|
getRuleCount(): number {
|
|
@@ -45,6 +169,128 @@ export class Linter {
|
|
|
45
169
|
return (rule.constructor as any).type === "source"
|
|
46
170
|
}
|
|
47
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Execute a single rule and return its unbound offenses.
|
|
174
|
+
* Handles rule type checking (Lexer/Parser/Source) and isEnabled checks.
|
|
175
|
+
*/
|
|
176
|
+
private executeRule(
|
|
177
|
+
rule: Rule,
|
|
178
|
+
parseResult: ParseResult,
|
|
179
|
+
lexResult: LexResult,
|
|
180
|
+
source: string,
|
|
181
|
+
context?: Partial<LintContext>
|
|
182
|
+
): UnboundLintOffense[] {
|
|
183
|
+
if (this.config && context?.fileName) {
|
|
184
|
+
if (!this.config.isRuleEnabledForPath(rule.name, context.fileName)) {
|
|
185
|
+
return []
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (context?.fileName && !this.config?.linter?.rules?.[rule.name]?.exclude) {
|
|
190
|
+
const defaultExclude = rule.defaultConfig?.exclude ?? DEFAULT_RULE_CONFIG.exclude
|
|
191
|
+
|
|
192
|
+
if (defaultExclude && defaultExclude.length > 0) {
|
|
193
|
+
const isExcluded = defaultExclude.some((pattern: string) => minimatch(context.fileName!, pattern))
|
|
194
|
+
|
|
195
|
+
if (isExcluded) {
|
|
196
|
+
return []
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let isEnabled = true
|
|
202
|
+
let ruleOffenses: UnboundLintOffense[]
|
|
203
|
+
|
|
204
|
+
if (this.isLexerRule(rule)) {
|
|
205
|
+
if (rule.isEnabled) {
|
|
206
|
+
isEnabled = rule.isEnabled(lexResult, context)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (isEnabled) {
|
|
210
|
+
ruleOffenses = (rule as LexerRule).check(lexResult, context)
|
|
211
|
+
} else {
|
|
212
|
+
ruleOffenses = []
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
} else if (this.isSourceRule(rule)) {
|
|
216
|
+
if (rule.isEnabled) {
|
|
217
|
+
isEnabled = rule.isEnabled(source, context)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isEnabled) {
|
|
221
|
+
ruleOffenses = (rule as SourceRule).check(source, context)
|
|
222
|
+
} else {
|
|
223
|
+
ruleOffenses = []
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
if (rule.isEnabled) {
|
|
227
|
+
isEnabled = rule.isEnabled(parseResult, context)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (isEnabled) {
|
|
231
|
+
ruleOffenses = (rule as ParserRule).check(parseResult, context)
|
|
232
|
+
} else {
|
|
233
|
+
ruleOffenses = []
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return ruleOffenses
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private filterOffenses(
|
|
241
|
+
ruleOffenses: LintOffense[],
|
|
242
|
+
ruleName: string,
|
|
243
|
+
ignoredOffensesByLine?: Map<number, Set<string>>,
|
|
244
|
+
herbDisableCache?: Map<number, string[]>,
|
|
245
|
+
ignoreDisableComments?: boolean
|
|
246
|
+
): { kept: LintOffense[], ignored: LintOffense[], wouldBeIgnored: LintOffense[] } {
|
|
247
|
+
const kept: LintOffense[] = []
|
|
248
|
+
const ignored: LintOffense[] = []
|
|
249
|
+
const wouldBeIgnored: LintOffense[] = []
|
|
250
|
+
|
|
251
|
+
if (this.nonExcludableRules.includes(ruleName)) {
|
|
252
|
+
return { kept: ruleOffenses, ignored: [], wouldBeIgnored: [] }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (ignoreDisableComments) {
|
|
256
|
+
for (const offense of ruleOffenses) {
|
|
257
|
+
const line = offense.location.start.line
|
|
258
|
+
const disabledRules = herbDisableCache?.get(line) || []
|
|
259
|
+
|
|
260
|
+
if (disabledRules.includes(ruleName) || disabledRules.includes("all")) {
|
|
261
|
+
wouldBeIgnored.push(offense)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { kept: ruleOffenses, ignored: [], wouldBeIgnored }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (const offense of ruleOffenses) {
|
|
269
|
+
const line = offense.location.start.line
|
|
270
|
+
const disabledRules = herbDisableCache?.get(line) || []
|
|
271
|
+
|
|
272
|
+
if (disabledRules.includes(ruleName) || disabledRules.includes("all")) {
|
|
273
|
+
ignored.push(offense)
|
|
274
|
+
|
|
275
|
+
if (ignoredOffensesByLine) {
|
|
276
|
+
if (!ignoredOffensesByLine.has(line)) {
|
|
277
|
+
ignoredOffensesByLine.set(line, new Set())
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const usedRuleName = disabledRules.includes(ruleName) ? ruleName : "all"
|
|
281
|
+
ignoredOffensesByLine.get(line)!.add(usedRuleName)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
kept.push(offense)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { kept, ignored, wouldBeIgnored: [] }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
48
294
|
/**
|
|
49
295
|
* Lint source code using Parser/AST, Lexer, and Source rules.
|
|
50
296
|
* @param source - The source code to lint
|
|
@@ -53,58 +299,277 @@ export class Linter {
|
|
|
53
299
|
lint(source: string, context?: Partial<LintContext>): LintResult {
|
|
54
300
|
this.offenses = []
|
|
55
301
|
|
|
302
|
+
let ignoredCount = 0
|
|
303
|
+
let wouldBeIgnoredCount = 0
|
|
304
|
+
|
|
56
305
|
const parseResult = this.herb.parse(source, { track_whitespace: true })
|
|
57
306
|
const lexResult = this.herb.lex(source)
|
|
307
|
+
const hasParserErrors = parseResult.recursiveErrors().length > 0
|
|
308
|
+
const sourceLines = source.split("\n")
|
|
309
|
+
const ignoredOffensesByLine = new Map<number, Set<string>>()
|
|
310
|
+
const herbDisableCache = new Map<number, string[]>()
|
|
311
|
+
|
|
312
|
+
if (hasParserErrors) {
|
|
313
|
+
const hasParserRule = this.rules.find(RuleClass => (new RuleClass()).name === "parser-no-errors")
|
|
314
|
+
|
|
315
|
+
if (hasParserRule) {
|
|
316
|
+
const rule = new ParserNoErrorsRule()
|
|
317
|
+
const offenses = rule.check(parseResult)
|
|
318
|
+
this.offenses.push(...offenses)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
offenses: this.offenses,
|
|
323
|
+
errors: this.offenses.filter(o => o.severity === "error").length,
|
|
324
|
+
warnings: this.offenses.filter(o => o.severity === "warning").length,
|
|
325
|
+
info: this.offenses.filter(o => o.severity === "info").length,
|
|
326
|
+
hints: this.offenses.filter(o => o.severity === "hint").length,
|
|
327
|
+
ignored: 0
|
|
328
|
+
}
|
|
329
|
+
}
|
|
58
330
|
|
|
59
|
-
for (
|
|
331
|
+
for (let i = 0; i < sourceLines.length; i++) {
|
|
332
|
+
const line = sourceLines[i]
|
|
333
|
+
|
|
334
|
+
if (line.includes("herb:disable")) {
|
|
335
|
+
const herbDisable = parseHerbDisableLine(line)
|
|
336
|
+
herbDisableCache.set(i + 1, herbDisable?.ruleNames || [])
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
context = {
|
|
341
|
+
...context,
|
|
342
|
+
validRuleNames: this.getAvailableRules().map(RuleClass => new RuleClass().name),
|
|
343
|
+
ignoredOffensesByLine
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const regularRules = this.rules.filter(RuleClass => {
|
|
60
347
|
const rule = new RuleClass()
|
|
61
348
|
|
|
62
|
-
|
|
63
|
-
|
|
349
|
+
return rule.name !== "herb-disable-comment-unnecessary"
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
for (const RuleClass of regularRules) {
|
|
353
|
+
const rule = new RuleClass()
|
|
354
|
+
const unboundOffenses = this.executeRule(rule, parseResult, lexResult, source, context)
|
|
355
|
+
const boundOffenses = this.bindSeverity(unboundOffenses, rule.name)
|
|
356
|
+
|
|
357
|
+
const { kept, ignored, wouldBeIgnored } = this.filterOffenses(
|
|
358
|
+
boundOffenses,
|
|
359
|
+
rule.name,
|
|
360
|
+
ignoredOffensesByLine,
|
|
361
|
+
herbDisableCache,
|
|
362
|
+
context?.ignoreDisableComments
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
ignoredCount += ignored.length
|
|
366
|
+
wouldBeIgnoredCount += wouldBeIgnored.length
|
|
367
|
+
this.offenses.push(...kept)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const unnecessaryRuleClass = this.rules.find(RuleClass => {
|
|
371
|
+
const rule = new RuleClass()
|
|
372
|
+
|
|
373
|
+
return rule.name === "herb-disable-comment-unnecessary"
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
if (unnecessaryRuleClass) {
|
|
377
|
+
const unnecessaryRule = new unnecessaryRuleClass() as ParserRule
|
|
378
|
+
const unboundOffenses = unnecessaryRule.check(parseResult, context)
|
|
379
|
+
const boundOffenses = this.bindSeverity(unboundOffenses, unnecessaryRule.name)
|
|
380
|
+
|
|
381
|
+
this.offenses.push(...boundOffenses)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const finalOffenses = this.offenses
|
|
385
|
+
|
|
386
|
+
const errors = finalOffenses.filter(offense => offense.severity === "error").length
|
|
387
|
+
const warnings = finalOffenses.filter(offense => offense.severity === "warning").length
|
|
388
|
+
const info = finalOffenses.filter(offense => offense.severity === "info").length
|
|
389
|
+
const hints = finalOffenses.filter(offense => offense.severity === "hint").length
|
|
390
|
+
|
|
391
|
+
const result: LintResult = {
|
|
392
|
+
offenses: finalOffenses,
|
|
393
|
+
errors,
|
|
394
|
+
warnings,
|
|
395
|
+
info,
|
|
396
|
+
hints,
|
|
397
|
+
ignored: ignoredCount
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (wouldBeIgnoredCount > 0) {
|
|
401
|
+
result.wouldBeIgnored = wouldBeIgnoredCount
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return result
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Bind severity to unbound offenses based on rule's defaultConfig and user config overrides.
|
|
409
|
+
*
|
|
410
|
+
* Priority:
|
|
411
|
+
* 1. User config severity override (if specified in config)
|
|
412
|
+
* 2. Rule's default severity (from defaultConfig.severity)
|
|
413
|
+
*
|
|
414
|
+
* @param unboundOffenses - Array of offenses without severity
|
|
415
|
+
* @param ruleName - Name of the rule that produced the offenses
|
|
416
|
+
* @returns Array of offenses with severity bound
|
|
417
|
+
*/
|
|
418
|
+
protected bindSeverity(unboundOffenses: UnboundLintOffense[], ruleName: string): LintOffense[] {
|
|
419
|
+
const RuleClass = this.rules.find(rule => {
|
|
420
|
+
const instance = new rule()
|
|
421
|
+
return instance.name === ruleName
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
if (!RuleClass) {
|
|
425
|
+
return unboundOffenses.map(offense => ({
|
|
426
|
+
...offense,
|
|
427
|
+
severity: "error" as const
|
|
428
|
+
}))
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const ruleInstance = new RuleClass()
|
|
432
|
+
const defaultSeverity = ruleInstance.defaultConfig?.severity ?? DEFAULT_RULE_CONFIG.severity
|
|
64
433
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
434
|
+
const userRuleConfig = this.config?.linter?.rules?.[ruleName]
|
|
435
|
+
const severity = userRuleConfig?.severity ?? defaultSeverity
|
|
436
|
+
|
|
437
|
+
return unboundOffenses.map(offense => ({
|
|
438
|
+
...offense,
|
|
439
|
+
severity
|
|
440
|
+
}))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Automatically fix offenses in the source code.
|
|
445
|
+
* Uses AST mutation for parser rules and token mutation for lexer rules.
|
|
446
|
+
* @param source - The source code to fix
|
|
447
|
+
* @param context - Optional context for linting (e.g., fileName)
|
|
448
|
+
* @param offensesToFix - Optional array of specific offenses to fix. If not provided, all fixable offenses will be fixed.
|
|
449
|
+
* @returns AutofixResult containing the corrected source and lists of fixed/unfixed offenses
|
|
450
|
+
*/
|
|
451
|
+
autofix(source: string, context?: Partial<LintContext>, offensesToFix?: LintOffense[]): AutofixResult {
|
|
452
|
+
const lintResult = offensesToFix ? { offenses: offensesToFix } : this.lint(source, context)
|
|
453
|
+
|
|
454
|
+
const parserOffenses: LintOffense[] = []
|
|
455
|
+
const lexerOffenses: LintOffense[] = []
|
|
456
|
+
const sourceOffenses: LintOffense[] = []
|
|
457
|
+
|
|
458
|
+
for (const offense of lintResult.offenses) {
|
|
459
|
+
const RuleClass = this.rules.find(rule => {
|
|
460
|
+
const instance = new rule()
|
|
461
|
+
|
|
462
|
+
return instance.name === offense.rule
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
if (!RuleClass) continue
|
|
466
|
+
|
|
467
|
+
if ((RuleClass as any).type === "lexer") {
|
|
468
|
+
lexerOffenses.push(offense)
|
|
469
|
+
} else if ((RuleClass as any).type === "source") {
|
|
470
|
+
sourceOffenses.push(offense)
|
|
471
|
+
} else {
|
|
472
|
+
parserOffenses.push(offense)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let currentSource = source
|
|
477
|
+
const fixed: LintOffense[] = []
|
|
478
|
+
const unfixed: LintOffense[] = []
|
|
479
|
+
|
|
480
|
+
if (parserOffenses.length > 0) {
|
|
481
|
+
const parseResult = this.herb.parse(currentSource, { track_whitespace: true })
|
|
482
|
+
|
|
483
|
+
for (const offense of parserOffenses) {
|
|
484
|
+
const RuleClass = this.rules.find(rule => new rule().name === offense.rule)
|
|
485
|
+
|
|
486
|
+
if (!RuleClass) {
|
|
487
|
+
unfixed.push(offense)
|
|
488
|
+
|
|
489
|
+
continue
|
|
68
490
|
}
|
|
69
491
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
492
|
+
const rule = new RuleClass() as ParserRule
|
|
493
|
+
|
|
494
|
+
if (!rule.autofix) {
|
|
495
|
+
unfixed.push(offense)
|
|
496
|
+
|
|
497
|
+
continue
|
|
74
498
|
}
|
|
75
499
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
500
|
+
if (offense.autofixContext) {
|
|
501
|
+
const originalNodeType = offense.autofixContext.node.type
|
|
502
|
+
const location: Location = offense.autofixContext.node.location ? Location.from(offense.autofixContext.node.location) : offense.location
|
|
503
|
+
|
|
504
|
+
const freshNode = findNodeByLocation(
|
|
505
|
+
parseResult.value,
|
|
506
|
+
location,
|
|
507
|
+
(node) => node.type === originalNodeType
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if (freshNode) {
|
|
511
|
+
offense.autofixContext.node = freshNode
|
|
512
|
+
} else {
|
|
513
|
+
unfixed.push(offense)
|
|
514
|
+
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
79
517
|
}
|
|
80
518
|
|
|
81
|
-
|
|
82
|
-
|
|
519
|
+
const fixedResult = rule.autofix(offense, parseResult, context)
|
|
520
|
+
|
|
521
|
+
if (fixedResult) {
|
|
522
|
+
fixed.push(offense)
|
|
83
523
|
} else {
|
|
84
|
-
|
|
524
|
+
unfixed.push(offense)
|
|
85
525
|
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (fixed.length > 0) {
|
|
529
|
+
const printer = new IdentityPrinter()
|
|
530
|
+
currentSource = printer.print(parseResult.value)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (sourceOffenses.length > 0) {
|
|
535
|
+
const sortedSourceOffenses = sourceOffenses.sort((a, b) => {
|
|
536
|
+
if (a.location.start.line !== b.location.start.line) {
|
|
537
|
+
return b.location.start.line - a.location.start.line
|
|
89
538
|
}
|
|
90
539
|
|
|
91
|
-
|
|
92
|
-
|
|
540
|
+
return b.location.start.column - a.location.start.column
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
for (const offense of sortedSourceOffenses) {
|
|
544
|
+
const RuleClass = this.rules.find(rule => new rule().name === offense.rule)
|
|
545
|
+
|
|
546
|
+
if (!RuleClass) {
|
|
547
|
+
unfixed.push(offense)
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const rule = new RuleClass() as SourceRule
|
|
552
|
+
|
|
553
|
+
if (!rule.autofix) {
|
|
554
|
+
unfixed.push(offense)
|
|
555
|
+
continue
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const correctedSource = rule.autofix(offense, currentSource, context)
|
|
559
|
+
|
|
560
|
+
if (correctedSource) {
|
|
561
|
+
currentSource = correctedSource
|
|
562
|
+
fixed.push(offense)
|
|
93
563
|
} else {
|
|
94
|
-
|
|
564
|
+
unfixed.push(offense)
|
|
95
565
|
}
|
|
96
566
|
}
|
|
97
|
-
|
|
98
|
-
this.offenses.push(...ruleOffenses)
|
|
99
567
|
}
|
|
100
568
|
|
|
101
|
-
const errors = this.offenses.filter(offense => offense.severity === "error").length
|
|
102
|
-
const warnings = this.offenses.filter(offense => offense.severity === "warning").length
|
|
103
|
-
|
|
104
569
|
return {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
570
|
+
source: currentSource,
|
|
571
|
+
fixed,
|
|
572
|
+
unfixed
|
|
108
573
|
}
|
|
109
574
|
}
|
|
110
575
|
}
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export * from "./index.js"
|
|
2
|
+
|
|
3
|
+
export { CustomRuleLoader } from "./custom-rule-loader.js"
|
|
4
|
+
export type { CustomRuleLoaderOptions } from "./custom-rule-loader.js"
|
|
5
|
+
|
|
6
|
+
import { CustomRuleLoader } from "./custom-rule-loader.js"
|
|
7
|
+
import type { RuleClass } from "./types.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Loads custom rules from the filesystem.
|
|
11
|
+
* Only available in Node.js environments.
|
|
12
|
+
*/
|
|
13
|
+
export async function loadCustomRules(options?: {
|
|
14
|
+
baseDir?: string
|
|
15
|
+
patterns?: string[]
|
|
16
|
+
silent?: boolean
|
|
17
|
+
}): Promise<{
|
|
18
|
+
rules: RuleClass[]
|
|
19
|
+
ruleInfo: Array<{ name: string, path: string }>
|
|
20
|
+
warnings: string[]
|
|
21
|
+
}> {
|
|
22
|
+
const loader = new CustomRuleLoader(options)
|
|
23
|
+
const { rules: customRules, ruleInfo, duplicateWarnings } = await loader.loadRulesWithInfo()
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
rules: customRules,
|
|
27
|
+
ruleInfo,
|
|
28
|
+
warnings: duplicateWarnings
|
|
29
|
+
}
|
|
30
|
+
}
|