Skip to content

Commit a27ec57

Browse files
committed
Linter: Implement -actionview-strict-locals-partial-only rule
1 parent 9afabc7 commit a27ec57

File tree

8 files changed

+319
-21
lines changed

8 files changed

+319
-21
lines changed

javascript/packages/linter/docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This page contains documentation for all Herb Linter rules.
1010
- [`actionview-no-silent-render`](./actionview-no-silent-render.md) - Disallow calling `render` without outputting the result
1111
- [`actionview-no-void-element-content`](./actionview-no-void-element-content.md) - Disallow content arguments for void Action View elements
1212
- [`actionview-strict-locals-first-line`](./actionview-strict-locals-first-line.md) - Require strict locals on the first line of partials with a blank line after.
13+
- [`actionview-strict-locals-partial-only`](./actionview-strict-locals-partial-only.md) - Only allow strict local definitions in partial files.
1314

1415

1516
#### ERB
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Linter Rule: Only allow strict local definitions in partial files
2+
3+
**Rule:** `actionview-strict-locals-partial-only`
4+
5+
## Description
6+
7+
Detects strict locals declarations in files that are not Rails partials. Strict locals are only supported in partials (templates whose filename begins with an underscore), so a declaration in any other template has no effect.
8+
9+
## Rationale
10+
11+
A `<%# locals: (...) %>` comment in a non-partial file (such as a view or layout) is misleading. It looks like it constrains the available local variables, but Rails only processes strict locals in partials. Flagging these declarations prevents confusion and keeps templates honest about their actual contract.
12+
13+
## Examples
14+
15+
### ✅ Good
16+
17+
```erb [app/views/users/_card.html.erb]
18+
<%# locals: (user:) %>
19+
20+
<div class="user-card">
21+
<%= user.name %>
22+
</div>
23+
```
24+
25+
### 🚫 Bad
26+
27+
```erb [app/views/users/show.html.erb]
28+
<%# locals: (user:) %>
29+
30+
<div class="user-card">
31+
<%= user.name %>
32+
</div>
33+
```
34+
35+
## References
36+
37+
- [Action View - Strict Locals](https://guides.rubyonrails.org/action_view_overview.html#strict-locals)

javascript/packages/linter/src/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ActionViewNoSilentHelperRule } from "./rules/actionview-no-silent-helpe
44
import { ActionViewNoSilentRenderRule } from "./rules/actionview-no-silent-render.js"
55
import { ActionViewNoVoidElementContentRule } from "./rules/actionview-no-void-element-content.js"
66
import { ActionViewStrictLocalsFirstLineRule } from "./rules/actionview-strict-locals-first-line.js"
7+
import { ActionViewStrictLocalsPartialOnlyRule } from "./rules/actionview-strict-locals-partial-only.js"
78

89
import { ERBCommentSyntax } from "./rules/erb-comment-syntax.js";
910
import { ERBNoCaseNodeChildrenRule } from "./rules/erb-no-case-node-children.js"
@@ -95,6 +96,7 @@ export const rules: RuleClass[] = [
9596
ActionViewNoSilentRenderRule,
9697
ActionViewNoVoidElementContentRule,
9798
ActionViewStrictLocalsFirstLineRule,
99+
ActionViewStrictLocalsPartialOnlyRule,
98100

99101
ERBCommentSyntax,
100102
ERBNoCaseNodeChildrenRule,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { BaseRuleVisitor } from "./rule-utils.js"
2+
import { ParserRule } from "../types.js"
3+
4+
import { isHTMLTextNode } from "@herb-tools/core"
5+
import { isPartialFile } from "./file-utils.js"
6+
7+
import type { ParseResult, ERBStrictLocalsNode } from "@herb-tools/core"
8+
import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
9+
10+
class ActionViewStrictLocalsPartialOnlyVisitor extends BaseRuleVisitor {
11+
visitERBStrictLocalsNode(node: ERBStrictLocalsNode): void {
12+
this.addOffense(
13+
"Strict locals declarations are only supported in partials. This file is not a partial.",
14+
node.location,
15+
)
16+
}
17+
}
18+
19+
export class ActionViewStrictLocalsPartialOnlyRule extends ParserRule {
20+
static unsafeAutocorrectable = true
21+
static ruleName = "actionview-strict-locals-partial-only"
22+
static introducedIn = this.version("unreleased")
23+
24+
get parserOptions() {
25+
return { strict_locals: true }
26+
}
27+
28+
get defaultConfig(): FullRuleConfig {
29+
return {
30+
enabled: true,
31+
severity: "warning",
32+
}
33+
}
34+
35+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
36+
if (isPartialFile(context?.fileName) !== false) return []
37+
38+
const visitor = new ActionViewStrictLocalsPartialOnlyVisitor(this.ruleName, context)
39+
visitor.visit(result.value)
40+
41+
return visitor.offenses
42+
}
43+
44+
autofix(offense: LintOffense, result: ParseResult): ParseResult | null {
45+
const children = result.value.children
46+
47+
const index = children.findIndex(child =>
48+
child.location.start.line === offense.location.start.line &&
49+
child.location.start.column === offense.location.start.column
50+
)
51+
52+
if (index === -1) return null
53+
54+
children.splice(index, 1)
55+
56+
if (index < children.length) {
57+
const next = children[index]
58+
59+
if (isHTMLTextNode(next) && /^\s*\n/.test(next.content)) {
60+
children.splice(index, 1)
61+
}
62+
}
63+
64+
return result
65+
}
66+
}

javascript/packages/linter/src/rules/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from "./actionview-no-silent-helper.js"
77
export * from "./actionview-no-silent-render.js"
88
export * from "./actionview-no-void-element-content.js"
99
export * from "./actionview-strict-locals-first-line.js"
10+
export * from "./actionview-strict-locals-partial-only.js"
1011

1112
export * from "./erb-comment-syntax.js"
1213
export * from "./erb-no-case-node-children.js"

javascript/packages/linter/test/__snapshots__/cli.test.ts.snap

Lines changed: 42 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)