scopes come from the parser's tables: a script is no longer walked, a template's javascript neither, and a reference finds its ancestors through the parser's parent links

goodbye-acorn
Nic 2 days ago
parent b7451891c2
commit 69b0bcd0c8

@ -746,11 +746,12 @@ const instance_script = {
// Analyze declaration bindings to see if they're exclusively updated within a single reactive statement // Analyze declaration bindings to see if they're exclusively updated within a single reactive statement
const possible_derived = bindings.every((binding) => const possible_derived = bindings.every((binding) =>
binding.references.every((reference) => { binding.references.every((reference) => {
const declaration = reference.path.find((el) => el.type === 'VariableDeclaration'); const path = reference.path;
const assignment = reference.path.find((el) => el.type === 'AssignmentExpression'); const declaration = path.find((el) => el.type === 'VariableDeclaration');
const update = reference.path.find((el) => el.type === 'UpdateExpression'); const assignment = path.find((el) => el.type === 'AssignmentExpression');
const update = path.find((el) => el.type === 'UpdateExpression');
const labeled = /** @type {LabeledStatement | undefined} */ ( const labeled = /** @type {LabeledStatement | undefined} */ (
reference.path.find((el) => el.type === 'LabeledStatement' && el.label.name === '$') path.find((el) => el.type === 'LabeledStatement' && el.label.name === '$')
); );
if ( if (

@ -4,6 +4,7 @@
import * as teasel from '@teasel/parser'; import * as teasel from '@teasel/parser';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import { find_matching_bracket } from './utils/bracket.js'; import { find_matching_bracket } from './utils/bracket.js';
import { parsed } from '../../utils/ast.js';
/** /**
* A standalone module, as `analyze_module` reads one. * A standalone module, as `analyze_module` reads one.
@ -28,12 +29,23 @@ export function parse(source, comments, typescript) {
add_comments(source, comments, /** @type {teasel.Comment[]} */ (ast.comments)); add_comments(source, comments, /** @type {teasel.Comment[]} */ (ast.comments));
delete ast.comments; delete ast.comments;
delete ast.scopes; parsed.set(ast, tables(ast));
delete ast.bindings;
return ast; return ast;
} }
/**
* Takes the tables off an answer, so they reach the scope analysis without showing in the tree.
* @param {{ scopes?: any; bindings?: any; references?: any }} answer
*/
function tables(answer) {
const { scopes, bindings, references } = answer;
delete answer.scopes;
delete answer.bindings;
delete answer.references;
return { scopes, bindings, references };
}
/** /**
* The program inside a `<script>`, with the positions of the whole template. * The program inside a `<script>`, with the positions of the whole template.
* @param {Parser} parser * @param {Parser} parser
@ -55,8 +67,7 @@ export function parse_script(parser, start, end) {
/** @type {teasel.Comment[]} */ (ast.comments) /** @type {teasel.Comment[]} */ (ast.comments)
); );
delete ast.comments; delete ast.comments;
delete ast.scopes; parsed.set(ast, tables(ast));
delete ast.bindings;
unsupported(ast.typescript); unsupported(ast.typescript);
delete ast.typescript; delete ast.typescript;
@ -101,7 +112,9 @@ export function read_expression(parser, until, opening_token = '{') {
const start = parser.index; const start = parser.index;
try { try {
return read(parser, (js) => js.parseExpressionAt(start, until)).node; const answer = read(parser, (js) => js.parseExpressionAt(start, until));
parsed.set(answer.node, tables(answer));
return answer.node;
} catch (err) { } catch (err) {
if (parser.loose) { if (parser.loose) {
// Find the next } and treat it as the end of the expression // Find the next } and treat it as the end of the expression
@ -143,7 +156,9 @@ export function read_pattern(parser) {
} }
} }
return read(parser, (js) => js.parsePatternAt(start)).node; const answer = read(parser, (js) => js.parsePatternAt(start));
parsed.set(answer.node, tables(answer));
return answer.node;
} }
/** /**
@ -152,7 +167,9 @@ export function read_pattern(parser) {
*/ */
export function read_params(parser) { export function read_params(parser) {
const start = parser.index; const start = parser.index;
return read(parser, (js) => js.parseParamsAt(start)).params; const answer = read(parser, (js) => js.parseParamsAt(start));
parsed.set(answer.params, tables(answer));
return answer.params;
} }
/** /**
@ -162,7 +179,7 @@ export function read_params(parser) {
export function read_statement(parser) { export function read_statement(parser) {
const start = parser.index; const start = parser.index;
return read(parser, (js) => { const answer = read(parser, (js) => {
try { try {
return js.parseStatementAt(start); return js.parseStatementAt(start);
} catch (err) { } catch (err) {
@ -170,7 +187,9 @@ export function read_statement(parser) {
e.unexpected_eof(parser.template.length); e.unexpected_eof(parser.template.length);
throw err; throw err;
} }
}).node; });
parsed.set(answer.node, tables(answer));
return answer.node;
} }
/** What erasure leaves in place needs a compiler, not this one */ /** What erasure leaves in place needs a compiler, not this one */

@ -379,20 +379,11 @@ export function analyze_component(root, source, options) {
)) ))
) { ) {
let is_nested_store_subscription_node = undefined; let is_nested_store_subscription_node = undefined;
search: for (const reference of references) { for (const reference of references) {
for (let i = reference.path.length - 1; i >= 0; i--) { const owner = reference.scope.owner(store_name);
const scope = if (!!owner && owner !== module.scope && owner !== instance.scope) {
scopes.get(reference.path[i]) || is_nested_store_subscription_node = reference.node;
module.scopes.get(reference.path[i]) || break;
instance.scopes.get(reference.path[i]);
if (scope) {
const owner = scope?.owner(store_name);
if (!!owner && owner !== module.scope && owner !== instance.scope) {
is_nested_store_subscription_node = reference.node;
break search;
}
break;
}
} }
} }
@ -635,7 +626,7 @@ export function analyze_component(root, source, options) {
if ( if (
path[path.length - 1].type === 'StyleDirective' || path[path.length - 1].type === 'StyleDirective' ||
path.some((node) => node.type === 'Fragment') || path.some((node) => node.type === 'Fragment') ||
(path[1].type === 'LabeledStatement' && path[1].label.name === '$') (path[1]?.type === 'LabeledStatement' && path[1].label.name === '$')
) { ) {
binding.kind = 'state'; binding.kind = 'state';
} }

@ -1,6 +1,7 @@
/** @import { Expression, LabeledStatement } from 'estree' */ /** @import { Expression, LabeledStatement } from 'estree' */
/** @import { AST, ReactiveStatement } from '#compiler' */ /** @import { AST, ReactiveStatement } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { extract_identifiers, object } from '../../../utils/ast.js'; import { extract_identifiers, object } from '../../../utils/ast.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
@ -36,28 +37,13 @@ export function LabeledStatement(node, context) {
// Every referenced binding becomes a dependency, unless it's on // Every referenced binding becomes a dependency, unless it's on
// the left-hand side of an `=` assignment // the left-hand side of an `=` assignment
const assigned = assigned_in(node.body);
for (const [name, nodes] of context.state.scope.references) { for (const [name, nodes] of context.state.scope.references) {
const binding = context.state.scope.get(name); const binding = context.state.scope.get(name);
if (binding === null) continue; if (binding === null) continue;
for (const { node, path } of nodes) { for (const { node } of nodes) {
/** @type {Expression} */ if (assigned.has(node)) continue;
let left = node;
let i = path.length - 1;
let parent = /** @type {Expression} */ (path.at(i));
while (parent.type === 'MemberExpression') {
left = parent;
parent = /** @type {Expression} */ (path.at(--i));
}
if (
parent.type === 'AssignmentExpression' &&
parent.operator === '=' &&
parent.left === left
) {
continue;
}
reactive_statement.dependencies.push(binding); reactive_statement.dependencies.push(binding);
break; break;
@ -93,3 +79,29 @@ export function LabeledStatement(node, context) {
context.next(); context.next();
} }
/**
* The identifiers on the left-hand side of the `=` assignments in a statement, the members of
* the target included: `a[b].c = 1` assigns through `a` and `b`.
* @param {import('estree').Node} node
* @returns {Set<import('estree').Node>}
*/
function assigned_in(node) {
/** @type {Set<import('estree').Node>} */
const assigned = new Set();
/** @param {import('estree').Node} target */
const collect = (target) => {
if (target.type === 'Identifier') assigned.add(target);
else if (target.type === 'MemberExpression') {
collect(target.object);
collect(target.property);
}
};
walk(node, null, {
AssignmentExpression(node, { next }) {
if (node.operator === '=') collect(node.left);
next();
}
});
return assigned;
}

@ -1,13 +1,14 @@
/** @import { BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, SimpleLiteral, FunctionExpression, ArrowFunctionExpression } from 'estree' */ /** @import { BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, SimpleLiteral, FunctionExpression, ArrowFunctionExpression, Program } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */ /** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import { walk } from 'zimmerframe'; 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, referenceOf, scopeOf } from '@teasel/parser'; import { bindingOf, parentOf } from '@teasel/parser';
import { import {
extract_identifiers, extract_identifiers,
parsed,
extract_identifiers_from_destructuring, extract_identifiers_from_destructuring,
is_reference, is_reference,
object, object,
@ -86,6 +87,44 @@ const global_constants = {
'Math.SQRT1_2': Math.SQRT1_2 'Math.SQRT1_2': Math.SQRT1_2
}; };
/** A reference an identifier makes: its ancestors, and the scope it is made from. */
export class Reference {
/** @type {Identifier} */
node;
/** @type {Scope} */
scope;
/** @type {AST.SvelteNode[] | undefined} */
#path;
/** @type {AST.SvelteNode[]} the template's ancestors, above the JavaScript the identifier sits in */
#template;
/**
* @param {Identifier} node
* @param {Scope} scope
* @param {AST.SvelteNode[]} path the ancestors, or the template's alone when the identifier
* comes from the parser, whose parent links supply the rest
* @param {boolean} [complete]
*/
constructor(node, scope, path, complete = true) {
this.node = node;
this.scope = scope;
this.#template = path;
if (complete) this.#path = path;
}
/** The ancestors of the identifier, outermost first: the template's, then the JavaScript's. */
get path() {
if (this.#path === undefined) {
const inside = [];
for (let parent = parentOf(this.node); parent !== undefined; parent = parentOf(parent)) {
inside.push(/** @type {AST.SvelteNode} */ (parent));
}
this.#path = this.#template.concat(inside.reverse());
}
return this.#path;
}
}
export class Binding { export class Binding {
/** @type {Scope} */ /** @type {Scope} */
scope; scope;
@ -106,7 +145,7 @@ export class Binding {
*/ */
initial = null; initial = null;
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */ /** @type {Reference[]} */
references = []; references = [];
/** /**
@ -635,7 +674,7 @@ export class Scope {
/** /**
* A set of all the names referenced with this scope * A set of all the names referenced with this scope
* useful for generating unique names * useful for generating unique names
* @type {Map<string, { node: Identifier; path: AST.SvelteNode[] }[]>} * @type {Map<string, Reference[]>}
*/ */
references = new Map(); references = new Map();
@ -772,25 +811,26 @@ export class Scope {
/** /**
* @param {Identifier} node * @param {Identifier} node
* @param {AST.SvelteNode[]} path * @param {AST.SvelteNode[]} path the ancestors, or the template's alone for an identifier the
* parser knows the parents of
* @param {Binding | null | undefined} [binding] what the parser resolved the reference to * @param {Binding | null | undefined} [binding] what the parser resolved the reference to
* @param {Reference} [reference] the record, once the walk up made it
* @returns {Binding | null} what the reference resolved to; null for a global * @returns {Binding | null} what the reference resolved to; null for a global
*/ */
reference(node, path, binding = undefined) { reference(node, path, binding = undefined, reference = new Reference(node, this, path)) {
path = [...path]; // ensure that mutations to path afterwards don't affect this reference
let references = this.references.get(node.name); let references = this.references.get(node.name);
if (!references) this.references.set(node.name, (references = [])); if (!references) this.references.set(node.name, (references = []));
references.push({ node, path }); references.push(reference);
// the parser resolved the reference already, or the name walks up // the parser resolved the reference already, or the name walks up
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(reference);
return binding; return binding;
} }
if (this.parent) return this.parent.reference(node, path, binding); if (this.parent) return this.parent.reference(node, path, binding, reference);
// 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);
@ -936,7 +976,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/** @type {State} */ /** @type {State} */
const state = { scope }; const state = { scope };
/** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */ /**
* Every reference with the scope it is made from, and what the parser knows of it.
* @type {[Scope, Reference, Binding | undefined, import('@teasel/parser').Reference | undefined][]}
*/
const references = []; const references = [];
/** @type {[Scope, Pattern | MemberExpression, Expression][]} */ /** @type {[Scope, Pattern | MemberExpression, Expression][]} */
@ -956,8 +999,9 @@ 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 {boolean} [template] the declarations are a const tag's
*/ */
function declare_parsed(scope, parsed, params) { function declare_parsed(scope, parsed, params, template = false) {
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
@ -989,7 +1033,162 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (declaration?.type === 'FunctionDeclaration' || declaration?.type === 'ClassDeclaration') { if (declaration?.type === 'FunctionDeclaration' || declaration?.type === 'ClassDeclaration') {
initial = declaration; initial = declaration;
} }
from_parser.set(binding, scope.declare(binding.node, 'normal', kind, initial)); const ours = scope.declare(binding.node, template ? 'template' : 'normal', kind, initial);
from_parser.set(binding, ours);
if (declaration?.type === 'VariableDeclarator') {
ours.metadata = { is_template_declaration: true };
declarators.push([declaration, ours]);
}
}
}
/**
* Each declarator with what it declares, for the scope at its own position: a `var` hoists
* past the block it sits in, and the block is the scope a visitor of the declaration sees.
* @type {[VariableDeclarator, Binding][]}
*/
const declarators = [];
/**
* The `$:` statements of an instance script, each with the scope it opens, for what sits in them.
* @type {[number, number, Scope][]}
*/
const labeled = [];
/**
* The scopes, declarations and references of one parsed answer, from the parser's tables: a
* script's program, or a template's expression, pattern, parameter list or declaration.
* @param {Scope} scope what the answer's outermost scope is here
* @param {NonNullable<ReturnType<typeof parsed.get>>} answer
* @param {AST.SvelteNode[]} path the answer's ancestors
* @param {boolean} template the answer's declarations are a const tag's
*/
function from_tables(scope, answer, path, template) {
const outermost = answer.scopes[0];
/** @type {Map<import('@teasel/parser').Scope, Scope>} the parser's scopes and ours; a function's holds its parameters */
const ours = new Map([[outermost, scope]]);
/** @type {Map<import('@teasel/parser').Scope, Scope>} a function's body, which holds the rest */
const bodies = new Map();
/**
* Our scope at a position inside the parser's: a function's body from where it starts, a
* `$:` statement's inside one.
* @param {import('@teasel/parser').Scope} scope
* @param {number} position
*/
function at(scope, position) {
const body = bodies.get(scope);
if (body !== undefined && position >= /** @type {any} */ (scope.node).body.start) return body;
if (scope === outermost) {
for (const [start, end, inside] of labeled) {
if (position >= start && position < end) return inside;
}
}
return /** @type {Scope} */ (ours.get(scope));
}
declare_parsed(scope, outermost, false, template);
for (let i = 1; i < answer.scopes.length; i++) {
const parsed = answer.scopes[i];
const node = /** @type {any} */ (parsed.node);
const parent = at(
/** @type {import('@teasel/parser').Scope} */ (parsed.parent),
node?.start ?? 0
);
switch (parsed.kind) {
case 'function': {
// the parameters live one above the body, which holds the non-porous function scope
const params = parent.child(true);
scopes.set(node, params);
ours.set(parsed, params);
if (parsed.parent?.kind === 'function-name') ours.set(parsed.parent, params);
declare_parsed(params, parsed, true);
if (
node.body.type !== 'BlockStatement' ||
(node.type === 'FunctionExpression' && node.id)
) {
declare_parsed(params, parsed, false);
}
if (node.body.type === 'BlockStatement') {
const body = params.child();
scopes.set(node.body, body);
bodies.set(parsed, body);
declare_parsed(body, parsed, false);
}
break;
}
case 'block':
case 'for':
case 'switch':
case 'catch':
case 'static-block': {
const inside = parent.child(true);
scopes.set(node, inside);
ours.set(parsed, inside);
declare_parsed(inside, parsed, false);
break;
}
default:
ours.set(parsed, parent);
}
}
/** @type {[Identifier, Scope, Binding | undefined, import('@teasel/parser').Reference | undefined][]} */
const entries = [];
// a declaring identifier counts as a reference to what it declares, as the walk had it;
// an import's did not, since the walk never entered an import declaration
for (const binding of answer.bindings) {
if (binding.node === null || binding.kind === 'import') continue;
const declared = from_parser.get(binding);
entries.push([
binding.node,
declared?.scope ?? at(binding.scope, /** @type {number} */ (binding.node.start)),
declared,
undefined
]);
}
for (const reference of answer.references) {
const binding = reference.binding === null ? undefined : from_parser.get(reference.binding);
entries.push([
reference.node,
at(reference.scope, /** @type {number} */ (reference.node.start)),
binding,
reference
]);
}
/**
* The innermost of the parser's scopes around a position, by the ranges of the nodes that open them.
* @param {number} position
*/
const innermost = (position) => {
let found = outermost;
for (const parsed of answer.scopes) {
const node = /** @type {any} */ (parsed.node);
if (
node &&
node.start <= position &&
position < node.end &&
parsed !== outermost &&
ours.has(parsed)
) {
const current = /** @type {any} */ (found.node);
if (!current || node.start >= current.start) found = parsed;
}
}
return found;
};
for (const [declarator, binding] of declarators.splice(0)) {
const position = /** @type {number} */ (declarator.start);
const here = at(innermost(position), position);
let declared = here.declarators.get(declarator);
if (!declared) here.declarators.set(declarator, (declared = []));
declared.push(binding);
}
entries.sort((a, b) => /** @type {number} */ (a[0].start) - /** @type {number} */ (b[0].start));
for (const [node, scope, binding, reference] of entries) {
references.push([scope, new Reference(node, scope, path, false), binding, reference]);
} }
} }
@ -1010,11 +1209,45 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
let has_await = false; let has_await = false;
if (ast.type === 'Program') { // a script comes entirely from the parser's tables: nothing in it is walked
const parsed = scopeOf(ast); const program = ast.type === 'Program' ? parsed.get(ast) : undefined;
if (parsed) { if (program !== undefined) {
declare_parsed(scope, parsed, false); has_await = program.scopes[0].topLevelAwait;
has_await = parsed.topLevelAwait; for (const node of /** @type {Program} */ (ast).body) {
if (
allow_reactive_declarations &&
node.type === 'LabeledStatement' &&
node.label.name === '$'
) {
// create a scope for the $: block
const inside = scope.child();
scopes.set(node, inside);
labeled.push([
/** @type {number} */ (node.start),
/** @type {number} */ (node.end),
inside
]);
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
for (const id of extract_identifiers(node.body.expression.left)) {
if (!id.name.startsWith('$')) {
possible_implicit_declarations.push(id);
}
}
}
}
}
from_tables(scope, program, [], false);
for (const node of /** @type {Program} */ (ast).body) {
if (node.type !== 'ImportDeclaration') continue;
// a type-only import binds nothing to the parser; the declaration is the initial value
for (const specifier of node.specifiers) {
const binding =
declared(specifier.local) ?? scope.declare(specifier.local, 'normal', 'import', node);
binding.initial = node;
}
} }
} }
@ -1079,304 +1312,239 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
} }
}; };
walk(ast, state, { if (program === undefined)
// the scopes JavaScript opens, and what they declare, as the parser found them walk(ast, state, {
_(node, context) { // the JavaScript of a template, parsed on its own: its scopes come from the parser's tables
const parsed = scopeOf(/** @type {any} */ (node)); _(node, context) {
const parent = context.path.at(-1); const answer = parsed.get(node);
if (parsed === undefined) { if (answer === undefined) return context.next();
const of_function = has_await ||= answer.scopes[0].topLevelAwait;
node.type === 'BlockStatement' && parent && scopeOf(/** @type {any} */ (parent)); from_tables(
if ( context.state.scope,
of_function && answer,
(parent.type === 'FunctionDeclaration' || context.path.slice(),
parent.type === 'FunctionExpression' || context.path.at(-1)?.type === 'ConstTag'
parent.type === 'ArrowFunctionExpression') );
) { },
// the body holds the non-porous function scope; the parameters live one above
const scope = context.state.scope.child(); // an identifier the template built itself, a shorthand attribute's say, is no parser's
scopes.set(node, scope); Identifier(node, { path, state }) {
declare_parsed(scope, of_function, false); if (is_reference(node, path.at(-1))) {
return context.next({ scope }); references.push([
} state.scope,
return context.next(); new Reference(node, state.scope, path.slice()),
} undefined,
switch (parsed.kind) { undefined
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);
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': // a const tag's declaration is built around its parsed pattern and initializer, so the
case 'catch': // parser has no tables for it; its pattern and initializer are visited as usual
case 'static-block': { VariableDeclaration(node, { state, next }) {
const scope = context.state.scope.child(true); for (const declarator of node.declarations) {
scopes.set(node, scope); /** @type {Binding[]} */
declare_parsed(scope, parsed, false); const bindings = [];
return context.next({ scope }); state.scope.declarators.set(declarator, bindings);
} for (const id of extract_identifiers(declarator.id)) {
default: const binding = state.scope.declare(id, 'template', node.kind, declarator.init);
return context.next(); binding.metadata = { is_template_declaration: true };
} bindings.push(binding);
},
Identifier(node, { path, state }) {
if (is_reference(node, path.at(-1))) {
references.push([state.scope, { node, path: path.slice() }]);
}
},
LabeledStatement(node, { path, next }) {
if (path.length > 1 || !allow_reactive_declarations) return next();
if (node.label.name !== '$') return next();
// create a scope for the $: block
const scope = state.scope.child();
scopes.set(node, scope);
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
for (const id of extract_identifiers(node.body.expression.left)) {
if (!id.name.startsWith('$')) {
possible_implicit_declarations.push(id);
} }
} }
} next();
},
next({ scope });
},
SvelteFragment, SvelteFragment,
SlotElement: SvelteFragment, SlotElement: SvelteFragment,
SvelteElement: SvelteFragment, SvelteElement: SvelteFragment,
RegularElement: SvelteFragment, RegularElement: SvelteFragment,
LetDirective(node, context) { LetDirective(node, context) {
const scope = context.state.scope; const scope = context.state.scope;
/** @type {Binding[]} */ /** @type {Binding[]} */
const bindings = []; const bindings = [];
scope.declarators.set(node, bindings); scope.declarators.set(node, bindings);
if (node.expression) { if (node.expression) {
for (const id of extract_identifiers_from_destructuring(node.expression)) { for (const id of extract_identifiers_from_destructuring(node.expression)) {
const binding = scope.declare(id, 'template', 'const');
scope.reference(id, [context.path[context.path.length - 1], node]);
bindings.push(binding);
}
} else {
/** @type {Identifier} */
const id = {
name: node.name,
type: 'Identifier',
start: node.start,
end: node.end
};
const binding = scope.declare(id, 'template', 'const'); const binding = scope.declare(id, 'template', 'const');
scope.reference(id, [context.path[context.path.length - 1], node]); scope.reference(id, [context.path[context.path.length - 1], node]);
bindings.push(binding); bindings.push(binding);
} }
} else { },
/** @type {Identifier} */
const id = { Component: (node, context) => {
name: node.name, context.state.scope.reference(b.id(node.name.split('.')[0]), context.path);
type: 'Identifier', Component(node, context);
start: node.start, },
end: node.end SvelteSelf: Component,
}; SvelteComponent: Component,
const binding = scope.declare(id, 'template', 'const');
scope.reference(id, [context.path[context.path.length - 1], node]); EachBlock(node, { state, visit }) {
bindings.push(binding); visit(node.expression);
}
}, // context and children are a new scope
const scope = state.scope.child();
Component: (node, context) => { scopes.set(node, scope);
context.state.scope.reference(b.id(node.name.split('.')[0]), context.path);
Component(node, context); if (node.context) {
}, // declarations
SvelteSelf: Component, for (const id of extract_identifiers(node.context)) {
SvelteComponent: Component, const binding = scope.declare(id, 'each', 'const');
ImportDeclaration(node, { state }) { let inside_rest = false;
for (const specifier of node.specifiers) { let is_rest_id = false;
const binding = walk(node.context, null, {
declared(specifier.local) ?? Identifier(node) {
state.scope.declare(specifier.local, 'normal', 'import', node); if (inside_rest && node === id) {
binding.initial = node; is_rest_id = true;
} }
}, },
RestElement(_, { next }) {
const prev = inside_rest;
inside_rest = true;
next();
inside_rest = prev;
}
});
VariableDeclaration(node, { state, path, next }) { binding.metadata = { inside_rest: is_rest_id };
const is_parent_const_tag = path.at(-1)?.type === 'ConstTag'; }
for (const declarator of node.declarations) {
/** @type {Binding[]} */
const bindings = [];
state.scope.declarators.set(declarator, bindings); // Visit to pick up references from default initializers
visit(node.context, { scope });
for (const id of extract_identifiers(declarator.id)) {
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);
} }
}
next();
},
EachBlock(node, { state, visit }) { if (node.index) {
visit(node.expression); const is_keyed =
node.key &&
// context and children are a new scope (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index);
const scope = state.scope.child(); scope.declare(b.id(node.index), is_keyed ? 'template' : 'static', 'const', node);
scopes.set(node, scope);
if (node.context) {
// declarations
for (const id of extract_identifiers(node.context)) {
const binding = scope.declare(id, 'each', 'const');
let inside_rest = false;
let is_rest_id = false;
walk(node.context, null, {
Identifier(node) {
if (inside_rest && node === id) {
is_rest_id = true;
}
},
RestElement(_, { next }) {
const prev = inside_rest;
inside_rest = true;
next();
inside_rest = prev;
}
});
binding.metadata = { inside_rest: is_rest_id };
} }
if (node.key) visit(node.key, { scope });
// Visit to pick up references from default initializers // children
visit(node.context, { scope }); for (const child of node.body.nodes) {
} visit(child, { scope });
}
if (node.index) { if (node.fallback) visit(node.fallback, { scope });
const is_keyed =
node.key && node.metadata = {
(node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index); expression: new ExpressionMetadata(),
scope.declare(b.id(node.index), is_keyed ? 'template' : 'static', 'const', node); keyed: false,
} contains_group_binding: false,
if (node.key) visit(node.key, { scope }); index: scope.root.unique('$$index'),
declarations: scope.declarations,
is_controlled: false,
// filled in during analysis
transitive_deps: new Set()
};
},
// children AwaitBlock(node, context) {
for (const child of node.body.nodes) { context.visit(node.expression);
visit(child, { scope });
}
if (node.fallback) visit(node.fallback, { scope });
node.metadata = {
expression: new ExpressionMetadata(),
keyed: false,
contains_group_binding: false,
index: scope.root.unique('$$index'),
declarations: scope.declarations,
is_controlled: false,
// filled in during analysis
transitive_deps: new Set()
};
},
AwaitBlock(node, context) {
context.visit(node.expression);
if (node.pending) { if (node.pending) {
context.visit(node.pending); context.visit(node.pending);
} }
if (node.then) { if (node.then) {
context.visit(node.then); context.visit(node.then);
if (node.value) { if (node.value) {
const then_scope = /** @type {Scope} */ (scopes.get(node.then)); const then_scope = /** @type {Scope} */ (scopes.get(node.then));
const value_scope = context.state.scope.child(); const value_scope = context.state.scope.child();
scopes.set(node.value, value_scope); scopes.set(node.value, value_scope);
context.visit(node.value, { scope: value_scope }); context.visit(node.value, { scope: value_scope });
for (const id of extract_identifiers(node.value)) { for (const id of extract_identifiers(node.value)) {
then_scope.declare(id, 'template', 'const'); then_scope.declare(id, 'template', 'const');
value_scope.declare(id, 'normal', 'const'); value_scope.declare(id, 'normal', 'const');
}
} }
} }
}
if (node.catch) { if (node.catch) {
context.visit(node.catch); context.visit(node.catch);
if (node.error) { if (node.error) {
const catch_scope = /** @type {Scope} */ (scopes.get(node.catch)); const catch_scope = /** @type {Scope} */ (scopes.get(node.catch));
const error_scope = context.state.scope.child(); const error_scope = context.state.scope.child();
scopes.set(node.error, error_scope); scopes.set(node.error, error_scope);
context.visit(node.error, { scope: error_scope }); context.visit(node.error, { scope: error_scope });
for (const id of extract_identifiers(node.error)) { for (const id of extract_identifiers(node.error)) {
catch_scope.declare(id, 'template', 'const'); catch_scope.declare(id, 'template', 'const');
error_scope.declare(id, 'normal', 'const'); error_scope.declare(id, 'normal', 'const');
}
} }
} }
} },
},
SnippetBlock(node, context) { SnippetBlock(node, context) {
const state = context.state; const state = context.state;
let scope = state.scope; let scope = state.scope;
scope.declare(node.expression, 'normal', 'function', node); scope.declare(node.expression, 'normal', 'function', node);
const child_scope = state.scope.child(); const child_scope = state.scope.child();
scopes.set(node, child_scope); scopes.set(node, child_scope);
for (const param of node.parameters) { for (const param of node.parameters) {
for (const id of extract_identifiers(param)) { for (const id of extract_identifiers(param)) {
child_scope.declare(id, 'snippet', 'let'); child_scope.declare(id, 'snippet', 'let');
}
} }
}
context.next({ scope: child_scope });
},
Fragment: (node, context) => { const params = parsed.get(node.parameters);
const scope = context.state.scope.child(node.metadata.transparent); if (params !== undefined) from_tables(child_scope, params, [...context.path, node], false);
scopes.set(node, scope); for (const child of node.body.nodes) {
context.next({ scope }); context.visit(child, { scope: child_scope });
}, }
},
BindDirective(node, context) {
if (node.expression.type !== 'SequenceExpression') { Fragment: (node, context) => {
const expression = /** @type {Identifier | MemberExpression} */ (node.expression); const scope = context.state.scope.child(node.metadata.transparent);
updates.push([context.state.scope, expression, expression]); scopes.set(node, scope);
} context.next({ scope });
},
BindDirective(node, context) {
if (node.expression.type !== 'SequenceExpression') {
const expression = /** @type {Identifier | MemberExpression} */ (node.expression);
updates.push([context.state.scope, expression, expression]);
}
context.next(); context.next();
}, },
TransitionDirective: SvelteDirective, TransitionDirective: SvelteDirective,
AnimateDirective: SvelteDirective, AnimateDirective: SvelteDirective,
UseDirective: SvelteDirective, UseDirective: SvelteDirective,
// using it's own function instead of `SvelteDirective` because // using it's own function instead of `SvelteDirective` because
// StyleDirective doesn't have expressions and are generally already // StyleDirective doesn't have expressions and are generally already
// handled by `Identifier`. This is the special case for the shorthand // handled by `Identifier`. This is the special case for the shorthand
// eg <button style:height /> where the variable has the same name of // eg <button style:height /> where the variable has the same name of
// the css property // the css property
StyleDirective(node, { path, state, next }) { StyleDirective(node, { path, state, next }) {
if (node.value === true) { if (node.value === true) {
state.scope.reference(b.id(node.name), path.concat(node)); state.scope.reference(b.id(node.name), path.concat(node));
}
next();
} }
next();
}
// TODO others // TODO others
}); });
for (const id of possible_implicit_declarations) { for (const id of possible_implicit_declarations) {
const binding = scope.get(id.name); const binding = scope.get(id.name);
@ -1387,14 +1555,13 @@ 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, reference, resolved, parsed] of references) {
const binding = scope.reference(node, path, declared(node)); const binding = scope.reference(reference.node, reference.path, resolved, reference);
// what the parser saw the identifier do; a declaring identifier is no reference to it // 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 (binding === null || parsed === undefined) continue;
if (parsed.write) { if (parsed.write) {
binding.reassigned = true; binding.reassigned = true;
binding.assignments.push({ value: parsed.writeExpr ?? node, scope }); binding.assignments.push({ value: parsed.writeExpr ?? reference.node, scope });
} }
if (parsed.mutate) binding.mutated = true; if (parsed.mutate) binding.mutated = true;
} }

@ -2,6 +2,13 @@
/** @import * as ESTree from 'estree' */ /** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { bindingOf } from '@teasel/parser'; import { bindingOf } from '@teasel/parser';
/**
* The parser's answer for each JavaScript root it parsed, a script's program or a template's
* expression, pattern, parameter list or declaration: its scope, binding and reference tables.
* @type {WeakMap<object, { scopes: import('@teasel/parser').Scope[]; bindings: import('@teasel/parser').Binding[]; references: import('@teasel/parser').Reference[] }>}
*/
export const parsed = new WeakMap();
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
/** /**

Loading…
Cancel
Save