Skip to content

Commit 4eb4ce0

Browse files
committed
add autofix
1 parent c8facf4 commit 4eb4ce0

File tree

6 files changed

+172
-17
lines changed

6 files changed

+172
-17
lines changed

javascript/packages/linter/docs/rules/erb-space-indentation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Detects indentation with tabs. Consistent use of spaces for indentation improves
2424
</div>
2525
```
2626

27+
## Autofix
28+
29+
This rule is autocorrectable. Each tab character in leading whitespace is replaced with spaces based on the configured `indentWidth` (default: 2).
30+
2731
## References
2832

2933
- [Shopify/erb_lint - `SpaceIndentation`](https://github.com/Shopify/erb_lint/blob/main/lib/erb_lint/linters/space_indentation.rb)

javascript/packages/linter/src/linter.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,8 @@ export class Linter {
368368
context = {
369369
...context,
370370
validRuleNames: this.getAvailableRules().map(ruleClass => ruleClass.ruleName),
371-
ignoredOffensesByLine
371+
ignoredOffensesByLine,
372+
indentWidth: context?.indentWidth ?? this.config?.formatter?.indentWidth
372373
}
373374

374375
const regularRules = this.rules.filter(ruleClass => ruleClass.ruleName !== "herb-disable-comment-unnecessary")
@@ -481,6 +482,12 @@ export class Linter {
481482
*/
482483
autofix(source: string, context?: Partial<LintContext>, offensesToFix?: LintOffense[], options?: { includeUnsafe?: boolean }): AutofixResult {
483484
const includeUnsafe = options?.includeUnsafe ?? false
485+
486+
context = {
487+
...context,
488+
indentWidth: context?.indentWidth ?? this.config?.formatter?.indentWidth
489+
}
490+
484491
const lintResult = offensesToFix ? { offenses: offensesToFix } : this.lint(source, context)
485492

486493
const parserOffenses: LintOffense[] = []

javascript/packages/linter/src/rules/erb-space-indentation.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Location } from "@herb-tools/core"
22

33
import { BaseSourceRuleVisitor, positionFromOffset } from "./rule-utils.js"
44
import { SourceRule } from "../types.js"
5-
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5+
import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
66

77
const START_BLANKS = /^[^\S\n]*\t[^\S\n]*/
88

@@ -31,6 +31,7 @@ class ERBSpaceIndentationVisitor extends BaseSourceRuleVisitor {
3131
}
3232

3333
export class ERBSpaceIndentationRule extends SourceRule {
34+
static autocorrectable = true
3435
static ruleName = "erb-space-indentation"
3536

3637
get defaultConfig(): FullRuleConfig {
@@ -47,4 +48,21 @@ export class ERBSpaceIndentationRule extends SourceRule {
4748

4849
return visitor.offenses
4950
}
51+
52+
autofix(_offense: LintOffense, source: string, context?: Partial<LintContext>): string | null {
53+
const indentWidth = context?.indentWidth ?? 2
54+
const lines = source.split("\n")
55+
const result = lines.map((line) => {
56+
const match = line.match(START_BLANKS)
57+
58+
if (match) {
59+
const replaced = match[0].replace(/\t/g, " ".repeat(indentWidth))
60+
return replaced + line.substring(match[0].length)
61+
}
62+
63+
return line
64+
})
65+
66+
return result.join("\n")
67+
}
5068
}

javascript/packages/linter/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export interface LintContext {
216216
validRuleNames: string[] | undefined
217217
ignoredOffensesByLine: Map<number, Set<string>> | undefined
218218
ignoreDisableComments: boolean | undefined
219+
indentWidth: number | undefined
219220
}
220221

221222
/**
@@ -225,7 +226,8 @@ export const DEFAULT_LINT_CONTEXT: LintContext = {
225226
fileName: undefined,
226227
validRuleNames: undefined,
227228
ignoredOffensesByLine: undefined,
228-
ignoreDisableComments: undefined
229+
ignoreDisableComments: undefined,
230+
indentWidth: undefined
229231
} as const
230232

231233
export abstract class SourceRule<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> {
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { describe, test, expect, beforeAll } from "vitest"
2+
3+
import { Herb } from "@herb-tools/node-wasm"
4+
import { Config } from "@herb-tools/config"
5+
import { Linter } from "../../src/linter.js"
6+
7+
import { ERBSpaceIndentationRule } from "../../src/rules/erb-space-indentation.js"
8+
9+
describe("erb-space-indentation autofix", () => {
10+
beforeAll(async () => {
11+
await Herb.load()
12+
})
13+
14+
test("replaces tab indentation with spaces", () => {
15+
const input = "\tthis is a line\n\tanother line\n"
16+
const expected = " this is a line\n another line\n"
17+
18+
const linter = new Linter(Herb, [ERBSpaceIndentationRule])
19+
const result = linter.autofix(input)
20+
21+
expect(result.source).toBe(expected)
22+
expect(result.fixed).toHaveLength(2)
23+
expect(result.unfixed).toHaveLength(0)
24+
})
25+
26+
test("replaces multiple tab indentation with spaces", () => {
27+
const input = "\t\tthis is a line\n"
28+
const expected = " this is a line\n"
29+
30+
const linter = new Linter(Herb, [ERBSpaceIndentationRule])
31+
const result = linter.autofix(input)
32+
33+
expect(result.source).toBe(expected)
34+
expect(result.fixed).toHaveLength(1)
35+
expect(result.unfixed).toHaveLength(0)
36+
})
37+
38+
test("replaces tabs in mixed indentation", () => {
39+
const input = " \t this is a line\n \t another line\n"
40+
const expected = " this is a line\n another line\n"
41+
42+
const linter = new Linter(Herb, [ERBSpaceIndentationRule])
43+
const result = linter.autofix(input)
44+
45+
expect(result.source).toBe(expected)
46+
expect(result.fixed).toHaveLength(2)
47+
expect(result.unfixed).toHaveLength(0)
48+
})
49+
50+
test("ignores space-only indentation", () => {
51+
const input = " this is a line\n another line\n"
52+
53+
const linter = new Linter(Herb, [ERBSpaceIndentationRule])
54+
const result = linter.autofix(input)
55+
56+
expect(result.source).toBe(input)
57+
expect(result.fixed).toHaveLength(0)
58+
expect(result.unfixed).toHaveLength(0)
59+
})
60+
61+
test("ignores lines without indentation", () => {
62+
const input = "this is a line\n"
63+
64+
const linter = new Linter(Herb, [ERBSpaceIndentationRule])
65+
const result = linter.autofix(input)
66+
67+
expect(result.source).toBe(input)
68+
expect(result.fixed).toHaveLength(0)
69+
expect(result.unfixed).toHaveLength(0)
70+
})
71+
72+
test("ignores tabs in the middle of a line", () => {
73+
const input = "hello\tworld\n"
74+
75+
const linter = new Linter(Herb, [ERBSpaceIndentationRule])
76+
const result = linter.autofix(input)
77+
78+
expect(result.source).toBe(input)
79+
expect(result.fixed).toHaveLength(0)
80+
expect(result.unfixed).toHaveLength(0)
81+
})
82+
83+
test("handles HTML content with tab indentation", () => {
84+
const input = "<div>\n\t<p>hello</p>\n</div>\n"
85+
const expected = "<div>\n <p>hello</p>\n</div>\n"
86+
87+
const linter = new Linter(Herb, [ERBSpaceIndentationRule])
88+
const result = linter.autofix(input)
89+
90+
expect(result.source).toBe(expected)
91+
expect(result.fixed).toHaveLength(1)
92+
expect(result.unfixed).toHaveLength(0)
93+
})
94+
95+
test("uses custom indentWidth from autofix context", () => {
96+
const input = "\tthis is a line\n\t\tindented twice\n"
97+
const expected = " this is a line\n indented twice\n"
98+
99+
const linter = new Linter(Herb, [ERBSpaceIndentationRule])
100+
const result = linter.autofix(input, { indentWidth: 4 })
101+
102+
expect(result.source).toBe(expected)
103+
expect(result.fixed).toHaveLength(2)
104+
expect(result.unfixed).toHaveLength(0)
105+
})
106+
107+
test("defaults to indentWidth from formatter config", () => {
108+
const input = "\tthis is a line\n\t\tindented twice\n"
109+
const expected = " this is a line\n indented twice\n"
110+
111+
const config = Config.fromObject({
112+
formatter: {
113+
indentWidth: 4
114+
}
115+
})
116+
117+
const linter = Linter.from(Herb, config)
118+
const result = linter.autofix(input)
119+
120+
expect(result.source).toBe(expected)
121+
expect(result.fixed).toHaveLength(2)
122+
expect(result.unfixed).toHaveLength(0)
123+
})
124+
})

javascript/packages/linter/test/rules/erb-space-indentation.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,41 @@ import { createLinterTest } from "../helpers/linter-test-helper.js"
66
const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(ERBSpaceIndentationRule)
77

88
describe("ERBSpaceIndentationRule", () => {
9-
test("no indentation present", () => {
9+
test("ignores empty lines", () => {
10+
expectNoOffenses("\n\n\n")
11+
})
12+
13+
test("passes when no indentation", () => {
1014
expectNoOffenses("this is a line")
1115
})
1216

13-
test("space indentation present", () => {
17+
test("passes when space indentation", () => {
1418
expectNoOffenses(" this is a line\n another line\n")
1519
})
1620

17-
test("tab indentation", () => {
21+
test("fails with tab indentation", () => {
1822
expectError("Indent with spaces instead of tabs.", [1])
1923
expectError("Indent with spaces instead of tabs.", [2])
24+
2025
assertOffenses("\t\tthis is a line\n\t\tanother line\n")
2126
})
2227

23-
test("tab and spaces indentation", () => {
28+
test("handles mixed indentation", () => {
2429
expectError("Indent with spaces instead of tabs.", [1])
2530
expectError("Indent with spaces instead of tabs.", [2])
31+
2632
assertOffenses(" \t this is a line\n \t another line\n")
2733
})
2834

29-
test("mixed content with tabs", () => {
35+
test("handles html template with tabs", () => {
3036
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-
})
3737

38-
test("empty lines are fine", () => {
39-
expectNoOffenses("\n\n\n")
38+
assertOffenses("<div>\n\t<p>hello</p>\n</div>\n")
4039
})
4140

42-
test("ERB content with tab indentation", () => {
41+
test("fails ERB content with tab indentation", () => {
4342
expectError("Indent with spaces instead of tabs.", [2])
43+
4444
assertOffenses("<div>\n\t<%= hello %>\n</div>\n")
4545
})
4646
})

0 commit comments

Comments
 (0)