@herb-tools/linter 0.6.1 → 0.7.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 (93) hide show
  1. package/README.md +60 -16
  2. package/dist/herb-lint.js +364 -181
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +321 -100
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +270 -89
  7. package/dist/index.js.map +1 -1
  8. package/dist/package.json +11 -5
  9. package/dist/src/cli/argument-parser.js +11 -6
  10. package/dist/src/cli/argument-parser.js.map +1 -1
  11. package/dist/src/cli/file-processor.js +5 -6
  12. package/dist/src/cli/file-processor.js.map +1 -1
  13. package/dist/src/cli/formatters/detailed-formatter.js +3 -5
  14. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
  15. package/dist/src/cli/formatters/github-actions-formatter.js +55 -11
  16. package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
  17. package/dist/src/cli/index.js +1 -0
  18. package/dist/src/cli/index.js.map +1 -1
  19. package/dist/src/cli/output-manager.js +23 -5
  20. package/dist/src/cli/output-manager.js.map +1 -1
  21. package/dist/src/cli/summary-reporter.js +2 -11
  22. package/dist/src/cli/summary-reporter.js.map +1 -1
  23. package/dist/src/cli.js +88 -4
  24. package/dist/src/cli.js.map +1 -1
  25. package/dist/src/default-rules.js +8 -4
  26. package/dist/src/default-rules.js.map +1 -1
  27. package/dist/src/linter.js.map +1 -1
  28. package/dist/src/rules/erb-prefer-image-tag-helper.js +1 -1
  29. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  30. package/dist/src/rules/html-boolean-attributes-no-value.js +8 -8
  31. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  32. package/dist/src/rules/html-no-empty-attributes.js +56 -0
  33. package/dist/src/rules/html-no-empty-attributes.js.map +1 -0
  34. package/dist/src/rules/html-no-positive-tab-index.js +1 -1
  35. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
  36. package/dist/src/rules/html-no-underscores-in-attribute-names.js +36 -0
  37. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -0
  38. package/dist/src/rules/index.js +3 -0
  39. package/dist/src/rules/index.js.map +1 -1
  40. package/dist/src/rules/rule-utils.js +11 -7
  41. package/dist/src/rules/rule-utils.js.map +1 -1
  42. package/dist/src/rules/svg-tag-name-capitalization.js +2 -2
  43. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/dist/types/cli/argument-parser.d.ts +2 -1
  46. package/dist/types/cli/file-processor.d.ts +6 -1
  47. package/dist/types/cli/formatters/github-actions-formatter.d.ts +6 -1
  48. package/dist/types/cli/index.d.ts +1 -0
  49. package/dist/types/cli/output-manager.d.ts +1 -0
  50. package/dist/types/cli.d.ts +20 -5
  51. package/dist/types/linter.d.ts +7 -7
  52. package/dist/types/rules/html-no-empty-attributes.d.ts +7 -0
  53. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
  54. package/dist/types/rules/index.d.ts +3 -0
  55. package/dist/types/rules/rule-utils.d.ts +7 -5
  56. package/dist/types/src/cli/argument-parser.d.ts +2 -1
  57. package/dist/types/src/cli/file-processor.d.ts +6 -1
  58. package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +6 -1
  59. package/dist/types/src/cli/index.d.ts +1 -0
  60. package/dist/types/src/cli/output-manager.d.ts +1 -0
  61. package/dist/types/src/cli.d.ts +20 -5
  62. package/dist/types/src/linter.d.ts +7 -7
  63. package/dist/types/src/rules/html-no-empty-attributes.d.ts +7 -0
  64. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
  65. package/dist/types/src/rules/index.d.ts +3 -0
  66. package/dist/types/src/rules/rule-utils.d.ts +7 -5
  67. package/docs/rules/README.md +2 -0
  68. package/docs/rules/html-img-require-alt.md +0 -2
  69. package/docs/rules/html-no-empty-attributes.md +77 -0
  70. package/docs/rules/html-no-underscores-in-attribute-names.md +45 -0
  71. package/package.json +11 -5
  72. package/src/cli/argument-parser.ts +15 -7
  73. package/src/cli/file-processor.ts +11 -7
  74. package/src/cli/formatters/detailed-formatter.ts +5 -7
  75. package/src/cli/formatters/github-actions-formatter.ts +64 -11
  76. package/src/cli/index.ts +2 -0
  77. package/src/cli/output-manager.ts +27 -5
  78. package/src/cli/summary-reporter.ts +3 -11
  79. package/src/cli.ts +125 -20
  80. package/src/default-rules.ts +8 -4
  81. package/src/linter.ts +6 -6
  82. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +1 -1
  83. package/src/rules/erb-prefer-image-tag-helper.ts +2 -2
  84. package/src/rules/erb-require-whitespace-inside-tags.ts +2 -2
  85. package/src/rules/html-attribute-double-quotes.ts +1 -1
  86. package/src/rules/html-boolean-attributes-no-value.ts +9 -11
  87. package/src/rules/html-no-empty-attributes.ts +75 -0
  88. package/src/rules/html-no-positive-tab-index.ts +1 -1
  89. package/src/rules/html-no-underscores-in-attribute-names.ts +58 -0
  90. package/src/rules/html-tag-name-lowercase.ts +1 -1
  91. package/src/rules/index.ts +3 -0
  92. package/src/rules/rule-utils.ts +15 -11
  93. package/src/rules/svg-tag-name-capitalization.ts +2 -2
