import { getPropertyName } from '@eslint-community/eslint-utils'; import { keyword } from 'esutils'; import { createRule } from '../utils/index.js'; import { findAttribute, isExpressionIdentifier, findVariable } from '../utils/ast-utils.js'; import { getSvelteContext } from '../utils/svelte-context.js'; import { SVELTE_RUNES } from '../shared/runes.js'; export default createRule('prefer-destructured-store-props', { meta: { docs: { description: 'destructure values from object stores for better change tracking & fewer redraws', category: 'Best Practices', recommended: false }, hasSuggestions: true, schema: [], messages: { useDestructuring: `Destructure {{property}} from {{store}} for better change tracking & fewer redraws`, fixUseDestructuring: `Using destructuring like $: ({ {{property}} } = {{store}}); will run faster`, fixUseVariable: `Using the predefined reactive variable {{variable}}` }, type: 'suggestion' }, create(context) { let mainScript = null; const svelteContext = getSvelteContext(context); // Store off instances of probably-destructurable statements const reports = []; let inScriptElement = false; const storeMemberAccessStack = []; /** Find for defined reactive variables. */ function* findReactiveVariable(object, propName) { const storeVar = findVariable(context, object); if (!storeVar) { return; } for (const reference of storeVar.references) { const id = reference.identifier; if (id.name !== object.name) continue; if (isReactiveVariableDefinitionWithMemberExpression(id)) { // $: target = $store.prop yield id.parent.parent.left; } else if (isReactiveVariableDefinitionWithDestructuring(id)) { const prop = id.parent.left.properties.find((prop) => prop.type === 'Property' && prop.value.type === 'Identifier' && getPropertyName(prop) === propName); if (prop) { // $: ({prop: target} = $store) yield prop.value; } } } /** Checks whether the given node is reactive variable definition with member expression. */ function isReactiveVariableDefinitionWithMemberExpression(node) { return (node.type === 'Identifier' && node.parent?.type === 'MemberExpression' && node.parent.object === node && getPropertyName(node.parent) === propName && node.parent.parent?.type === 'AssignmentExpression' && node.parent.parent.right === node.parent && node.parent.parent.left.type === 'Identifier' && node.parent.parent.parent?.type === 'ExpressionStatement' && node.parent.parent.parent.parent?.type === 'SvelteReactiveStatement'); } /** Checks whether the given node is reactive variable definition with destructuring. */ function isReactiveVariableDefinitionWithDestructuring(node) { return (node.type === 'Identifier' && node.parent?.type === 'AssignmentExpression' && node.parent.right === node && node.parent.left.type === 'ObjectPattern' && node.parent.parent?.type === 'ExpressionStatement' && node.parent.parent.parent?.type === 'SvelteReactiveStatement'); } } /** Checks whether the given name is already defined as a variable. */ function hasTopLevelVariable(name) { const scopeManager = context.sourceCode.scopeManager; if (scopeManager.globalScope?.set.has(name)) { return true; } const moduleScope = scopeManager.globalScope?.childScopes.find((s) => s.type === 'module'); return moduleScope?.set.has(name) || false; } return { SvelteScriptElement(node) { inScriptElement = true; const scriptContext = findAttribute(node, 'context'); const contextValue = scriptContext?.value.length === 1 && scriptContext.value[0]; if (contextValue && contextValue.type === 'SvelteLiteral' && contextValue.value === 'module') { // It is