@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
|
@@ -4,12 +4,18 @@ export interface SummaryData {
|
|
|
4
4
|
files: string[]
|
|
5
5
|
totalErrors: number
|
|
6
6
|
totalWarnings: number
|
|
7
|
+
totalInfo?: number
|
|
8
|
+
totalHints?: number
|
|
9
|
+
totalIgnored: number
|
|
10
|
+
totalWouldBeIgnored?: number
|
|
7
11
|
filesWithOffenses: number
|
|
8
12
|
ruleCount: number
|
|
9
13
|
startTime: number
|
|
10
14
|
startDate: Date
|
|
11
15
|
showTiming: boolean
|
|
12
16
|
ruleOffenses: Map<string, { count: number, files: Set<string> }>
|
|
17
|
+
autofixableCount: number
|
|
18
|
+
ignoreDisableComments?: boolean
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export class SummaryReporter {
|
|
@@ -18,7 +24,7 @@ export class SummaryReporter {
|
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
displaySummary(data: SummaryData): void {
|
|
21
|
-
const { files, totalErrors, totalWarnings, filesWithOffenses, ruleCount, startTime, startDate, showTiming } = data
|
|
27
|
+
const { files, totalErrors, totalWarnings, totalInfo = 0, totalHints = 0, totalIgnored, totalWouldBeIgnored, filesWithOffenses, ruleCount, startTime, startDate, showTiming, autofixableCount, ignoreDisableComments } = data
|
|
22
28
|
|
|
23
29
|
console.log("\n")
|
|
24
30
|
console.log(` ${colorize("Summary:", "bold")}`)
|
|
@@ -62,6 +68,18 @@ export class SummaryReporter {
|
|
|
62
68
|
parts.push(colorize(colorize(`${totalWarnings} ${this.pluralize(totalWarnings, "warning")}`, "green"), "bold"))
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
if (totalInfo > 0) {
|
|
72
|
+
parts.push(colorize(colorize(`${totalInfo} info`, "brightBlue"), "bold"))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (totalHints > 0) {
|
|
76
|
+
parts.push(colorize(colorize(`${totalHints} ${this.pluralize(totalHints, "hint")}`, "gray"), "bold"))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (totalIgnored > 0) {
|
|
80
|
+
parts.push(colorize(colorize(`${totalIgnored} ignored`, "gray"), "bold"))
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
if (parts.length === 0) {
|
|
66
84
|
offensesSummary = colorize(colorize("0 offenses", "green"), "bold")
|
|
67
85
|
} else {
|
|
@@ -69,17 +87,36 @@ export class SummaryReporter {
|
|
|
69
87
|
|
|
70
88
|
let detailText = ""
|
|
71
89
|
|
|
72
|
-
const totalOffenses = totalErrors + totalWarnings
|
|
90
|
+
const totalOffenses = totalErrors + totalWarnings + totalInfo + totalHints
|
|
73
91
|
|
|
74
92
|
if (filesWithOffenses > 0) {
|
|
75
93
|
detailText = `${totalOffenses} ${this.pluralize(totalOffenses, "offense")} across ${filesWithOffenses} ${this.pluralize(filesWithOffenses, "file")}`
|
|
76
94
|
}
|
|
77
95
|
|
|
78
|
-
|
|
96
|
+
if (detailText) {
|
|
97
|
+
offensesSummary += ` ${colorize(colorize(`(${detailText})`, "gray"), "dim")}`
|
|
98
|
+
}
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
console.log(` ${colorize(pad("Offenses"), "gray")} ${offensesSummary}`)
|
|
82
102
|
|
|
103
|
+
if (ignoreDisableComments && totalWouldBeIgnored && totalWouldBeIgnored > 0) {
|
|
104
|
+
const message = `${colorize(colorize(`${totalWouldBeIgnored} additional ${this.pluralize(totalWouldBeIgnored, "offense")} reported (would have been ignored)`, "cyan"), "bold")}`
|
|
105
|
+
console.log(` ${colorize(pad("Note"), "gray")} ${message}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const totalOffenses = totalErrors + totalWarnings + totalInfo + totalHints
|
|
109
|
+
|
|
110
|
+
if (autofixableCount > 0 || totalOffenses > 0) {
|
|
111
|
+
let fixableLine = `${colorize(colorize(`${totalOffenses} ${this.pluralize(totalOffenses, "offense")}`, "brightRed"), "bold")}`
|
|
112
|
+
|
|
113
|
+
if (autofixableCount > 0) {
|
|
114
|
+
fixableLine += ` | ${colorize(colorize(`${autofixableCount} autocorrectable using \`--fix\``, "green"), "bold")}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(` ${colorize(pad("Fixable"), "gray")} ${fixableLine}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
83
120
|
if (showTiming) {
|
|
84
121
|
const duration = Date.now() - startTime
|
|
85
122
|
const timeString = startDate.toTimeString().split(' ')[0]
|
package/src/cli.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { glob } from "glob"
|
|
2
2
|
import { Herb } from "@herb-tools/node-wasm"
|
|
3
|
+
import { Config, addHerbExtensionRecommendation, getExtensionsJsonRelativePath } from "@herb-tools/config"
|
|
4
|
+
|
|
3
5
|
import { existsSync, statSync } from "fs"
|
|
4
6
|
import { dirname, resolve, relative } from "path"
|
|
5
7
|
|
|
6
|
-
import { ArgumentParser
|
|
8
|
+
import { ArgumentParser } from "./cli/argument-parser.js"
|
|
7
9
|
import { FileProcessor } from "./cli/file-processor.js"
|
|
8
10
|
import { OutputManager } from "./cli/output-manager.js"
|
|
11
|
+
import { version } from "../package.json"
|
|
12
|
+
|
|
13
|
+
import type { ProcessingContext } from "./cli/file-processor.js"
|
|
14
|
+
import type { FormatOption } from "./cli/argument-parser.js"
|
|
9
15
|
|
|
10
16
|
export * from "./cli/index.js"
|
|
11
17
|
|
|
@@ -19,44 +25,6 @@ export class CLI {
|
|
|
19
25
|
return this.projectPath
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
protected findProjectRoot(startPath: string): string {
|
|
23
|
-
let currentPath = resolve(startPath)
|
|
24
|
-
|
|
25
|
-
if (existsSync(currentPath) && statSync(currentPath).isFile()) {
|
|
26
|
-
currentPath = dirname(currentPath)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const projectIndicators = [
|
|
30
|
-
'package.json',
|
|
31
|
-
'Gemfile',
|
|
32
|
-
'.git',
|
|
33
|
-
'tsconfig.json',
|
|
34
|
-
'composer.json',
|
|
35
|
-
'pyproject.toml',
|
|
36
|
-
'requirements.txt',
|
|
37
|
-
'.herb.yml'
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
while (currentPath !== '/') {
|
|
41
|
-
for (const indicator of projectIndicators) {
|
|
42
|
-
if (existsSync(resolve(currentPath, indicator))) {
|
|
43
|
-
return currentPath
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const parentPath = dirname(currentPath)
|
|
48
|
-
if (parentPath === currentPath) {
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
currentPath = parentPath
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return existsSync(startPath) && statSync(startPath).isDirectory()
|
|
56
|
-
? startPath
|
|
57
|
-
: dirname(startPath)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
28
|
protected exitWithError(message: string, formatOption: FormatOption, exitCode: number = 1) {
|
|
61
29
|
this.outputManager.outputError(message, {
|
|
62
30
|
formatOption,
|
|
@@ -87,7 +55,8 @@ export class CLI {
|
|
|
87
55
|
process.exit(exitCode)
|
|
88
56
|
}
|
|
89
57
|
|
|
90
|
-
protected determineProjectPath(
|
|
58
|
+
protected determineProjectPath(patterns: string[]): void {
|
|
59
|
+
const pattern = patterns[0]
|
|
91
60
|
if (pattern) {
|
|
92
61
|
const resolvedPattern = resolve(pattern)
|
|
93
62
|
|
|
@@ -97,15 +66,15 @@ export class CLI {
|
|
|
97
66
|
if (stats.isDirectory()) {
|
|
98
67
|
this.projectPath = resolvedPattern
|
|
99
68
|
} else {
|
|
100
|
-
this.projectPath =
|
|
69
|
+
this.projectPath = dirname(resolvedPattern)
|
|
101
70
|
}
|
|
102
71
|
}
|
|
103
72
|
}
|
|
104
73
|
}
|
|
105
74
|
|
|
106
|
-
protected adjustPattern(pattern: string | undefined): string {
|
|
75
|
+
protected adjustPattern(pattern: string | undefined, configGlobPattern: string): string {
|
|
107
76
|
if (!pattern) {
|
|
108
|
-
return
|
|
77
|
+
return configGlobPattern
|
|
109
78
|
}
|
|
110
79
|
|
|
111
80
|
const resolvedPattern = resolve(pattern)
|
|
@@ -114,7 +83,7 @@ export class CLI {
|
|
|
114
83
|
const stats = statSync(resolvedPattern)
|
|
115
84
|
|
|
116
85
|
if (stats.isDirectory()) {
|
|
117
|
-
return
|
|
86
|
+
return configGlobPattern
|
|
118
87
|
} else if (stats.isFile()) {
|
|
119
88
|
return relative(this.projectPath, resolvedPattern)
|
|
120
89
|
}
|
|
@@ -123,6 +92,41 @@ export class CLI {
|
|
|
123
92
|
return pattern
|
|
124
93
|
}
|
|
125
94
|
|
|
95
|
+
protected async resolvePatternToFiles(pattern: string, config: Config, force: boolean): Promise<{ files: string[], explicitFile: string | undefined }> {
|
|
96
|
+
const resolvedPattern = resolve(pattern)
|
|
97
|
+
const isExplicitFile = existsSync(resolvedPattern) && statSync(resolvedPattern).isFile()
|
|
98
|
+
let explicitFile: string | undefined
|
|
99
|
+
|
|
100
|
+
if (isExplicitFile) {
|
|
101
|
+
explicitFile = pattern
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const filesConfig = config.getFilesConfigForTool('linter')
|
|
105
|
+
const configGlobPattern = filesConfig.include && filesConfig.include.length > 0
|
|
106
|
+
? (filesConfig.include.length === 1 ? filesConfig.include[0] : `{${filesConfig.include.join(',')}}`)
|
|
107
|
+
: '**/*.html.erb'
|
|
108
|
+
const adjustedPattern = this.adjustPattern(pattern, configGlobPattern)
|
|
109
|
+
|
|
110
|
+
let files = await glob(adjustedPattern, {
|
|
111
|
+
cwd: this.projectPath,
|
|
112
|
+
ignore: filesConfig.exclude || []
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (explicitFile && files.length === 0) {
|
|
116
|
+
if (!force) {
|
|
117
|
+
console.error(`⚠️ File ${explicitFile} is excluded by configuration patterns.`)
|
|
118
|
+
console.error(` Use --force to lint it anyway.\n`)
|
|
119
|
+
process.exit(0)
|
|
120
|
+
} else {
|
|
121
|
+
console.log(`⚠️ Forcing linter on excluded file: ${explicitFile}`)
|
|
122
|
+
console.log()
|
|
123
|
+
files = [adjustedPattern]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { files, explicitFile }
|
|
128
|
+
}
|
|
129
|
+
|
|
126
130
|
protected async beforeProcess(): Promise<void> {
|
|
127
131
|
// Hook for subclasses to add custom output before processing
|
|
128
132
|
}
|
|
@@ -137,11 +141,36 @@ export class CLI {
|
|
|
137
141
|
const startTime = Date.now()
|
|
138
142
|
const startDate = new Date()
|
|
139
143
|
|
|
140
|
-
let {
|
|
144
|
+
let { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, ignoreDisableComments, force, init, loadCustomRules } = this.argumentParser.parse(process.argv)
|
|
141
145
|
|
|
142
|
-
this.determineProjectPath(
|
|
146
|
+
this.determineProjectPath(patterns)
|
|
143
147
|
|
|
144
|
-
|
|
148
|
+
if (init) {
|
|
149
|
+
const configPath = configFile || this.projectPath
|
|
150
|
+
|
|
151
|
+
if (Config.exists(configPath)) {
|
|
152
|
+
const fullPath = configFile || Config.configPathFromProjectPath(this.projectPath)
|
|
153
|
+
console.error(`\n✗ Configuration file already exists at ${fullPath}`)
|
|
154
|
+
console.error(` Use --config-file to specify a different location.\n`)
|
|
155
|
+
process.exit(1)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const config = await Config.loadForCLI(configPath, version, true)
|
|
159
|
+
const extensionAdded = addHerbExtensionRecommendation(this.projectPath)
|
|
160
|
+
|
|
161
|
+
console.log(`\n✓ Configuration initialized at ${config.path}`)
|
|
162
|
+
|
|
163
|
+
if (extensionAdded) {
|
|
164
|
+
console.log(`✓ VSCode extension recommended in ${getExtensionsJsonRelativePath()}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(` Edit this file to customize linter and formatter settings.\n`)
|
|
168
|
+
process.exit(0)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const silent = formatOption === 'json'
|
|
172
|
+
const config = await Config.load(configFile || this.projectPath, { version, exitOnError: true, createIfMissing: false, silent })
|
|
173
|
+
const linterConfig = config.options.linter || {}
|
|
145
174
|
|
|
146
175
|
const outputOptions = {
|
|
147
176
|
formatOption,
|
|
@@ -157,15 +186,69 @@ export class CLI {
|
|
|
157
186
|
try {
|
|
158
187
|
await this.beforeProcess()
|
|
159
188
|
|
|
160
|
-
|
|
189
|
+
if (linterConfig.enabled === false && !force) {
|
|
190
|
+
this.exitWithInfo("Linter is disabled in .herb.yml configuration. Use --force to lint anyway.", formatOption, 0, { startTime, startDate, showTiming })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (force && linterConfig.enabled === false) {
|
|
194
|
+
console.log("⚠️ Forcing linter run (disabled in .herb.yml)")
|
|
195
|
+
console.log()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let files: string[]
|
|
199
|
+
let explicitFiles: string[] = []
|
|
200
|
+
|
|
201
|
+
if (patterns.length === 0) {
|
|
202
|
+
files = await config.findFilesForTool('linter', this.projectPath)
|
|
203
|
+
} else {
|
|
204
|
+
const allFiles: string[] = []
|
|
205
|
+
|
|
206
|
+
for (const pattern of patterns) {
|
|
207
|
+
const { files: patternFiles, explicitFile } = await this.resolvePatternToFiles(pattern, config, force)
|
|
208
|
+
|
|
209
|
+
if (patternFiles.length === 0) {
|
|
210
|
+
console.error(`✗ No files found matching pattern: ${pattern}`)
|
|
211
|
+
process.exit(1)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
allFiles.push(...patternFiles)
|
|
215
|
+
if (explicitFile) {
|
|
216
|
+
explicitFiles.push(explicitFile)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
files = [...new Set(allFiles)]
|
|
221
|
+
}
|
|
161
222
|
|
|
162
223
|
if (files.length === 0) {
|
|
163
|
-
this.exitWithInfo(`No files found matching
|
|
224
|
+
this.exitWithInfo(`No files found matching patterns: ${patterns.join(', ') || 'from config'}`, formatOption, 0, { startTime, startDate, showTiming })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let processingConfig = config
|
|
228
|
+
|
|
229
|
+
if (force && explicitFiles.length > 0) {
|
|
230
|
+
const modifiedConfig = Object.create(Object.getPrototypeOf(config))
|
|
231
|
+
Object.assign(modifiedConfig, config)
|
|
232
|
+
|
|
233
|
+
modifiedConfig.config = {
|
|
234
|
+
...config.config,
|
|
235
|
+
linter: {
|
|
236
|
+
...config.config.linter,
|
|
237
|
+
exclude: []
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
processingConfig = modifiedConfig
|
|
164
242
|
}
|
|
165
243
|
|
|
166
|
-
const context = {
|
|
244
|
+
const context: ProcessingContext = {
|
|
167
245
|
projectPath: this.projectPath,
|
|
168
|
-
pattern:
|
|
246
|
+
pattern: patterns.join(' '),
|
|
247
|
+
fix,
|
|
248
|
+
ignoreDisableComments,
|
|
249
|
+
linterConfig,
|
|
250
|
+
config: processingConfig,
|
|
251
|
+
loadCustomRules
|
|
169
252
|
}
|
|
170
253
|
|
|
171
254
|
const results = await this.fileProcessor.processFiles(files, formatOption, context)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { pathToFileURL } from "url"
|
|
2
|
+
import { glob } from "glob"
|
|
3
|
+
|
|
4
|
+
import type { RuleClass } from "./types.js"
|
|
5
|
+
|
|
6
|
+
export interface CustomRuleLoaderOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Base directory to search for custom rules
|
|
9
|
+
* Defaults to current working directory
|
|
10
|
+
*/
|
|
11
|
+
baseDir?: string
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Glob patterns to search for custom rule files
|
|
15
|
+
* Defaults to looking in common locations
|
|
16
|
+
*/
|
|
17
|
+
patterns?: string[]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether to suppress errors when loading custom rules
|
|
21
|
+
* Defaults to false
|
|
22
|
+
*/
|
|
23
|
+
silent?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_PATTERNS = [
|
|
27
|
+
".herb/rules/**/*.mjs",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Loads custom linter rules from the user's project
|
|
32
|
+
*/
|
|
33
|
+
export class CustomRuleLoader {
|
|
34
|
+
private baseDir: string
|
|
35
|
+
private patterns: string[]
|
|
36
|
+
private silent: boolean
|
|
37
|
+
|
|
38
|
+
constructor(options: CustomRuleLoaderOptions = {}) {
|
|
39
|
+
this.baseDir = options.baseDir || process.cwd()
|
|
40
|
+
this.patterns = options.patterns || DEFAULT_PATTERNS
|
|
41
|
+
this.silent = options.silent || false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Discovers custom rule files in the project
|
|
46
|
+
*/
|
|
47
|
+
async discoverRuleFiles(): Promise<string[]> {
|
|
48
|
+
const allFiles: string[] = []
|
|
49
|
+
|
|
50
|
+
for (const pattern of this.patterns) {
|
|
51
|
+
try {
|
|
52
|
+
const files = await glob(pattern, {
|
|
53
|
+
cwd: this.baseDir,
|
|
54
|
+
absolute: true,
|
|
55
|
+
nodir: true
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
allFiles.push(...files)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!this.silent) {
|
|
61
|
+
console.warn(`Warning: Failed to search pattern "${pattern}": ${error}`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [...new Set(allFiles)]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Loads a single rule file
|
|
71
|
+
*/
|
|
72
|
+
async loadRuleFile(filePath: string): Promise<RuleClass[]> {
|
|
73
|
+
try {
|
|
74
|
+
const fileUrl = pathToFileURL(filePath).href
|
|
75
|
+
const cacheBustedUrl = `${fileUrl}?t=${Date.now()}`
|
|
76
|
+
const module = await import(cacheBustedUrl)
|
|
77
|
+
|
|
78
|
+
if (module.default && this.isValidRuleClass(module.default)) {
|
|
79
|
+
return [module.default]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!this.silent) {
|
|
83
|
+
console.warn(`Warning: No valid default export found in "${filePath}". Custom rules must use default export.`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return []
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!this.silent) {
|
|
89
|
+
console.error(`Error loading rule file "${filePath}": ${error}`)
|
|
90
|
+
}
|
|
91
|
+
return []
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Type guard to check if an export is a valid rule class
|
|
97
|
+
*/
|
|
98
|
+
private isValidRuleClass(value: any): value is RuleClass {
|
|
99
|
+
if (typeof value !== 'function') return false
|
|
100
|
+
if (!value.prototype) return false
|
|
101
|
+
|
|
102
|
+
const instance = new value()
|
|
103
|
+
|
|
104
|
+
return typeof instance.check === 'function' && typeof instance.name === 'string'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Loads all custom rules from the project
|
|
109
|
+
*/
|
|
110
|
+
async loadRules(): Promise<RuleClass[]> {
|
|
111
|
+
const ruleFiles = await this.discoverRuleFiles()
|
|
112
|
+
|
|
113
|
+
if (ruleFiles.length === 0) {
|
|
114
|
+
return []
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const allRules: RuleClass[] = []
|
|
118
|
+
|
|
119
|
+
for (const filePath of ruleFiles) {
|
|
120
|
+
const rules = await this.loadRuleFile(filePath)
|
|
121
|
+
allRules.push(...rules)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return allRules
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Loads all custom rules and returns detailed information about each rule
|
|
129
|
+
*/
|
|
130
|
+
async loadRulesWithInfo(): Promise<{ rules: RuleClass[], ruleInfo: Array<{ name: string, path: string }>, duplicateWarnings: string[] }> {
|
|
131
|
+
const ruleFiles = await this.discoverRuleFiles()
|
|
132
|
+
|
|
133
|
+
if (ruleFiles.length === 0) {
|
|
134
|
+
return { rules: [], ruleInfo: [], duplicateWarnings: [] }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const allRules: RuleClass[] = []
|
|
138
|
+
const ruleInfo: Array<{ name: string, path: string }> = []
|
|
139
|
+
const duplicateWarnings: string[] = []
|
|
140
|
+
const seenNames = new Map<string, string>()
|
|
141
|
+
|
|
142
|
+
for (const filePath of ruleFiles) {
|
|
143
|
+
const rules = await this.loadRuleFile(filePath)
|
|
144
|
+
for (const RuleClass of rules) {
|
|
145
|
+
const instance = new RuleClass()
|
|
146
|
+
const ruleName = instance.name
|
|
147
|
+
|
|
148
|
+
if (seenNames.has(ruleName)) {
|
|
149
|
+
const firstPath = seenNames.get(ruleName)!
|
|
150
|
+
duplicateWarnings.push(
|
|
151
|
+
`Custom rule "${ruleName}" is defined in multiple files: "${firstPath}" and "${filePath}". The later one will be used.`
|
|
152
|
+
)
|
|
153
|
+
} else {
|
|
154
|
+
seenNames.set(ruleName, filePath)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
allRules.push(RuleClass)
|
|
158
|
+
ruleInfo.push({
|
|
159
|
+
name: ruleName,
|
|
160
|
+
path: filePath
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { rules: allRules, ruleInfo, duplicateWarnings }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Static helper to check if custom rules exist in a project
|
|
170
|
+
*/
|
|
171
|
+
static async hasCustomRules(baseDir: string = process.cwd()): Promise<boolean> {
|
|
172
|
+
const loader = new CustomRuleLoader({ baseDir, silent: true })
|
|
173
|
+
const files = await loader.discoverRuleFiles()
|
|
174
|
+
return files.length > 0
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Static helper to load custom rules and merge with default rules
|
|
179
|
+
*/
|
|
180
|
+
static async loadAndMergeRules(
|
|
181
|
+
defaultRules: RuleClass[],
|
|
182
|
+
options: CustomRuleLoaderOptions = {}
|
|
183
|
+
): Promise<RuleClass[]> {
|
|
184
|
+
const loader = new CustomRuleLoader(options)
|
|
185
|
+
const customRules = await loader.loadRules()
|
|
186
|
+
|
|
187
|
+
return [...defaultRules, ...customRules]
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for parsing herb:disable comments
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Information about a single rule name in a herb:disable comment
|
|
7
|
+
*/
|
|
8
|
+
export interface HerbDisableRuleName {
|
|
9
|
+
/** The rule name */
|
|
10
|
+
name: string
|
|
11
|
+
/** The starting offset of this rule name within the content/line */
|
|
12
|
+
offset: number
|
|
13
|
+
/** The length of the rule name */
|
|
14
|
+
length: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Result of parsing a herb:disable comment
|
|
19
|
+
*/
|
|
20
|
+
export interface HerbDisableComment {
|
|
21
|
+
/** The full matched string */
|
|
22
|
+
match: string
|
|
23
|
+
/** Array of rule names specified in the comment */
|
|
24
|
+
ruleNames: string[]
|
|
25
|
+
/** Array of rule name information with positions */
|
|
26
|
+
ruleNameDetails: HerbDisableRuleName[]
|
|
27
|
+
/** The original rules string (e.g., "rule1, rule2") */
|
|
28
|
+
rulesString: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prefix for herb:disable comments
|
|
33
|
+
*/
|
|
34
|
+
const HERB_DISABLE_PREFIX = "herb:disable"
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse a herb:disable comment from ERB comment content.
|
|
38
|
+
* Use this when you have the content inside <%# ... %> (e.g., from ERBContentNode.content.value)
|
|
39
|
+
*
|
|
40
|
+
* @param content - The content string (without <%# %> delimiters)
|
|
41
|
+
* @returns Parsed comment data or null if not a valid herb:disable comment
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const result = parseHerbDisableContent("herb:disable rule1, rule2")
|
|
46
|
+
* // { match: "herb:disable rule1, rule2", ruleNames: ["rule1", "rule2"], rulesString: "rule1, rule2" }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function parseHerbDisableContent(content: string): HerbDisableComment | null {
|
|
50
|
+
const trimmed = content.trim()
|
|
51
|
+
|
|
52
|
+
if (!trimmed.startsWith(HERB_DISABLE_PREFIX)) return null
|
|
53
|
+
|
|
54
|
+
const afterPrefix = trimmed.substring(HERB_DISABLE_PREFIX.length).trimStart()
|
|
55
|
+
if (afterPrefix.length === 0) return null
|
|
56
|
+
|
|
57
|
+
const rulesString = afterPrefix.trimEnd()
|
|
58
|
+
const ruleNames = rulesString.split(',').map(name => name.trim())
|
|
59
|
+
|
|
60
|
+
if (ruleNames.some(name => name.length === 0)) return null
|
|
61
|
+
if (ruleNames.length === 0) return null
|
|
62
|
+
|
|
63
|
+
const herbDisablePrefix = content.indexOf(HERB_DISABLE_PREFIX)
|
|
64
|
+
const searchStart = herbDisablePrefix + HERB_DISABLE_PREFIX.length
|
|
65
|
+
const rulesStringOffset = content.indexOf(rulesString, searchStart)
|
|
66
|
+
|
|
67
|
+
const ruleNameDetails: HerbDisableRuleName[] = []
|
|
68
|
+
|
|
69
|
+
let currentOffset = 0
|
|
70
|
+
|
|
71
|
+
for (const ruleName of ruleNames) {
|
|
72
|
+
const ruleOffset = rulesString.indexOf(ruleName, currentOffset)
|
|
73
|
+
|
|
74
|
+
ruleNameDetails.push({
|
|
75
|
+
name: ruleName,
|
|
76
|
+
offset: rulesStringOffset + ruleOffset,
|
|
77
|
+
length: ruleName.length
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
currentOffset = ruleOffset + ruleName.length
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
match: trimmed,
|
|
85
|
+
ruleNames,
|
|
86
|
+
ruleNameDetails,
|
|
87
|
+
rulesString
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse a herb:disable comment from a full source line.
|
|
93
|
+
* Use this when you have a complete line that may contain <%# herb:disable ... %>
|
|
94
|
+
*
|
|
95
|
+
* @param line - The source line that may contain a herb:disable comment
|
|
96
|
+
* @returns Parsed comment data or null if not a valid herb:disable comment
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const result = parseHerbDisableLine("<div>test</div> <%# herb:disable rule1, rule2 %>")
|
|
101
|
+
* // { match: "<%# herb:disable rule1, rule2 %>", ruleNames: ["rule1", "rule2"], rulesString: "rule1, rule2" }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function parseHerbDisableLine(line: string): HerbDisableComment | null {
|
|
105
|
+
const startTag = "<%#"
|
|
106
|
+
const endTag = "%>"
|
|
107
|
+
|
|
108
|
+
const startIndex = line.indexOf(startTag)
|
|
109
|
+
if (startIndex === -1) return null
|
|
110
|
+
|
|
111
|
+
const endIndex = line.indexOf(endTag, startIndex)
|
|
112
|
+
if (endIndex === -1) return null
|
|
113
|
+
|
|
114
|
+
const content = line.substring(startIndex + startTag.length, endIndex).trim()
|
|
115
|
+
|
|
116
|
+
if (!content.startsWith(HERB_DISABLE_PREFIX)) return null
|
|
117
|
+
|
|
118
|
+
const afterPrefix = content.substring(HERB_DISABLE_PREFIX.length).trimStart()
|
|
119
|
+
if (afterPrefix.length === 0) return null
|
|
120
|
+
|
|
121
|
+
const rulesString = afterPrefix.trimEnd()
|
|
122
|
+
const ruleNames = rulesString.split(',').map(name => name.trim())
|
|
123
|
+
|
|
124
|
+
if (ruleNames.some(name => name.length === 0)) return null
|
|
125
|
+
if (ruleNames.length === 0) return null
|
|
126
|
+
|
|
127
|
+
const herbDisablePrefix = line.indexOf(HERB_DISABLE_PREFIX)
|
|
128
|
+
const searchStart = herbDisablePrefix + HERB_DISABLE_PREFIX.length
|
|
129
|
+
const rulesStringOffset = line.indexOf(rulesString, searchStart)
|
|
130
|
+
|
|
131
|
+
const ruleNameDetails: HerbDisableRuleName[] = []
|
|
132
|
+
|
|
133
|
+
let currentOffset = 0
|
|
134
|
+
|
|
135
|
+
for (const ruleName of ruleNames) {
|
|
136
|
+
const ruleOffset = rulesString.indexOf(ruleName, currentOffset)
|
|
137
|
+
|
|
138
|
+
ruleNameDetails.push({
|
|
139
|
+
name: ruleName,
|
|
140
|
+
offset: rulesStringOffset + ruleOffset,
|
|
141
|
+
length: ruleName.length
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
currentOffset = ruleOffset + ruleName.length
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const fullMatch = line.substring(startIndex, endIndex + endTag.length)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
match: fullMatch,
|
|
151
|
+
ruleNames,
|
|
152
|
+
ruleNameDetails,
|
|
153
|
+
rulesString
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if an ERB comment content contains a herb:disable directive.
|
|
159
|
+
*
|
|
160
|
+
* @param content - The content string (without <%# %> delimiters)
|
|
161
|
+
* @returns true if the content contains a herb:disable directive
|
|
162
|
+
*/
|
|
163
|
+
export function isHerbDisableContent(content: string): boolean {
|
|
164
|
+
return parseHerbDisableContent(content) !== null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if a source line contains a herb:disable comment.
|
|
169
|
+
*
|
|
170
|
+
* @param line - The source line
|
|
171
|
+
* @returns true if the line contains a herb:disable comment
|
|
172
|
+
*/
|
|
173
|
+
export function isHerbDisableLine(line: string): boolean {
|
|
174
|
+
return parseHerbDisableLine(line) !== null
|
|
175
|
+
}
|