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 2 days ago
parent 0bc2e55cae
commit b7451891c2

@ -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 {
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) {

Loading…
Cancel
Save