Skip to content

Commit 32ba915

Browse files
feat: add no-at-const-tags rule (#1545)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent c89f891 commit 32ba915

15 files changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
feat: add `svelte/no-at-const-tags` rule

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ These rules relate to better ways of doing things to help you avoid problems:
334334
| [svelte/block-lang](https://sveltejs.github.io/eslint-plugin-svelte/rules/block-lang/) | disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks. | :bulb: |
335335
| [svelte/button-has-type](https://sveltejs.github.io/eslint-plugin-svelte/rules/button-has-type/) | disallow usage of button without an explicit type attribute | |
336336
| [svelte/no-add-event-listener](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-add-event-listener/) | Warns against the use of `addEventListener` | :bulb: |
337+
| [svelte/no-at-const-tags](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-const-tags/) | disallow the use of `{@const}` in favor of `{const ...}` declaration tags | :wrench: |
337338
| [svelte/no-at-debug-tags](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star::bulb: |
338339
| [svelte/no-ignored-unsubscribe](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-ignored-unsubscribe/) | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores. | |
339340
| [svelte/no-immutable-reactive-statements](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-immutable-reactive-statements/) | disallow reactive statements that don't reference reactive values. | :star: |

docs/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ These rules relate to better ways of doing things to help you avoid problems:
5555
| [svelte/block-lang](./rules/block-lang.md) | disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks. | :bulb: |
5656
| [svelte/button-has-type](./rules/button-has-type.md) | disallow usage of button without an explicit type attribute | |
5757
| [svelte/no-add-event-listener](./rules/no-add-event-listener.md) | Warns against the use of `addEventListener` | :bulb: |
58+
| [svelte/no-at-const-tags](./rules/no-at-const-tags.md) | disallow the use of `{@const}` in favor of `{const ...}` declaration tags | :wrench: |
5859
| [svelte/no-at-debug-tags](./rules/no-at-debug-tags.md) | disallow the use of `{@debug}` | :star::bulb: |
5960
| [svelte/no-ignored-unsubscribe](./rules/no-ignored-unsubscribe.md) | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores. | |
6061
| [svelte/no-immutable-reactive-statements](./rules/no-immutable-reactive-statements.md) | disallow reactive statements that don't reference reactive values. | :star: |

docs/rules/no-at-const-tags.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
pageClass: 'rule-details'
3+
sidebarDepth: 0
4+
title: 'svelte/no-at-const-tags'
5+
description: 'disallow the use of `{@const}` in favor of `{const ...}` declaration tags'
6+
---
7+
8+
# svelte/no-at-const-tags
9+
10+
> disallow the use of `{@const}` in favor of `{const ...}` declaration tags
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
14+
15+
## :book: Rule Details
16+
17+
This rule reports uses of `{@const ...}` in runes mode.
18+
19+
`{let/const ...}` declaration tags were introduced in Svelte 5.56.0. This rule only reports and fixes when running on Svelte >=5.56.0.
20+
21+
In Svelte 5, the `{@const ...}` tag is considered legacy. Use the `{const ...}` declaration tag instead, which can be placed anywhere inside the component.
22+
23+
`{@const ...}` is reactive — its value is re-evaluated when its dependencies change. To preserve that behavior, the initializer must be wrapped in `$derived(...)`, and the auto-fix does this automatically.
24+
25+
This rule does nothing in non-runes mode: it neither reports nor fixes there. `$derived(...)` is unavailable outside runes mode, so the reactive behavior of `{@const ...}` cannot be preserved when converting to a `{const ...}` declaration tag.
26+
27+
<!--eslint-skip-->
28+
29+
```svelte
30+
<script>
31+
/* eslint svelte/no-at-const-tags: "error" */
32+
let boxes = $state([
33+
{ width: 10, height: 10 },
34+
{ width: 15, height: 15 }
35+
]);
36+
</script>
37+
38+
{#each boxes as box}
39+
<!-- ✓ GOOD -->
40+
{const area = $derived(box.width * box.height)}
41+
42+
<!-- ✗ BAD -->
43+
{@const label = `${box.width} ⨉ ${box.height}`}
44+
45+
{label}: {area}
46+
{/each}
47+
```
48+
49+
## :wrench: Options
50+
51+
Nothing.
52+
53+
## :books: Further Reading
54+
55+
- [Svelte - `{@const ...}`](https://svelte.dev/docs/svelte/@const)
56+
- [Svelte - `{let/const ...}`](https://svelte.dev/docs/svelte/declaration-tags)
57+
58+
## :mag: Implementation
59+
60+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-at-const-tags.ts)
61+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-at-const-tags.ts)

packages/eslint-plugin-svelte/src/rule-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ export interface RuleOptions {
110110
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-add-event-listener/
111111
*/
112112
'svelte/no-add-event-listener'?: Linter.RuleEntry<[]>
113+
/**
114+
* disallow the use of `{@const}` in favor of `{const ...}` declaration tags
115+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-const-tags/
116+
*/
117+
'svelte/no-at-const-tags'?: Linter.RuleEntry<[]>
113118
/**
114119
* disallow the use of `{@debug}`
115120
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { createRule } from '../utils/index.js';
2+
import { getSvelteContext } from '../utils/svelte-context.js';
3+
import { VERSION as SVELTE_VERSION } from 'svelte/compiler';
4+
import semver from 'semver';
5+
6+
const shouldRun = semver.satisfies(SVELTE_VERSION, '>=5.56.0');
7+
8+
export default createRule('no-at-const-tags', {
9+
meta: {
10+
docs: {
11+
description: 'disallow the use of `{@const}` in favor of `{const ...}` declaration tags',
12+
category: 'Best Practices',
13+
recommended: false
14+
},
15+
fixable: 'code',
16+
schema: [],
17+
messages: {
18+
unexpected: 'Use `{const ...}` declaration tag instead of legacy `{@const ...}`.'
19+
},
20+
type: 'suggestion',
21+
conditions: [
22+
{
23+
svelteVersions: ['5']
24+
}
25+
]
26+
},
27+
create(context) {
28+
if (!shouldRun) {
29+
return {};
30+
}
31+
const sourceCode = context.sourceCode;
32+
const runes = getSvelteContext(context)?.runes;
33+
// Only report and fix in runes mode, since preserving reactivity requires
34+
// `$derived(...)`, which is unavailable outside runes mode.
35+
if (runes !== true) {
36+
return {};
37+
}
38+
return {
39+
SvelteConstTag(node) {
40+
context.report({
41+
node,
42+
messageId: 'unexpected',
43+
*fix(fixer) {
44+
const text = sourceCode.getText(node);
45+
const match = /^\{(\s*)@const\b/u.exec(text);
46+
if (!match) {
47+
return;
48+
}
49+
const atOffset = node.range[0] + 1 + match[1].length;
50+
yield fixer.removeRange([atOffset, atOffset + 1]);
51+
52+
const init = node.declarations[0].init;
53+
if (init == null) {
54+
return;
55+
}
56+
// Preserve the reactivity of legacy `{@const}` by wrapping the
57+
// initializer in `$derived(...)`. Skip when already wrapped.
58+
if (
59+
init.type === 'CallExpression' &&
60+
init.callee.type === 'Identifier' &&
61+
init.callee.name === '$derived'
62+
) {
63+
return;
64+
}
65+
yield fixer.insertTextBefore(init, '$derived(');
66+
yield fixer.insertTextAfter(init, ')');
67+
}
68+
});
69+
}
70+
};
71+
}
72+
});

packages/eslint-plugin-svelte/src/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import maxAttributesPerLine from '../rules/max-attributes-per-line.js';
2121
import maxLinesPerBlock from '../rules/max-lines-per-block.js';
2222
import mustacheSpacing from '../rules/mustache-spacing.js';
2323
import noAddEventListener from '../rules/no-add-event-listener.js';
24+
import noAtConstTags from '../rules/no-at-const-tags.js';
2425
import noAtDebugTags from '../rules/no-at-debug-tags.js';
2526
import noAtHtmlTags from '../rules/no-at-html-tags.js';
2627
import noDomManipulating from '../rules/no-dom-manipulating.js';
@@ -105,6 +106,7 @@ export const rules = [
105106
maxLinesPerBlock,
106107
mustacheSpacing,
107108
noAddEventListener,
109+
noAtConstTags,
108110
noAtDebugTags,
109111
noAtHtmlTags,
110112
noDomManipulating,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0"
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Use `{const ...}` declaration tag instead of legacy `{@const ...}`.
2+
line: 9
3+
column: 2
4+
suggestions: null
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
let boxes = $state([
3+
{ width: 10, height: 10 },
4+
{ width: 15, height: 15 }
5+
]);
6+
</script>
7+
8+
{#each boxes as box}
9+
{@const area = box.width * box.height}
10+
{box.width} * {box.height} = {area}
11+
{/each}

0 commit comments

Comments
 (0)