diff --git a/packages/svelte/src/compiler/phases/2-analyze/utils.js b/packages/svelte/src/compiler/phases/2-analyze/utils.js new file mode 100644 index 0000000000..5c68a9b453 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/utils.js @@ -0,0 +1,55 @@ +import { error } from '../../errors.js'; + +/** + * @param {import('../../errors.js').NodeLike} node + * @param {import('estree').Pattern | import('estree').Expression} argument + * @param {import('../scope').Scope} scope + * @param {boolean} is_binding + */ +export function validate_no_const_assignment(node, argument, scope, is_binding) { + if (argument.type === 'Identifier') { + const binding = scope.get(argument.name); + if (binding?.declaration_kind === 'const' && binding.kind !== 'each') { + error( + node, + 'invalid-const-assignment', + is_binding, + // This takes advantage of the fact that we don't assign initial for let directives and then/catch variables. + // If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message. + binding.kind !== 'state' && (binding.kind !== 'normal' || !binding.initial) + ); + } + } +} + +/** + * @param {import('estree').AssignmentExpression | import('estree').UpdateExpression} node + * @param {import('estree').Pattern | import('estree').Expression} argument + * @param {import('./types.js').AnalysisState} state + */ +export function validate_assignment(node, argument, state) { + validate_no_const_assignment(node, argument, state.scope, false); + + let left = /** @type {import('estree').Expression | import('estree').Super} */ (argument); + + /** @type {import('estree').Expression | import('estree').PrivateIdentifier | null} */ + let property = null; + + while (left.type === 'MemberExpression') { + property = left.property; + left = left.object; + } + + if (left.type === 'Identifier') { + const binding = state.scope.get(left.name); + if (binding?.kind === 'derived') { + error(node, 'invalid-derived-assignment'); + } + } + + if (left.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') { + if (state.private_derived_state.includes(property.name)) { + error(node, 'invalid-derived-assignment'); + } + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/validate-a11y.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/validate-a11y.js index 2e34e113b2..be61364516 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/validate-a11y.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/validate-a11y.js @@ -1159,7 +1159,7 @@ function check_element(node, state, path) { /** * @type {import('zimmerframe').Visitors} */ -export const a11y_validators = { +export const validate_a11y = { RegularElement(node, context) { check_element(node, context.state, context.path); }, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/validate-legacy.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/validate-legacy.js new file mode 100644 index 0000000000..5d5b16408d --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/validate-legacy.js @@ -0,0 +1,39 @@ +import { error } from '../../../errors.js'; +import { validate_assignment } from '../utils.js'; + +/** @type {import('../types').Visitors} */ +export const validate_legacy = { + VariableDeclarator(node) { + if (node.init?.type !== 'CallExpression') return; + + const callee = node.init.callee; + if ( + callee.type !== 'Identifier' || + (callee.name !== '$state' && callee.name !== '$derived' && callee.name !== '$props') + ) { + return; + } + + // TODO check if it's a store subscription that's called? How likely is it that someone uses a store that contains a function? + error(node.init, 'invalid-rune-usage', callee.name); + }, + ExportNamedDeclaration(node) { + if ( + node.declaration && + node.declaration.type !== 'VariableDeclaration' && + node.declaration.type !== 'FunctionDeclaration' + ) { + error(node, 'TODO', 'whatever this is'); + } + }, + AssignmentExpression(node, { state, path }) { + // TODO this is also in validation_runes, DRY out + const parent = path.at(-1); + if (parent && parent.type === 'ConstTag') return; + validate_assignment(node, node.left, state); + }, + UpdateExpression(node, { state }) { + // TODO this is also in validation_runes, DRY out + validate_assignment(node, node.argument, state); + } +}; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/validate.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/validate.js index 35a29a1bf6..47d0a2da37 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/validate.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/validate.js @@ -8,7 +8,9 @@ import { is_custom_element_node } from '../../nodes.js'; import { regex_not_whitespace, regex_only_whitespaces } from '../../patterns.js'; import { Scope, get_rune } from '../../scope.js'; import { merge } from '../../visitors.js'; -import { a11y_validators } from './validate-a11y.js'; +import { validate_assignment, validate_no_const_assignment } from '../utils.js'; +import { validate_a11y } from './validate-a11y.js'; +import { validate_legacy } from './validate-legacy.js'; /** * @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node @@ -438,39 +440,7 @@ export const validation = { } }; -export const validation_legacy = merge(validation, a11y_validators, { - VariableDeclarator(node) { - if (node.init?.type !== 'CallExpression') return; - - const callee = node.init.callee; - if ( - callee.type !== 'Identifier' || - (callee.name !== '$state' && callee.name !== '$derived' && callee.name !== '$props') - ) { - return; - } - - // TODO check if it's a store subscription that's called? How likely is it that someone uses a store that contains a function? - error(node.init, 'invalid-rune-usage', callee.name); - }, - ExportNamedDeclaration(node) { - if ( - node.declaration && - node.declaration.type !== 'VariableDeclaration' && - node.declaration.type !== 'FunctionDeclaration' - ) { - error(node, 'TODO', 'whatever this is'); - } - }, - AssignmentExpression(node, { state, path }) { - const parent = path.at(-1); - if (parent && parent.type === 'ConstTag') return; - validate_assignment(node, node.left, state); - }, - UpdateExpression(node, { state }) { - validate_assignment(node, node.argument, state); - } -}); +export const validation_legacy = merge(validation, validate_a11y, validate_legacy); /** * @@ -580,61 +550,7 @@ export const validation_runes_js = { } }; -/** - * @param {import('../../../errors.js').NodeLike} node - * @param {import('estree').Pattern | import('estree').Expression} argument - * @param {Scope} scope - * @param {boolean} is_binding - */ -function validate_no_const_assignment(node, argument, scope, is_binding) { - if (argument.type === 'Identifier') { - const binding = scope.get(argument.name); - if (binding?.declaration_kind === 'const' && binding.kind !== 'each') { - error( - node, - 'invalid-const-assignment', - is_binding, - // This takes advantage of the fact that we don't assign initial for let directives and then/catch variables. - // If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message. - binding.kind !== 'state' && (binding.kind !== 'normal' || !binding.initial) - ); - } - } -} - -/** - * @param {import('estree').AssignmentExpression | import('estree').UpdateExpression} node - * @param {import('estree').Pattern | import('estree').Expression} argument - * @param {import('../types.js').AnalysisState} state - */ -function validate_assignment(node, argument, state) { - validate_no_const_assignment(node, argument, state.scope, false); - - let left = /** @type {import('estree').Expression | import('estree').Super} */ (argument); - - /** @type {import('estree').Expression | import('estree').PrivateIdentifier | null} */ - let property = null; - - while (left.type === 'MemberExpression') { - property = left.property; - left = left.object; - } - - if (left.type === 'Identifier') { - const binding = state.scope.get(left.name); - if (binding?.kind === 'derived') { - error(node, 'invalid-derived-assignment'); - } - } - - if (left.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') { - if (state.private_derived_state.includes(property.name)) { - error(node, 'invalid-derived-assignment'); - } - } -} - -export const validation_runes = merge(validation, a11y_validators, { +export const validation_runes = merge(validation, validate_a11y, { AssignmentExpression(node, { state, path }) { const parent = path.at(-1); if (parent && parent.type === 'ConstTag') return;