@herb-tools/linter 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +34 -0
  2. package/bin/herb-lint +3 -0
  3. package/dist/herb-lint.js +16505 -0
  4. package/dist/herb-lint.js.map +1 -0
  5. package/dist/index.cjs +834 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.js +820 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/package.json +49 -0
  10. package/dist/src/cli/argument-parser.js +96 -0
  11. package/dist/src/cli/argument-parser.js.map +1 -0
  12. package/dist/src/cli/file-processor.js +58 -0
  13. package/dist/src/cli/file-processor.js.map +1 -0
  14. package/dist/src/cli/formatters/base-formatter.js +3 -0
  15. package/dist/src/cli/formatters/base-formatter.js.map +1 -0
  16. package/dist/src/cli/formatters/detailed-formatter.js +62 -0
  17. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -0
  18. package/dist/src/cli/formatters/index.js +4 -0
  19. package/dist/src/cli/formatters/index.js.map +1 -0
  20. package/dist/src/cli/formatters/simple-formatter.js +31 -0
  21. package/dist/src/cli/formatters/simple-formatter.js.map +1 -0
  22. package/dist/src/cli/index.js +5 -0
  23. package/dist/src/cli/index.js.map +1 -0
  24. package/dist/src/cli/summary-reporter.js +96 -0
  25. package/dist/src/cli/summary-reporter.js.map +1 -0
  26. package/dist/src/cli.js +50 -0
  27. package/dist/src/cli.js.map +1 -0
  28. package/dist/src/default-rules.js +31 -0
  29. package/dist/src/default-rules.js.map +1 -0
  30. package/dist/src/herb-lint.js +5 -0
  31. package/dist/src/herb-lint.js.map +1 -0
  32. package/dist/src/index.js +4 -0
  33. package/dist/src/index.js.map +1 -0
  34. package/dist/src/linter.js +39 -0
  35. package/dist/src/linter.js.map +1 -0
  36. package/dist/src/rules/erb-no-empty-tags.js +23 -0
  37. package/dist/src/rules/erb-no-empty-tags.js.map +1 -0
  38. package/dist/src/rules/erb-no-output-control-flow.js +47 -0
  39. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -0
  40. package/dist/src/rules/erb-require-whitespace-inside-tags.js +43 -0
  41. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -0
  42. package/dist/src/rules/html-anchor-require-href.js +25 -0
  43. package/dist/src/rules/html-anchor-require-href.js.map +1 -0
  44. package/dist/src/rules/html-aria-role-heading-requires-level.js +26 -0
  45. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -0
  46. package/dist/src/rules/html-attribute-double-quotes.js +21 -0
  47. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -0
  48. package/dist/src/rules/html-attribute-values-require-quotes.js +22 -0
  49. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -0
  50. package/dist/src/rules/html-boolean-attributes-no-value.js +19 -0
  51. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -0
  52. package/dist/src/rules/html-img-require-alt.js +29 -0
  53. package/dist/src/rules/html-img-require-alt.js.map +1 -0
  54. package/dist/src/rules/html-no-block-inside-inline.js +59 -0
  55. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -0
  56. package/dist/src/rules/html-no-duplicate-attributes.js +43 -0
  57. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -0
  58. package/dist/src/rules/html-no-empty-headings.js +148 -0
  59. package/dist/src/rules/html-no-empty-headings.js.map +1 -0
  60. package/dist/src/rules/html-no-nested-links.js +45 -0
  61. package/dist/src/rules/html-no-nested-links.js.map +1 -0
  62. package/dist/src/rules/html-tag-name-lowercase.js +39 -0
  63. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -0
  64. package/dist/src/rules/index.js +13 -0
  65. package/dist/src/rules/index.js.map +1 -0
  66. package/dist/src/rules/rule-utils.js +198 -0
  67. package/dist/src/rules/rule-utils.js.map +1 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/tsconfig.tsbuildinfo +1 -0
  71. package/dist/types/cli/argument-parser.d.ts +14 -0
  72. package/dist/types/cli/file-processor.d.ts +21 -0
  73. package/dist/types/cli/formatters/base-formatter.d.ts +6 -0
  74. package/dist/types/cli/formatters/detailed-formatter.d.ts +13 -0
  75. package/dist/types/cli/formatters/index.d.ts +3 -0
  76. package/dist/types/cli/formatters/simple-formatter.d.ts +7 -0
  77. package/dist/types/cli/summary-reporter.d.ts +22 -0
  78. package/dist/types/cli.d.ts +6 -0
  79. package/dist/types/default-rules.d.ts +2 -0
  80. package/dist/types/herb-lint.d.ts +2 -0
  81. package/dist/types/index.d.ts +3 -0
  82. package/dist/types/linter.d.ts +18 -0
  83. package/dist/types/rules/erb-no-empty-tags.d.ts +6 -0
  84. package/dist/types/rules/erb-no-output-control-flow.d.ts +6 -0
  85. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +6 -0
  86. package/dist/types/rules/html-anchor-require-href.d.ts +6 -0
  87. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +6 -0
  88. package/dist/types/rules/html-attribute-double-quotes.d.ts +6 -0
  89. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +6 -0
  90. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +6 -0
  91. package/dist/types/rules/html-img-require-alt.d.ts +6 -0
  92. package/dist/types/rules/html-no-block-inside-inline.d.ts +6 -0
  93. package/dist/types/rules/html-no-duplicate-attributes.d.ts +6 -0
  94. package/dist/types/rules/html-no-empty-headings.d.ts +6 -0
  95. package/dist/types/rules/html-no-nested-links.d.ts +6 -0
  96. package/dist/types/rules/html-tag-name-lowercase.d.ts +6 -0
  97. package/dist/types/rules/index.d.ts +12 -0
  98. package/dist/types/rules/rule-utils.d.ts +89 -0
  99. package/dist/types/src/cli/argument-parser.d.ts +14 -0
  100. package/dist/types/src/cli/file-processor.d.ts +21 -0
  101. package/dist/types/src/cli/formatters/base-formatter.d.ts +6 -0
  102. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +13 -0
  103. package/dist/types/src/cli/formatters/index.d.ts +3 -0
  104. package/dist/types/src/cli/formatters/simple-formatter.d.ts +7 -0
  105. package/dist/types/src/cli/index.d.ts +4 -0
  106. package/dist/types/src/cli/summary-reporter.d.ts +22 -0
  107. package/dist/types/src/cli.d.ts +6 -0
  108. package/dist/types/src/default-rules.d.ts +2 -0
  109. package/dist/types/src/herb-lint.d.ts +2 -0
  110. package/dist/types/src/index.d.ts +3 -0
  111. package/dist/types/src/linter.d.ts +18 -0
  112. package/dist/types/src/rules/erb-no-empty-tags.d.ts +6 -0
  113. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +6 -0
  114. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +6 -0
  115. package/dist/types/src/rules/html-anchor-require-href.d.ts +6 -0
  116. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +6 -0
  117. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +6 -0
  118. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +6 -0
  119. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +6 -0
  120. package/dist/types/src/rules/html-img-require-alt.d.ts +6 -0
  121. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +6 -0
  122. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +6 -0
  123. package/dist/types/src/rules/html-no-empty-headings.d.ts +6 -0
  124. package/dist/types/src/rules/html-no-nested-links.d.ts +6 -0
  125. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +6 -0
  126. package/dist/types/src/rules/index.d.ts +12 -0
  127. package/dist/types/src/rules/rule-utils.d.ts +89 -0
  128. package/dist/types/src/types.d.ts +26 -0
  129. package/dist/types/types.d.ts +26 -0
  130. package/docs/rules/README.md +39 -0
  131. package/docs/rules/erb-no-empty-tags.md +38 -0
  132. package/docs/rules/erb-no-output-control-flow.md +45 -0
  133. package/docs/rules/erb-require-whitespace-inside-tags.md +43 -0
  134. package/docs/rules/html-anchor-require-href.md +32 -0
  135. package/docs/rules/html-aria-role-heading-requires-level.md +34 -0
  136. package/docs/rules/html-attribute-double-quotes.md +43 -0
  137. package/docs/rules/html-attribute-values-require-quotes.md +43 -0
  138. package/docs/rules/html-boolean-attributes-no-value.md +39 -0
  139. package/docs/rules/html-img-require-alt.md +44 -0
  140. package/docs/rules/html-no-block-inside-inline.md +66 -0
  141. package/docs/rules/html-no-duplicate-attributes.md +35 -0
  142. package/docs/rules/html-no-empty-headings.md +78 -0
  143. package/docs/rules/html-no-nested-links.md +44 -0
  144. package/docs/rules/html-tag-name-lowercase.md +44 -0
  145. package/package.json +49 -0
  146. package/src/cli/argument-parser.ts +125 -0
  147. package/src/cli/file-processor.ts +86 -0
  148. package/src/cli/formatters/base-formatter.ts +11 -0
  149. package/src/cli/formatters/detailed-formatter.ts +74 -0
  150. package/src/cli/formatters/index.ts +3 -0
  151. package/src/cli/formatters/simple-formatter.ts +40 -0
  152. package/src/cli/index.ts +4 -0
  153. package/src/cli/summary-reporter.ts +127 -0
  154. package/src/cli.ts +60 -0
  155. package/src/default-rules.ts +33 -0
  156. package/src/herb-lint.ts +6 -0
  157. package/src/index.ts +3 -0
  158. package/src/linter.ts +50 -0
  159. package/src/rules/erb-no-empty-tags.ts +34 -0
  160. package/src/rules/erb-no-output-control-flow.ts +61 -0
  161. package/src/rules/erb-require-whitespace-inside-tags.ts +61 -0
  162. package/src/rules/html-anchor-require-href.ts +39 -0
  163. package/src/rules/html-aria-role-heading-requires-level.ts +44 -0
  164. package/src/rules/html-attribute-double-quotes.ts +28 -0
  165. package/src/rules/html-attribute-values-require-quotes.ts +30 -0
  166. package/src/rules/html-boolean-attributes-no-value.ts +27 -0
  167. package/src/rules/html-img-require-alt.ts +42 -0
  168. package/src/rules/html-no-block-inside-inline.ts +84 -0
  169. package/src/rules/html-no-duplicate-attributes.ts +59 -0
  170. package/src/rules/html-no-empty-headings.ts +185 -0
  171. package/src/rules/html-no-nested-links.ts +65 -0
  172. package/src/rules/html-tag-name-lowercase.ts +50 -0
  173. package/src/rules/index.ts +12 -0
  174. package/src/rules/rule-utils.ts +257 -0
  175. package/src/types.ts +32 -0
