Skip to content
Open
1 change: 1 addition & 0 deletions javascript/packages/linter/docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ This page contains documentation for all Herb Linter rules.

#### Turbo

- [`turbo-permanent-no-misleading-value`](./turbo-permanent-no-misleading-value.md) - Disallow misleading values on `data-turbo-permanent`
- [`turbo-permanent-require-id`](./turbo-permanent-require-id.md) - Require `id` attribute on elements with `data-turbo-permanent`


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Linter Rule: Disallow misleading values on `data-turbo-permanent`

**Rule:** `turbo-permanent-no-misleading-value`

## Description

Ensure that `data-turbo-permanent` is used correctly on HTML elements. The `data-turbo-permanent` attribute marks elements that should persist across page navigations in Turbo Drive.

## Rationale

`data-turbo-permanent` is active whenever the attribute is present, so `data-turbo-permanent="false"` behaves the same as `data-turbo-permanent` or `data-turbo-permanent="true"`. Allowing values is misleading and should be disallowed to avoid confusion.

## Examples

### ✅ Good

```html
<div id="cart-counter" data-turbo-permanent>1 item</div>
```

### 🚫 Bad

```html
<div id="cart-counter" data-turbo-permanent="true">1 item</div>
<div id="cart-counter" data-turbo-permanent="false">1 item</div>
<div id="cart-counter" data-turbo-permanent="foo">1 item</div>
```

## References

- [Turbo: `data-turbo-permanent` attribute](https://turbo.hotwired.dev/handbook/drive#permanent-elements)
- [HTML: Boolean Attributes](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML)
2 changes: 2 additions & 0 deletions javascript/packages/linter/src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ import { SourceIndentationRule } from "./rules/source-indentation.js"

import { SVGTagNameCapitalizationRule } from "./rules/svg-tag-name-capitalization.js"

import { TurboPermanentNoMisleadingValueRule } from "./rules/turbo-permanent-no-misleading-value.js"
import { TurboPermanentRequireIdRule } from "./rules/turbo-permanent-require-id.js"

export const rules: RuleClass[] = [
Expand Down Expand Up @@ -215,5 +216,6 @@ export const rules: RuleClass[] = [

SVGTagNameCapitalizationRule,

TurboPermanentNoMisleadingValueRule,
TurboPermanentRequireIdRule,
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { BaseRuleVisitor } from "./rule-utils.js"
import { getAttribute, hasAttributeValue } from "@herb-tools/core"

import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
import type { HTMLAttributeNode, HTMLOpenTagNode, ParseResult } from "@herb-tools/core"

interface TurboPermanentAutofixContext extends BaseAutofixContext {
node: Mutable<HTMLAttributeNode>
}

class TurboPermanentNoMisleadingValueVisitor extends BaseRuleVisitor<TurboPermanentAutofixContext> {
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
this.checkTurboPermanentAttribute(node)
super.visitHTMLOpenTagNode(node)
}

private checkTurboPermanentAttribute(node: HTMLOpenTagNode): void {
const attribute = getAttribute(node, "data-turbo-permanent")

if (!attribute) return
if (!hasAttributeValue(attribute)) return

this.addOffense(
"Attribute `data-turbo-permanent` should not contain any value. Its presence alone enables the behavior, so values like `\"true\"` or `\"false\"` are misleading.",
attribute.value!.location,
{
node: attribute
}
)
}
}

export class TurboPermanentNoMisleadingValueRule extends ParserRule<TurboPermanentAutofixContext> {
static autocorrectable = true
static ruleName = "turbo-permanent-no-misleading-value"
static introducedIn = this.version("0.9.0")

get defaultConfig(): FullRuleConfig {
return {
enabled: true,
severity: "error"
}
}

check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<TurboPermanentAutofixContext>[] {
const visitor = new TurboPermanentNoMisleadingValueVisitor(this.ruleName, context)

visitor.visit(result.value)

return visitor.offenses
}

autofix(offense: LintOffense<TurboPermanentAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
if (!offense.autofixContext) return null

const { node } = offense.autofixContext

node.equals = null
node.value = null

return result
}
}
50 changes: 25 additions & 25 deletions javascript/packages/linter/test/__snapshots__/cli.test.ts.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading