Skip to content

Commit 86f1a7e

Browse files
committed
implement base
1 parent 3bdf5d0 commit 86f1a7e

17 files changed

+265
-0
lines changed

docs/rules/prefer-writable-derived.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# (svelte/prefer-writable-derived)
2+
3+
> description
4+
5+
## :book: Rule Details
6+
7+
This rule reports ???.
8+
9+
<!--eslint-skip-->
10+
11+
```svelte
12+
<script>
13+
/* eslint svelte/prefer-writable-derived: "error" */
14+
</script>
15+
16+
<!-- ✓ GOOD -->
17+
18+
<!-- ✗ BAD -->
19+
```
20+
21+
## :wrench: Options
22+
23+
```json
24+
{
25+
"svelte/prefer-writable-derived": ["error", {}]
26+
}
27+
```
28+
29+
-
30+
31+
## :books: Further Reading
32+
33+
-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { TSESTree } from '@typescript-eslint/types';
2+
import { createRule } from '../utils/index.js';
3+
import { getScope } from 'src/utils/ast-utils.js';
4+
import { getSourceCode } from 'src/utils/compat.js';
5+
6+
function isEffectOrEffectPre(node: TSESTree.CallExpression) {
7+
if (node.callee.type === 'Identifier') {
8+
return node.callee.name === '$effect';
9+
}
10+
if (node.callee.type === 'MemberExpression') {
11+
return (
12+
node.callee.object.type === 'Identifier' &&
13+
node.callee.object.name === '$effect' &&
14+
node.callee.property.type === 'Identifier' &&
15+
node.callee.property.name === 'pre'
16+
);
17+
}
18+
19+
return false;
20+
}
21+
22+
export default createRule('prefer-writable-derived', {
23+
meta: {
24+
docs: {
25+
description: 'Prefer using writable $derived instead of $state and $effect',
26+
category: 'Best Practices',
27+
recommended: true
28+
},
29+
schema: [],
30+
messages: {
31+
unexpected: 'Prefer using writable $derived instead of $state and $effect'
32+
},
33+
type: 'suggestion',
34+
conditions: [],
35+
fixable: 'code'
36+
},
37+
create(context) {
38+
return {
39+
CallExpression: (node: TSESTree.CallExpression) => {
40+
if (!isEffectOrEffectPre(node)) {
41+
return;
42+
}
43+
44+
if (node.arguments.length !== 1) {
45+
return;
46+
}
47+
48+
const argument = node.arguments[0];
49+
if (argument.type !== 'FunctionExpression' && argument.type !== 'ArrowFunctionExpression') {
50+
return;
51+
}
52+
53+
if (argument.params.length !== 0) {
54+
return;
55+
}
56+
57+
if (argument.body.type !== 'BlockStatement') {
58+
return;
59+
}
60+
61+
const body = argument.body.body;
62+
if (body.length !== 1) {
63+
return;
64+
}
65+
66+
const statement = body[0];
67+
if (statement.type !== 'ExpressionStatement') {
68+
return;
69+
}
70+
71+
const expression = statement.expression;
72+
if (expression.type !== 'AssignmentExpression') {
73+
return;
74+
}
75+
76+
const { left, right, operator } = expression;
77+
if (operator !== '=' || left.type !== 'Identifier') {
78+
return;
79+
}
80+
81+
const scope = getScope(context, statement);
82+
const reference = scope.references.find((reference) => {
83+
return (
84+
reference.identifier.type === 'Identifier' && reference.identifier.name === left.name
85+
);
86+
});
87+
const defs = reference?.resolved?.defs;
88+
if (defs == null || defs.length !== 1) {
89+
return;
90+
}
91+
92+
const def = defs[0];
93+
if (def.type !== 'Variable' || def.node.type !== 'VariableDeclarator') {
94+
return;
95+
}
96+
97+
const init = def.node.init;
98+
if (init == null || init.type !== 'CallExpression') {
99+
return;
100+
}
101+
102+
if (init.callee.type !== 'Identifier' || init.callee.name !== '$state') {
103+
return;
104+
}
105+
106+
context.report({
107+
node: def.node,
108+
messageId: 'unexpected',
109+
fix: (fixer) => {
110+
const rightCode = getSourceCode(context).getText(right);
111+
return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)];
112+
}
113+
});
114+
}
115+
};
116+
}
117+
});

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

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import preferClassDirective from '../rules/prefer-class-directive.js';
6060
import preferConst from '../rules/prefer-const.js';
6161
import preferDestructuredStoreProps from '../rules/prefer-destructured-store-props.js';
6262
import preferStyleDirective from '../rules/prefer-style-directive.js';
63+
import preferWritableDerived from 'src/rules/prefer-writable-derived.js';
6364
import requireEachKey from '../rules/require-each-key.js';
6465
import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js';
6566
import requireOptimizedStyleAttribute from '../rules/require-optimized-style-attribute.js';
@@ -135,6 +136,7 @@ export const rules = [
135136
preferConst,
136137
preferDestructuredStoreProps,
137138
preferStyleDirective,
139+
preferWritableDerived,
138140
requireEachKey,
139141
requireEventDispatcherTypes,
140142
requireOptimizedStyleAttribute,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Prefer using writable $derived instead of $state and $effect
2+
line: 4
3+
column: 6
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
const { albumName } = $props();
3+
4+
let newAlbumName = $state(albumName);
5+
$effect(() => {
6+
newAlbumName = albumName;
7+
});
8+
</script>
9+
10+
<input bind:value={newAlbumName} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
const { albumName } = $props();
3+
4+
let newAlbumName = $derived(albumName);
5+
;
6+
</script>
7+
8+
<input bind:value={newAlbumName} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Prefer using writable $derived instead of $state and $effect
2+
line: 4
3+
column: 6
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
const { albumName } = $props();
3+
4+
let newAlbumName = $state(albumName);
5+
$effect(() => {
6+
newAlbumName = albumName + albumName;
7+
});
8+
</script>
9+
10+
<input bind:value={newAlbumName} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
const { albumName } = $props();
3+
4+
let newAlbumName = $derived(albumName + albumName);
5+
;
6+
</script>
7+
8+
<input bind:value={newAlbumName} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Prefer using writable $derived instead of $state and $effect
2+
line: 4
3+
column: 6
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
const { albumName } = $props();
3+
4+
let newAlbumName = $state(albumName);
5+
$effect.pre(() => {
6+
newAlbumName = albumName;
7+
});
8+
</script>
9+
10+
<input bind:value={newAlbumName} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
const { albumName } = $props();
3+
4+
let newAlbumName = $derived(albumName);
5+
;
6+
</script>
7+
8+
<input bind:value={newAlbumName} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Prefer using writable $derived instead of $state and $effect
2+
line: 4
3+
column: 6
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
const { albumName } = $props();
3+
4+
let newAlbumName = $state(albumName);
5+
$effect.pre(() => {
6+
newAlbumName = albumName + albumName;
7+
});
8+
</script>
9+
10+
<input bind:value={newAlbumName} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
const { albumName } = $props();
3+
4+
let newAlbumName = $derived(albumName + albumName);
5+
;
6+
</script>
7+
8+
<input bind:value={newAlbumName} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
const { albumName } = $props();
3+
4+
let newAlbumName = $state(albumName);
5+
$effect(() => {
6+
if (albumName === '') {
7+
return;
8+
}
9+
newAlbumName = albumName;
10+
});
11+
</script>
12+
13+
<input bind:value={newAlbumName} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { RuleTester } from '../../utils/eslint-compat.js';
2+
import rule from '../../../src/rules/prefer-writable-derived.js';
3+
import { loadTestCases } from '../../utils/utils.js';
4+
5+
const tester = new RuleTester({
6+
languageOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: 'module'
9+
}
10+
});
11+
12+
tester.run('prefer-writable-derived', rule as any, loadTestCases('prefer-writable-derived'));

0 commit comments

Comments
 (0)