Created
September 7, 2025 22:39
-
-
Save dimfeld/04d96e8315186f42939ee534b0be05ec to your computer and use it in GitHub Desktop.
Eslint rule to catch Svelte `$derived(() => expression)`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * ESLint rule to detect $derived(() => expression) patterns and suggest using $derived(expression) instead | |
| */ | |
| export default { | |
| meta: { | |
| type: 'suggestion', | |
| docs: { | |
| description: | |
| 'prefer using $derived(expression) over $derived(() => expression) for simple expressions', | |
| category: 'Stylistic Issues', | |
| recommended: true, | |
| }, | |
| fixable: 'code', | |
| schema: [], | |
| messages: { | |
| preferDirectDerived: | |
| 'Use $derived(expression) instead of $derived(() => expression) for simple expressions.', | |
| preferDerivedBy: | |
| 'Use $derived.by(() => { ... }) instead of $derived(() => { ... }) for complex logic.', | |
| }, | |
| }, | |
| create(context) { | |
| /** | |
| * Check if a node is a $derived call expression | |
| */ | |
| function isDerivedCall(node) { | |
| return node.callee.type === 'Identifier' && node.callee.name === '$derived'; | |
| } | |
| /** | |
| * Check if the argument is an arrow function with a simple return expression | |
| */ | |
| function isSimpleArrowFunction(node) { | |
| if (node.type !== 'ArrowFunctionExpression') { | |
| return false; | |
| } | |
| // Only handle functions with no parameters | |
| if (node.params.length > 0) { | |
| return false; | |
| } | |
| // Check if it's a simple expression (not a block statement) | |
| if (node.body.type === 'BlockStatement') { | |
| // For block statements, check if it's a single return statement | |
| if (node.body.body.length === 1) { | |
| const stmt = node.body.body[0]; | |
| return stmt.type === 'ReturnStatement' && stmt.argument != null; | |
| } | |
| return false; | |
| } | |
| // It's an expression body, which is what we want to simplify | |
| return true; | |
| } | |
| /** | |
| * Check if the argument is a regular function with a simple return | |
| */ | |
| function isSimpleFunctionExpression(node) { | |
| if (node.type !== 'FunctionExpression') { | |
| return false; | |
| } | |
| // Only handle functions with no parameters | |
| if (node.params.length > 0) { | |
| return false; | |
| } | |
| // Must have a block body with a single return statement | |
| if (node.body.type === 'BlockStatement' && node.body.body.length === 1) { | |
| const stmt = node.body.body[0]; | |
| return stmt.type === 'ReturnStatement' && stmt.argument != null; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Check if the argument is a complex function (has parameters or complex body) | |
| */ | |
| function isComplexFunction(node) { | |
| if (node.type !== 'ArrowFunctionExpression' && node.type !== 'FunctionExpression') { | |
| return false; | |
| } | |
| // Has parameters | |
| if (node.params.length > 0) { | |
| return true; | |
| } | |
| // Arrow function with block statement (not simple expression) | |
| if (node.type === 'ArrowFunctionExpression' && node.body.type === 'BlockStatement') { | |
| // If it's not a simple single return, it's complex | |
| if (node.body.body.length !== 1) { | |
| return true; | |
| } | |
| const stmt = node.body.body[0]; | |
| if (stmt.type !== 'ReturnStatement' || stmt.argument == null) { | |
| return true; | |
| } | |
| return false; // It's actually simple | |
| } | |
| // Function expression with complex body | |
| if (node.type === 'FunctionExpression' && node.body.type === 'BlockStatement') { | |
| // If it's not a simple single return, it's complex | |
| if (node.body.body.length !== 1) { | |
| return true; | |
| } | |
| const stmt = node.body.body[0]; | |
| if (stmt.type !== 'ReturnStatement' || stmt.argument == null) { | |
| return true; | |
| } | |
| return false; // It's actually simple | |
| } | |
| return false; | |
| } | |
| /** | |
| * Extract the expression from a simple function | |
| */ | |
| function extractExpression(node) { | |
| if (node.type === 'ArrowFunctionExpression') { | |
| if (node.body.type === 'BlockStatement') { | |
| const stmt = node.body.body[0]; | |
| return stmt.argument; | |
| } | |
| return node.body; | |
| } | |
| if (node.type === 'FunctionExpression' && node.body.type === 'BlockStatement') { | |
| const stmt = node.body.body[0]; | |
| return stmt.argument; | |
| } | |
| return null; | |
| } | |
| return { | |
| CallExpression(node) { | |
| // Check if this is a $derived call | |
| if (!isDerivedCall(node)) { | |
| return; | |
| } | |
| // Must have exactly one argument | |
| if (node.arguments.length !== 1) { | |
| return; | |
| } | |
| const arg = node.arguments[0]; | |
| // Check if the argument is a simple arrow function or function expression | |
| if (isSimpleArrowFunction(arg) || isSimpleFunctionExpression(arg)) { | |
| const expression = extractExpression(arg); | |
| if (!expression) { | |
| return; | |
| } | |
| context.report({ | |
| node: arg, | |
| messageId: 'preferDirectDerived', | |
| fix(fixer) { | |
| const sourceCode = context.getSourceCode(); | |
| const expressionText = sourceCode.getText(expression); | |
| // Replace the entire function argument with just the expression | |
| return fixer.replaceText(arg, expressionText); | |
| }, | |
| }); | |
| } else if (isComplexFunction(arg)) { | |
| // Flag complex functions and suggest using $derived.by | |
| context.report({ | |
| node: arg, | |
| messageId: 'preferDerivedBy', | |
| fix(fixer) { | |
| const sourceCode = context.getSourceCode(); | |
| const functionText = sourceCode.getText(arg); | |
| // Replace $derived with $derived.by | |
| return fixer.replaceText(node.callee, '$derived.by'); | |
| }, | |
| }); | |
| } | |
| }, | |
| }; | |
| }, | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment