diff --git a/javascript/packages/linter/docs/rules/README.md b/javascript/packages/linter/docs/rules/README.md index 4b4089afe..45b93e98f 100644 --- a/javascript/packages/linter/docs/rules/README.md +++ b/javascript/packages/linter/docs/rules/README.md @@ -78,6 +78,7 @@ This page contains documentation for all Herb Linter rules. - [`html-require-script-nonce`](./html-require-script-nonce.md) - Require `nonce` attribute on script tags and helpers - [`html-tag-name-lowercase`](./html-tag-name-lowercase.md) - Enforces lowercase tag names in HTML - [`parser-no-errors`](./parser-no-errors.md) - Disallow parser errors in HTML+ERB documents +- [`source-indentation`](./source-indentation.md) - Indent with spaces instead of tabs. - [`svg-tag-name-capitalization`](./svg-tag-name-capitalization.md) - Enforces proper camelCase capitalization for SVG elements - [`turbo-permanent-require-id`](./turbo-permanent-require-id.md) - Require `id` attribute on elements with `data-turbo-permanent` diff --git a/javascript/packages/linter/docs/rules/source-indentation.md b/javascript/packages/linter/docs/rules/source-indentation.md new file mode 100644 index 000000000..68856bd6a --- /dev/null +++ b/javascript/packages/linter/docs/rules/source-indentation.md @@ -0,0 +1,33 @@ +# Linter Rule: Indentation + +**Rule:** `source-indentation` + +## Description + +Detects indentation with tabs. Consistent use of spaces for indentation improves readability and avoids alignment issues across editors and tools. + +## Rationale + +Mixing tabs and spaces for indentation causes inconsistent visual formatting across different editors, tools, and environments. Tabs render at different widths depending on the viewer's settings, which can make code appear misaligned or harder to read. Standardizing on space indentation ensures that code appears the same regardless of editor or tool, diffs and code reviews display consistently, and the codebase maintains a uniform visual style. + +## Examples + +### ✅ Good + +```erb +
+

Hello

+
+``` + +### 🚫 Bad + +```erb +
+

Hello

+
+``` + +## References + +- [Shopify/erb_lint - `SpaceIndentation`](https://github.com/Shopify/erb_lint/blob/main/lib/erb_lint/linters/space_indentation.rb) diff --git a/javascript/packages/linter/src/linter.ts b/javascript/packages/linter/src/linter.ts index 657acfca5..c68c93aed 100644 --- a/javascript/packages/linter/src/linter.ts +++ b/javascript/packages/linter/src/linter.ts @@ -409,7 +409,8 @@ export class Linter { context = { ...context, validRuleNames: this.getAvailableRules().map(ruleClass => ruleClass.ruleName), - ignoredOffensesByLine + ignoredOffensesByLine, + indentWidth: context?.indentWidth ?? this.config?.formatter?.indentWidth } const regularRules = this.rules.filter(ruleClass => ruleClass.ruleName !== "herb-disable-comment-unnecessary") @@ -522,6 +523,12 @@ export class Linter { */ autofix(source: string, context?: Partial, offensesToFix?: LintOffense[], options?: { includeUnsafe?: boolean }): AutofixResult { const includeUnsafe = options?.includeUnsafe ?? false + + context = { + ...context, + indentWidth: context?.indentWidth ?? this.config?.formatter?.indentWidth + } + const lintResult = offensesToFix ? { offenses: offensesToFix } : this.lint(source, context) const parserOffenses: LintOffense[] = [] diff --git a/javascript/packages/linter/src/rules.ts b/javascript/packages/linter/src/rules.ts index f0bdd8206..050aed818 100644 --- a/javascript/packages/linter/src/rules.ts +++ b/javascript/packages/linter/src/rules.ts @@ -83,6 +83,8 @@ import { HTMLTagNameLowercaseRule } from "./rules/html-tag-name-lowercase.js" import { ParserNoErrorsRule } from "./rules/parser-no-errors.js" +import { SourceIndentationRule } from "./rules/source-indentation.js" + import { SVGTagNameCapitalizationRule } from "./rules/svg-tag-name-capitalization.js" import { TurboPermanentRequireIdRule } from "./rules/turbo-permanent-require-id.js" @@ -171,6 +173,8 @@ export const rules: RuleClass[] = [ ParserNoErrorsRule, + SourceIndentationRule, + SVGTagNameCapitalizationRule, TurboPermanentRequireIdRule, diff --git a/javascript/packages/linter/src/rules/index.ts b/javascript/packages/linter/src/rules/index.ts index 02887e2ba..15123bbfd 100644 --- a/javascript/packages/linter/src/rules/index.ts +++ b/javascript/packages/linter/src/rules/index.ts @@ -81,4 +81,6 @@ export * from "./html-require-closing-tags.js" export * from "./html-require-script-nonce.js" export * from "./html-tag-name-lowercase.js" +export * from "./source-indentation.js" + export * from "./svg-tag-name-capitalization.js" diff --git a/javascript/packages/linter/src/rules/source-indentation.ts b/javascript/packages/linter/src/rules/source-indentation.ts new file mode 100644 index 000000000..63144f0d8 --- /dev/null +++ b/javascript/packages/linter/src/rules/source-indentation.ts @@ -0,0 +1,69 @@ +import { Location } from "@herb-tools/core" + +import { BaseSourceRuleVisitor, positionFromOffset } from "./rule-utils.js" +import { SourceRule } from "../types.js" +import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js" + +const START_BLANKS = /^[^\S\n]*\t[^\S\n]*/ + +class SourceIndentationVisitor extends BaseSourceRuleVisitor { + protected visitSource(source: string): void { + const lines = source.split("\n") + let offset = 0 + + lines.forEach((line) => { + const match = line.match(START_BLANKS) + + if (match) { + const start = positionFromOffset(source, offset) + const end = positionFromOffset(source, offset + match[0].length) + const location = new Location(start, end) + + this.addOffense( + "Indent with spaces instead of tabs.", + location, + ) + } + + offset += line.length + 1 + }) + } +} + +export class SourceIndentationRule extends SourceRule { + static autocorrectable = true + static ruleName = "source-indentation" + static introducedIn = this.version("unreleased") + + get defaultConfig(): FullRuleConfig { + return { + enabled: true, + severity: "error" + } + } + + check(source: string, context?: Partial): UnboundLintOffense[] { + const visitor = new SourceIndentationVisitor(this.ruleName, context) + + visitor.visit(source) + + return visitor.offenses + } + + autofix(_offense: LintOffense, source: string, context?: Partial): string | null { + const indentWidth = context?.indentWidth ?? 2 + const lines = source.split("\n") + const result = lines.map((line) => { + const match = line.match(START_BLANKS) + + if (match) { + const replaced = match[0].replace(/\t/g, " ".repeat(indentWidth)) + return replaced + line.substring(match[0].length) + } + + return line + }) + + return result.join("\n") + } +} diff --git a/javascript/packages/linter/src/types.ts b/javascript/packages/linter/src/types.ts index 4d0df8ca8..7f82e4043 100644 --- a/javascript/packages/linter/src/types.ts +++ b/javascript/packages/linter/src/types.ts @@ -242,6 +242,7 @@ export interface LintContext { validRuleNames: string[] | undefined ignoredOffensesByLine: Map> | undefined ignoreDisableComments: boolean | undefined + indentWidth: number | undefined } /** @@ -251,7 +252,8 @@ export const DEFAULT_LINT_CONTEXT: LintContext = { fileName: undefined, validRuleNames: undefined, ignoredOffensesByLine: undefined, - ignoreDisableComments: undefined + ignoreDisableComments: undefined, + indentWidth: undefined } as const export abstract class SourceRule { diff --git a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap index 3a119c447..b8ada0255 100644 --- a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap +++ b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap @@ -111,10 +111,11 @@ test/fixtures/ignored.html.erb:8:8 Fixable 7 offenses | 4 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -180,10 +181,11 @@ test/fixtures/test-file-with-errors.html.erb:2:22 Fixable 3 offenses | 2 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once." @@ -211,10 +213,11 @@ test/fixtures/no-trailing-newline.html.erb:1:29 Fixable 1 offense | 1 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once." @@ -268,10 +271,11 @@ test/fixtures/erb-no-extra-whitespace-inside-tags.html.erb:9:3 Fixable 3 offenses | 3 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -318,10 +322,11 @@ test/fixtures/ignored.html.erb:6:14 Fixable 2 offenses | 2 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -353,10 +358,11 @@ test/fixtures/parser-errors.html.erb:2:16 Fixable 1 offense New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -577,10 +583,11 @@ test/fixtures/multiple-rule-offenses.html.erb:4:2 Fixable 14 offenses | 4 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -683,10 +690,11 @@ test/fixtures/few-rule-offenses.html.erb:6:0 Fixable 6 offenses | 3 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -731,10 +739,11 @@ test/fixtures/bad-file.html.erb:1:16 Fixable 2 offenses | 2 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once." @@ -749,10 +758,11 @@ exports[`CLI Output Formatting > formats GitHub Actions output correctly for cle Offenses 0 offenses New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once." @@ -815,10 +825,11 @@ test/fixtures/test-file-with-errors.html.erb:2:22 Fixable 3 offenses | 2 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once." @@ -860,10 +871,11 @@ test/fixtures/test-file-simple.html.erb:2:22 Fixable 2 offenses | 2 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -1073,10 +1085,11 @@ test/fixtures/test-file-with-errors.html.erb:2:22 Fixable 3 offenses | 2 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -1101,10 +1114,11 @@ exports[`CLI Output Formatting > formats simple output correctly 1`] = ` Fixable 2 offenses | 2 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -1129,10 +1143,11 @@ exports[`CLI Output Formatting > formats simple output for bad-file correctly 1` Fixable 2 offenses | 2 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -1150,10 +1165,11 @@ exports[`CLI Output Formatting > formats success output correctly 1`] = ` Offenses 0 offenses New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -1171,10 +1187,11 @@ exports[`CLI Output Formatting > handles boolean attributes 1`] = ` Offenses 0 offenses New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -1215,10 +1232,11 @@ test/fixtures/bad-file.html.erb:1:16 Fixable 2 offenses | 2 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -1356,10 +1374,11 @@ test/fixtures/disabled-1.html.erb:14:19 Fixable 8 offenses | 1 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -1384,7 +1403,7 @@ test/fixtures/disabled-2.html.erb:6:30 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/13] ⎯⎯⎯⎯ -[warning] Unknown rule \`this-rule-doesnt-exist\`. Did you mean \`erb-no-empty-tags\`? (herb-disable-comment-valid-rule-name) +[warning] Unknown rule \`this-rule-doesnt-exist\`. Did you mean \`source-indentation\`? (herb-disable-comment-valid-rule-name) test/fixtures/disabled-2.html.erb:4:30 @@ -1398,7 +1417,7 @@ test/fixtures/disabled-2.html.erb:4:30 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [2/13] ⎯⎯⎯⎯ -[warning] Unknown rule \`this-rule-doesnt-exist\`. Did you mean \`erb-no-empty-tags\`? (herb-disable-comment-valid-rule-name) +[warning] Unknown rule \`this-rule-doesnt-exist\`. Did you mean \`source-indentation\`? (herb-disable-comment-valid-rule-name) test/fixtures/disabled-2.html.erb:6:35 @@ -1413,7 +1432,7 @@ test/fixtures/disabled-2.html.erb:6:35 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [3/13] ⎯⎯⎯⎯ -[warning] Unknown rule \`this-rule-doesnt-exist\`. Did you mean \`erb-no-empty-tags\`? (herb-disable-comment-valid-rule-name) +[warning] Unknown rule \`this-rule-doesnt-exist\`. Did you mean \`source-indentation\`? (herb-disable-comment-valid-rule-name) test/fixtures/disabled-2.html.erb:8:55 @@ -1568,10 +1587,11 @@ test/fixtures/disabled-2.html.erb:2:44 Fixable 13 offenses | 6 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once. @@ -1637,10 +1657,11 @@ test/fixtures/test-file-with-errors.html.erb:2:22 Fixable 3 offenses | 2 autocorrectable using \`--fix\` New rules available: - Your .herb.yml version is 0.9.2. 2 new rules are disabled to ease upgrades: + Your .herb.yml version is 0.9.2. 3 new rules are disabled to ease upgrades: actionview-no-void-element-content (introduced in next release) html-require-script-nonce (introduced in next release) + source-indentation (introduced in next release) Run herb-lint --upgrade to update the version and disable all new rules, or update the version in your .herb.yml to "0.9.2" to enable them all at once." diff --git a/javascript/packages/linter/test/autofix/source-indentation.autofix.test.ts b/javascript/packages/linter/test/autofix/source-indentation.autofix.test.ts new file mode 100644 index 000000000..1e14462cc --- /dev/null +++ b/javascript/packages/linter/test/autofix/source-indentation.autofix.test.ts @@ -0,0 +1,129 @@ +import { describe, test, expect, beforeAll } from "vitest" + +import { Herb } from "@herb-tools/node-wasm" +import { Config } from "@herb-tools/config" +import { Linter } from "../../src/linter.js" + +import { SourceIndentationRule } from "../../src/rules/source-indentation.js" + +describe("source-indentation autofix", () => { + beforeAll(async () => { + await Herb.load() + }) + + test("replaces tab indentation with spaces", () => { + const input = "\tthis is a line\n\tanother line\n" + const expected = " this is a line\n another line\n" + + const linter = new Linter(Herb, [SourceIndentationRule]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(2) + expect(result.unfixed).toHaveLength(0) + }) + + test("replaces multiple tab indentation with spaces", () => { + const input = "\t\tthis is a line\n" + const expected = " this is a line\n" + + const linter = new Linter(Herb, [SourceIndentationRule]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("replaces tabs in mixed indentation", () => { + const input = " \t this is a line\n \t another line\n" + const expected = " this is a line\n another line\n" + + const linter = new Linter(Herb, [SourceIndentationRule]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(2) + expect(result.unfixed).toHaveLength(0) + }) + + test("ignores space-only indentation", () => { + const input = " this is a line\n another line\n" + + const linter = new Linter(Herb, [SourceIndentationRule]) + const result = linter.autofix(input) + + expect(result.source).toBe(input) + expect(result.fixed).toHaveLength(0) + expect(result.unfixed).toHaveLength(0) + }) + + test("ignores lines without indentation", () => { + const input = "this is a line\n" + + const linter = new Linter(Herb, [SourceIndentationRule]) + const result = linter.autofix(input) + + expect(result.source).toBe(input) + expect(result.fixed).toHaveLength(0) + expect(result.unfixed).toHaveLength(0) + }) + + test("ignores tabs in the middle of a line", () => { + const input = "hello\tworld\n" + + const linter = new Linter(Herb, [SourceIndentationRule]) + const result = linter.autofix(input) + + expect(result.source).toBe(input) + expect(result.fixed).toHaveLength(0) + expect(result.unfixed).toHaveLength(0) + }) + + test("handles HTML content with tab indentation", () => { + const input = "
\n\t

hello

\n
\n" + const expected = "
\n

hello

\n
\n" + + const linter = new Linter(Herb, [SourceIndentationRule]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("uses custom indentWidth from autofix context", () => { + const input = "\tthis is a line\n\t\tindented twice\n" + const expected = " this is a line\n indented twice\n" + + const linter = new Linter(Herb, [SourceIndentationRule]) + const result = linter.autofix(input, { indentWidth: 4 }) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(2) + expect(result.unfixed).toHaveLength(0) + }) + + test("defaults to indentWidth from formatter config", () => { + const input = "\tthis is a line\n\t\tindented twice\n" + const expected = " this is a line\n indented twice\n" + + const config = Config.fromObject({ + formatter: { + indentWidth: 4 + }, + linter: { + rules: { + "source-indentation": { enabled: true } + } + } + }) + + const linter = Linter.from(Herb, config) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(2) + expect(result.unfixed).toHaveLength(0) + }) +}) diff --git a/javascript/packages/linter/test/rules/source-indentation.test.ts b/javascript/packages/linter/test/rules/source-indentation.test.ts new file mode 100644 index 000000000..b3c5b44e2 --- /dev/null +++ b/javascript/packages/linter/test/rules/source-indentation.test.ts @@ -0,0 +1,46 @@ +import { describe, test } from "vitest" + +import { SourceIndentationRule } from "../../src/rules/source-indentation.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" + +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(SourceIndentationRule) + +describe("SourceIndentationRule", () => { + test("ignores empty lines", () => { + expectNoOffenses("\n\n\n") + }) + + test("passes when no indentation", () => { + expectNoOffenses("this is a line") + }) + + test("passes when space indentation", () => { + expectNoOffenses(" this is a line\n another line\n") + }) + + test("fails with tab indentation", () => { + expectError("Indent with spaces instead of tabs.", [1]) + expectError("Indent with spaces instead of tabs.", [2]) + + assertOffenses("\t\tthis is a line\n\t\tanother line\n") + }) + + test("handles mixed indentation", () => { + expectError("Indent with spaces instead of tabs.", [1]) + expectError("Indent with spaces instead of tabs.", [2]) + + assertOffenses(" \t this is a line\n \t another line\n") + }) + + test("handles html template with tabs", () => { + expectError("Indent with spaces instead of tabs.", [2]) + + assertOffenses("
\n\t

hello

\n
\n") + }) + + test("fails ERB content with tab indentation", () => { + expectError("Indent with spaces instead of tabs.", [2]) + + assertOffenses("
\n\t<%= hello %>\n
\n") + }) +})