Skip to content

Commit c8facf4

Browse files
committed
Linter: Implement erb-space-indentation rule
1 parent ecc8d24 commit c8facf4

File tree

7 files changed

+132
-3
lines changed

7 files changed

+132
-3
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ This page contains documentation for all Herb Linter rules.
3434
- [`erb-require-trailing-newline`](./erb-require-trailing-newline.md) - Enforces that all HTML+ERB template files end with exactly one trailing newline character.
3535
- [`erb-require-whitespace-inside-tags`](./erb-require-whitespace-inside-tags.md) - Requires whitespace around ERB tags
3636
- [`erb-right-trim`](./erb-right-trim.md) - Enforce consistent right-trimming syntax.
37+
- [`erb-space-indentation`](./erb-space-indentation.md) - Indent with spaces instead of tabs.
3738
- [`erb-strict-locals-comment-syntax`](./erb-strict-locals-comment-syntax.md) - Enforce strict locals comment syntax.
3839
- [`herb-disable-comment-malformed`](./herb-disable-comment-malformed.md) - Detect malformed `herb:disable` comments.
3940
- [`herb-disable-comment-missing-rules`](./herb-disable-comment-missing-rules.md) - Require rule names in `herb:disable` comments.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Linter Rule: Space Indentation
2+
3+
**Rule:** `erb-space-indentation`
4+
5+
## Description
6+
7+
Detects indentation with tabs. Consistent use of spaces for indentation improves readability and avoids alignment issues across editors and tools.
8+
9+
## Examples
10+
11+
### ✅ Good
12+
13+
```erb
14+
<div>
15+
<p>Hello</p>
16+
</div>
17+
```
18+
19+
### ❌ Bad
20+
21+
```erb
22+
<div>
23+
<p>Hello</p>
24+
</div>
25+
```
26+
27+
## References
28+
29+
- [Shopify/erb_lint - `SpaceIndentation`](https://github.com/Shopify/erb_lint/blob/main/lib/erb_lint/linters/space_indentation.rb)

javascript/packages/linter/src/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { ERBPreferImageTagHelperRule } from "./rules/erb-prefer-image-tag-helper
3232
import { ERBRequireTrailingNewlineRule } from "./rules/erb-require-trailing-newline.js"
3333
import { ERBRequireWhitespaceRule } from "./rules/erb-require-whitespace-inside-tags.js"
3434
import { ERBRightTrimRule } from "./rules/erb-right-trim.js"
35+
import { ERBSpaceIndentationRule } from "./rules/erb-space-indentation.js"
3536
import { ERBStrictLocalsCommentSyntaxRule } from "./rules/erb-strict-locals-comment-syntax.js"
3637
import { ERBStrictLocalsRequiredRule } from "./rules/erb-strict-locals-required.js"
3738

@@ -117,6 +118,7 @@ export const rules: RuleClass[] = [
117118
ERBNoUnsafeScriptInterpolationRule,
118119
ERBPreferImageTagHelperRule,
119120
ERBRequireTrailingNewlineRule,
121+
ERBSpaceIndentationRule,
120122
ERBRequireWhitespaceRule,
121123
ERBRightTrimRule,
122124
ERBStrictLocalsCommentSyntaxRule,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Location } from "@herb-tools/core"
2+
3+
import { BaseSourceRuleVisitor, positionFromOffset } from "./rule-utils.js"
4+
import { SourceRule } from "../types.js"
5+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6+
7+
const START_BLANKS = /^[^\S\n]*\t[^\S\n]*/
8+
9+
class ERBSpaceIndentationVisitor extends BaseSourceRuleVisitor {
10+
protected visitSource(source: string): void {
11+
const lines = source.split("\n")
12+
let offset = 0
13+
14+
lines.forEach((line) => {
15+
const match = line.match(START_BLANKS)
16+
17+
if (match) {
18+
const start = positionFromOffset(source, offset)
19+
const end = positionFromOffset(source, offset + match[0].length)
20+
const location = new Location(start, end)
21+
22+
this.addOffense(
23+
"Indent with spaces instead of tabs.",
24+
location,
25+
)
26+
}
27+
28+
offset += line.length + 1
29+
})
30+
}
31+
}
32+
33+
export class ERBSpaceIndentationRule extends SourceRule {
34+
static ruleName = "erb-space-indentation"
35+
36+
get defaultConfig(): FullRuleConfig {
37+
return {
38+
enabled: true,
39+
severity: "error"
40+
}
41+
}
42+
43+
check(source: string, context?: Partial<LintContext>): UnboundLintOffense[] {
44+
const visitor = new ERBSpaceIndentationVisitor(this.ruleName, context)
45+
46+
visitor.visit(source)
47+
48+
return visitor.offenses
49+
}
50+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export * from "./erb-prefer-image-tag-helper.js"
3333
export * from "./erb-require-trailing-newline.js"
3434
export * from "./erb-require-whitespace-inside-tags.js"
3535
export * from "./erb-right-trim.js"
36+
export * from "./erb-space-indentation.js"
3637
export * from "./erb-strict-locals-comment-syntax.js"
3738
export * from "./erb-strict-locals-required.js"
3839

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

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, test } from "vitest"
2+
3+
import { ERBSpaceIndentationRule } from "../../src/rules/erb-space-indentation.js"
4+
import { createLinterTest } from "../helpers/linter-test-helper.js"
5+
6+
const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(ERBSpaceIndentationRule)
7+
8+
describe("ERBSpaceIndentationRule", () => {
9+
test("no indentation present", () => {
10+
expectNoOffenses("this is a line")
11+
})
12+
13+
test("space indentation present", () => {
14+
expectNoOffenses(" this is a line\n another line\n")
15+
})
16+
17+
test("tab indentation", () => {
18+
expectError("Indent with spaces instead of tabs.", [1])
19+
expectError("Indent with spaces instead of tabs.", [2])
20+
assertOffenses("\t\tthis is a line\n\t\tanother line\n")
21+
})
22+
23+
test("tab and spaces indentation", () => {
24+
expectError("Indent with spaces instead of tabs.", [1])
25+
expectError("Indent with spaces instead of tabs.", [2])
26+
assertOffenses(" \t this is a line\n \t another line\n")
27+
})
28+
29+
test("mixed content with tabs", () => {
30+
expectError("Indent with spaces instead of tabs.", [2])
31+
assertOffenses("<div>\n\t<p>hello</p>\n</div>\n")
32+
})
33+
34+
test("tabs only at start of line", () => {
35+
expectNoOffenses("hello\tworld\n")
36+
})
37+
38+
test("empty lines are fine", () => {
39+
expectNoOffenses("\n\n\n")
40+
})
41+
42+
test("ERB content with tab indentation", () => {
43+
expectError("Indent with spaces instead of tabs.", [2])
44+
assertOffenses("<div>\n\t<%= hello %>\n</div>\n")
45+
})
46+
})

0 commit comments

Comments
 (0)