Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions javascript/packages/linter/docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ This page contains documentation for all Herb Linter rules.
- [`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`

#### UJS

- [`ujs-no-remote-attribute`](./ujs-no-remote-attribute.md) - Disallow the deprecated `data-remote` attribute and helper option
- [`ujs-prefer-turbo-confirm`](./ujs-prefer-turbo-confirm.md) - Prefer `data-turbo-confirm` over the deprecated `data-confirm`
- [`ujs-prefer-turbo-method`](./ujs-prefer-turbo-method.md) - Prefer `data-turbo-method` over the deprecated `data-method`
- [`ujs-prefer-turbo-submits-with`](./ujs-prefer-turbo-submits-with.md) - Prefer `data-turbo-submits-with` over the deprecated `data-disable-with`


## Contributing

Expand Down
59 changes: 59 additions & 0 deletions javascript/packages/linter/docs/rules/ujs-no-remote-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Linter Rule: Disallow the deprecated `data-remote` attribute

**Rule:** `ujs-no-remote-attribute`

## Description

Disallow the `data-remote` attribute and the Action View helper options that render it, namely `remote:` and `data: { remote: ... }`. Unlike the other deprecated `@rails/ujs` attributes, this one has no Turbo attribute to swap in. Turbo handles links and form submissions by default, so the attribute is removed rather than replaced.

## Rationale

Before Rails 7, Rails shipped `@rails/ujs` by default, which added JavaScript behavior to elements through helper options and `data-*` attributes. Rails 7 stopped including it, and Turbo covers the same behavior with its own attributes.

`data-remote` made `@rails/ujs` issue the request over Ajax instead of navigating, and hand the response to the browser as executable JavaScript. Turbo Drive intercepts links and form submissions on the whole page already, so there is nothing to opt into and no `data-turbo-remote` to write.

Once `@rails/ujs` is gone the attribute is inert, and what is left is markup that claims a behavior the page no longer has. Because the attribute reads as deliberate, it hides the fact that these requests are now plain navigations.

## Examples

### ✅ Good

```erb
<a href="/posts">Load posts</a>
```

```erb
<%= link_to "Load posts", posts_path %>
```

### 🚫 Bad

```erb
<a href="/posts" data-remote="true">Load posts</a>
```

```erb
<%= link_to "Load posts", posts_path, remote: true %>
```

```erb
<%= link_to "Load posts", posts_path, data: { remote: true } %>
```

## Migration

For most links and forms the attribute is simply deleted, because Turbo Drive already does what `data-remote` asked for.

This is the one deprecated `@rails/ujs` attribute whose removal is not always a drop-in change, so it is worth checking what the endpoint returns before deleting it. A `data-remote` request whose response rendered JavaScript needs that response to become a Turbo Stream, or the element needs to live inside a Turbo Frame. Removing the attribute without making that change turns what was a background request into a full page navigation.

## Related Rules

* [`ujs-prefer-turbo-method`](./ujs-prefer-turbo-method.md)
* [`ujs-prefer-turbo-confirm`](./ujs-prefer-turbo-confirm.md)
* [`ujs-prefer-turbo-submits-with`](./ujs-prefer-turbo-submits-with.md)

## References

* [Rails Guides: Working with JavaScript in Rails](https://guides.rubyonrails.org/working_with_javascript_in_rails.html)
* [Turbo Handbook: Drive](https://turbo.hotwired.dev/handbook/drive)
* [Turbo Handbook: Streams](https://turbo.hotwired.dev/handbook/streams)
57 changes: 57 additions & 0 deletions javascript/packages/linter/docs/rules/ujs-prefer-turbo-confirm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Linter Rule: Prefer `data-turbo-confirm` over the deprecated `data-confirm`

**Rule:** `ujs-prefer-turbo-confirm`

## Description

Disallow the `data-confirm` attribute and the Action View helper option that renders it, `data: { confirm: ... }`. Use `data-turbo-confirm` or `data: { turbo_confirm: ... }` instead.

## Rationale

Before Rails 7, Rails shipped `@rails/ujs` by default, which added JavaScript behavior to elements through helper options and `data-*` attributes. Rails 7 stopped including it, and Turbo covers the same behavior with its own attributes.

`data-confirm` made `@rails/ujs` prompt the user with the given question before proceeding, and cancel the action if the user declined. `data-turbo-confirm` is a drop-in replacement, so the migration is mechanical.

Once `@rails/ujs` is gone the attribute is inert, and the failure is silent rather than loud: the link or button still works, but the confirmation prompt simply stops appearing. A destructive action that was guarded now fires on the first click.

## Examples

### ✅ Good

```erb
<a href="/posts/1" data-turbo-confirm="Are you sure?">Delete</a>
```

```erb
<%= link_to "Delete", post_path(@post), data: { turbo_confirm: "Are you sure?" } %>
```

```erb
<%= button_to "Delete", post_path(@post), data: { turbo_confirm: "Are you sure?" } %>
```

### 🚫 Bad

```erb
<a href="/posts/1" data-confirm="Are you sure?">Delete</a>
```

```erb
<%= link_to "Delete", post_path(@post), data: { confirm: "Are you sure?" } %>
```

```erb
<%= button_to "Delete", post_path(@post), data: { confirm: "Are you sure?" } %>
```

## Related Rules

* [`ujs-prefer-turbo-method`](./ujs-prefer-turbo-method.md)
* [`ujs-prefer-turbo-submits-with`](./ujs-prefer-turbo-submits-with.md)
* [`ujs-no-remote-attribute`](./ujs-no-remote-attribute.md)

## References

* [Rails `link_to` API](https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to)
* [Rails Guides: Working with JavaScript in Rails](https://guides.rubyonrails.org/working_with_javascript_in_rails.html)
* [Turbo Handbook: Drive](https://turbo.hotwired.dev/handbook/drive)
59 changes: 59 additions & 0 deletions javascript/packages/linter/docs/rules/ujs-prefer-turbo-method.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Linter Rule: Prefer `data-turbo-method` over the deprecated `data-method`

**Rule:** `ujs-prefer-turbo-method`

## Description

Disallow the `data-method` attribute and the Action View link helper options that render it, namely `method:` and `data: { method: ... }`. Use `data-turbo-method` or `data: { turbo_method: ... }` instead.

## Rationale

Before Rails 7, Rails shipped `@rails/ujs` by default, which added JavaScript behavior to elements through helper options and `data-*` attributes. Rails 7 stopped including it, and Turbo covers the same behavior with its own attributes.

`data-method` made `@rails/ujs` build a hidden form and submit it with the given verb, so that a plain link could issue a `DELETE`, `PATCH`, `POST` or `PUT` request. `data-turbo-method` is a drop-in replacement, so the migration is mechanical.

Once `@rails/ujs` is gone the attribute is inert, and the failure is silent rather than loud: the link still works, but it issues a `GET` to the same URL. A "Delete" link quietly turns into a link that shows the record instead of destroying it.

Note that `method:` on `button_to` and the `form_*` helpers is unaffected. Those render a real form and set the verb through a hidden `_method` field, which never involved `@rails/ujs`.

## Examples

### ✅ Good

```erb
<a href="/posts/1" data-turbo-method="delete">Delete</a>
```

```erb
<%= link_to "Delete", post_path(@post), data: { turbo_method: :delete } %>
```

```erb
<%= button_to "Delete", post_path(@post), method: :delete %>
```

### 🚫 Bad

```erb
<a href="/posts/1" data-method="delete">Delete</a>
```

```erb
<%= link_to "Delete", post_path(@post), method: :delete %>
```

```erb
<%= link_to "Delete", post_path(@post), data: { method: :delete } %>
```

## Related Rules

* [`ujs-prefer-turbo-confirm`](./ujs-prefer-turbo-confirm.md)
* [`ujs-prefer-turbo-submits-with`](./ujs-prefer-turbo-submits-with.md)
* [`ujs-no-remote-attribute`](./ujs-no-remote-attribute.md)

## References

* [Rails `link_to` API](https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to)
* [Rails Guides: Working with JavaScript in Rails](https://guides.rubyonrails.org/working_with_javascript_in_rails.html)
* [Turbo Handbook: Drive](https://turbo.hotwired.dev/handbook/drive)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Linter Rule: Prefer `data-turbo-submits-with` over the deprecated `data-disable-with`

**Rule:** `ujs-prefer-turbo-submits-with`

## Description

Disallow the `data-disable-with` attribute and the Action View helper option that renders it, `data: { disable_with: ... }`. Use `data-turbo-submits-with` or `data: { turbo_submits_with: ... }` instead.

## Rationale

Before Rails 7, Rails shipped `@rails/ujs` by default, which added JavaScript behavior to elements through helper options and `data-*` attributes. Rails 7 stopped including it, and Turbo covers the same behavior with its own attributes.

`data-disable-with` made `@rails/ujs` disable the submit button and swap its label for the given text while the request was in flight, which is what stopped users from double submitting a form. `data-turbo-submits-with` is a drop-in replacement, so the migration is mechanical.

Once `@rails/ujs` is gone the attribute is inert, and the failure is silent rather than loud: the form still submits, but the button stays live and keeps its original label. Double submissions become possible again on exactly the forms that were annotated to prevent them.

## Examples

### ✅ Good

```erb
<button data-turbo-submits-with="Saving...">Save</button>
```

```erb
<%= f.submit "Save", data: { turbo_submits_with: "Saving..." } %>
```

```erb
<%= submit_tag "Save", data: { turbo_submits_with: "Saving..." } %>
```

### 🚫 Bad

```erb
<button data-disable-with="Saving...">Save</button>
```

```erb
<%= f.submit "Save", data: { disable_with: "Saving..." } %>
```

```erb
<%= submit_tag "Save", data: { disable_with: "Saving..." } %>
```

## Related Rules

* [`ujs-prefer-turbo-method`](./ujs-prefer-turbo-method.md)
* [`ujs-prefer-turbo-confirm`](./ujs-prefer-turbo-confirm.md)
* [`ujs-no-remote-attribute`](./ujs-no-remote-attribute.md)

## References

* [Rails `submit_tag` API](https://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-submit_tag)
* [Rails Guides: Working with JavaScript in Rails](https://guides.rubyonrails.org/working_with_javascript_in_rails.html)
* [Turbo Handbook: Drive](https://turbo.hotwired.dev/handbook/drive)
10 changes: 10 additions & 0 deletions javascript/packages/linter/src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ import { SVGTagNameCapitalizationRule } from "./rules/svg-tag-name-capitalizatio
import { TurboPermanentNoMisleadingValueRule } from "./rules/turbo-permanent-no-misleading-value.js"
import { TurboPermanentRequireIdRule } from "./rules/turbo-permanent-require-id.js"

import { UJSNoRemoteAttributeRule } from "./rules/ujs-no-remote-attribute.js"
import { UJSPreferTurboConfirmRule } from "./rules/ujs-prefer-turbo-confirm.js"
import { UJSPreferTurboMethodRule } from "./rules/ujs-prefer-turbo-method.js"
import { UJSPreferTurboSubmitsWithRule } from "./rules/ujs-prefer-turbo-submits-with.js"

export const rules: RuleClass[] = [
A11yAvoidGenericLinkTextRule,
A11yDisabledAttributeRule,
Expand Down Expand Up @@ -266,4 +271,9 @@ export const rules: RuleClass[] = [

TurboPermanentNoMisleadingValueRule,
TurboPermanentRequireIdRule,

UJSNoRemoteAttributeRule,
UJSPreferTurboConfirmRule,
UJSPreferTurboMethodRule,
UJSPreferTurboSubmitsWithRule,
]
11 changes: 10 additions & 1 deletion javascript/packages/linter/src/rules/action-view-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isPrismNodeType, getHelperEntries } from "@herb-tools/core"
import { isPrismNodeType, getHelperEntries, getHelpersForTag } from "@herb-tools/core"
import type { PrismNode } from "@herb-tools/core"

const ACTION_VIEW_HELPER_NAMES = new Set(
Expand All @@ -7,6 +7,15 @@ const ACTION_VIEW_HELPER_NAMES = new Set(
.flatMap(helper => [helper.name, ...helper.aliases])
)

export function helperNamesForTags(...tagNames: string[]): ReadonlySet<string> {
return new Set(
tagNames
.flatMap(tagName => getHelpersForTag(tagName))
.filter(helper => helper.visibility === "public")
.flatMap(helper => [helper.name, ...helper.aliases])
)
}

export function isTagBuilderCall(prismNode: PrismNode): boolean {
if (!isPrismNodeType(prismNode, "CallNode")) return false
if (!isPrismNodeType(prismNode.receiver, "CallNode")) return false
Expand Down
6 changes: 6 additions & 0 deletions javascript/packages/linter/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from "./file-utils.js"
export * from "./string-utils.js"
export * from "./action-view-utils.js"
export * from "./herb-disable-comment-base.js"
export * from "./ujs-base.js"

export * from "./actionview-no-dynamic-partial-path.js"
export * from "./actionview-no-helper-shadowing.js"
Expand Down Expand Up @@ -129,3 +130,8 @@ export * from "./html-tag-name-lowercase.js"
export * from "./source-indentation.js"

export * from "./svg-tag-name-capitalization.js"

export * from "./ujs-no-remote-attribute.js"
export * from "./ujs-prefer-turbo-confirm.js"
export * from "./ujs-prefer-turbo-method.js"
export * from "./ujs-prefer-turbo-submits-with.js"
Loading
Loading