scopes take a declaration's initial value, a write's value and top-level await from the parser instead of walking for them

goodbye-acorn
Nic 3 days ago
parent 0bc2e55cae
commit b7451891c2

@ -5,7 +5,7 @@ import { walk } from 'zimmerframe';
import { ExpressionMetadata } from './nodes.js'; import { ExpressionMetadata } from './nodes.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import * as e from '../errors.js'; import * as e from '../errors.js';
import { bindingOf, scopeOf } from '@teasel/parser'; import { bindingOf, referenceOf, scopeOf } from '@teasel/parser';
import { import {
extract_identifiers, extract_identifiers,
extract_identifiers_from_destructuring, extract_identifiers_from_destructuring,
@ -774,6 +774,7 @@ export class Scope {
* @param {Identifier} node * @param {Identifier} node
* @param {AST.SvelteNode[]} path * @param {AST.SvelteNode[]} path
* @param {Binding | null | undefined} [binding] what the parser resolved the reference to * @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) { reference(node, path, binding = undefined) {
path = [...path]; // ensure that mutations to path afterwards don't affect this reference 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); binding ??= this.declarations.get(node.name);
if (binding !== undefined && binding !== null && binding.scope === this) { if (binding !== undefined && binding !== null && binding.scope === this) {
binding.references.push({ node, path }); binding.references.push({ node, path });
} else if (this.parent) { return binding;
this.parent.reference(node, path, binding); }
} else { if (this.parent) return this.parent.reference(node, path, binding);
// no binding was found, and this is the top level scope, // no binding was found, and this is the top level scope,
// which means this is a global // which means this is a global
this.root.conflicts.add(node.name); 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 {Scope} scope
* @param {import('@teasel/parser').Scope} parsed * @param {import('@teasel/parser').Scope} parsed
* @param {boolean} params * @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) { for (const binding of parsed.bindings) {
if ((binding.kind === 'param') !== params) continue; if ((binding.kind === 'param') !== params) continue;
// no node: `arguments`, or a declaration erased with the TypeScript it belonged to // no node: `arguments`, or a declaration erased with the TypeScript it belonged to
if (binding.node === null || binding.kind === 'class-name' || binding.kind === 'pattern') if (binding.node === null || binding.kind === 'class-name' || binding.kind === 'pattern')
continue; continue;
const declaration = /** @type {any} */ (binding.declaration);
/** @type {DeclarationKind} */ /** @type {DeclarationKind} */
let kind = 'let'; let kind = 'let';
/** @type {Binding['initial']} */
let initial = null;
switch (binding.kind) { switch (binding.kind) {
case 'var': case 'var':
case 'let': case 'let':
case 'const': case 'const':
kind = binding.kind;
initial = declaration?.init ?? null;
break;
case 'function': case 'function':
case 'import': case 'import':
kind = binding.kind; kind = binding.kind;
@ -977,10 +983,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
kind = 'function'; kind = 'function';
break; break;
case 'param': 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; 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); 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 */ /** The parser's declaration a Svelte binding stands for, once declared. @param {Identifier} id */
function declared(id) { function declared(id) {
const parsed = bindingOf(id); const parsed = bindingOf(id);
return parsed ? from_parser.get(parsed) : undefined; return parsed ? from_parser.get(parsed) : undefined;
} }
let has_await = false;
if (ast.type === 'Program') { if (ast.type === 'Program') {
const parsed = scopeOf(ast); 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, { walk(ast, state, {
// the scopes JavaScript opens, and what they declare, as the parser found them // the scopes JavaScript opens, and what they declare, as the parser found them
_(node, context) { _(node, context) {
@ -1096,10 +1102,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
return context.next(); return context.next();
} }
switch (parsed.kind) { 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': { case 'function': {
const scope = context.state.scope.child(true); const scope = context.state.scope.child(true);
scopes.set(node, scope); 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); if (node.type === 'FunctionExpression' && node.id) declare_parsed(scope, parsed, false);
else if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement') else if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement')
declare_parsed(scope, parsed, false); 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 }) { Identifier(node, { path, state }) {
if (is_reference(node, path.at(-1))) { if (is_reference(node, path.at(-1))) {
references.push([state.scope, { node, path: path.slice() }]); references.push([state.scope, { node, path: path.slice() }]);
@ -1202,18 +1196,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
SvelteSelf: Component, SvelteSelf: Component,
SvelteComponent: 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 }) { ImportDeclaration(node, { state }) {
for (const specifier of node.specifiers) { for (const specifier of node.specifiers) {
const binding = 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 }) { VariableDeclaration(node, { state, path, next }) {
const is_parent_const_tag = path.at(-1)?.type === 'ConstTag'; const is_parent_const_tag = path.at(-1)?.type === 'ConstTag';
for (const declarator of node.declarations) { 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); state.scope.declarators.set(declarator, bindings);
for (const id of extract_identifiers(declarator.id)) { for (const id of extract_identifiers(declarator.id)) {
let binding = declared(id); const binding =
if (binding) { declared(id) ??
if (declarator.init) set_initial(binding, declarator.init); state.scope.declare(
} else {
binding = state.scope.declare(
id, id,
is_parent_const_tag ? 'template' : 'normal', is_parent_const_tag ? 'template' : 'normal',
node.kind, node.kind,
declarator.init declarator.init
); );
}
binding.metadata = { is_template_declaration: true }; binding.metadata = { is_template_declaration: true };
bindings.push(binding); 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 // we do this after the fact, so that we don't need to worry
// about encountering references before their declarations // about encountering references before their declarations
for (const [scope, { node, path }] of references) { 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) { for (const [scope, node, value] of updates) {

Loading…
Cancel
Save