@herb-tools/linter 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +16 -2
  2. package/dist/herb-lint.js +426 -116
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +322 -61
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +317 -63
  7. package/dist/index.js.map +1 -1
  8. package/dist/package.json +4 -4
  9. package/dist/src/cli/file-processor.js +2 -4
  10. package/dist/src/cli/file-processor.js.map +1 -1
  11. package/dist/src/default-rules.js +6 -0
  12. package/dist/src/default-rules.js.map +1 -1
  13. package/dist/src/linter.js +37 -6
  14. package/dist/src/linter.js.map +1 -1
  15. package/dist/src/rules/erb-no-empty-tags.js +4 -3
  16. package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
  17. package/dist/src/rules/erb-no-output-control-flow.js +4 -3
  18. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
  19. package/dist/src/rules/erb-prefer-image-tag-helper.js +93 -0
  20. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -0
  21. package/dist/src/rules/erb-require-whitespace-inside-tags.js +4 -3
  22. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  23. package/dist/src/rules/erb-requires-trailing-newline.js +22 -0
  24. package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -0
  25. package/dist/src/rules/html-anchor-require-href.js +4 -3
  26. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  27. package/dist/src/rules/html-aria-attribute-must-be-valid.js +4 -3
  28. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  29. package/dist/src/rules/html-aria-level-must-be-valid.js +27 -0
  30. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -0
  31. package/dist/src/rules/html-aria-role-heading-requires-level.js +4 -3
  32. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  33. package/dist/src/rules/html-aria-role-must-be-valid.js +4 -3
  34. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  35. package/dist/src/rules/html-attribute-double-quotes.js +4 -3
  36. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  37. package/dist/src/rules/html-attribute-values-require-quotes.js +4 -3
  38. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  39. package/dist/src/rules/html-boolean-attributes-no-value.js +4 -3
  40. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  41. package/dist/src/rules/html-img-require-alt.js +4 -3
  42. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  43. package/dist/src/rules/html-no-block-inside-inline.js +4 -3
  44. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  45. package/dist/src/rules/html-no-duplicate-attributes.js +4 -3
  46. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  47. package/dist/src/rules/html-no-duplicate-ids.js +4 -3
  48. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  49. package/dist/src/rules/html-no-empty-headings.js +4 -3
  50. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  51. package/dist/src/rules/html-no-nested-links.js +4 -3
  52. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  53. package/dist/src/rules/html-tag-name-lowercase.js +4 -3
  54. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  55. package/dist/src/rules/index.js +3 -0
  56. package/dist/src/rules/index.js.map +1 -1
  57. package/dist/src/rules/rule-utils.js +125 -2
  58. package/dist/src/rules/rule-utils.js.map +1 -1
  59. package/dist/src/rules/svg-tag-name-capitalization.js +4 -3
  60. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  61. package/dist/src/types.js +15 -1
  62. package/dist/src/types.js.map +1 -1
  63. package/dist/tsconfig.tsbuildinfo +1 -1
  64. package/dist/types/linter.d.ts +20 -5
  65. package/dist/types/rules/erb-no-empty-tags.d.ts +4 -3
  66. package/dist/types/rules/erb-no-output-control-flow.d.ts +4 -3
  67. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +7 -0
  68. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +4 -3
  69. package/dist/types/rules/erb-requires-trailing-newline.d.ts +6 -0
  70. package/dist/types/rules/html-anchor-require-href.d.ts +4 -3
  71. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +4 -3
  72. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +7 -0
  73. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +4 -3
  74. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +4 -3
  75. package/dist/types/rules/html-attribute-double-quotes.d.ts +4 -3
  76. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +4 -3
  77. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +4 -3
  78. package/dist/types/rules/html-img-require-alt.d.ts +4 -3
  79. package/dist/types/rules/html-no-block-inside-inline.d.ts +4 -3
  80. package/dist/types/rules/html-no-duplicate-attributes.d.ts +4 -3
  81. package/dist/types/rules/html-no-duplicate-ids.d.ts +4 -3
  82. package/dist/types/rules/html-no-empty-headings.d.ts +4 -3
  83. package/dist/types/rules/html-no-nested-links.d.ts +4 -3
  84. package/dist/types/rules/html-tag-name-lowercase.d.ts +4 -3
  85. package/dist/types/rules/index.d.ts +3 -0
  86. package/dist/types/rules/rule-utils.d.ts +73 -4
  87. package/dist/types/rules/svg-tag-name-capitalization.d.ts +4 -3
  88. package/dist/types/src/linter.d.ts +20 -5
  89. package/dist/types/src/rules/erb-no-empty-tags.d.ts +4 -3
  90. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +4 -3
  91. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +7 -0
  92. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +4 -3
  93. package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +6 -0
  94. package/dist/types/src/rules/html-anchor-require-href.d.ts +4 -3
  95. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +4 -3
  96. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +7 -0
  97. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +4 -3
  98. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +4 -3
  99. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +4 -3
  100. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +4 -3
  101. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +4 -3
  102. package/dist/types/src/rules/html-img-require-alt.d.ts +4 -3
  103. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +4 -3
  104. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +4 -3
  105. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +4 -3
  106. package/dist/types/src/rules/html-no-empty-headings.d.ts +4 -3
  107. package/dist/types/src/rules/html-no-nested-links.d.ts +4 -3
  108. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +4 -3
  109. package/dist/types/src/rules/index.d.ts +3 -0
  110. package/dist/types/src/rules/rule-utils.d.ts +73 -4
  111. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +4 -3
  112. package/dist/types/src/types.d.ts +49 -6
  113. package/dist/types/types.d.ts +49 -6
  114. package/docs/rules/README.md +4 -1
  115. package/docs/rules/erb-prefer-image-tag-helper.md +65 -0
  116. package/docs/rules/erb-requires-trailing-newline.md +37 -0
  117. package/docs/rules/html-anchor-require-href.md +1 -1
  118. package/docs/rules/html-aria-level-must-be-valid.md +37 -0
  119. package/package.json +4 -4
  120. package/src/cli/file-processor.ts +2 -4
  121. package/src/default-rules.ts +6 -0
  122. package/src/linter.ts +42 -8
  123. package/src/rules/erb-no-empty-tags.ts +5 -4
  124. package/src/rules/erb-no-output-control-flow.ts +6 -4
  125. package/src/rules/erb-prefer-image-tag-helper.ts +124 -0
  126. package/src/rules/erb-require-whitespace-inside-tags.ts +5 -4
  127. package/src/rules/erb-requires-trailing-newline.ts +27 -0
  128. package/src/rules/html-anchor-require-href.ts +5 -4
  129. package/src/rules/html-aria-attribute-must-be-valid.ts +5 -5
  130. package/src/rules/html-aria-level-must-be-valid.ts +42 -0
  131. package/src/rules/html-aria-role-heading-requires-level.ts +5 -4
  132. package/src/rules/html-aria-role-must-be-valid.ts +5 -4
  133. package/src/rules/html-attribute-double-quotes.ts +5 -4
  134. package/src/rules/html-attribute-values-require-quotes.ts +5 -4
  135. package/src/rules/html-boolean-attributes-no-value.ts +5 -4
  136. package/src/rules/html-img-require-alt.ts +5 -4
  137. package/src/rules/html-no-block-inside-inline.ts +5 -4
  138. package/src/rules/html-no-duplicate-attributes.ts +5 -4
  139. package/src/rules/html-no-duplicate-ids.ts +5 -5
  140. package/src/rules/html-no-empty-headings.ts +5 -4
  141. package/src/rules/html-no-nested-links.ts +5 -4
  142. package/src/rules/html-tag-name-lowercase.ts +5 -4
  143. package/src/rules/index.ts +3 -0
  144. package/src/rules/rule-utils.ts +156 -4
  145. package/src/rules/svg-tag-name-capitalization.ts +5 -4
  146. package/src/types.ts +60 -6
