@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.
- package/README.md +60 -16
- package/dist/herb-lint.js +364 -181
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +321 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +270 -89
- package/dist/index.js.map +1 -1
- package/dist/package.json +11 -5
- package/dist/src/cli/argument-parser.js +11 -6
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +5 -6
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli/formatters/detailed-formatter.js +3 -5
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
- package/dist/src/cli/formatters/github-actions-formatter.js +55 -11
- package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
- package/dist/src/cli/index.js +1 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/output-manager.js +23 -5
- package/dist/src/cli/output-manager.js.map +1 -1
- package/dist/src/cli/summary-reporter.js +2 -11
- package/dist/src/cli/summary-reporter.js.map +1 -1
- package/dist/src/cli.js +88 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/default-rules.js +8 -4
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/linter.js.map +1 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js +1 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
- package/dist/src/rules/html-boolean-attributes-no-value.js +8 -8
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-no-empty-attributes.js +56 -0
- package/dist/src/rules/html-no-empty-attributes.js.map +1 -0
- package/dist/src/rules/html-no-positive-tab-index.js +1 -1
- package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
- package/dist/src/rules/html-no-underscores-in-attribute-names.js +36 -0
- package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -0
- package/dist/src/rules/index.js +3 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/rule-utils.js +11 -7
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +2 -2
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/cli/argument-parser.d.ts +2 -1
- package/dist/types/cli/file-processor.d.ts +6 -1
- package/dist/types/cli/formatters/github-actions-formatter.d.ts +6 -1
- package/dist/types/cli/index.d.ts +1 -0
- package/dist/types/cli/output-manager.d.ts +1 -0
- package/dist/types/cli.d.ts +20 -5
- package/dist/types/linter.d.ts +7 -7
- package/dist/types/rules/html-no-empty-attributes.d.ts +7 -0
- package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
- package/dist/types/rules/index.d.ts +3 -0
- package/dist/types/rules/rule-utils.d.ts +7 -5
- package/dist/types/src/cli/argument-parser.d.ts +2 -1
- package/dist/types/src/cli/file-processor.d.ts +6 -1
- package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +6 -1
- package/dist/types/src/cli/index.d.ts +1 -0
- package/dist/types/src/cli/output-manager.d.ts +1 -0
- package/dist/types/src/cli.d.ts +20 -5
- package/dist/types/src/linter.d.ts +7 -7
- package/dist/types/src/rules/html-no-empty-attributes.d.ts +7 -0
- package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
- package/dist/types/src/rules/index.d.ts +3 -0
- package/dist/types/src/rules/rule-utils.d.ts +7 -5
- package/docs/rules/README.md +2 -0
- package/docs/rules/html-img-require-alt.md +0 -2
- package/docs/rules/html-no-empty-attributes.md +77 -0
- package/docs/rules/html-no-underscores-in-attribute-names.md +45 -0
- package/package.json +11 -5
- package/src/cli/argument-parser.ts +15 -7
- package/src/cli/file-processor.ts +11 -7
- package/src/cli/formatters/detailed-formatter.ts +5 -7
- package/src/cli/formatters/github-actions-formatter.ts +64 -11
- package/src/cli/index.ts +2 -0
- package/src/cli/output-manager.ts +27 -5
- package/src/cli/summary-reporter.ts +3 -11
- package/src/cli.ts +125 -20
- package/src/default-rules.ts +8 -4
- package/src/linter.ts +6 -6
- package/src/rules/erb-no-silent-tag-in-attribute-name.ts +1 -1
- package/src/rules/erb-prefer-image-tag-helper.ts +2 -2
- package/src/rules/erb-require-whitespace-inside-tags.ts +2 -2
- package/src/rules/html-attribute-double-quotes.ts +1 -1
- package/src/rules/html-boolean-attributes-no-value.ts +9 -11
- package/src/rules/html-no-empty-attributes.ts +75 -0
- package/src/rules/html-no-positive-tab-index.ts +1 -1
- package/src/rules/html-no-underscores-in-attribute-names.ts +58 -0
- package/src/rules/html-tag-name-lowercase.ts +1 -1
- package/src/rules/index.ts +3 -0
- package/src/rules/rule-utils.ts +15 -11
- package/src/rules/svg-tag-name-capitalization.ts +2 -2
package/dist/types/linter.d.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
220
|
+
protected checkStaticAttributeStaticValue(_params: StaticAttributeStaticValueParams): void;
|
|
219
221
|
/**
|
|
220
222
|
* Static attribute name with dynamic value: class="<%= css_class %>"
|
|
221
223
|
*/
|
|
222
|
-
protected checkStaticAttributeDynamicValue(
|
|
224
|
+
protected checkStaticAttributeDynamicValue(_params: StaticAttributeDynamicValueParams): void;
|
|
223
225
|
/**
|
|
224
226
|
* Dynamic attribute name with static value: data-<%= key %>="foo"
|
|
225
227
|
*/
|
|
226
|
-
protected checkDynamicAttributeStaticValue(
|
|
228
|
+
protected checkDynamicAttributeStaticValue(_params: DynamicAttributeStaticValueParams): void;
|
|
227
229
|
/**
|
|
228
230
|
* Dynamic attribute name with dynamic value: data-<%= key %>="<%= value %>"
|
|
229
231
|
*/
|
|
230
|
-
protected checkDynamicAttributeDynamicValue(
|
|
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"
|
|
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;
|
package/dist/types/src/cli.d.ts
CHANGED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
220
|
+
protected checkStaticAttributeStaticValue(_params: StaticAttributeStaticValueParams): void;
|
|
219
221
|
/**
|
|
220
222
|
* Static attribute name with dynamic value: class="<%= css_class %>"
|
|
221
223
|
*/
|
|
222
|
-
protected checkStaticAttributeDynamicValue(
|
|
224
|
+
protected checkStaticAttributeDynamicValue(_params: StaticAttributeDynamicValueParams): void;
|
|
223
225
|
/**
|
|
224
226
|
* Dynamic attribute name with static value: data-<%= key %>="foo"
|
|
225
227
|
*/
|
|
226
|
-
protected checkDynamicAttributeStaticValue(
|
|
228
|
+
protected checkDynamicAttributeStaticValue(_params: DynamicAttributeStaticValueParams): void;
|
|
227
229
|
/**
|
|
228
230
|
* Dynamic attribute name with dynamic value: data-<%= key %>="<%= value %>"
|
|
229
231
|
*/
|
|
230
|
-
protected checkDynamicAttributeDynamicValue(
|
|
232
|
+
protected checkDynamicAttributeDynamicValue(_params: DynamicAttributeDynamicValueParams): void;
|
|
231
233
|
}
|
|
232
234
|
/**
|
|
233
235
|
* Checks if an attribute value is quoted
|
package/docs/rules/README.md
CHANGED
|
@@ -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
|
|
|
@@ -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.
|
|
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.
|
|
37
|
-
"@herb-tools/highlighter": "0.
|
|
38
|
-
"@herb-tools/node-wasm": "0.
|
|
39
|
-
"@herb-tools/printer": "0.
|
|
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"
|
|
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
|
|
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
|
|
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"
|
|
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
|
-
|
|
92
|
-
|
|
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 {
|