From b7451891c2e64fac4c81e253baeeae6a6f82b183 Mon Sep 17 00:00:00 2001 From: Nic <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:08:31 -0400 Subject: [PATCH] scopes take a declaration's initial value, a write's value and top-level await from the parser instead of walking for them --- packages/svelte/src/compiler/phases/scope.js | 113 +++++++------------ 1 file changed, 43 insertions(+), 70 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 0125f8c244..0bad17bce1 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -5,7 +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 { bindingOf, referenceOf, scopeOf } from '@teasel/parser'; import { extract_identifiers, extract_identifiers_from_destructuring, @@ -774,6 +774,7 @@ export class Scope { * @param {Identifier} node * @param {AST.SvelteNode[]} path * @param {Binding | null | undefined} [binding] what the parser resolved the reference to + * @returns {Binding | null} what the reference resolved to; null for a global */ reference(node, path, binding = undefined) { path = [...path]; // ensure that mutations to path afterwards don't affect this reference @@ -787,13 +788,13 @@ export class Scope { binding ??= this.declarations.get(node.name); if (binding !== undefined && binding !== null && binding.scope === this) { binding.references.push({ node, path }); - } else if (this.parent) { - this.parent.reference(node, path, binding); - } else { - // no binding was found, and this is the top level scope, - // which means this is a global - this.root.conflicts.add(node.name); + return binding; } + if (this.parent) return this.parent.reference(node, path, binding); + // no binding was found, and this is the top level scope, + // which means this is a global + this.root.conflicts.add(node.name); + return null; } /** @@ -955,20 +956,25 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { * @param {Scope} scope * @param {import('@teasel/parser').Scope} parsed * @param {boolean} params - * @param {Node | null} [holder] the function, for which parameters are rest parameters */ - function declare_parsed(scope, parsed, params, holder = null) { + function declare_parsed(scope, parsed, params) { 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; + const declaration = /** @type {any} */ (binding.declaration); /** @type {DeclarationKind} */ let kind = 'let'; + /** @type {Binding['initial']} */ + let initial = null; switch (binding.kind) { case 'var': case 'let': case 'const': + kind = binding.kind; + initial = declaration?.init ?? null; + break; case 'function': case 'import': kind = binding.kind; @@ -977,10 +983,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { kind = 'function'; break; case 'param': - kind = is_rest_param(/** @type {any} */ (holder), binding.node) ? 'rest_param' : 'param'; + kind = is_rest_param(declaration, binding.node) ? 'rest_param' : 'param'; break; } - from_parser.set(binding, scope.declare(binding.node, 'normal', kind)); + if (declaration?.type === 'FunctionDeclaration' || declaration?.type === 'ClassDeclaration') { + initial = declaration; + } + from_parser.set(binding, scope.declare(binding.node, 'normal', kind, initial)); } } @@ -993,21 +1002,20 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { return last?.type === 'RestElement' && extract_identifiers(last).includes(id); } - /** @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; } + let has_await = false; + if (ast.type === 'Program') { const parsed = scopeOf(ast); - if (parsed) declare_parsed(scope, parsed, false); + if (parsed) { + declare_parsed(scope, parsed, false); + has_await = parsed.topLevelAwait; + } } /** @@ -1071,8 +1079,6 @@ 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) { @@ -1096,10 +1102,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { return context.next(); } switch (parsed.kind) { + case 'fragment': + // a template expression parsed on its own; an await in it runs when the template does + has_await ||= parsed.topLevelAwait; + return context.next(); case 'function': { const scope = context.state.scope.child(true); scopes.set(node, scope); - declare_parsed(scope, parsed, true, /** @type {any} */ (node)); + declare_parsed(scope, parsed, true); 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); @@ -1120,22 +1130,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } }, - AwaitExpression(node, context) { - // this doesn't _really_ belong here, but it allows us to - // automatically opt into runes mode on encountering - // blocking awaits, without doing an additional walk - // before the analysis occurs - // TODO remove this in Svelte 7.0 or whenever we get rid of legacy support - has_await ||= context.path.every( - ({ type }) => - type !== 'ArrowFunctionExpression' && - type !== 'FunctionExpression' && - type !== 'FunctionDeclaration' - ); - - context.next(); - }, - Identifier(node, { path, state }) { if (is_reference(node, path.at(-1))) { references.push([state.scope, { node, path: path.slice() }]); @@ -1202,18 +1196,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { SvelteSelf: Component, SvelteComponent: Component, - // updates - AssignmentExpression(node, { state, next }) { - updates.push([state.scope, node.left, node.right]); - next(); - }, - - UpdateExpression(node, { state, next }) { - const expression = /** @type {Identifier | MemberExpression} */ (node.argument); - updates.push([state.scope, expression, expression]); - next(); - }, - ImportDeclaration(node, { state }) { for (const specifier of node.specifiers) { const binding = @@ -1223,20 +1205,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } }, - FunctionDeclaration(node, { next }) { - const binding = node.id && declared(node.id); - if (binding) set_initial(binding, node); - next(); - }, - - ClassDeclaration(node, { state, next }) { - if (node.id) { - const binding = declared(node.id) ?? state.scope.declare(node.id, 'normal', 'let', node); - set_initial(binding, node); - } - next(); - }, - VariableDeclaration(node, { state, path, next }) { const is_parent_const_tag = path.at(-1)?.type === 'ConstTag'; for (const declarator of node.declarations) { @@ -1246,17 +1214,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { state.scope.declarators.set(declarator, bindings); for (const id of extract_identifiers(declarator.id)) { - let binding = declared(id); - if (binding) { - if (declarator.init) set_initial(binding, declarator.init); - } else { - binding = state.scope.declare( + const binding = + declared(id) ?? + state.scope.declare( id, is_parent_const_tag ? 'template' : 'normal', node.kind, declarator.init ); - } binding.metadata = { is_template_declaration: true }; bindings.push(binding); } @@ -1423,7 +1388,15 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { // we do this after the fact, so that we don't need to worry // about encountering references before their declarations for (const [scope, { node, path }] of references) { - scope.reference(node, path, declared(node)); + const binding = scope.reference(node, path, declared(node)); + // what the parser saw the identifier do; a declaring identifier is no reference to it + const parsed = referenceOf(node); + if (binding === null || parsed === undefined) continue; + if (parsed.write) { + binding.reassigned = true; + binding.assignments.push({ value: parsed.writeExpr ?? node, scope }); + } + if (parsed.mutate) binding.mutated = true; } for (const [scope, node, value] of updates) {