@@ -1,6 +1,7 @@
1
- import type { LintOffense, Rule } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLAriaAttributeMustBeValid implements Rule {
4
+ export declare class HTMLAriaAttributeMustBeValid extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -0,0 +1,7 @@
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
3
+ import type { Node } from "@herb-tools/core";
4
+ export declare class HTMLAriaLevelMustBeValidRule extends ParserRule {
5
+ name: string;
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
7
+ }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLAriaRoleHeadingRequiresLevelRule implements Rule {
4
+ export declare class HTMLAriaRoleHeadingRequiresLevelRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLAriaRoleMustBeValidRule implements Rule {
4
+ export declare class HTMLAriaRoleMustBeValidRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLAttributeDoubleQuotesRule implements Rule {
4
+ export declare class HTMLAttributeDoubleQuotesRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLAttributeValuesRequireQuotesRule implements Rule {
4
+ export declare class HTMLAttributeValuesRequireQuotesRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLBooleanAttributesNoValueRule implements Rule {
4
+ export declare class HTMLBooleanAttributesNoValueRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLImgRequireAltRule implements Rule {
4
+ export declare class HTMLImgRequireAltRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLNoBlockInsideInlineRule implements Rule {
4
+ export declare class HTMLNoBlockInsideInlineRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLNoDuplicateAttributesRule implements Rule {
4
+ export declare class HTMLNoDuplicateAttributesRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
+ import { ParserRule } from "../types";
1
2
  import type { Node } from "@herb-tools/core";
2
- import type { LintOffense, Rule } from "../types";
3
- export declare class HTMLNoDuplicateIdsRule implements Rule {
3
+ import type { LintOffense, LintContext } from "../types";
4
+ export declare class HTMLNoDuplicateIdsRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLNoEmptyHeadingsRule implements Rule {
4
+ export declare class HTMLNoEmptyHeadingsRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLNoNestedLinksRule implements Rule {
4
+ export declare class HTMLNoNestedLinksRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class HTMLTagNameLowercaseRule implements Rule {
4
+ export declare class HTMLTagNameLowercaseRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,6 +1,9 @@
1
1
  export * from "./erb-no-empty-tags.js";
2
2
  export * from "./erb-no-output-control-flow.js";
3
+ export * from "./erb-prefer-image-tag-helper.js";
4
+ export * from "./erb-requires-trailing-newline.js";
3
5
  export * from "./html-anchor-require-href.js";
6
+ export * from "./html-aria-level-must-be-valid.js";
4
7
  export * from "./html-aria-role-heading-requires-level.js";
5
8
  export * from "./html-aria-role-must-be-valid.js";
6
9
  export * from "./html-attribute-double-quotes.js";
@@ -1,13 +1,14 @@
1
- import { Visitor } from "@herb-tools/core";
2
- import type { HTMLAttributeNode, HTMLOpenTagNode, HTMLSelfCloseTagNode, Location } from "@herb-tools/core";
3
- import type { LintOffense, LintSeverity } from "../types.js";
1
+ import { Visitor, Location } from "@herb-tools/core";
2
+ import type { HTMLAttributeNode, HTMLOpenTagNode, HTMLSelfCloseTagNode, LexResult, Token } from "@herb-tools/core";
3
+ import type { LintOffense, LintSeverity, LintContext } from "../types.js";
4
4
  /**
5
5
  * Base visitor class that provides common functionality for rule visitors
6
6
  */
7
7
  export declare abstract class BaseRuleVisitor extends Visitor {
8
8
  readonly offenses: LintOffense[];
9
9
  protected ruleName: string;
10
- constructor(ruleName: string);
10
+ protected context: LintContext;
11
+ constructor(ruleName: string, context?: Partial<LintContext>);
11
12
  /**
12
13
  * Helper method to create a lint offense
13
14
  */
@@ -67,6 +68,10 @@ export declare const SVG_CAMEL_CASE_ELEMENTS: Set<string>;
67
68
  export declare const SVG_LOWERCASE_TO_CAMELCASE: Map<string, string>;
68
69
  export declare const VALID_ARIA_ROLES: Set<string>;
69
70
  export declare const ARIA_ATTRIBUTES: Set<string>;
71
+ /**
72
+ * Helper function to create a location at the end of the source with a 1-character range
73
+ */
74
+ export declare function createEndOfFileLocation(source: string): Location;
70
75
  /**
71
76
  * Checks if an element is inline
72
77
  */
@@ -85,6 +90,7 @@ export declare function isBooleanAttribute(attributeName: string): boolean;
85
90
  * and attribute iteration logic. Provides simplified interface with extracted attribute info.
86
91
  */
87
92
  export declare abstract class AttributeVisitorMixin extends BaseRuleVisitor {
93
+ constructor(ruleName: string, context?: Partial<LintContext>);
88
94
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void;
89
95
  visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void;
90
96
  private checkAttributesOnNode;
@@ -98,3 +104,66 @@ export declare function isAttributeValueQuoted(attributeNode: HTMLAttributeNode)
98
104
  * Iterates over all attributes of a tag node, calling the callback for each attribute
99
105
  */
100
106
  export declare function forEachAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, callback: (attributeNode: HTMLAttributeNode) => void): void;
107
+ /**
108
+ * Base lexer visitor class that provides common functionality for lexer-based rule visitors
109
+ */
110
+ export declare abstract class BaseLexerRuleVisitor {
111
+ readonly offenses: LintOffense[];
112
+ protected ruleName: string;
113
+ protected context: LintContext;
114
+ constructor(ruleName: string, context?: Partial<LintContext>);
115
+ /**
116
+ * Helper method to create a lint offense for lexer rules
117
+ */
118
+ protected createOffense(message: string, location: Location, severity?: LintSeverity): LintOffense;
119
+ /**
120
+ * Helper method to add an offense to the offenses array
121
+ */
122
+ protected addOffense(message: string, location: Location, severity?: LintSeverity): void;
123
+ /**
124
+ * Main entry point for lexer rule visitors
125
+ * @param lexResult - The lexer result containing tokens and source
126
+ */
127
+ visit(lexResult: LexResult): void;
128
+ /**
129
+ * Visit all tokens
130
+ * Override this method to implement token-level checks
131
+ */
132
+ protected visitTokens(tokens: Token[]): void;
133
+ /**
134
+ * Visit individual tokens
135
+ * Override this method to implement per-token checks
136
+ */
137
+ protected visitToken(_token: Token): void;
138
+ }
139
+ /**
140
+ * Base source visitor class that provides common functionality for source-based rule visitors
141
+ */
142
+ export declare abstract class BaseSourceRuleVisitor {
143
+ readonly offenses: LintOffense[];
144
+ protected ruleName: string;
145
+ protected context: LintContext;
146
+ constructor(ruleName: string, context?: Partial<LintContext>);
147
+ /**
148
+ * Helper method to create a lint offense for source rules
149
+ */
150
+ protected createOffense(message: string, location: Location, severity?: LintSeverity): LintOffense;
151
+ /**
152
+ * Helper method to add an offense to the offenses array
153
+ */
154
+ protected addOffense(message: string, location: Location, severity?: LintSeverity): void;
155
+ /**
156
+ * Main entry point for source rule visitors
157
+ * @param source - The raw source code
158
+ */
159
+ visit(source: string): void;
160
+ /**
161
+ * Visit the source code directly
162
+ * Override this method to implement source-level checks
163
+ */
164
+ protected abstract visitSource(source: string): void;
165
+ /**
166
+ * Helper method to create a location for a specific position in the source
167
+ */
168
+ protected createLocationAt(source: string, position: number): Location;
169
+ }
@@ -1,6 +1,7 @@
1
- import type { Rule, LintOffense } from "../types.js";
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
2
3
  import type { Node } from "@herb-tools/core";
3
- export declare class SVGTagNameCapitalizationRule implements Rule {
4
+ export declare class SVGTagNameCapitalizationRule extends ParserRule {
4
5
  name: string;
5
- check(node: Node): LintOffense[];
6
+ check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
7
  }
@@ -1,4 +1,4 @@
1
- import { Node, Diagnostic } from "@herb-tools/core";
1
+ import { Node, Diagnostic, LexResult } from "@herb-tools/core";
2
2
  import type { defaultRules } from "./default-rules.js";
3
3
  export type LintSeverity = "error" | "warning";
4
4
  /**
@@ -15,12 +15,55 @@ export interface LintResult {
15
15
  errors: number;
16
16
  warnings: number;
17
17
  }
18
- export interface Rule {
19
- name: string;
20
- check(node: Node): LintOffense[];
18
+ export declare abstract class ParserRule {
19
+ static type: "parser";
20
+ abstract name: string;
21
+ abstract check(node: Node, context?: Partial<LintContext>): LintOffense[];
22
+ }
23
+ export declare abstract class LexerRule {
24
+ static type: "lexer";
25
+ abstract name: string;
26
+ abstract check(lexResult: LexResult, context?: Partial<LintContext>): LintOffense[];
27
+ }
28
+ export interface LexerRuleConstructor {
29
+ type: "lexer";
30
+ new (): LexerRule;
21
31
  }
22
32
  /**
23
- * Type representing a rule class constructor.
33
+ * Complete lint context with all properties defined.
34
+ * Use Partial<LintContext> when passing context to rules.
35
+ */
36
+ export interface LintContext {
37
+ fileName: string | undefined;
38
+ }
39
+ /**
40
+ * Default context object with all keys defined but set to undefined
41
+ */
42
+ export declare const DEFAULT_LINT_CONTEXT: LintContext;
43
+ export declare abstract class SourceRule {
44
+ static type: "source";
45
+ abstract name: string;
46
+ abstract check(source: string, context?: Partial<LintContext>): LintOffense[];
47
+ }
48
+ export interface SourceRuleConstructor {
49
+ type: "source";
50
+ new (): SourceRule;
51
+ }
52
+ /**
53
+ * Type representing a parser/AST rule class constructor.
24
54
  * The Linter accepts rule classes rather than instances for better performance and memory usage.
55
+ * Parser rules are the default and don't require static properties.
56
+ */
57
+ export type ParserRuleClass = (new () => ParserRule) & {
58
+ type?: "parser";
59
+ };
60
+ export type LexerRuleClass = LexerRuleConstructor;
61
+ export type SourceRuleClass = SourceRuleConstructor;
62
+ /**
63
+ * Union type for any rule instance (Parser/AST, Lexer, or Source)
64
+ */
65
+ export type Rule = ParserRule | LexerRule | SourceRule;
66
+ /**
67
+ * Union type for any rule class (Parser/AST, Lexer, or Source)
25
68
  */
26
- export type RuleClass = new () => Rule;
69
+ export type RuleClass = ParserRuleClass | LexerRuleClass | SourceRuleClass;
@@ -1,4 +1,4 @@
1
- import { Node, Diagnostic } from "@herb-tools/core";
1
+ import { Node, Diagnostic, LexResult } from "@herb-tools/core";
2
2
  import type { defaultRules } from "./default-rules.js";
3
3
  export type LintSeverity = "error" | "warning";
4
4
  /**
@@ -15,12 +15,55 @@ export interface LintResult {
15
15
  errors: number;
16
16
  warnings: number;
17
17
  }
18
- export interface Rule {
19
- name: string;
20
- check(node: Node): LintOffense[];
18
+ export declare abstract class ParserRule {
19
+ static type: "parser";
20
+ abstract name: string;
21
+ abstract check(node: Node, context?: Partial<LintContext>): LintOffense[];
22
+ }
23
+ export declare abstract class LexerRule {
24
+ static type: "lexer";
25
+ abstract name: string;
26
+ abstract check(lexResult: LexResult, context?: Partial<LintContext>): LintOffense[];
27
+ }
28
+ export interface LexerRuleConstructor {
29
+ type: "lexer";
30
+ new (): LexerRule;
21
31
  }
22
32
  /**
23
- * Type representing a rule class constructor.
33
+ * Complete lint context with all properties defined.
34
+ * Use Partial<LintContext> when passing context to rules.
35
+ */
36
+ export interface LintContext {
37
+ fileName: string | undefined;
38
+ }
39
+ /**
40
+ * Default context object with all keys defined but set to undefined
41
+ */
42
+ export declare const DEFAULT_LINT_CONTEXT: LintContext;
43
+ export declare abstract class SourceRule {
44
+ static type: "source";
45
+ abstract name: string;
46
+ abstract check(source: string, context?: Partial<LintContext>): LintOffense[];
47
+ }
48
+ export interface SourceRuleConstructor {
49
+ type: "source";
50
+ new (): SourceRule;
51
+ }
52
+ /**
53
+ * Type representing a parser/AST rule class constructor.
24
54
  * The Linter accepts rule classes rather than instances for better performance and memory usage.
55
+ * Parser rules are the default and don't require static properties.
56
+ */
57
+ export type ParserRuleClass = (new () => ParserRule) & {
58
+ type?: "parser";
59
+ };
60
+ export type LexerRuleClass = LexerRuleConstructor;
61
+ export type SourceRuleClass = SourceRuleConstructor;
62
+ /**
63
+ * Union type for any rule instance (Parser/AST, Lexer, or Source)
64
+ */
65
+ export type Rule = ParserRule | LexerRule | SourceRule;
66
+ /**
67
+ * Union type for any rule class (Parser/AST, Lexer, or Source)
25
68
  */
26
- export type RuleClass = new () => Rule;
69
+ export type RuleClass = ParserRuleClass | LexerRuleClass | SourceRuleClass;
@@ -6,9 +6,12 @@ This page contains documentation for all Herb Linter rules.
6
6
 
7
7
  - [`erb-no-empty-tags`](./erb-no-empty-tags.md) - Disallow empty ERB tags
8
8
  - [`erb-no-output-control-flow`](./erb-no-output-control-flow.md) - Prevents outputting control flow blocks
9
+ - [`erb-prefer-image-tag-helper`](./erb-prefer-image-tag-helper.md) - Prefer `image_tag` helper over `<img>` with ERB expressions
9
10
  - [`erb-require-whitespace-inside-tags`](./erb-require-whitespace-inside-tags.md) - Requires whitespace around erb tags
11
+ - [`erb-requires-trailing-newline`](./erb-requires-trailing-newline.md) - Enforces that all HTML+ERB template files end with exactly one trailing newline character.
10
12
  - [`html-anchor-require-href`](./html-anchor-require-href.md) - Requires an href attribute on anchor tags
11
13
  - [`html-aria-attribute-must-be-valid`](./html-aria-attribute-must-be-valid.md) - Disallow invalid or unknown `aria-*` attributes.
14
+ - [`html-aria-level-must-be-valid`](./html-aria-level-must-be-valid.md) - `aria-level` must be between 1 and 6
12
15
  - [`html-aria-role-heading-requires-level`](./html-aria-role-heading-requires-level.md) - Requires `aria-level` when supplying a `role`
13
16
  - [`html-aria-role-must-be-valid`](./html-aria-role-must-be-valid.md) - The `role` attribute must have a valid WAI-ARIA Role.
14
17
  - [`html-attribute-double-quotes`](./html-attribute-double-quotes.md) - Enforces double quotes for attribute values
@@ -16,8 +19,8 @@ This page contains documentation for all Herb Linter rules.
16
19
  - [`html-boolean-attributes-no-value`](./html-boolean-attributes-no-value.md) - Prevents values on boolean attributes
17
20
  - [`html-img-require-alt`](./html-img-require-alt.md) - Requires alt attributes on img tags
18
21
  - [`html-no-block-inside-inline`](./html-no-block-inside-inline.md) - Prevents block-level elements inside inline elements
19
- - [`html-no-duplicate-ids`](./html-no-duplicate-ids.md) - Prevents duplicate IDs within a document
20
22
  - [`html-no-duplicate-attributes`](./html-no-duplicate-attributes.md) - Prevents duplicate attributes on HTML elements
23
+ - [`html-no-duplicate-ids`](./html-no-duplicate-ids.md) - Prevents duplicate IDs within a document
21
24
  - [`html-no-nested-links`](./html-no-nested-links.md) - Prevents nested anchor tags
22
25
  - [`html-tag-name-lowercase`](./html-tag-name-lowercase.md) - Enforces lowercase tag names in HTML
23
26
  - [`svg-tag-name-capitalization`](./svg-tag-name-capitalization.md) - Enforces proper camelCase capitalization for SVG elements
@@ -0,0 +1,65 @@
1
+ # Linter Rule: Prefer `image_tag` helper over `<img>` with ERB expressions
2
+
3
+ **Rule:** `erb-prefer-image-tag-helper`
4
+
5
+ ## Description
6
+
7
+ Prefer using Rails' `image_tag` helper over manual `<img>` tags with dynamic ERB expressions like `image_path` or `asset_path`.
8
+
9
+ ## Rationale
10
+
11
+ The `image_tag` helper provides several advantages over manual `<img>` tags with dynamic ERB expressions. It properly escapes the `src` value to prevent XSS vulnerabilities and ensures consistent rendering across different contexts. Using `image_tag` also reduces template complexity by eliminating the need for manual string interpolation and makes it easier to add additional attributes like `alt`, `class`, or `data-*` attributes in a clean, readable way. Additionally, it prevents common interpolation issues that can arise when mixing ERB expressions with static text in attribute values.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```erb
18
+ <!-- Simple image_tag usage -->
19
+ <%= image_tag "logo.png", alt: "Logo" %>
20
+ <%= image_tag "banner.jpg", alt: "Banner", class: "hero-image" %>
21
+ <%= image_tag "icon.svg", alt: "Icon", size: "24x24" %>
22
+
23
+ <!-- Dynamic expressions -->
24
+ <%= image_tag user.avatar.url, alt: "User avatar" %>
25
+
26
+ <!-- Mixed content using string interpolation -->
27
+ <%= image_tag "#{root_url}/banner.jpg", alt: "Banner" %>
28
+ <%= image_tag "#{base_url}#{image_path('icon.png')}", alt: "Icon" %>
29
+
30
+ <!-- Static image paths are fine -->
31
+ <img src="/static/logo.png" alt="Logo">
32
+ ```
33
+
34
+ ### 🚫 Bad
35
+
36
+ ```erb
37
+ <!-- Single ERB expressions -->
38
+ <img src="<%= image_path("logo.png") %>" alt="Logo">
39
+
40
+ <img src="<%= asset_path("banner.jpg") %>" alt="Banner">
41
+
42
+ <img src="<%= user.avatar.url %>" alt="User avatar">
43
+
44
+ <img src="<%= product.image %>" alt="Product image">
45
+
46
+
47
+ <!-- Mixed ERB and text content -->
48
+ <img src="<%= Rails.application.routes.url_helpers.root_url %>/icon.png" alt="Logo">
49
+
50
+ <img src="<%= root_url %>/banner.jpg" alt="Banner">
51
+
52
+ <img src="<%= admin_path %>/icon.png" alt="Admin icon">
53
+
54
+
55
+ <!-- Multiple ERB expressions -->
56
+ <img src="<%= base_url %><%= image_path("logo.png") %>" alt="Logo">
57
+
58
+ <img src="<%= root_path %><%= "icon.png" %>" alt="Icon">
59
+ ```
60
+
61
+ ## References
62
+
63
+ * [Rails `image_tag` helper documentation](https://api.rubyonrails.org/classes/ActionView/Helpers/AssetTagHelper.html#method-i-image_tag)
64
+ * [Rails `image_path` helper documentation](https://api.rubyonrails.org/classes/ActionView/Helpers/AssetUrlHelper.html#method-i-image_path)
65
+ * [Rails `asset_path` helper documentation](https://api.rubyonrails.org/classes/ActionView/Helpers/AssetUrlHelper.html#method-i-asset_path)
@@ -0,0 +1,37 @@
1
+ # Linter Rule: Enforce trailing newline
2
+
3
+ **Rule:** `erb-requires-trailing-newline`
4
+
5
+ ## Description
6
+
7
+ This rule enforces that all HTML+ERB template files end with exactly one trailing newline character. This is a formatting convention widely adopted across many languages and tools.
8
+
9
+ ## Rationale
10
+
11
+ Ensuring HTML+ERB files end with a single trailing newline aligns with POSIX conventions, where text files should end with a newline character.
12
+
13
+ This practice avoids unnecessary diffs from editors or formatters that auto-insert final newlines, improving compatibility with command-line tools and version control systems. It also helps maintain a clean, predictable structure across view files.
14
+
15
+ Trailing newlines are a common convention in Ruby and are enforced by tools like RuboCop and many Git-based workflows.
16
+
17
+ ## Examples
18
+
19
+ ### ✅ Good
20
+
21
+ ```
22
+ <%= render partial: "header" %>
23
+ <%= render partial: "footer" %>
24
+ ```
25
+
26
+ ### 🚫 Bad
27
+
28
+ ```erb
29
+ <%= render partial: "header" %>
30
+ <%= render partial: "footer" %>▌
31
+ ```
32
+
33
+ ## References
34
+
35
+ - [POSIX: Text files and trailing newlines](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_206)
36
+ - [Git: Trailing newlines and diffs](https://git-scm.com/docs/git-diff#_generating_patches_with_p)
37
+ - [EditorConfig: `insert_final_newline`](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#insert_final_newline)
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Description
6
6
 
7
- Disallow the use of anchor tags without anhref attribute in HTML templates. Use if you want to perform an action without having the user navigated to a new URL.
7
+ Disallow the use of anchor tags without an `href` attribute in HTML templates. Use if you want to perform an action without having the user navigated to a new URL.
8
8
 
9
9
  ## Rationale
10
10
 
@@ -0,0 +1,37 @@
1
+ # Linter Rule: `aria-level` must be between 1 and 6
2
+
3
+ **Rule:** `html-aria-level-must-be-valid`
4
+
5
+ ## Description
6
+
7
+ Ensure that the value of the `aria-level` attribute is a valid heading level: an integer between `1` and `6`. This attribute is used with `role="heading"` to indicate a heading level for non-semantic elements like `<div>` or `<span>`.
8
+
9
+ ## Rationale
10
+
11
+ The WAI-ARIA specification defines `aria-level` as an integer between `1` (highest/most important) and `6` (lowest/subheading). Any other value is invalid and may confuse screen readers or fail accessibility audits.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```erb
18
+ <div role="heading" aria-level="1">Main</div>
19
+ <div role="heading" aria-level="6">Footnote</div>
20
+ ```
21
+
22
+ ### 🚫 Bad
23
+
24
+ ```erb
25
+ <div role="heading" aria-level="-1">Negative</div>
26
+
27
+ <div role="heading" aria-level="0">Main</div>
28
+
29
+ <div role="heading" aria-level="7">Too deep</div>
30
+
31
+ <div role="heading" aria-level="foo">Invalid</div>
32
+ ```
33
+
34
+ ## References
35
+
36
+ - [ARIA: `heading` role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/heading_role)
37
+ - [ARIA: `aria-level` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-level)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/linter",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "HTML+ERB linter for validating HTML structure and enforcing best practices",
5
5
  "license": "MIT",
6
6
  "homepage": "https://herb-tools.dev",
@@ -33,9 +33,9 @@
33
33
  }
34
34
  },
35
35
  "dependencies": {
36
- "@herb-tools/core": "0.4.2",
37
- "@herb-tools/highlighter": "0.4.2",
38
- "@herb-tools/node-wasm": "0.4.2",
36
+ "@herb-tools/core": "0.4.3",
37
+ "@herb-tools/highlighter": "0.4.3",
38
+ "@herb-tools/node-wasm": "0.4.3",
39
39
  "glob": "^11.0.3"
40
40
  },
41
41
  "files": [
@@ -50,12 +50,11 @@ export class FileProcessor {
50
50
  }
51
51
 
52
52
  if (!this.linter) {
53
- this.linter = new Linter()
53
+ this.linter = new Linter(Herb)
54
54
  }
55
55
 
56
- const lintResult = this.linter.lint(parseResult.value)
56
+ const lintResult = this.linter.lint(content, { fileName: filename })
57
57
 
58
- // Get rule count on first file
59
58
  if (ruleCount === 0) {
60
59
  ruleCount = this.linter.getRuleCount()
61
60
  }
@@ -65,7 +64,6 @@ export class FileProcessor {
65
64
  console.log(`${colorize("✓", "brightGreen")} ${colorize(filename, "cyan")} - ${colorize("No issues found", "green")}`)
66
65
  }
67
66
  } else {
68
- // Collect messages for later display
69
67
  for (const offense of lintResult.offenses) {
70
68
  allDiagnostics.push({ filename, diagnostic: offense, content })
71
69