eslint-plugin-svelte
Version:
ESLint plugin for Svelte using AST
108 lines (107 loc) • 3.99 kB
JavaScript
import { createRule } from '../utils/index.js';
import { getScope } from '../utils/ast-utils.js';
import { VERSION as SVELTE_VERSION } from 'svelte/compiler';
import semver from 'semver';
// Writable derived were introduced in Svelte 5.25.0
const shouldRun = semver.satisfies(SVELTE_VERSION, '>=5.25.0');
function isEffectOrEffectPre(node) {
if (node.callee.type === 'Identifier') {
return node.callee.name === '$effect';
}
if (node.callee.type === 'MemberExpression') {
return (node.callee.object.type === 'Identifier' &&
node.callee.object.name === '$effect' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'pre');
}
return false;
}
function isValidFunctionArgument(argument) {
if ((argument.type !== 'FunctionExpression' && argument.type !== 'ArrowFunctionExpression') ||
argument.params.length !== 0) {
return false;
}
if (argument.body.type !== 'BlockStatement') {
return false;
}
return argument.body.body.length === 1;
}
function isValidAssignment(statement) {
if (statement.type !== 'ExpressionStatement')
return false;
const { expression } = statement;
return (expression.type === 'AssignmentExpression' &&
expression.operator === '=' &&
expression.left.type === 'Identifier');
}
function isStateVariable(init) {
return (init?.type === 'CallExpression' &&
init.callee.type === 'Identifier' &&
init.callee.name === '$state');
}
export default createRule('prefer-writable-derived', {
meta: {
docs: {
description: 'Prefer using writable $derived instead of $state and $effect',
category: 'Best Practices',
recommended: true
},
schema: [],
messages: {
unexpected: 'Prefer using writable $derived instead of $state and $effect',
suggestRewrite: 'Rewrite $state and $effect to $derived'
},
type: 'suggestion',
conditions: [
{
svelteVersions: ['5'],
runes: [true, 'undetermined']
}
],
hasSuggestions: true
},
create(context) {
if (!shouldRun) {
return {};
}
return {
CallExpression: (node) => {
if (!isEffectOrEffectPre(node) || node.arguments.length !== 1) {
return;
}
const argument = node.arguments[0];
if (!isValidFunctionArgument(argument)) {
return;
}
const statement = argument.body.body[0];
if (!isValidAssignment(statement)) {
return;
}
const { left, right } = statement.expression;
const scope = getScope(context, statement);
const reference = scope.references.find((ref) => ref.identifier.type === 'Identifier' && ref.identifier.name === left.name);
const def = reference?.resolved?.defs?.[0];
if (!def || def.type !== 'Variable' || def.node.type !== 'VariableDeclarator') {
return;
}
const { init } = def.node;
if (!isStateVariable(init)) {
return;
}
context.report({
node: def.node,
messageId: 'unexpected',
suggest: [
{
messageId: 'suggestRewrite',
fix: (fixer) => {
const rightCode = context.sourceCode.getText(right);
return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)];
}
}
]
});
}
};
}
});