Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 5 additions & 0 deletions .changeset/sixty-cars-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-svelte': minor
---

feat: add `excludedRunes` option to the `prefer-const` rule
6 changes: 4 additions & 2 deletions docs/rules/prefer-const.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ since: 'v3.0.0-next.6'

## :book: Rule Details

This rule reports the same as the base ESLint `prefer-const` rule, except that ignores Svelte reactive values such as `$derived` and `$props`. If this rule is active, make sure to disable the base `prefer-const` rule, as it will conflict with this rule.
This rule reports the same as the base ESLint `prefer-const` rule, except that ignores Svelte reactive values such as `$derived` and `$props` as default. If this rule is active, make sure to disable the base `prefer-const` rule, as it will conflict with this rule.

<!--eslint-skip-->

Expand Down Expand Up @@ -46,7 +46,8 @@ This rule reports the same as the base ESLint `prefer-const` rule, except that i
"error",
{
"destructuring": "any",
"ignoreReadonly": true
"ignoreReadonly": true,
"excludedRunes": ["$props", "$state"]
}
]
}
Expand All @@ -56,6 +57,7 @@ This rule reports the same as the base ESLint `prefer-const` rule, except that i
- `any` (default): if any variables in destructuring should be const, this rule warns for those variables.
- `all`: if all variables in destructuring should be const, this rule warns the variables. Otherwise, ignores them.
- `ignoreReadonly`: If `true`, this rule will ignore variables that are read between the declaration and the _first_ assignment.
- `excludedRunes`: An array of rune names that should be ignored. Even if a rune is declared with `let`, it will still be ignored.

## :books: Further Reading

Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin-svelte/src/rule-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ type SveltePreferClassDirective = []|[{
type SveltePreferConst = []|[{
destructuring?: ("any" | "all")
ignoreReadBeforeAssign?: boolean
excludedRunes?: string[]
}]
// ----- svelte/shorthand-attribute -----
type SvelteShorthandAttribute = []|[{
Expand Down
33 changes: 24 additions & 9 deletions packages/eslint-plugin-svelte/src/rules/prefer-const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function findDeclarationCallee(node: TSESTree.Expression) {
* Determines if a declaration should be skipped in the const preference analysis.
* Specifically checks for Svelte's state management utilities ($props, $derived).
*/
function shouldSkipDeclaration(declaration: TSESTree.Expression | null) {
function shouldSkipDeclaration(declaration: TSESTree.Expression | null, excludedRunes: string[]) {
if (!declaration) {
return false;
}
Expand All @@ -31,19 +31,15 @@ function shouldSkipDeclaration(declaration: TSESTree.Expression | null) {
return false;
}

if (callee.type === 'Identifier' && ['$props', '$derived'].includes(callee.name)) {
if (callee.type === 'Identifier' && excludedRunes.includes(callee.name)) {
return true;
}

if (callee.type !== 'MemberExpression' || callee.object.type !== 'Identifier') {
return false;
}

if (
callee.object.name === '$derived' &&
callee.property.type === 'Identifier' &&
callee.property.name === 'by'
) {
if (excludedRunes.includes(callee.object.name)) {
return true;
}

Expand All @@ -58,16 +54,35 @@ export default createRule('prefer-const', {
category: 'Best Practices',
recommended: false,
extensionRule: 'prefer-const'
}
},
schema: [
{
type: 'object',
properties: {
destructuring: { enum: ['any', 'all'] },
ignoreReadBeforeAssign: { type: 'boolean' },
excludedRunes: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}
]
},
create(context) {
const config = context.options[0] ?? {};
const excludedRunes = config.excludedRunes ?? ['$props', '$derived'];

return defineWrapperListener(coreRule, context, {
createListenerProxy(coreListener) {
return {
...coreListener,
VariableDeclaration(node) {
for (const decl of node.declarations) {
if (shouldSkipDeclaration(decl.init)) {
if (shouldSkipDeclaration(decl.init, excludedRunes)) {
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"options": [{ "excludedRunes": [] }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
- message: "'prop1' is never reassigned. Use 'const' instead."
line: 2
column: 8
suggestions: null
- message: "'prop2' is never reassigned. Use 'const' instead."
line: 2
column: 15
suggestions: null
- message: "'zero' is never reassigned. Use 'const' instead."
line: 3
column: 6
suggestions: null
- message: "'derived' is never reassigned. Use 'const' instead."
line: 4
column: 6
suggestions: null
- message: "'derivedBy' is never reassigned. Use 'const' instead."
line: 5
column: 6
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
let { prop1, prop2 } = $props();
let zero = $state(0);
let derived = $derived(zero * 2);
let derivedBy = $derived.by(calc());
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
const { prop1, prop2 } = $props();
const zero = $state(0);
const derived = $derived(zero * 2);
const derivedBy = $derived.by(calc());
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"options": [{ "excludedRunes": ["$state"] }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
- message: "'prop1' is never reassigned. Use 'const' instead."
line: 2
column: 8
suggestions: null
- message: "'prop2' is never reassigned. Use 'const' instead."
line: 2
column: 15
suggestions: null
- message: "'derived' is never reassigned. Use 'const' instead."
line: 4
column: 6
suggestions: null
- message: "'derivedBy' is never reassigned. Use 'const' instead."
line: 5
column: 6
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
let { prop1, prop2 } = $props();
let zero = $state(0);
let derived = $derived(zero * 2);
let derivedBy = $derived.by(calc());
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
const { prop1, prop2 } = $props();
let zero = $state(0);
const derived = $derived(zero * 2);
const derivedBy = $derived.by(calc());
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"options": [{ "excludedRunes": [] }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
const { prop1, prop2 } = $props();
const zero = $state(0);
const derived = $derived(zero * 2);
const derivedBy = $derived.by(calc());
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"options": [{ "excludedRunes": ["$props", "$derived", "$state"] }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
let { prop1, prop2 } = $props();
let zero = $state(0);
let derived = $derived(zero * 2);
let derivedBy = $derived.by(calc());
</script>
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<script>
const a = {};
let { prop1, prop2 } = $props();
const zero = $state(0);
let derived = $derived(zero * 2);
let derivedBy = $derived.by(calc());
</script>