diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 5a3ace3ffd..c12e9066aa 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -5,6 +5,7 @@ import { walk } from 'zimmerframe'; import { ExpressionMetadata } from './nodes.js'; import * as b from '#compiler/builders'; import * as e from '../errors.js'; +import { bindingOf, scopeOf } from '@teasel/parser'; import { extract_identifiers, extract_identifiers_from_destructuring, @@ -944,27 +945,67 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { */ const possible_implicit_declarations = []; + /** @type {Map} the parser's bindings and ours */ + const from_parser = new Map(); + /** + * Declares in `scope` what the parser found declared in `parsed`: its parameters, or the rest. * @param {Scope} scope - * @param {Pattern[]} params + * @param {import('@teasel/parser').Scope} parsed + * @param {boolean} params + * @param {Node | null} [holder] the function, for which parameters are rest parameters */ - function add_params(scope, params) { - for (const param of params) { - for (const node of extract_identifiers(param)) { - scope.declare(node, 'normal', param.type === 'RestElement' ? 'rest_param' : 'param'); + function declare_parsed(scope, parsed, params, holder = null) { + for (const binding of parsed.bindings) { + if ((binding.kind === 'param') !== params) continue; + // no node: `arguments`, or a declaration erased with the TypeScript it belonged to + if (binding.node === null || binding.kind === 'class-name' || binding.kind === 'pattern') continue; + /** @type {DeclarationKind} */ + let kind = 'let'; + switch (binding.kind) { + case 'var': + case 'let': + case 'const': + case 'function': + case 'import': + kind = binding.kind; + break; + case 'function-name': + kind = 'function'; + break; + case 'param': + kind = is_rest_param(/** @type {any} */ (holder), binding.node) ? 'rest_param' : 'param'; + break; } + from_parser.set(binding, scope.declare(binding.node, 'normal', kind)); } } /** - * @type {Visitor} + * @param {{ params: Pattern[] } | null} holder + * @param {Identifier} id */ - const create_block_scope = (node, { state, next }) => { - const scope = state.scope.child(true); - scopes.set(node, scope); + function is_rest_param(holder, id) { + const last = holder?.params.at(-1); + return last?.type === 'RestElement' && extract_identifiers(last).includes(id); + } - next({ scope }); - }; + /** @param {Binding} binding @param {Expression | Node} initial */ + function set_initial(binding, initial) { + binding.initial = /** @type {any} */ (initial); + binding.assignments.push({ value: /** @type {Expression} */ (initial), scope: binding.scope }); + } + + /** The parser's declaration a Svelte binding stands for, once declared. @param {Identifier} id */ + function declared(id) { + const parsed = bindingOf(id); + return parsed ? from_parser.get(parsed) : undefined; + } + + if (ast.type === 'Program') { + const parsed = scopeOf(ast); + if (parsed) declare_parsed(scope, parsed, false); + } /** * @type {Visitor} @@ -1030,6 +1071,48 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { let has_await = false; walk(ast, state, { + // the scopes JavaScript opens, and what they declare, as the parser found them + _(node, context) { + const parsed = scopeOf(/** @type {any} */ (node)); + const parent = context.path.at(-1); + if (parsed === undefined) { + const of_function = node.type === 'BlockStatement' && parent && scopeOf(/** @type {any} */ (parent)); + if ( + of_function && + (parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression' || parent.type === 'ArrowFunctionExpression') + ) { + // the body holds the non-porous function scope; the parameters live one above + const scope = context.state.scope.child(); + scopes.set(node, scope); + declare_parsed(scope, of_function, false); + return context.next({ scope }); + } + return context.next(); + } + switch (parsed.kind) { + case 'function': { + const scope = context.state.scope.child(true); + scopes.set(node, scope); + declare_parsed(scope, parsed, true, /** @type {any} */ (node)); + if (node.type === 'FunctionExpression' && node.id) declare_parsed(scope, parsed, false); + else if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement') declare_parsed(scope, parsed, false); + return context.next({ scope }); + } + case 'block': + case 'for': + case 'switch': + case 'catch': + case 'static-block': { + const scope = context.state.scope.child(true); + scopes.set(node, scope); + declare_parsed(scope, parsed, false); + return context.next({ scope }); + } + default: + return context.next(); + } + }, + AwaitExpression(node, context) { // this doesn't _really_ belong here, but it allows us to // automatically opt into runes mode on encountering @@ -1126,61 +1209,22 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { ImportDeclaration(node, { state }) { for (const specifier of node.specifiers) { - state.scope.declare(specifier.local, 'normal', 'import', node); + const binding = declared(specifier.local) ?? state.scope.declare(specifier.local, 'normal', 'import', node); + binding.initial = node; } }, - FunctionExpression(node, { state, next }) { - const scope = state.scope.child(true); - scopes.set(node, scope); - - if (node.id) scope.declare(node.id, 'normal', 'function'); - - add_params(scope, node.params); - next({ scope }); - }, - - FunctionDeclaration(node, { state, next }) { - if (node.id) state.scope.declare(node.id, 'normal', 'function', node); - - const scope = state.scope.child(true); - scopes.set(node, scope); - - add_params(scope, node.params); - next({ scope }); - }, - - ArrowFunctionExpression(node, { state, next }) { - const scope = state.scope.child(true); - scopes.set(node, scope); - - add_params(scope, node.params); - next({ scope }); - }, - - ForStatement: create_block_scope, - ForInStatement: create_block_scope, - ForOfStatement: create_block_scope, - SwitchStatement: create_block_scope, - BlockStatement(node, context) { - const parent = context.path.at(-1); - if ( - parent?.type === 'FunctionDeclaration' || - parent?.type === 'FunctionExpression' || - parent?.type === 'ArrowFunctionExpression' - ) { - // The scopes created for the function nodes above handle the function identifier and - // parameters, but the block statement itself holds the non-porous function scope - const scope = context.state.scope.child(); - scopes.set(node, scope); - context.next({ scope }); - } else { - create_block_scope(node, context); - } + FunctionDeclaration(node, { next }) { + const binding = node.id && declared(node.id); + if (binding) set_initial(binding, node); + next(); }, ClassDeclaration(node, { state, next }) { - if (node.id) state.scope.declare(node.id, 'normal', 'let', node); + if (node.id) { + const binding = declared(node.id) ?? state.scope.declare(node.id, 'normal', 'let', node); + set_initial(binding, node); + } next(); }, @@ -1193,12 +1237,17 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { state.scope.declarators.set(declarator, bindings); for (const id of extract_identifiers(declarator.id)) { - const binding = state.scope.declare( - id, - is_parent_const_tag ? 'template' : 'normal', - node.kind, - declarator.init - ); + let binding = declared(id); + if (binding) { + if (declarator.init) set_initial(binding, declarator.init); + } else { + binding = state.scope.declare( + id, + is_parent_const_tag ? 'template' : 'normal', + node.kind, + declarator.init + ); + } binding.metadata = { is_template_declaration: true }; bindings.push(binding); } @@ -1207,21 +1256,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { next(); }, - CatchClause(node, { state, next }) { - if (node.param) { - const scope = state.scope.child(true); - scopes.set(node, scope); - - for (const id of extract_identifiers(node.param)) { - scope.declare(id, 'normal', 'let'); - } - - next({ scope }); - } else { - next(); - } - }, - EachBlock(node, { state, visit }) { visit(node.expression);