@@ -1,9 +1,9 @@
1
- import type { RuleClass, LintResult, LintContext } from "./types.js";
1
+ import type { RuleClass, Rule, LexerRule, SourceRule, LintResult, LintOffense, LintContext } from "./types.js";
2
2
  import type { HerbBackend } from "@herb-tools/core";
3
3
  export declare class Linter {
4
- private rules;
5
- private herb;
6
- private offenses;
4
+ protected rules: RuleClass[];
5
+ protected herb: HerbBackend;
6
+ protected offenses: LintOffense[];
7
7
  /**
8
8
  * Creates a new Linter instance.
9
9
  * @param herb - The Herb backend instance for parsing and lexing
@@ -14,16 +14,16 @@ export declare class Linter {
14
14
  * Returns the default set of rule classes used by the linter.
15
15
  * @returns Array of rule classes
16
16
  */
17
- private getDefaultRules;
17
+ protected getDefaultRules(): RuleClass[];
18
18
  getRuleCount(): number;
19
19
  /**
20
20
  * Type guard to check if a rule is a LexerRule
21
21
  */
22
- private isLexerRule;
22
+ protected isLexerRule(rule: Rule): rule is LexerRule;
23
23
  /**
24
24
  * Type guard to check if a rule is a SourceRule
25
25
  */
26
- private isSourceRule;
26
+ protected isSourceRule(rule: Rule): rule is SourceRule;
27
27
  /**
28
28
  * Lint source code using Parser/AST, Lexer, and Source rules.
29
29
  * @param source - The source code to lint
@@ -0,0 +1,7 @@
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
3
+ import type { ParseResult } from "@herb-tools/core";
4
+ export declare class HTMLNoEmptyAttributesRule extends ParserRule {
5
+ name: string;
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
+ }
@@ -0,0 +1,7 @@
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintContext, LintOffense } from "../types.js";
3
+ import type { ParseResult } from "@herb-tools/core";
4
+ export declare class HTMLNoUnderscoresInAttributeNamesRule extends ParserRule {
5
+ name: string;
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
+ }
@@ -1,3 +1,4 @@
1
+ export * from "./rule-utils.js";
1
2
  export * from "./erb-no-empty-tags.js";
2
3
  export * from "./erb-no-output-control-flow.js";
3
4
  export * from "./erb-no-silent-tag-in-attribute-name.js";
@@ -20,6 +21,7 @@ export * from "./html-no-aria-hidden-on-focusable.js";
20
21
  export * from "./html-no-block-inside-inline.js";
21
22
  export * from "./html-no-duplicate-attributes.js";
22
23
  export * from "./html-no-duplicate-ids.js";
24
+ export * from "./html-no-empty-attributes.js";
23
25
  export * from "./html-no-empty-headings.js";
24
26
  export * from "./html-no-nested-links.js";
25
27
  export * from "./html-no-positive-tab-index.js";
@@ -27,3 +29,4 @@ export * from "./html-no-self-closing.js";
27
29
  export * from "./html-no-title-attribute.js";
28
30
  export * from "./html-tag-name-lowercase.js";
29
31
  export * from "./svg-tag-name-capitalization.js";
32
+ export * from "./html-no-underscores-in-attribute-names.js";
@@ -69,7 +69,7 @@ export declare function getTagName(node: HTMLOpenTagNode): string | null;
69
69
  * Gets the attribute name from an HTMLAttributeNode (lowercased)
70
70
  * Returns null if the attribute name contains dynamic content (ERB)
71
71
  */
72
- export declare function getAttributeName(attributeNode: HTMLAttributeNode): string | null;
72
+ export declare function getAttributeName(attributeNode: HTMLAttributeNode, lowercase?: boolean): string | null;
73
73
  /**
74
74
  * Checks if an attribute has a dynamic (ERB-containing) name
75
75
  */
@@ -153,12 +153,14 @@ export interface StaticAttributeStaticValueParams {
153
153
  attributeName: string;
154
154
  attributeValue: string;
155
155
  attributeNode: HTMLAttributeNode;
156
+ originalAttributeName: string;
156
157
  parentNode: HTMLOpenTagNode;
157
158
  }
158
159
  export interface StaticAttributeDynamicValueParams {
159
160
  attributeName: string;
160
161
  valueNodes: Node[];
161
162
  attributeNode: HTMLAttributeNode;
163
+ originalAttributeName: string;
162
164
  parentNode: HTMLOpenTagNode;
163
165
  combinedValue?: string | null;
164
166
  }
@@ -215,19 +217,19 @@ export declare abstract class AttributeVisitorMixin extends BaseRuleVisitor {
215
217
  /**
216
218
  * Static attribute name with static value: class="container"
217
219
  */
218
- protected checkStaticAttributeStaticValue(params: StaticAttributeStaticValueParams): void;
220
+ protected checkStaticAttributeStaticValue(_params: StaticAttributeStaticValueParams): void;
219
221
  /**
220
222
  * Static attribute name with dynamic value: class="<%= css_class %>"
221
223
  */
222
- protected checkStaticAttributeDynamicValue(params: StaticAttributeDynamicValueParams): void;
224
+ protected checkStaticAttributeDynamicValue(_params: StaticAttributeDynamicValueParams): void;
223
225
  /**
224
226
  * Dynamic attribute name with static value: data-<%= key %>="foo"
225
227
  */
226
- protected checkDynamicAttributeStaticValue(params: DynamicAttributeStaticValueParams): void;
228
+ protected checkDynamicAttributeStaticValue(_params: DynamicAttributeStaticValueParams): void;
227
229
  /**
228
230
  * Dynamic attribute name with dynamic value: data-<%= key %>="<%= value %>"
229
231
  */
230
- protected checkDynamicAttributeDynamicValue(params: DynamicAttributeDynamicValueParams): void;
232
+ protected checkDynamicAttributeDynamicValue(_params: DynamicAttributeDynamicValueParams): void;
231
233
  }
232
234
  /**
233
235
  * Checks if an attribute value is quoted
@@ -1,5 +1,5 @@
1
1
  import type { ThemeInput } from "@herb-tools/highlighter";
2
- export type FormatOption = "simple" | "detailed" | "json" | "github";
2
+ export type FormatOption = "simple" | "detailed" | "json";
3
3
  export interface ParsedArguments {
4
4
  pattern: string;
5
5
  formatOption: FormatOption;
@@ -7,6 +7,7 @@ export interface ParsedArguments {
7
7
  theme: ThemeInput;
8
8
  wrapLines: boolean;
9
9
  truncateLines: boolean;
10
+ useGitHubActions: boolean;
10
11
  }
11
12
  export declare class ArgumentParser {
12
13
  private readonly usage;
@@ -5,6 +5,10 @@ export interface ProcessedFile {
5
5
  offense: Diagnostic;
6
6
  content: string;
7
7
  }
8
+ export interface ProcessingContext {
9
+ projectPath?: string;
10
+ pattern?: string;
11
+ }
8
12
  export interface ProcessingResult {
9
13
  totalErrors: number;
10
14
  totalWarnings: number;
@@ -15,8 +19,9 @@ export interface ProcessingResult {
15
19
  count: number;
16
20
  files: Set<string>;
17
21
  }>;
22
+ context?: ProcessingContext;
18
23
  }
19
24
  export declare class FileProcessor {
20
25
  private linter;
21
- processFiles(files: string[], formatOption?: FormatOption): Promise<ProcessingResult>;
26
+ processFiles(files: string[], formatOption?: FormatOption, context?: ProcessingContext): Promise<ProcessingResult>;
22
27
  }
@@ -2,9 +2,14 @@ import { BaseFormatter } from "./base-formatter.js";
2
2
  import type { Diagnostic } from "@herb-tools/core";
3
3
  import type { ProcessedFile } from "../file-processor.js";
4
4
  export declare class GitHubActionsFormatter extends BaseFormatter {
5
+ private highlighter;
6
+ private wrapLines;
7
+ private truncateLines;
8
+ constructor(wrapLines?: boolean, truncateLines?: boolean);
5
9
  private static readonly MESSAGE_ESCAPE_MAP;
6
10
  private static readonly PARAM_ESCAPE_MAP;
7
- format(allDiagnostics: ProcessedFile[]): Promise<void>;
11
+ format(allDiagnostics: ProcessedFile[], _isSingleFile?: boolean): Promise<void>;
12
+ formatAnnotations(allDiagnostics: ProcessedFile[]): Promise<void>;
8
13
  formatFile(filename: string, diagnostics: Diagnostic[]): void;
9
14
  private formatDiagnostic;
10
15
  private escapeMessage;
@@ -1,4 +1,5 @@
1
1
  export { ArgumentParser } from "./argument-parser.js";
2
2
  export { FileProcessor } from "./file-processor.js";
3
3
  export { SummaryReporter } from "./summary-reporter.js";
4
+ export { OutputManager } from "./output-manager.js";
4
5
  export * from "./formatters/index.js";
@@ -7,6 +7,7 @@ interface OutputOptions {
7
7
  wrapLines: boolean;
8
8
  truncateLines: boolean;
9
9
  showTiming: boolean;
10
+ useGitHubActions: boolean;
10
11
  startTime: number;
11
12
  startDate: Date;
12
13
  }
@@ -1,8 +1,23 @@
1
+ import { ArgumentParser, type FormatOption } from "./cli/argument-parser.js";
2
+ import { FileProcessor } from "./cli/file-processor.js";
3
+ import { OutputManager } from "./cli/output-manager.js";
4
+ export * from "./cli/index.js";
1
5
  export declare class CLI {
2
- private argumentParser;
3
- private fileProcessor;
4
- private outputManager;
5
- private exitWithError;
6
- private exitWithInfo;
6
+ protected argumentParser: ArgumentParser;
7
+ protected fileProcessor: FileProcessor;
8
+ protected outputManager: OutputManager;
9
+ protected projectPath: string;
10
+ getProjectPath(): string;
11
+ protected findProjectRoot(startPath: string): string;
12
+ protected exitWithError(message: string, formatOption: FormatOption, exitCode?: number): void;
13
+ protected exitWithInfo(message: string, formatOption: FormatOption, exitCode?: number, timingData?: {
14
+ startTime: number;
15
+ startDate: Date;
16
+ showTiming: boolean;
17
+ }): void;
18
+ protected determineProjectPath(pattern: string | undefined): void;
19
+ protected adjustPattern(pattern: string | undefined): string;
20
+ protected beforeProcess(): Promise<void>;
21
+ protected afterProcess(_results: any, _outputOptions: any): Promise<void>;
7
22
  run(): Promise<void>;
8
23
  }
@@ -1,9 +1,9 @@
1
- import type { RuleClass, LintResult, LintContext } from "./types.js";
1
+ import type { RuleClass, Rule, LexerRule, SourceRule, LintResult, LintOffense, LintContext } from "./types.js";
2
2
  import type { HerbBackend } from "@herb-tools/core";
3
3
  export declare class Linter {
4
- private rules;
5
- private herb;
6
- private offenses;
4
+ protected rules: RuleClass[];
5
+ protected herb: HerbBackend;
6
+ protected offenses: LintOffense[];
7
7
  /**
8
8
  * Creates a new Linter instance.
9
9
  * @param herb - The Herb backend instance for parsing and lexing
@@ -14,16 +14,16 @@ export declare class Linter {
14
14
  * Returns the default set of rule classes used by the linter.
15
15
  * @returns Array of rule classes
16
16
  */
17
- private getDefaultRules;
17
+ protected getDefaultRules(): RuleClass[];
18
18
  getRuleCount(): number;
19
19
  /**
20
20
  * Type guard to check if a rule is a LexerRule
21
21
  */
22
- private isLexerRule;
22
+ protected isLexerRule(rule: Rule): rule is LexerRule;
23
23
  /**
24
24
  * Type guard to check if a rule is a SourceRule
25
25
  */
26
- private isSourceRule;
26
+ protected isSourceRule(rule: Rule): rule is SourceRule;
27
27
  /**
28
28
  * Lint source code using Parser/AST, Lexer, and Source rules.
29
29
  * @param source - The source code to lint
@@ -0,0 +1,7 @@
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
3
+ import type { ParseResult } from "@herb-tools/core";
4
+ export declare class HTMLNoEmptyAttributesRule extends ParserRule {
5
+ name: string;
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
+ }
@@ -0,0 +1,7 @@
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintContext, LintOffense } from "../types.js";
3
+ import type { ParseResult } from "@herb-tools/core";
4
+ export declare class HTMLNoUnderscoresInAttributeNamesRule extends ParserRule {
5
+ name: string;
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
+ }
@@ -1,3 +1,4 @@
1
+ export * from "./rule-utils.js";
1
2
  export * from "./erb-no-empty-tags.js";
2
3
  export * from "./erb-no-output-control-flow.js";
3
4
  export * from "./erb-no-silent-tag-in-attribute-name.js";
@@ -20,6 +21,7 @@ export * from "./html-no-aria-hidden-on-focusable.js";
20
21
  export * from "./html-no-block-inside-inline.js";
21
22
  export * from "./html-no-duplicate-attributes.js";
22
23
  export * from "./html-no-duplicate-ids.js";
24
+ export * from "./html-no-empty-attributes.js";
23
25
  export * from "./html-no-empty-headings.js";
24
26
  export * from "./html-no-nested-links.js";
25
27
  export * from "./html-no-positive-tab-index.js";
@@ -27,3 +29,4 @@ export * from "./html-no-self-closing.js";
27
29
  export * from "./html-no-title-attribute.js";
28
30
  export * from "./html-tag-name-lowercase.js";
29
31
  export * from "./svg-tag-name-capitalization.js";
32
+ export * from "./html-no-underscores-in-attribute-names.js";
@@ -69,7 +69,7 @@ export declare function getTagName(node: HTMLOpenTagNode): string | null;
69
69
  * Gets the attribute name from an HTMLAttributeNode (lowercased)
70
70
  * Returns null if the attribute name contains dynamic content (ERB)
71
71
  */
72
- export declare function getAttributeName(attributeNode: HTMLAttributeNode): string | null;
72
+ export declare function getAttributeName(attributeNode: HTMLAttributeNode, lowercase?: boolean): string | null;
73
73
  /**
74
74
  * Checks if an attribute has a dynamic (ERB-containing) name
75
75
  */
@@ -153,12 +153,14 @@ export interface StaticAttributeStaticValueParams {
153
153
  attributeName: string;
154
154
  attributeValue: string;
155
155
  attributeNode: HTMLAttributeNode;
156
+ originalAttributeName: string;
156
157
  parentNode: HTMLOpenTagNode;
157
158
  }
158
159
  export interface StaticAttributeDynamicValueParams {
159
160
  attributeName: string;
160
161
  valueNodes: Node[];
161
162
  attributeNode: HTMLAttributeNode;
163
+ originalAttributeName: string;
162
164
  parentNode: HTMLOpenTagNode;
163
165
  combinedValue?: string | null;
164
166
  }
@@ -215,19 +217,19 @@ export declare abstract class AttributeVisitorMixin extends BaseRuleVisitor {
215
217
  /**
216
218
  * Static attribute name with static value: class="container"
217
219
  */
218
- protected checkStaticAttributeStaticValue(params: StaticAttributeStaticValueParams): void;
220
+ protected checkStaticAttributeStaticValue(_params: StaticAttributeStaticValueParams): void;
219
221
  /**
220
222
  * Static attribute name with dynamic value: class="<%= css_class %>"
221
223
  */
222
- protected checkStaticAttributeDynamicValue(params: StaticAttributeDynamicValueParams): void;
224
+ protected checkStaticAttributeDynamicValue(_params: StaticAttributeDynamicValueParams): void;
223
225
  /**
224
226
  * Dynamic attribute name with static value: data-<%= key %>="foo"
225
227
  */
226
- protected checkDynamicAttributeStaticValue(params: DynamicAttributeStaticValueParams): void;
228
+ protected checkDynamicAttributeStaticValue(_params: DynamicAttributeStaticValueParams): void;
227
229
  /**
228
230
  * Dynamic attribute name with dynamic value: data-<%= key %>="<%= value %>"
229
231
  */
230
- protected checkDynamicAttributeDynamicValue(params: DynamicAttributeDynamicValueParams): void;
232
+ protected checkDynamicAttributeDynamicValue(_params: DynamicAttributeDynamicValueParams): void;
231
233
  }
232
234
  /**
233
235
  * Checks if an attribute value is quoted
@@ -28,11 +28,13 @@ This page contains documentation for all Herb Linter rules.
28
28
  - [`html-no-block-inside-inline`](./html-no-block-inside-inline.md) - Prevents block-level elements inside inline elements
29
29
  - [`html-no-duplicate-attributes`](./html-no-duplicate-attributes.md) - Prevents duplicate attributes on HTML elements
30
30
  - [`html-no-duplicate-ids`](./html-no-duplicate-ids.md) - Prevents duplicate IDs within a document
31
+ - [`html-no-empty-attributes`](./html-no-empty-attributes.md) - Attributes must not have empty values
31
32
  - [`html-no-nested-links`](./html-no-nested-links.md) - Prevents nested anchor tags
32
33
  - [`html-no-positive-tab-index`](./html-no-positive-tab-index.md) - Avoid positive `tabindex` values
33
34
  - [`html-no-self-closing`](./html-no-self-closing.md.md) - Disallow self closing tags
34
35
  - [`html-no-title-attribute`](./html-no-title-attribute.md) - Avoid using the `title` attribute
35
36
  - [`html-tag-name-lowercase`](./html-tag-name-lowercase.md) - Enforces lowercase tag names in HTML
37
+ - [`html-no-underscores-in-attribute-names`](./html-no-underscores-in-attribute-names.md) - Disallow underscores in HTML attribute names
36
38
  - [`parser-no-errors`](./parser-no-errors.md) - Disallow parser errors in HTML+ERB documents
37
39
  - [`svg-tag-name-capitalization`](./svg-tag-name-capitalization.md) - Enforces proper camelCase capitalization for SVG elements
38
40
 
@@ -33,8 +33,6 @@ Omitting the `alt` attribute entirely leads to poor accessibility and can negati
33
33
 
34
34
  <img src="/avatar.jpg" alt> <!-- TODO -->
35
35
 
36
- <img src="/divider.png" alt=> <!-- TODO -->
37
-
38
36
  <%= image_tag image_path("logo.png") %> <!-- TODO -->
39
37
  ```
40
38
 
@@ -0,0 +1,77 @@
1
+ # Linter Rule: Attributes must not have empty values
2
+
3
+ **Rule:** `html-no-empty-attributes`
4
+
5
+ ## Description
6
+
7
+ Warn when certain restricted attributes are present but have an empty string as their value. These attributes are required to have meaningful values to function properly, and leaving them empty is typically either a mistake or unnecessary.
8
+
9
+ In most cases, if the value is not available, it's better to omit the attribute entirely.
10
+
11
+ ### Restricted attributes
12
+
13
+ - `id`
14
+ - `class`
15
+ - `name`
16
+ - `for`
17
+ - `src`
18
+ - `href`
19
+ - `title`
20
+ - `data`
21
+ - `role`
22
+ - `data-*`
23
+ - `aria-*`
24
+
25
+ ## Rationale
26
+
27
+ Many HTML attributes are only useful when they carry a value. Leaving these attributes empty can:
28
+
29
+ - Produce confusing or misleading markup (e.g., `id=""`, `class=""`)
30
+ - Create inaccessible or invalid HTML
31
+ - Interfere with CSS or JS selectors expecting meaningful values
32
+ - Indicate unused or unfinished logic in ERB
33
+
34
+ This rule helps ensure that required attributes are only added when they are populated.
35
+
36
+ ## Examples
37
+
38
+ ### ✅ Good
39
+
40
+ ```erb
41
+ <div id="header"></div>
42
+ <img src="/logo.png" alt="Company logo">
43
+ <input type="text" name="email">
44
+
45
+ <!-- Dynamic attributes with meaningful values -->
46
+ <div data-<%= key %>="<%= value %>" aria-<%= prop %>="<%= description %>">
47
+ Dynamic content
48
+ </div>
49
+
50
+ <!-- if no class should be set, omit it completely -->
51
+ <div>Plain div</div>
52
+ ```
53
+
54
+ ### 🚫 Bad
55
+
56
+ ```erb
57
+ <div id=""></div>
58
+ <img src="">
59
+ <input name="">
60
+
61
+ <div data-config="">Content</div>
62
+ <button aria-label="">×</button>
63
+
64
+ <div class="">Plain div</div>
65
+
66
+ <!-- Dynamic attribute names with empty static values -->
67
+ <div data-<%= key %>="" aria-<%= prop %>=" ">
68
+ Problematic dynamic attributes
69
+ </div>
70
+ ```
71
+
72
+ ## References
73
+
74
+ - [HTML Living Standard - Global attributes](https://html.spec.whatwg.org/multipage/dom.html#global-attributes)
75
+ - [WCAG 2.2 - Text Alternatives](https://www.w3.org/WAI/WCAG22/Understanding/text-alternatives.html)
76
+ - [MDN - HTML attribute reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes)
77
+ - [ARIA in HTML](https://www.w3.org/TR/html-aria/)
@@ -0,0 +1,45 @@
1
+ # Linter Rule: No underscores on attribute names
2
+
3
+ **Rule:** `html-no-underscores-in-attribute-names`
4
+
5
+ ## Description
6
+
7
+ ---
8
+
9
+ Warn when an HTML attribute name contains an underscore (`_`). According to the HTML specification, attribute names should use only lowercase letters, digits, hyphens (`-`), and colons (`:`) in specific namespaces (e.g., `xlink:href` in SVG). Underscores are not valid in standard HTML attribute names and may lead to unpredictable behavior or be ignored by browsers entirely.
10
+
11
+ ## Rationale
12
+
13
+ ---
14
+
15
+ Underscores in attribute names violate the HTML specification and are not supported in standard markup. Their use is almost always accidental (e.g., mistyping `data-attr_name` instead of `data-attr-name`) or stems from inconsistent naming conventions across backend or templating layers.
16
+
17
+ ## Examples
18
+
19
+ ---
20
+
21
+ ✅ Good
22
+
23
+ ```html
24
+ <div data-user-id="123"></div>
25
+
26
+ <img aria-label="Close">
27
+
28
+ <div data-<%= key %>-attribute="value"></div>
29
+ ```
30
+
31
+ 🚫 Bad
32
+
33
+ ```html
34
+ <div data_user_id="123"></div>
35
+
36
+ <img aria_label="Close">
37
+
38
+ <div data-<%= key %>_attribute="value"></div>
39
+ ```
40
+
41
+ ## References
42
+
43
+ ---
44
+
45
+ \-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/linter",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
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",
@@ -21,6 +21,7 @@
21
21
  "build": "yarn clean && tsc -b && rollup -c",
22
22
  "watch": "tsc -b -w",
23
23
  "test": "vitest run",
24
+ "test:watch": "vitest --watch",
24
25
  "prepublishOnly": "yarn clean && yarn build && yarn test"
25
26
  },
26
27
  "exports": {
@@ -30,13 +31,18 @@
30
31
  "import": "./dist/index.js",
31
32
  "require": "./dist/index.cjs",
32
33
  "default": "./dist/index.js"
34
+ },
35
+ "./cli": {
36
+ "types": "./dist/types/src/cli.d.ts",
37
+ "require": "./dist/src/cli.js",
38
+ "default": "./dist/src/cli.js"
33
39
  }
34
40
  },
35
41
  "dependencies": {
36
- "@herb-tools/core": "0.6.1",
37
- "@herb-tools/highlighter": "0.6.1",
38
- "@herb-tools/node-wasm": "0.6.1",
39
- "@herb-tools/printer": "0.6.1",
42
+ "@herb-tools/core": "0.7.0",
43
+ "@herb-tools/highlighter": "0.7.0",
44
+ "@herb-tools/node-wasm": "0.7.0",
45
+ "@herb-tools/printer": "0.7.0",
40
46
  "glob": "^11.0.3"
41
47
  },
42
48
  "files": [
@@ -11,7 +11,7 @@ import type { ThemeInput } from "@herb-tools/highlighter"
11
11
 
12
12
  import { name, version } from "../../package.json"
13
13
 
14
- export type FormatOption = "simple" | "detailed" | "json" | "github"
14
+ export type FormatOption = "simple" | "detailed" | "json"
15
15
 
16
16
  export interface ParsedArguments {
17
17
  pattern: string
@@ -20,6 +20,7 @@ export interface ParsedArguments {
20
20
  theme: ThemeInput
21
21
  wrapLines: boolean
22
22
  truncateLines: boolean
23
+ useGitHubActions: boolean
23
24
  }
24
25
 
25
26
  export class ArgumentParser {
@@ -34,10 +35,11 @@ export class ArgumentParser {
34
35
  Options:
35
36
  -h, --help show help
36
37
  -v, --version show version
37
- --format output format (simple|detailed|json|github) [default: detailed]
38
+ --format output format (simple|detailed|json) [default: detailed]
38
39
  --simple use simple output format (shortcut for --format simple)
39
40
  --json use JSON output format (shortcut for --format json)
40
- --github use GitHub Actions output format (shortcut for --format github)
41
+ --github enable GitHub Actions annotations (combines with --format)
42
+ --no-github disable GitHub Actions annotations (even in GitHub Actions environment)
41
43
  --theme syntax highlighting theme (${THEME_NAMES.join("|")}) or path to custom theme file [default: ${DEFAULT_THEME}]
42
44
  --no-color disable colored output
43
45
  --no-timing hide timing information
@@ -55,6 +57,7 @@ export class ArgumentParser {
55
57
  simple: { type: "boolean" },
56
58
  json: { type: "boolean" },
57
59
  github: { type: "boolean" },
60
+ "no-github": { type: "boolean" },
58
61
  theme: { type: "string" },
59
62
  "no-color": { type: "boolean" },
60
63
  "no-timing": { type: "boolean" },
@@ -75,8 +78,10 @@ export class ArgumentParser {
75
78
  process.exit(0)
76
79
  }
77
80
 
81
+ const isGitHubActions = process.env.GITHUB_ACTIONS === "true"
82
+
78
83
  let formatOption: FormatOption = "detailed"
79
- if (values.format && (values.format === "detailed" || values.format === "simple" || values.format === "json" || values.format === "github")) {
84
+ if (values.format && (values.format === "detailed" || values.format === "simple" || values.format === "json")) {
80
85
  formatOption = values.format
81
86
  }
82
87
 
@@ -88,8 +93,11 @@ export class ArgumentParser {
88
93
  formatOption = "json"
89
94
  }
90
95
 
91
- if (values.github) {
92
- formatOption = "github"
96
+ const useGitHubActions = (values.github || isGitHubActions) && !values["no-github"]
97
+
98
+ if (useGitHubActions && formatOption === "json") {
99
+ console.error("Error: --github cannot be used with --json format. JSON format is already structured for programmatic consumption.")
100
+ process.exit(1)
93
101
  }
94
102
 
95
103
  if (values["no-color"]) {
@@ -114,7 +122,7 @@ export class ArgumentParser {
114
122
  const theme = values.theme || DEFAULT_THEME
115
123
  const pattern = this.getFilePattern(positionals)
116
124
 
117
- return { pattern, formatOption, showTiming, theme, wrapLines, truncateLines }
125
+ return { pattern, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions }
118
126
  }
119
127
 
120
128
  private getFilePattern(positionals: string[]): string {