@@ -0,0 +1,89 @@
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";
4
+ /**
5
+ * Base visitor class that provides common functionality for rule visitors
6
+ */
7
+ export declare abstract class BaseRuleVisitor extends Visitor {
8
+ readonly offenses: LintOffense[];
9
+ protected ruleName: string;
10
+ constructor(ruleName: string);
11
+ /**
12
+ * Helper method to create a lint offense
13
+ */
14
+ protected createOffense(message: string, location: Location, severity?: LintSeverity): LintOffense;
15
+ /**
16
+ * Helper method to add an offense to the offenses array
17
+ */
18
+ protected addOffense(message: string, location: Location, severity?: LintSeverity): void;
19
+ }
20
+ /**
21
+ * Gets attributes from either an HTMLOpenTagNode or HTMLSelfCloseTagNode
22
+ */
23
+ export declare function getAttributes(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): any[];
24
+ /**
25
+ * Gets the tag name from an HTML tag node (lowercased)
26
+ */
27
+ export declare function getTagName(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): string | null;
28
+ /**
29
+ * Gets the attribute name from an HTMLAttributeNode (lowercased)
30
+ */
31
+ export declare function getAttributeName(attributeNode: HTMLAttributeNode): string | null;
32
+ /**
33
+ * Gets the attribute value content from an HTMLAttributeValueNode
34
+ */
35
+ export declare function getAttributeValue(attributeNode: HTMLAttributeNode): string | null;
36
+ /**
37
+ * Checks if an attribute has a value
38
+ */
39
+ export declare function hasAttributeValue(attributeNode: HTMLAttributeNode): boolean;
40
+ /**
41
+ * Gets the quote type used for an attribute value
42
+ */
43
+ export declare function getAttributeValueQuoteType(attributeNode: HTMLAttributeNode): "single" | "double" | "none" | null;
44
+ /**
45
+ * Finds an attribute by name in a list of attributes
46
+ */
47
+ export declare function findAttributeByName(attributes: any[], attributeName: string): HTMLAttributeNode | null;
48
+ /**
49
+ * Checks if a tag has a specific attribute
50
+ */
51
+ export declare function hasAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, attributeName: string): boolean;
52
+ /**
53
+ * Common HTML element categorization
54
+ */
55
+ export declare const HTML_INLINE_ELEMENTS: Set<string>;
56
+ export declare const HTML_BLOCK_ELEMENTS: Set<string>;
57
+ export declare const HTML_BOOLEAN_ATTRIBUTES: Set<string>;
58
+ export declare const HEADING_TAGS: Set<string>;
59
+ /**
60
+ * Checks if an element is inline
61
+ */
62
+ export declare function isInlineElement(tagName: string): boolean;
63
+ /**
64
+ * Checks if an element is block-level
65
+ */
66
+ export declare function isBlockElement(tagName: string): boolean;
67
+ /**
68
+ * Checks if an attribute is a boolean attribute
69
+ */
70
+ export declare function isBooleanAttribute(attributeName: string): boolean;
71
+ /**
72
+ * Abstract base class for rules that need to check individual attributes on HTML tags
73
+ * Eliminates duplication of visitHTMLOpenTagNode/visitHTMLSelfCloseTagNode patterns
74
+ * and attribute iteration logic. Provides simplified interface with extracted attribute info.
75
+ */
76
+ export declare abstract class AttributeVisitorMixin extends BaseRuleVisitor {
77
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void;
78
+ visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void;
79
+ private checkAttributesOnNode;
80
+ protected abstract checkAttribute(attributeName: string, attributeValue: string | null, attributeNode: HTMLAttributeNode, parentNode: HTMLOpenTagNode | HTMLSelfCloseTagNode): void;
81
+ }
82
+ /**
83
+ * Checks if an attribute value is quoted
84
+ */
85
+ export declare function isAttributeValueQuoted(attributeNode: HTMLAttributeNode): boolean;
86
+ /**
87
+ * Iterates over all attributes of a tag node, calling the callback for each attribute
88
+ */
89
+ export declare function forEachAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, callback: (attributeNode: HTMLAttributeNode) => void): void;
@@ -0,0 +1,14 @@
1
+ import type { ThemeInput } from "@herb-tools/highlighter";
2
+ export interface ParsedArguments {
3
+ pattern: string;
4
+ formatOption: 'simple' | 'detailed';
5
+ showTiming: boolean;
6
+ theme: ThemeInput;
7
+ wrapLines: boolean;
8
+ truncateLines: boolean;
9
+ }
10
+ export declare class ArgumentParser {
11
+ private readonly usage;
12
+ parse(argv: string[]): ParsedArguments;
13
+ private getFilePattern;
14
+ }
@@ -0,0 +1,21 @@
1
+ import type { Diagnostic } from "@herb-tools/core";
2
+ export interface ProcessedFile {
3
+ filename: string;
4
+ diagnostic: Diagnostic;
5
+ content: string;
6
+ }
7
+ export interface ProcessingResult {
8
+ totalErrors: number;
9
+ totalWarnings: number;
10
+ filesWithIssues: number;
11
+ ruleCount: number;
12
+ allDiagnostics: ProcessedFile[];
13
+ ruleViolations: Map<string, {
14
+ count: number;
15
+ files: Set<string>;
16
+ }>;
17
+ }
18
+ export declare class FileProcessor {
19
+ private linter;
20
+ processFiles(files: string[]): Promise<ProcessingResult>;
21
+ }
@@ -0,0 +1,6 @@
1
+ import type { Diagnostic } from "@herb-tools/core";
2
+ import type { ProcessedFile } from "../file-processor.js";
3
+ export declare abstract class BaseFormatter {
4
+ abstract format(allDiagnostics: ProcessedFile[], isSingleFile?: boolean): Promise<void>;
5
+ abstract formatFile(filename: string, diagnostics: Diagnostic[]): void;
6
+ }
@@ -0,0 +1,13 @@
1
+ import { type ThemeInput } from "@herb-tools/highlighter";
2
+ import { BaseFormatter } from "./base-formatter.js";
3
+ import type { Diagnostic } from "@herb-tools/core";
4
+ import type { ProcessedFile } from "../file-processor.js";
5
+ export declare class DetailedFormatter extends BaseFormatter {
6
+ private highlighter;
7
+ private theme;
8
+ private wrapLines;
9
+ private truncateLines;
10
+ constructor(theme?: ThemeInput, wrapLines?: boolean, truncateLines?: boolean);
11
+ format(allDiagnostics: ProcessedFile[], isSingleFile?: boolean): Promise<void>;
12
+ formatFile(_filename: string, _diagnostics: Diagnostic[]): void;
13
+ }
@@ -0,0 +1,3 @@
1
+ export { BaseFormatter } from "./base-formatter.js";
2
+ export { SimpleFormatter } from "./simple-formatter.js";
3
+ export { DetailedFormatter } from "./detailed-formatter.js";
@@ -0,0 +1,7 @@
1
+ import { BaseFormatter } from "./base-formatter.js";
2
+ import type { Diagnostic } from "@herb-tools/core";
3
+ import type { ProcessedFile } from "../file-processor.js";
4
+ export declare class SimpleFormatter extends BaseFormatter {
5
+ format(allDiagnostics: ProcessedFile[]): Promise<void>;
6
+ formatFile(filename: string, diagnostics: Diagnostic[]): void;
7
+ }
@@ -0,0 +1,4 @@
1
+ export { ArgumentParser } from "./argument-parser.js";
2
+ export { FileProcessor } from "./file-processor.js";
3
+ export { SummaryReporter } from "./summary-reporter.js";
4
+ export * from "./formatters/index.js";
@@ -0,0 +1,22 @@
1
+ export interface SummaryData {
2
+ files: string[];
3
+ totalErrors: number;
4
+ totalWarnings: number;
5
+ filesWithViolations: number;
6
+ ruleCount: number;
7
+ startTime: number;
8
+ startDate: Date;
9
+ showTiming: boolean;
10
+ ruleViolations: Map<string, {
11
+ count: number;
12
+ files: Set<string>;
13
+ }>;
14
+ }
15
+ export declare class SummaryReporter {
16
+ private pluralize;
17
+ displaySummary(data: SummaryData): void;
18
+ displayMostViolatedRules(ruleViolations: Map<string, {
19
+ count: number;
20
+ files: Set<string>;
21
+ }>, limit?: number): void;
22
+ }
@@ -0,0 +1,6 @@
1
+ export declare class CLI {
2
+ private argumentParser;
3
+ private fileProcessor;
4
+ private summaryReporter;
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { RuleClass } from "./types.js";
2
+ export declare const defaultRules: RuleClass[];
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from "./linter.js";
2
+ export * from "./rules/index.js";
3
+ export * from "./types.js";
@@ -0,0 +1,18 @@
1
+ import type { RuleClass, LintResult } from "./types.js";
2
+ import type { DocumentNode } from "@herb-tools/core";
3
+ export declare class Linter {
4
+ private rules;
5
+ private offenses;
6
+ /**
7
+ * Creates a new Linter instance.
8
+ * @param rules - Array of rule classes (not instances) to use. If not provided, uses default rules.
9
+ */
10
+ constructor(rules?: RuleClass[]);
11
+ /**
12
+ * Returns the default set of rule classes used by the linter.
13
+ * @returns Array of rule classes
14
+ */
15
+ private getDefaultRules;
16
+ getRuleCount(): number;
17
+ lint(document: DocumentNode): LintResult;
18
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class ERBNoEmptyTagsRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Node } from "@herb-tools/core";
2
+ import type { Rule, LintOffense } from "../types.js";
3
+ export declare class ERBNoOutputControlFlowRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Node } from "@herb-tools/core";
2
+ import type { LintOffense, Rule } from "../types.js";
3
+ export declare class ERBRequireWhitespaceRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLAnchorRequireHrefRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLAriaRoleHeadingRequiresLevelRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLAttributeDoubleQuotesRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLAttributeValuesRequireQuotesRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLBooleanAttributesNoValueRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLImgRequireAltRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLNoBlockInsideInlineRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLNoDuplicateAttributesRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLNoEmptyHeadingsRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLNoNestedLinksRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Rule, LintOffense } from "../types.js";
2
+ import type { Node } from "@herb-tools/core";
3
+ export declare class HTMLTagNameLowercaseRule implements Rule {
4
+ name: string;
5
+ check(node: Node): LintOffense[];
6
+ }
@@ -0,0 +1,12 @@
1
+ export * from "./erb-no-empty-tags.js";
2
+ export * from "./erb-no-output-control-flow.js";
3
+ export * from "./html-anchor-require-href.js";
4
+ export * from "./html-attribute-double-quotes.js";
5
+ export * from "./html-attribute-values-require-quotes.js";
6
+ export * from "./html-boolean-attributes-no-value.js";
7
+ export * from "./html-img-require-alt.js";
8
+ export * from "./html-no-block-inside-inline.js";
9
+ export * from "./html-no-duplicate-attributes.js";
10
+ export * from "./html-no-empty-headings.js";
11
+ export * from "./html-no-nested-links.js";
12
+ export * from "./html-tag-name-lowercase.js";
@@ -0,0 +1,89 @@
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";
4
+ /**
5
+ * Base visitor class that provides common functionality for rule visitors
6
+ */
7
+ export declare abstract class BaseRuleVisitor extends Visitor {
8
+ readonly offenses: LintOffense[];
9
+ protected ruleName: string;
10
+ constructor(ruleName: string);
11
+ /**
12
+ * Helper method to create a lint offense
13
+ */
14
+ protected createOffense(message: string, location: Location, severity?: LintSeverity): LintOffense;
15
+ /**
16
+ * Helper method to add an offense to the offenses array
17
+ */
18
+ protected addOffense(message: string, location: Location, severity?: LintSeverity): void;
19
+ }
20
+ /**
21
+ * Gets attributes from either an HTMLOpenTagNode or HTMLSelfCloseTagNode
22
+ */
23
+ export declare function getAttributes(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): any[];
24
+ /**
25
+ * Gets the tag name from an HTML tag node (lowercased)
26
+ */
27
+ export declare function getTagName(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): string | null;
28
+ /**
29
+ * Gets the attribute name from an HTMLAttributeNode (lowercased)
30
+ */
31
+ export declare function getAttributeName(attributeNode: HTMLAttributeNode): string | null;
32
+ /**
33
+ * Gets the attribute value content from an HTMLAttributeValueNode
34
+ */
35
+ export declare function getAttributeValue(attributeNode: HTMLAttributeNode): string | null;
36
+ /**
37
+ * Checks if an attribute has a value
38
+ */
39
+ export declare function hasAttributeValue(attributeNode: HTMLAttributeNode): boolean;
40
+ /**
41
+ * Gets the quote type used for an attribute value
42
+ */
43
+ export declare function getAttributeValueQuoteType(attributeNode: HTMLAttributeNode): "single" | "double" | "none" | null;
44
+ /**
45
+ * Finds an attribute by name in a list of attributes
46
+ */
47
+ export declare function findAttributeByName(attributes: any[], attributeName: string): HTMLAttributeNode | null;
48
+ /**
49
+ * Checks if a tag has a specific attribute
50
+ */
51
+ export declare function hasAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, attributeName: string): boolean;
52
+ /**
53
+ * Common HTML element categorization
54
+ */
55
+ export declare const HTML_INLINE_ELEMENTS: Set<string>;
56
+ export declare const HTML_BLOCK_ELEMENTS: Set<string>;
57
+ export declare const HTML_BOOLEAN_ATTRIBUTES: Set<string>;
58
+ export declare const HEADING_TAGS: Set<string>;
59
+ /**
60
+ * Checks if an element is inline
61
+ */
62
+ export declare function isInlineElement(tagName: string): boolean;
63
+ /**
64
+ * Checks if an element is block-level
65
+ */
66
+ export declare function isBlockElement(tagName: string): boolean;
67
+ /**
68
+ * Checks if an attribute is a boolean attribute
69
+ */
70
+ export declare function isBooleanAttribute(attributeName: string): boolean;
71
+ /**
72
+ * Abstract base class for rules that need to check individual attributes on HTML tags
73
+ * Eliminates duplication of visitHTMLOpenTagNode/visitHTMLSelfCloseTagNode patterns
74
+ * and attribute iteration logic. Provides simplified interface with extracted attribute info.
75
+ */
76
+ export declare abstract class AttributeVisitorMixin extends BaseRuleVisitor {
77
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void;
78
+ visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void;
79
+ private checkAttributesOnNode;
80
+ protected abstract checkAttribute(attributeName: string, attributeValue: string | null, attributeNode: HTMLAttributeNode, parentNode: HTMLOpenTagNode | HTMLSelfCloseTagNode): void;
81
+ }
82
+ /**
83
+ * Checks if an attribute value is quoted
84
+ */
85
+ export declare function isAttributeValueQuoted(attributeNode: HTMLAttributeNode): boolean;
86
+ /**
87
+ * Iterates over all attributes of a tag node, calling the callback for each attribute
88
+ */
89
+ export declare function forEachAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, callback: (attributeNode: HTMLAttributeNode) => void): void;
@@ -0,0 +1,26 @@
1
+ import { Node, Diagnostic } from "@herb-tools/core";
2
+ import type { defaultRules } from "./default-rules.js";
3
+ export type LintSeverity = "error" | "warning";
4
+ /**
5
+ * Automatically inferred union type of all available linter rule names.
6
+ * This type extracts the 'name' property from each rule class instance.
7
+ */
8
+ export type LinterRule = InstanceType<typeof defaultRules[number]>['name'];
9
+ export interface LintOffense extends Diagnostic {
10
+ rule: LinterRule;
11
+ severity: LintSeverity;
12
+ }
13
+ export interface LintResult {
14
+ offenses: LintOffense[];
15
+ errors: number;
16
+ warnings: number;
17
+ }
18
+ export interface Rule {
19
+ name: string;
20
+ check(node: Node): LintOffense[];
21
+ }
22
+ /**
23
+ * Type representing a rule class constructor.
24
+ * The Linter accepts rule classes rather than instances for better performance and memory usage.
25
+ */
26
+ export type RuleClass = new () => Rule;
@@ -0,0 +1,26 @@
1
+ import { Node, Diagnostic } from "@herb-tools/core";
2
+ import type { defaultRules } from "./default-rules.js";
3
+ export type LintSeverity = "error" | "warning";
4
+ /**
5
+ * Automatically inferred union type of all available linter rule names.
6
+ * This type extracts the 'name' property from each rule class instance.
7
+ */
8
+ export type LinterRule = InstanceType<typeof defaultRules[number]>['name'];
9
+ export interface LintOffense extends Diagnostic {
10
+ rule: LinterRule;
11
+ severity: LintSeverity;
12
+ }
13
+ export interface LintResult {
14
+ offenses: LintOffense[];
15
+ errors: number;
16
+ warnings: number;
17
+ }
18
+ export interface Rule {
19
+ name: string;
20
+ check(node: Node): LintOffense[];
21
+ }
22
+ /**
23
+ * Type representing a rule class constructor.
24
+ * The Linter accepts rule classes rather than instances for better performance and memory usage.
25
+ */
26
+ export type RuleClass = new () => Rule;
@@ -0,0 +1,39 @@
1
+ # Linter Rules
2
+
3
+ This page contains documentation for all Herb Linter rules.
4
+
5
+ ## Available Rules
6
+
7
+ - [`erb-no-empty-tags`](./erb-no-empty-tags.md) - Disallow empty ERB tags
8
+ - [`erb-no-output-control-flow`](./erb-no-output-control-flow.md) - Prevents outputting control flow blocks
9
+ - [`erb-require-whitespace-inside-tags`](./erb-require-whitespace-inside-tags.md) - Requires whitespace around erb tags
10
+ - [`html-aria-role-heading-requires-level`](./html-aria-role-heading-requires-level.md) - Requires `aria-level` when supplying a `role`
11
+ - [`html-attribute-double-quotes`](./html-attribute-double-quotes.md) - Enforces double quotes for attribute values
12
+ - [`html-attribute-values-require-quotes`](./html-attribute-values-require-quotes.md) - Requires quotes around attribute values
13
+ - [`html-boolean-attributes-no-value`](./html-boolean-attributes-no-value.md) - Prevents values on boolean attributes
14
+ - [`html-img-require-alt`](./html-img-require-alt.md) - Requires alt attributes on img tags
15
+ - [`html-no-block-inside-inline`](./html-no-block-inside-inline.md) - Prevents block-level elements inside inline elements
16
+ - [`html-no-duplicate-attributes`](./html-no-duplicate-attributes.md) - Prevents duplicate attributes on HTML elements
17
+ - [`html-no-nested-links`](./html-no-nested-links.md) - Prevents nested anchor tags
18
+ - [`html-tag-name-lowercase`](./html-tag-name-lowercase.md) - Enforces lowercase tag names in HTML
19
+
20
+ ## Contributing
21
+
22
+ To add a new linter rule you can scaffold a new rule by running:
23
+
24
+ ```bash
25
+ cd javascript/packages/linter
26
+
27
+ scripts/generate-rule
28
+ ```
29
+
30
+ The script creates the documentation, rule stub, and test stub based on the GitHub issue (requires the `linter` label and a `Rule name: [rule-name]` line).
31
+
32
+ Alternatively, you can create one manually:
33
+
34
+ 1. Create the rule class implementing the `Rule` interface
35
+ 2. Add comprehensive tests in `test/rules/`
36
+ 3. Add documentation in `docs/rules/`
37
+ 4. Update the main linter to include the rule by default (if appropriate)
38
+
39
+ See [`html-tag-name-lowercase.ts`](https://github.com/marcoroth/herb/blob/main/javascript/packages/linter/src/rules/html-tag-name-lowercase.ts) for an example implementation.
@@ -0,0 +1,38 @@
1
+ # Linter Rule: Disallow empty ERB tags
2
+
3
+ **Rule:** `erb-no-empty-tags`
4
+
5
+ ## Description
6
+
7
+ Disallow ERB tags (`<% %>` or `<%= %>`) that contain no meaningful content i.e., tags that are completely empty or contain only whitespace.
8
+
9
+ ## Rationale
10
+
11
+ Empty ERB tags serve no purpose and may confuse readers or indicate incomplete code. They clutter the template and may have been left behind accidentally after editing.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```html
18
+ <%= user.name %>
19
+
20
+ <% if user.admin? %>
21
+ Admin tools
22
+ <% end %>
23
+ ```
24
+
25
+ ### 🚫 Bad
26
+
27
+ ```erb
28
+ <% %>
29
+
30
+ <%= %>
31
+
32
+ <%
33
+ %>
34
+ ```
35
+
36
+ ## References
37
+
38
+ \-
@@ -0,0 +1,45 @@
1
+ # Linter Rule: Disallow output ERB tags with control flow
2
+
3
+ **Rule:** `erb-no-output-control-flow`
4
+
5
+ ## Description
6
+
7
+ Disallow using output ERB tags (`<%=`) for control flow statements like `if`, `unless`, `case`, `while`, etc. Control flow should be written with regular ERB tags (`<% ... %>`), since these do not produce output directly.
8
+
9
+ ## Rationale
10
+
11
+ Using `<%=` with control flow is typically a mistake or misunderstanding of ERB behavior. Output tags (`<%=`) are designed to render values into the HTML output, while control flow statements only affect execution and do not produce a value to render. This misuse can result in unexpected output, unnecessary blank spaces, or subtle bugs.
12
+
13
+ Reporting this as a warning can help developers catch likely mistakes while allowing flexibility for rare advanced cases.
14
+
15
+ ## Examples
16
+
17
+ ### ✅ Good
18
+
19
+ ```erb
20
+ <% if condition %>
21
+ Content here
22
+ <% end %>
23
+ ```
24
+
25
+ ```erb
26
+ <%= user.name %>
27
+ ```
28
+
29
+ ### 🚫 Bad
30
+
31
+ ```erb
32
+ <%= if condition %>
33
+ Content here
34
+ <% end %>
35
+ ```
36
+
37
+ ```erb
38
+ <%= unless user.nil? %>
39
+ Welcome!
40
+ <% end %>
41
+ ```
42
+
43
+ ## References
44
+
45
+ * [Inspiration](https://x.com/specialcasedev/status/1935013470069719231)