@herb-tools/linter 0.8.6 → 0.8.8
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 +54 -2
- package/dist/herb-lint.js +17157 -31275
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +473 -2113
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +468 -2115
- package/dist/index.js.map +1 -1
- package/dist/loader.cjs +6868 -11350
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.js +6862 -11351
- package/dist/loader.js.map +1 -1
- package/dist/package.json +9 -8
- package/dist/src/cli/argument-parser.js +18 -2
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +1 -1
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli.js +25 -10
- package/dist/src/cli.js.map +1 -1
- package/dist/src/custom-rule-loader.js +2 -2
- package/dist/src/custom-rule-loader.js.map +1 -1
- package/dist/src/linter.js +16 -3
- package/dist/src/linter.js.map +1 -1
- package/dist/src/rules/erb-strict-locals-comment-syntax.js +206 -0
- package/dist/src/rules/erb-strict-locals-comment-syntax.js.map +1 -0
- package/dist/src/rules/erb-strict-locals-required.js +38 -0
- package/dist/src/rules/erb-strict-locals-required.js.map +1 -0
- package/dist/src/rules/file-utils.js +21 -0
- package/dist/src/rules/file-utils.js.map +1 -0
- package/dist/src/rules/html-head-only-elements.js +2 -0
- package/dist/src/rules/html-head-only-elements.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-attributes.js +91 -21
- package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
- package/dist/src/rules/html-no-empty-headings.js +22 -36
- package/dist/src/rules/html-no-empty-headings.js.map +1 -1
- package/dist/src/rules/index.js +4 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/string-utils.js +72 -0
- package/dist/src/rules/string-utils.js.map +1 -0
- package/dist/src/rules.js +4 -0
- package/dist/src/rules.js.map +1 -1
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/cli/argument-parser.d.ts +3 -0
- package/dist/types/cli/file-processor.d.ts +1 -0
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/linter.d.ts +5 -1
- package/dist/types/rules/erb-strict-locals-comment-syntax.d.ts +9 -0
- package/dist/types/rules/erb-strict-locals-required.d.ts +9 -0
- package/dist/types/rules/file-utils.d.ts +13 -0
- package/dist/types/rules/index.d.ts +4 -0
- package/dist/types/rules/string-utils.d.ts +15 -0
- package/dist/types/src/cli/argument-parser.d.ts +3 -0
- package/dist/types/src/cli/file-processor.d.ts +1 -0
- package/dist/types/src/cli.d.ts +1 -1
- package/dist/types/src/linter.d.ts +5 -1
- package/dist/types/src/rules/erb-strict-locals-comment-syntax.d.ts +9 -0
- package/dist/types/src/rules/erb-strict-locals-required.d.ts +9 -0
- package/dist/types/src/rules/file-utils.d.ts +13 -0
- package/dist/types/src/rules/index.d.ts +4 -0
- package/dist/types/src/rules/string-utils.d.ts +15 -0
- package/dist/types/src/types.d.ts +6 -0
- package/dist/types/types.d.ts +6 -0
- package/docs/rules/README.md +1 -0
- package/docs/rules/erb-strict-locals-comment-syntax.md +153 -0
- package/docs/rules/erb-strict-locals-required.md +107 -0
- package/package.json +9 -8
- package/src/cli/argument-parser.ts +21 -2
- package/src/cli/file-processor.ts +2 -1
- package/src/cli.ts +34 -11
- package/src/custom-rule-loader.ts +2 -2
- package/src/linter.ts +19 -3
- package/src/rules/erb-strict-locals-comment-syntax.ts +274 -0
- package/src/rules/erb-strict-locals-required.ts +52 -0
- package/src/rules/file-utils.ts +23 -0
- package/src/rules/html-head-only-elements.ts +1 -0
- package/src/rules/html-no-duplicate-attributes.ts +141 -26
- package/src/rules/html-no-empty-headings.ts +21 -44
- package/src/rules/index.ts +4 -0
- package/src/rules/string-utils.ts +72 -0
- package/src/rules.ts +4 -0
- package/src/types.ts +6 -0
package/dist/types/linter.d.ts
CHANGED
|
@@ -123,7 +123,11 @@ export declare class Linter {
|
|
|
123
123
|
* @param source - The source code to fix
|
|
124
124
|
* @param context - Optional context for linting (e.g., fileName)
|
|
125
125
|
* @param offensesToFix - Optional array of specific offenses to fix. If not provided, all fixable offenses will be fixed.
|
|
126
|
+
* @param options - Options for autofix behavior
|
|
127
|
+
* @param options.includeUnsafe - If true, also apply unsafe fixes (rules with unsafeAutocorrectable = true)
|
|
126
128
|
* @returns AutofixResult containing the corrected source and lists of fixed/unfixed offenses
|
|
127
129
|
*/
|
|
128
|
-
autofix(source: string, context?: Partial<LintContext>, offensesToFix?: LintOffense[]
|
|
130
|
+
autofix(source: string, context?: Partial<LintContext>, offensesToFix?: LintOffense[], options?: {
|
|
131
|
+
includeUnsafe?: boolean;
|
|
132
|
+
}): AutofixResult;
|
|
129
133
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js";
|
|
2
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js";
|
|
3
|
+
import type { ParseResult } from "@herb-tools/core";
|
|
4
|
+
export declare const STRICT_LOCALS_PATTERN: RegExp;
|
|
5
|
+
export declare class ERBStrictLocalsCommentSyntaxRule extends ParserRule {
|
|
6
|
+
name: string;
|
|
7
|
+
get defaultConfig(): FullRuleConfig;
|
|
8
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[];
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SourceRule } from "../types.js";
|
|
2
|
+
import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js";
|
|
3
|
+
export declare class ERBStrictLocalsRequiredRule extends SourceRule {
|
|
4
|
+
static unsafeAutocorrectable: boolean;
|
|
5
|
+
name: string;
|
|
6
|
+
get defaultConfig(): FullRuleConfig;
|
|
7
|
+
check(source: string, context?: Partial<LintContext>): UnboundLintOffense[];
|
|
8
|
+
autofix(_offense: LintOffense, source: string, _context?: Partial<LintContext>): string | null;
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File path and naming utilities for linter rules
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Extracts the basename (filename) from a file path
|
|
6
|
+
* Works with both forward slashes and backslashes
|
|
7
|
+
*/
|
|
8
|
+
export declare function getBasename(filePath: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a file is a Rails partial (filename starts with `_`)
|
|
11
|
+
* Returns null if fileName is undefined (unknown context)
|
|
12
|
+
*/
|
|
13
|
+
export declare function isPartialFile(fileName: string | undefined): boolean | null;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "./rule-utils.js";
|
|
2
|
+
export * from "./file-utils.js";
|
|
3
|
+
export * from "./string-utils.js";
|
|
2
4
|
export * from "./herb-disable-comment-base.js";
|
|
3
5
|
export * from "./erb-comment-syntax.js";
|
|
4
6
|
export * from "./erb-no-case-node-children.js";
|
|
@@ -11,6 +13,8 @@ export * from "./erb-prefer-image-tag-helper.js";
|
|
|
11
13
|
export * from "./erb-require-trailing-newline.js";
|
|
12
14
|
export * from "./erb-require-whitespace-inside-tags.js";
|
|
13
15
|
export * from "./erb-right-trim.js";
|
|
16
|
+
export * from "./erb-strict-locals-comment-syntax.js";
|
|
17
|
+
export * from "./erb-strict-locals-required.js";
|
|
14
18
|
export * from "./herb-disable-comment-valid-rule-name.js";
|
|
15
19
|
export * from "./herb-disable-comment-no-redundant-all.js";
|
|
16
20
|
export * from "./herb-disable-comment-no-duplicate-rules.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if parentheses in a string are balanced
|
|
3
|
+
* Returns false if there are more closing parens than opening at any point
|
|
4
|
+
*/
|
|
5
|
+
export declare function hasBalancedParentheses(content: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Splits a string by commas at the top level only
|
|
8
|
+
* Respects nested parentheses, brackets, braces, and strings
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* splitByTopLevelComma("a, b, c") // ["a", " b", " c"]
|
|
12
|
+
* splitByTopLevelComma("a, (b, c), d") // ["a", " (b, c)", " d"]
|
|
13
|
+
* splitByTopLevelComma('a, "b, c", d') // ["a", ' "b, c"', " d"]
|
|
14
|
+
*/
|
|
15
|
+
export declare function splitByTopLevelComma(str: string): string[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ThemeInput } from "@herb-tools/highlighter";
|
|
2
|
+
import type { DiagnosticSeverity } from "@herb-tools/core";
|
|
2
3
|
export type FormatOption = "simple" | "detailed" | "json";
|
|
3
4
|
export interface ParsedArguments {
|
|
4
5
|
patterns: string[];
|
|
@@ -10,10 +11,12 @@ export interface ParsedArguments {
|
|
|
10
11
|
truncateLines: boolean;
|
|
11
12
|
useGitHubActions: boolean;
|
|
12
13
|
fix: boolean;
|
|
14
|
+
fixUnsafe: boolean;
|
|
13
15
|
ignoreDisableComments: boolean;
|
|
14
16
|
force: boolean;
|
|
15
17
|
init: boolean;
|
|
16
18
|
loadCustomRules: boolean;
|
|
19
|
+
failLevel?: DiagnosticSeverity;
|
|
17
20
|
}
|
|
18
21
|
export declare class ArgumentParser {
|
|
19
22
|
private readonly usage;
|
package/dist/types/src/cli.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare class CLI {
|
|
|
17
17
|
showTiming: boolean;
|
|
18
18
|
}): void;
|
|
19
19
|
protected determineProjectPath(patterns: string[]): void;
|
|
20
|
-
protected adjustPattern(pattern: string | undefined,
|
|
20
|
+
protected adjustPattern(pattern: string | undefined, configGlobPatterns: string[]): string;
|
|
21
21
|
protected resolvePatternToFiles(pattern: string, config: Config, force: boolean): Promise<{
|
|
22
22
|
files: string[];
|
|
23
23
|
explicitFile: string | undefined;
|
|
@@ -123,7 +123,11 @@ export declare class Linter {
|
|
|
123
123
|
* @param source - The source code to fix
|
|
124
124
|
* @param context - Optional context for linting (e.g., fileName)
|
|
125
125
|
* @param offensesToFix - Optional array of specific offenses to fix. If not provided, all fixable offenses will be fixed.
|
|
126
|
+
* @param options - Options for autofix behavior
|
|
127
|
+
* @param options.includeUnsafe - If true, also apply unsafe fixes (rules with unsafeAutocorrectable = true)
|
|
126
128
|
* @returns AutofixResult containing the corrected source and lists of fixed/unfixed offenses
|
|
127
129
|
*/
|
|
128
|
-
autofix(source: string, context?: Partial<LintContext>, offensesToFix?: LintOffense[]
|
|
130
|
+
autofix(source: string, context?: Partial<LintContext>, offensesToFix?: LintOffense[], options?: {
|
|
131
|
+
includeUnsafe?: boolean;
|
|
132
|
+
}): AutofixResult;
|
|
129
133
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js";
|
|
2
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js";
|
|
3
|
+
import type { ParseResult } from "@herb-tools/core";
|
|
4
|
+
export declare const STRICT_LOCALS_PATTERN: RegExp;
|
|
5
|
+
export declare class ERBStrictLocalsCommentSyntaxRule extends ParserRule {
|
|
6
|
+
name: string;
|
|
7
|
+
get defaultConfig(): FullRuleConfig;
|
|
8
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[];
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SourceRule } from "../types.js";
|
|
2
|
+
import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js";
|
|
3
|
+
export declare class ERBStrictLocalsRequiredRule extends SourceRule {
|
|
4
|
+
static unsafeAutocorrectable: boolean;
|
|
5
|
+
name: string;
|
|
6
|
+
get defaultConfig(): FullRuleConfig;
|
|
7
|
+
check(source: string, context?: Partial<LintContext>): UnboundLintOffense[];
|
|
8
|
+
autofix(_offense: LintOffense, source: string, _context?: Partial<LintContext>): string | null;
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File path and naming utilities for linter rules
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Extracts the basename (filename) from a file path
|
|
6
|
+
* Works with both forward slashes and backslashes
|
|
7
|
+
*/
|
|
8
|
+
export declare function getBasename(filePath: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a file is a Rails partial (filename starts with `_`)
|
|
11
|
+
* Returns null if fileName is undefined (unknown context)
|
|
12
|
+
*/
|
|
13
|
+
export declare function isPartialFile(fileName: string | undefined): boolean | null;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "./rule-utils.js";
|
|
2
|
+
export * from "./file-utils.js";
|
|
3
|
+
export * from "./string-utils.js";
|
|
2
4
|
export * from "./herb-disable-comment-base.js";
|
|
3
5
|
export * from "./erb-comment-syntax.js";
|
|
4
6
|
export * from "./erb-no-case-node-children.js";
|
|
@@ -11,6 +13,8 @@ export * from "./erb-prefer-image-tag-helper.js";
|
|
|
11
13
|
export * from "./erb-require-trailing-newline.js";
|
|
12
14
|
export * from "./erb-require-whitespace-inside-tags.js";
|
|
13
15
|
export * from "./erb-right-trim.js";
|
|
16
|
+
export * from "./erb-strict-locals-comment-syntax.js";
|
|
17
|
+
export * from "./erb-strict-locals-required.js";
|
|
14
18
|
export * from "./herb-disable-comment-valid-rule-name.js";
|
|
15
19
|
export * from "./herb-disable-comment-no-redundant-all.js";
|
|
16
20
|
export * from "./herb-disable-comment-no-duplicate-rules.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if parentheses in a string are balanced
|
|
3
|
+
* Returns false if there are more closing parens than opening at any point
|
|
4
|
+
*/
|
|
5
|
+
export declare function hasBalancedParentheses(content: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Splits a string by commas at the top level only
|
|
8
|
+
* Respects nested parentheses, brackets, braces, and strings
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* splitByTopLevelComma("a, b, c") // ["a", " b", " c"]
|
|
12
|
+
* splitByTopLevelComma("a, (b, c), d") // ["a", " (b, c)", " d"]
|
|
13
|
+
* splitByTopLevelComma('a, "b, c", d') // ["a", ' "b, c"', " d"]
|
|
14
|
+
*/
|
|
15
|
+
export declare function splitByTopLevelComma(str: string): string[];
|
|
@@ -68,6 +68,8 @@ export declare abstract class ParserRule<TAutofixContext extends BaseAutofixCont
|
|
|
68
68
|
static type: "parser";
|
|
69
69
|
/** Indicates whether this rule supports autofix. Defaults to false. */
|
|
70
70
|
static autocorrectable: boolean;
|
|
71
|
+
/** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
|
|
72
|
+
static unsafeAutocorrectable: boolean;
|
|
71
73
|
abstract name: string;
|
|
72
74
|
get defaultConfig(): FullRuleConfig;
|
|
73
75
|
abstract check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
|
|
@@ -96,6 +98,8 @@ export declare abstract class LexerRule<TAutofixContext extends BaseAutofixConte
|
|
|
96
98
|
static type: "lexer";
|
|
97
99
|
/** Indicates whether this rule supports autofix. Defaults to false. */
|
|
98
100
|
static autocorrectable: boolean;
|
|
101
|
+
/** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
|
|
102
|
+
static unsafeAutocorrectable: boolean;
|
|
99
103
|
abstract name: string;
|
|
100
104
|
get defaultConfig(): FullRuleConfig;
|
|
101
105
|
abstract check(lexResult: LexResult, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
|
|
@@ -139,6 +143,8 @@ export declare abstract class SourceRule<TAutofixContext extends BaseAutofixCont
|
|
|
139
143
|
static type: "source";
|
|
140
144
|
/** Indicates whether this rule supports autofix. Defaults to false. */
|
|
141
145
|
static autocorrectable: boolean;
|
|
146
|
+
/** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
|
|
147
|
+
static unsafeAutocorrectable: boolean;
|
|
142
148
|
abstract name: string;
|
|
143
149
|
get defaultConfig(): FullRuleConfig;
|
|
144
150
|
abstract check(source: string, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
|
package/dist/types/types.d.ts
CHANGED
|
@@ -68,6 +68,8 @@ export declare abstract class ParserRule<TAutofixContext extends BaseAutofixCont
|
|
|
68
68
|
static type: "parser";
|
|
69
69
|
/** Indicates whether this rule supports autofix. Defaults to false. */
|
|
70
70
|
static autocorrectable: boolean;
|
|
71
|
+
/** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
|
|
72
|
+
static unsafeAutocorrectable: boolean;
|
|
71
73
|
abstract name: string;
|
|
72
74
|
get defaultConfig(): FullRuleConfig;
|
|
73
75
|
abstract check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
|
|
@@ -96,6 +98,8 @@ export declare abstract class LexerRule<TAutofixContext extends BaseAutofixConte
|
|
|
96
98
|
static type: "lexer";
|
|
97
99
|
/** Indicates whether this rule supports autofix. Defaults to false. */
|
|
98
100
|
static autocorrectable: boolean;
|
|
101
|
+
/** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
|
|
102
|
+
static unsafeAutocorrectable: boolean;
|
|
99
103
|
abstract name: string;
|
|
100
104
|
get defaultConfig(): FullRuleConfig;
|
|
101
105
|
abstract check(lexResult: LexResult, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
|
|
@@ -139,6 +143,8 @@ export declare abstract class SourceRule<TAutofixContext extends BaseAutofixCont
|
|
|
139
143
|
static type: "source";
|
|
140
144
|
/** Indicates whether this rule supports autofix. Defaults to false. */
|
|
141
145
|
static autocorrectable: boolean;
|
|
146
|
+
/** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
|
|
147
|
+
static unsafeAutocorrectable: boolean;
|
|
142
148
|
abstract name: string;
|
|
143
149
|
get defaultConfig(): FullRuleConfig;
|
|
144
150
|
abstract check(source: string, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
|
package/docs/rules/README.md
CHANGED
|
@@ -15,6 +15,7 @@ This page contains documentation for all Herb Linter rules.
|
|
|
15
15
|
- [`erb-require-whitespace-inside-tags`](./erb-require-whitespace-inside-tags.md) - Requires whitespace around ERB tags
|
|
16
16
|
- [`erb-require-trailing-newline`](./erb-require-trailing-newline.md) - Enforces that all HTML+ERB template files end with exactly one trailing newline character.
|
|
17
17
|
- [`erb-right-trim`](./erb-right-trim.md) - Enforce consistent right-trimming syntax.
|
|
18
|
+
- [`erb-strict-locals-comment-syntax`](./erb-strict-locals-comment-syntax.md) - Enforce strict locals comment syntax.
|
|
18
19
|
- [`herb-disable-comment-malformed`](./herb-disable-comment-malformed.md) - Detect malformed `herb:disable` comments.
|
|
19
20
|
- [`herb-disable-comment-missing-rules`](./herb-disable-comment-missing-rules.md) - Require rule names in `herb:disable` comments.
|
|
20
21
|
- [`herb-disable-comment-no-duplicate-rules`](./herb-disable-comment-no-duplicate-rules.md) - Disallow duplicate rule names in `herb:disable` comments.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Linter Rule: Enforce strict locals comment syntax
|
|
2
|
+
|
|
3
|
+
**Rule:** `erb-strict-locals-comment-syntax`
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
Ensures that strict locals comments use the exact `locals: ( ... )` syntax so they are properly recognized by Rails and tooling. Also validates that only keyword arguments are used (no positional, block, or splat arguments).
|
|
8
|
+
|
|
9
|
+
## Rationale
|
|
10
|
+
|
|
11
|
+
Strict locals comments declare which locals are expected in a template. Misspellings or malformed syntax silently disable the declaration, leading to confusing runtime errors when required locals are missing.
|
|
12
|
+
|
|
13
|
+
Additionally, Rails only supports keyword arguments in strict locals declarations. Positional, block, and splat arguments will raise an `ActionView::Error` at render-time.
|
|
14
|
+
|
|
15
|
+
This rule catches invalid comment forms and argument types early during development.
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
### ✅ Good
|
|
20
|
+
|
|
21
|
+
Required keyword argument:
|
|
22
|
+
|
|
23
|
+
```erb
|
|
24
|
+
<%# locals: (user:) %>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Keyword argument with default value:
|
|
28
|
+
|
|
29
|
+
```erb
|
|
30
|
+
<%# locals: (user:, admin: false) %>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Complex default values:
|
|
34
|
+
|
|
35
|
+
```erb
|
|
36
|
+
<%# locals: (items: [], config: {}) %>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
No locals (empty):
|
|
40
|
+
|
|
41
|
+
```erb
|
|
42
|
+
<%# locals: () %>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Double-splat for optional keyword arguments:
|
|
46
|
+
|
|
47
|
+
```erb
|
|
48
|
+
<%# locals: (message: "Hello", **attributes) %>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 🚫 Bad
|
|
52
|
+
|
|
53
|
+
#### Wrong comment syntax
|
|
54
|
+
|
|
55
|
+
Missing colon after `locals`:
|
|
56
|
+
|
|
57
|
+
```erb
|
|
58
|
+
<%# locals() %>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Singular `local` instead of `locals`:
|
|
62
|
+
|
|
63
|
+
```erb
|
|
64
|
+
<%# local: (user:) %>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Missing colon before parentheses:
|
|
68
|
+
|
|
69
|
+
```erb
|
|
70
|
+
<%# locals (user:) %>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Missing parentheses around parameters:
|
|
74
|
+
|
|
75
|
+
```erb
|
|
76
|
+
<%# locals: user %>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Empty `locals:` without parentheses:
|
|
80
|
+
|
|
81
|
+
```erb
|
|
82
|
+
<%# locals: %>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Unbalanced parentheses:
|
|
86
|
+
|
|
87
|
+
```erb
|
|
88
|
+
<%# locals: (user: %>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Wrong tag type (must use ERB comment tag)
|
|
92
|
+
|
|
93
|
+
Ruby comment in execution tag:
|
|
94
|
+
|
|
95
|
+
```erb
|
|
96
|
+
<% # locals: (user:) %>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Unsupported argument types
|
|
100
|
+
|
|
101
|
+
Positional argument (use `user:` instead):
|
|
102
|
+
|
|
103
|
+
```erb
|
|
104
|
+
<%# locals: (user) %>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Block argument:
|
|
108
|
+
|
|
109
|
+
```erb
|
|
110
|
+
<%# locals: (&block) %>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Single splat argument:
|
|
114
|
+
|
|
115
|
+
```erb
|
|
116
|
+
<%# locals: (*args) %>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Note: Double-splat (`**attributes`) IS supported for optional keyword arguments.
|
|
120
|
+
|
|
121
|
+
#### Invalid Ruby syntax
|
|
122
|
+
|
|
123
|
+
Trailing comma:
|
|
124
|
+
|
|
125
|
+
```erb
|
|
126
|
+
<%# locals: (user:,) %>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Leading comma:
|
|
130
|
+
|
|
131
|
+
```erb
|
|
132
|
+
<%# locals: (, user:) %>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Double comma:
|
|
136
|
+
|
|
137
|
+
```erb
|
|
138
|
+
<%# locals: (user:,, admin:) %>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Duplicate declarations
|
|
142
|
+
|
|
143
|
+
Only one `locals:` comment is allowed per partial:
|
|
144
|
+
|
|
145
|
+
```erb
|
|
146
|
+
<%# locals: (user:) %>
|
|
147
|
+
<p>Content</p>
|
|
148
|
+
<%# locals: (admin:) %>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## References
|
|
152
|
+
|
|
153
|
+
- [Action View - Strict Locals](https://guides.rubyonrails.org/action_view_overview.html#strict-locals)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Linter Rule: Require strict locals in Rails partials
|
|
2
|
+
|
|
3
|
+
**Rule:** `erb-strict-locals-required`
|
|
4
|
+
|
|
5
|
+
**Default:** Disabled (opt-in)
|
|
6
|
+
|
|
7
|
+
## Description
|
|
8
|
+
|
|
9
|
+
Requires that every Rails partial template includes a strict locals declaration comment using the supported syntax:
|
|
10
|
+
|
|
11
|
+
```erb
|
|
12
|
+
<%# locals: () %>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A partial is any template whose filename begins with an underscore (e.g. `_card.html.erb`).
|
|
16
|
+
|
|
17
|
+
## Rationale
|
|
18
|
+
|
|
19
|
+
Partials often rely on implicit locals, which makes them harder to understand, refactor, and lint. Requiring strict locals:
|
|
20
|
+
|
|
21
|
+
- Documents the partial's public API at the top of the file
|
|
22
|
+
- Improves readability and onboarding
|
|
23
|
+
- Enables better static analysis (unknown locals, missing locals, unused locals)
|
|
24
|
+
- Reduces runtime surprises when locals are renamed or removed
|
|
25
|
+
|
|
26
|
+
This rule encourages partials to be explicit about what they expect. Partials that intentionally accept no locals should still declare an explicit empty signature.
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
This rule is disabled by default. To enable it, add to your [`.herb.yml`](/configuration):
|
|
31
|
+
|
|
32
|
+
```yaml [.herb.yml]
|
|
33
|
+
linter:
|
|
34
|
+
rules:
|
|
35
|
+
erb-strict-locals-required:
|
|
36
|
+
enabled: true
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Autofix
|
|
40
|
+
|
|
41
|
+
This rule supports **unsafe autofix** via `--fix-unsafely`. When applied, it inserts an empty strict locals declaration at the top of the file:
|
|
42
|
+
|
|
43
|
+
```erb
|
|
44
|
+
<%# locals: () %>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This is considered "unsafe" because:
|
|
48
|
+
- It changes the partial's behavior (strict mode will now error on undeclared locals)
|
|
49
|
+
- You may need to manually add the actual local variables your partial uses
|
|
50
|
+
|
|
51
|
+
To apply the autofix:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
herb-lint --fix-unsafely _partial.html.erb
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
After the autofix runs, review the file and update the locals declaration to include any variables your partial expects.
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
### ✅ Good
|
|
62
|
+
|
|
63
|
+
Partial with required keyword argument:
|
|
64
|
+
|
|
65
|
+
```erb
|
|
66
|
+
<%# locals: (user:) %>
|
|
67
|
+
|
|
68
|
+
<div class="user-card">
|
|
69
|
+
<%= user.name %>
|
|
70
|
+
</div>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Partial with keyword argument and default:
|
|
74
|
+
|
|
75
|
+
```erb [app/views/users/_card.html.erb]
|
|
76
|
+
<%# locals: (user:, admin: false) %>
|
|
77
|
+
|
|
78
|
+
<div class="user-card">
|
|
79
|
+
<%= user.name %>
|
|
80
|
+
|
|
81
|
+
<% if admin %>
|
|
82
|
+
<span class="badge">Admin</span>
|
|
83
|
+
<% end %>
|
|
84
|
+
</div>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Partial with no locals (empty declaration):
|
|
88
|
+
|
|
89
|
+
```erb [app/views/pages/_content.html.erb]
|
|
90
|
+
<%# locals: () %>
|
|
91
|
+
|
|
92
|
+
<p>Static content only</p>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 🚫 Bad
|
|
96
|
+
|
|
97
|
+
Partial without strict locals declaration:
|
|
98
|
+
|
|
99
|
+
```erb [app/views/users/_card.html.erb]
|
|
100
|
+
<div class="user-card">
|
|
101
|
+
<%= user.name %>
|
|
102
|
+
</div>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## References
|
|
106
|
+
|
|
107
|
+
- [Action View - Strict Locals](https://guides.rubyonrails.org/action_view_overview.html#strict-locals)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herb-tools/linter",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
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",
|
|
@@ -45,13 +45,14 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@herb-tools/config": "0.8.
|
|
49
|
-
"@herb-tools/core": "0.8.
|
|
50
|
-
"@herb-tools/highlighter": "0.8.
|
|
51
|
-
"@herb-tools/node-wasm": "0.8.
|
|
52
|
-
"@herb-tools/printer": "0.8.
|
|
53
|
-
"@herb-tools/rewriter": "0.8.
|
|
54
|
-
"
|
|
48
|
+
"@herb-tools/config": "0.8.8",
|
|
49
|
+
"@herb-tools/core": "0.8.8",
|
|
50
|
+
"@herb-tools/highlighter": "0.8.8",
|
|
51
|
+
"@herb-tools/node-wasm": "0.8.8",
|
|
52
|
+
"@herb-tools/printer": "0.8.8",
|
|
53
|
+
"@herb-tools/rewriter": "0.8.8",
|
|
54
|
+
"picomatch": "^4.0.2",
|
|
55
|
+
"tinyglobby": "^0.2.15"
|
|
55
56
|
},
|
|
56
57
|
"files": [
|
|
57
58
|
"package.json",
|
|
@@ -5,6 +5,7 @@ import { Herb } from "@herb-tools/node-wasm"
|
|
|
5
5
|
|
|
6
6
|
import { THEME_NAMES, DEFAULT_THEME } from "@herb-tools/highlighter"
|
|
7
7
|
import type { ThemeInput } from "@herb-tools/highlighter"
|
|
8
|
+
import type { DiagnosticSeverity } from "@herb-tools/core"
|
|
8
9
|
|
|
9
10
|
import { name, version, dependencies } from "../../package.json"
|
|
10
11
|
|
|
@@ -20,10 +21,12 @@ export interface ParsedArguments {
|
|
|
20
21
|
truncateLines: boolean
|
|
21
22
|
useGitHubActions: boolean
|
|
22
23
|
fix: boolean
|
|
24
|
+
fixUnsafe: boolean
|
|
23
25
|
ignoreDisableComments: boolean
|
|
24
26
|
force: boolean
|
|
25
27
|
init: boolean
|
|
26
28
|
loadCustomRules: boolean
|
|
29
|
+
failLevel?: DiagnosticSeverity
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export class ArgumentParser {
|
|
@@ -41,7 +44,9 @@ export class ArgumentParser {
|
|
|
41
44
|
-c, --config-file <path> explicitly specify path to .herb.yml config file
|
|
42
45
|
--force force linting even if disabled in .herb.yml
|
|
43
46
|
--fix automatically fix auto-correctable offenses
|
|
47
|
+
--fix-unsafely also apply unsafe auto-fixes (implies --fix)
|
|
44
48
|
--ignore-disable-comments report offenses even when suppressed with <%# herb:disable %> comments
|
|
49
|
+
--fail-level <severity> exit with error code when diagnostics of this severity or higher are present (error|warning|info|hint) [default: error]
|
|
45
50
|
--format output format (simple|detailed|json) [default: detailed]
|
|
46
51
|
--simple use simple output format (shortcut for --format simple)
|
|
47
52
|
--json use JSON output format (shortcut for --format json)
|
|
@@ -65,7 +70,9 @@ export class ArgumentParser {
|
|
|
65
70
|
"config-file": { type: "string", short: "c" },
|
|
66
71
|
force: { type: "boolean" },
|
|
67
72
|
fix: { type: "boolean" },
|
|
73
|
+
"fix-unsafely": { type: "boolean" },
|
|
68
74
|
"ignore-disable-comments": { type: "boolean" },
|
|
75
|
+
"fail-level": { type: "string" },
|
|
69
76
|
format: { type: "string" },
|
|
70
77
|
simple: { type: "boolean" },
|
|
71
78
|
json: { type: "boolean" },
|
|
@@ -137,14 +144,26 @@ export class ArgumentParser {
|
|
|
137
144
|
|
|
138
145
|
const theme = values.theme || DEFAULT_THEME
|
|
139
146
|
const patterns = this.getFilePatterns(positionals)
|
|
140
|
-
const
|
|
147
|
+
const fixUnsafe = values["fix-unsafely"] || false
|
|
148
|
+
const fix = values.fix || fixUnsafe // --fix-unsafely implies --fix
|
|
141
149
|
const force = !!values.force
|
|
142
150
|
const ignoreDisableComments = values["ignore-disable-comments"] || false
|
|
143
151
|
const configFile = values["config-file"]
|
|
144
152
|
const init = values.init || false
|
|
145
153
|
const loadCustomRules = !values["no-custom-rules"]
|
|
146
154
|
|
|
147
|
-
|
|
155
|
+
let failLevel: DiagnosticSeverity | undefined
|
|
156
|
+
if (values["fail-level"]) {
|
|
157
|
+
const level = values["fail-level"]
|
|
158
|
+
if (level === "error" || level === "warning" || level === "info" || level === "hint") {
|
|
159
|
+
failLevel = level
|
|
160
|
+
} else {
|
|
161
|
+
console.error(`Error: Invalid --fail-level value "${level}". Must be one of: error, warning, info, hint`)
|
|
162
|
+
process.exit(1)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel }
|
|
148
167
|
}
|
|
149
168
|
|
|
150
169
|
private getFilePatterns(positionals: string[]): string[] {
|
|
@@ -22,6 +22,7 @@ export interface ProcessingContext {
|
|
|
22
22
|
projectPath?: string
|
|
23
23
|
pattern?: string
|
|
24
24
|
fix?: boolean
|
|
25
|
+
fixUnsafe?: boolean
|
|
25
26
|
ignoreDisableComments?: boolean
|
|
26
27
|
linterConfig?: HerbConfigOptions['linter']
|
|
27
28
|
config?: Config
|
|
@@ -138,7 +139,7 @@ export class FileProcessor {
|
|
|
138
139
|
const autofixResult = this.linter.autofix(content, {
|
|
139
140
|
fileName: filename,
|
|
140
141
|
ignoreDisableComments: context?.ignoreDisableComments
|
|
141
|
-
})
|
|
142
|
+
}, undefined, { includeUnsafe: context?.fixUnsafe })
|
|
142
143
|
|
|
143
144
|
if (autofixResult.fixed.length > 0) {
|
|
144
145
|
writeFileSync(filePath, autofixResult.source, "utf-8")
|