@herb-tools/linter 0.8.7 → 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 +28 -2
- package/dist/herb-lint.js +5406 -15659
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +381 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +376 -39
- package/dist/index.js.map +1 -1
- package/dist/loader.cjs +1231 -7911
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.js +1225 -7912
- package/dist/loader.js.map +1 -1
- package/dist/package.json +7 -7
- package/dist/src/cli/argument-parser.js +5 -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 +14 -8
- 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 +14 -1
- 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-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 +1 -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 +1 -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 +7 -7
- package/src/cli/argument-parser.ts +6 -2
- package/src/cli/file-processor.ts +2 -1
- package/src/cli.ts +18 -8
- package/src/custom-rule-loader.ts +2 -2
- package/src/linter.ts +17 -1
- 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-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
|
@@ -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,12 +45,12 @@
|
|
|
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.
|
|
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
54
|
"picomatch": "^4.0.2",
|
|
55
55
|
"tinyglobby": "^0.2.15"
|
|
56
56
|
},
|
|
@@ -21,6 +21,7 @@ export interface ParsedArguments {
|
|
|
21
21
|
truncateLines: boolean
|
|
22
22
|
useGitHubActions: boolean
|
|
23
23
|
fix: boolean
|
|
24
|
+
fixUnsafe: boolean
|
|
24
25
|
ignoreDisableComments: boolean
|
|
25
26
|
force: boolean
|
|
26
27
|
init: boolean
|
|
@@ -43,6 +44,7 @@ export class ArgumentParser {
|
|
|
43
44
|
-c, --config-file <path> explicitly specify path to .herb.yml config file
|
|
44
45
|
--force force linting even if disabled in .herb.yml
|
|
45
46
|
--fix automatically fix auto-correctable offenses
|
|
47
|
+
--fix-unsafely also apply unsafe auto-fixes (implies --fix)
|
|
46
48
|
--ignore-disable-comments report offenses even when suppressed with <%# herb:disable %> comments
|
|
47
49
|
--fail-level <severity> exit with error code when diagnostics of this severity or higher are present (error|warning|info|hint) [default: error]
|
|
48
50
|
--format output format (simple|detailed|json) [default: detailed]
|
|
@@ -68,6 +70,7 @@ export class ArgumentParser {
|
|
|
68
70
|
"config-file": { type: "string", short: "c" },
|
|
69
71
|
force: { type: "boolean" },
|
|
70
72
|
fix: { type: "boolean" },
|
|
73
|
+
"fix-unsafely": { type: "boolean" },
|
|
71
74
|
"ignore-disable-comments": { type: "boolean" },
|
|
72
75
|
"fail-level": { type: "string" },
|
|
73
76
|
format: { type: "string" },
|
|
@@ -141,7 +144,8 @@ export class ArgumentParser {
|
|
|
141
144
|
|
|
142
145
|
const theme = values.theme || DEFAULT_THEME
|
|
143
146
|
const patterns = this.getFilePatterns(positionals)
|
|
144
|
-
const
|
|
147
|
+
const fixUnsafe = values["fix-unsafely"] || false
|
|
148
|
+
const fix = values.fix || fixUnsafe // --fix-unsafely implies --fix
|
|
145
149
|
const force = !!values.force
|
|
146
150
|
const ignoreDisableComments = values["ignore-disable-comments"] || false
|
|
147
151
|
const configFile = values["config-file"]
|
|
@@ -159,7 +163,7 @@ export class ArgumentParser {
|
|
|
159
163
|
}
|
|
160
164
|
}
|
|
161
165
|
|
|
162
|
-
return { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, ignoreDisableComments, force, init, loadCustomRules, failLevel }
|
|
166
|
+
return { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel }
|
|
163
167
|
}
|
|
164
168
|
|
|
165
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")
|
package/src/cli.ts
CHANGED
|
@@ -66,9 +66,9 @@ export class CLI {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
protected adjustPattern(pattern: string | undefined,
|
|
69
|
+
protected adjustPattern(pattern: string | undefined, configGlobPatterns: string[]): string {
|
|
70
70
|
if (!pattern) {
|
|
71
|
-
return
|
|
71
|
+
return configGlobPatterns.length === 1 ? configGlobPatterns[0] : `{${configGlobPatterns.join(',')}}`
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
const resolvedPattern = resolve(pattern)
|
|
@@ -77,7 +77,15 @@ export class CLI {
|
|
|
77
77
|
const stats = statSync(resolvedPattern)
|
|
78
78
|
|
|
79
79
|
if (stats.isDirectory()) {
|
|
80
|
-
|
|
80
|
+
const relativeDir = relative(this.projectPath, resolvedPattern)
|
|
81
|
+
|
|
82
|
+
if (relativeDir) {
|
|
83
|
+
const scopedPatterns = configGlobPatterns.map(pattern => `${relativeDir}/${pattern}`)
|
|
84
|
+
|
|
85
|
+
return scopedPatterns.length === 1 ? scopedPatterns[0] : `{${scopedPatterns.join(',')}}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return configGlobPatterns.length === 1 ? configGlobPatterns[0] : `{${configGlobPatterns.join(',')}}`
|
|
81
89
|
} else if (stats.isFile()) {
|
|
82
90
|
return relative(this.projectPath, resolvedPattern)
|
|
83
91
|
}
|
|
@@ -96,10 +104,11 @@ export class CLI {
|
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
const filesConfig = config.getFilesConfigForTool('linter')
|
|
99
|
-
const
|
|
100
|
-
?
|
|
101
|
-
: '**/*.html.erb'
|
|
102
|
-
|
|
107
|
+
const configGlobPatterns = filesConfig.include && filesConfig.include.length > 0
|
|
108
|
+
? filesConfig.include
|
|
109
|
+
: ['**/*.html.erb']
|
|
110
|
+
|
|
111
|
+
const adjustedPattern = this.adjustPattern(pattern, configGlobPatterns)
|
|
103
112
|
|
|
104
113
|
let files = await glob(adjustedPattern, {
|
|
105
114
|
cwd: this.projectPath,
|
|
@@ -135,7 +144,7 @@ export class CLI {
|
|
|
135
144
|
const startTime = Date.now()
|
|
136
145
|
const startDate = new Date()
|
|
137
146
|
|
|
138
|
-
let { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, ignoreDisableComments, force, init, loadCustomRules, failLevel } = this.argumentParser.parse(process.argv)
|
|
147
|
+
let { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel } = this.argumentParser.parse(process.argv)
|
|
139
148
|
|
|
140
149
|
this.determineProjectPath(patterns)
|
|
141
150
|
|
|
@@ -239,6 +248,7 @@ export class CLI {
|
|
|
239
248
|
projectPath: this.projectPath,
|
|
240
249
|
pattern: patterns.join(' '),
|
|
241
250
|
fix,
|
|
251
|
+
fixUnsafe,
|
|
242
252
|
ignoreDisableComments,
|
|
243
253
|
linterConfig,
|
|
244
254
|
config: processingConfig,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pathToFileURL } from "url"
|
|
2
|
-
import { glob } from "
|
|
2
|
+
import { glob } from "tinyglobby"
|
|
3
3
|
|
|
4
4
|
import type { RuleClass } from "./types.js"
|
|
5
5
|
|
|
@@ -52,7 +52,7 @@ export class CustomRuleLoader {
|
|
|
52
52
|
const files = await glob(pattern, {
|
|
53
53
|
cwd: this.baseDir,
|
|
54
54
|
absolute: true,
|
|
55
|
-
|
|
55
|
+
onlyFiles: true
|
|
56
56
|
})
|
|
57
57
|
|
|
58
58
|
allFiles.push(...files)
|
package/src/linter.ts
CHANGED
|
@@ -459,9 +459,12 @@ export class Linter {
|
|
|
459
459
|
* @param source - The source code to fix
|
|
460
460
|
* @param context - Optional context for linting (e.g., fileName)
|
|
461
461
|
* @param offensesToFix - Optional array of specific offenses to fix. If not provided, all fixable offenses will be fixed.
|
|
462
|
+
* @param options - Options for autofix behavior
|
|
463
|
+
* @param options.includeUnsafe - If true, also apply unsafe fixes (rules with unsafeAutocorrectable = true)
|
|
462
464
|
* @returns AutofixResult containing the corrected source and lists of fixed/unfixed offenses
|
|
463
465
|
*/
|
|
464
|
-
autofix(source: string, context?: Partial<LintContext>, offensesToFix?: LintOffense[]): AutofixResult {
|
|
466
|
+
autofix(source: string, context?: Partial<LintContext>, offensesToFix?: LintOffense[], options?: { includeUnsafe?: boolean }): AutofixResult {
|
|
467
|
+
const includeUnsafe = options?.includeUnsafe ?? false
|
|
465
468
|
const lintResult = offensesToFix ? { offenses: offensesToFix } : this.lint(source, context)
|
|
466
469
|
|
|
467
470
|
const parserOffenses: LintOffense[] = []
|
|
@@ -503,6 +506,7 @@ export class Linter {
|
|
|
503
506
|
}
|
|
504
507
|
|
|
505
508
|
const rule = new RuleClass() as ParserRule
|
|
509
|
+
const isUnsafe = (RuleClass as any).unsafeAutocorrectable === true
|
|
506
510
|
|
|
507
511
|
if (!rule.autofix) {
|
|
508
512
|
unfixed.push(offense)
|
|
@@ -510,6 +514,12 @@ export class Linter {
|
|
|
510
514
|
continue
|
|
511
515
|
}
|
|
512
516
|
|
|
517
|
+
if (isUnsafe && !includeUnsafe) {
|
|
518
|
+
unfixed.push(offense)
|
|
519
|
+
|
|
520
|
+
continue
|
|
521
|
+
}
|
|
522
|
+
|
|
513
523
|
if (offense.autofixContext) {
|
|
514
524
|
const originalNodeType = offense.autofixContext.node.type
|
|
515
525
|
const location: Location = offense.autofixContext.node.location ? Location.from(offense.autofixContext.node.location) : offense.location
|
|
@@ -562,12 +572,18 @@ export class Linter {
|
|
|
562
572
|
}
|
|
563
573
|
|
|
564
574
|
const rule = new RuleClass() as SourceRule
|
|
575
|
+
const isUnsafe = (RuleClass as any).unsafeAutocorrectable === true
|
|
565
576
|
|
|
566
577
|
if (!rule.autofix) {
|
|
567
578
|
unfixed.push(offense)
|
|
568
579
|
continue
|
|
569
580
|
}
|
|
570
581
|
|
|
582
|
+
if (isUnsafe && !includeUnsafe) {
|
|
583
|
+
unfixed.push(offense)
|
|
584
|
+
continue
|
|
585
|
+
}
|
|
586
|
+
|
|
571
587
|
const correctedSource = rule.autofix(offense, currentSource, context)
|
|
572
588
|
|
|
573
589
|
if (correctedSource) {
|