the parser says which tables each piece of JavaScript has, so the partition and the root collection go

goodbye-acorn
Nic 3 weeks ago
parent a7f4c5c5bb
commit 2cb85375aa

@ -1,12 +1,13 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Expression, Identifier, Node, Pattern, Program } from 'estree' */ /** @import { Expression, Identifier, Node, Pattern, Program } from 'estree' */
/** @import { Parsed, Scope } from '@teasel/parser' */ /** @import { Parsed } from '@teasel/parser' */
import { Source, parentOf, scopeOf } from '@teasel/parser'; /** @import { Tables } from '../../utils/ast.js' */
import { Source } from '@teasel/parser';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import * as state from '../../state.js'; import * as state from '../../state.js';
import { ExpressionMetadata, disallow_children } from '../nodes.js'; import { ExpressionMetadata, disallow_children } from '../nodes.js';
import { keep_tables } from '../../utils/ast.js'; import { keep_tables, tables_of } from '../../utils/ast.js';
import { grammar } from './grammar.js'; import { grammar } from './grammar.js';
import { dedent, unsupported } from './js.js'; import { dedent, unsupported } from './js.js';
import read_options from './options.js'; import read_options from './options.js';
@ -139,17 +140,12 @@ export function parse(template, loose = false, erase = false) {
root.end = template.length; root.end = template.length;
root.metadata = { ts }; root.metadata = { ts };
/** @type {(Node | Node[])[]} the JavaScript of the template, each piece with its own tables */ const roots = /** @type {import('@teasel/parser').Root[]} */ (answer.roots);
const roots = []; for (const piece of roots) keep_tables(piece.node, piece);
const finish = new Finish(trimmed, roots); new Finish(trimmed).root(root);
finish.root(root);
tables(answer, roots);
// a comment between attributes is kept whole, one in JavaScript loses its line's indentation // a comment between attributes is kept whole, one in JavaScript loses its line's indentation
const spans = roots.map((root) => { const spans = roots.map(({ node }) => [/** @type {number} */ (node.start), /** @type {number} */ (node.end)]);
const [first, last] = Array.isArray(root) ? [root[0], root[root.length - 1]] : [root, root];
return [/** @type {number} */ (first?.start), /** @type {number} */ (last?.end)];
});
for (const comment of root.comments) { for (const comment of root.comments) {
if (spans.some(([start, end]) => comment.start >= start && comment.end <= end)) if (spans.some(([start, end]) => comment.start >= start && comment.end <= end))
dedent(comment, trimmed); dedent(comment, trimmed);
@ -163,13 +159,9 @@ export function parse(template, loose = false, erase = false) {
* location of each name, the options read out of `<svelte:options>`. * location of each name, the options read out of `<svelte:options>`.
*/ */
class Finish { class Finish {
/** /** @param {string} template */
* @param {string} template constructor(template) {
* @param {(Node | Node[])[]} roots
*/
constructor(template, roots) {
this.template = template; this.template = template;
this.roots = roots;
} }
/** @param {AST.Root} root */ /** @param {AST.Root} root */
@ -203,7 +195,6 @@ class Finish {
e.script_reserved_attribute(attribute, attribute.name); e.script_reserved_attribute(attribute, attribute.name);
if (!SCRIPT_ALLOWED.includes(attribute.name)) w.script_unknown_attribute(attribute); if (!SCRIPT_ALLOWED.includes(attribute.name)) w.script_unknown_attribute(attribute);
} }
this.js(script.content);
const { loc } = script.content; const { loc } = script.content;
if (loc) { if (loc) {
// the legacy AST places the program at the tag, not at its contents // the legacy AST places the program at the tag, not at its contents
@ -310,10 +301,8 @@ class Finish {
// a tag named in text is the literal Svelte writes by hand, quoted its way // a tag named in text is the literal Svelte writes by hand, quoted its way
if (node.tag.type === 'Literal' && node.tag.raw === node.tag.value) if (node.tag.type === 'Literal' && node.tag.raw === node.tag.value)
node.tag.raw = `'${node.tag.value}'`; node.tag.raw = `'${node.tag.value}'`;
else this.js(node.tag);
node.metadata.expression = new ExpressionMetadata(); node.metadata.expression = new ExpressionMetadata();
} }
if (node.type === 'SvelteComponent') this.js(node.expression);
if (node.type === 'SvelteComponent' || node.type === 'Component') { if (node.type === 'SvelteComponent' || node.type === 'Component') {
node.metadata.expression = new ExpressionMetadata(); node.metadata.expression = new ExpressionMetadata();
} }
@ -341,7 +330,6 @@ class Finish {
this.expression(node, node.declaration); this.expression(node, node.declaration);
return; return;
case 'DebugTag': case 'DebugTag':
for (const identifier of node.identifiers) this.js(identifier);
return; return;
case 'IfBlock': case 'IfBlock':
this.expression(node, node.test); this.expression(node, node.test);
@ -352,17 +340,12 @@ class Finish {
const index = /** @type {Identifier | string | undefined} */ (node.index); const index = /** @type {Identifier | string | undefined} */ (node.index);
if (index !== undefined && typeof index !== 'string') node.index = index.name; if (index !== undefined && typeof index !== 'string') node.index = index.name;
node.metadata = /** @type {any} */ (null); // filled in later node.metadata = /** @type {any} */ (null); // filled in later
this.js(node.expression);
if (node.context) this.js(node.context);
if (node.key) this.js(node.key);
this.body(node.body); this.body(node.body);
this.body(node.fallback); this.body(node.fallback);
return; return;
} }
case 'AwaitBlock': case 'AwaitBlock':
this.expression(node, node.expression); this.expression(node, node.expression);
if (node.value) this.js(node.value);
if (node.error) this.js(node.error);
this.body(node.pending); this.body(node.pending);
this.body(node.then); this.body(node.then);
this.body(node.catch); this.body(node.catch);
@ -373,7 +356,6 @@ class Finish {
return; return;
case 'SnippetBlock': case 'SnippetBlock':
node.metadata = { can_hoist: false, sites: new Set() }; node.metadata = { can_hoist: false, sites: new Set() };
this.js(node.parameters);
this.body(node.body); this.body(node.body);
return; return;
} }
@ -385,7 +367,6 @@ class Finish {
*/ */
expression(node, expression) { expression(node, expression) {
/** @type {any} */ (node).metadata = { expression: new ExpressionMetadata() }; /** @type {any} */ (node).metadata = { expression: new ExpressionMetadata() };
if (expression) this.js(expression);
} }
/** @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes */ /** @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes */
@ -418,8 +399,10 @@ class Finish {
) { ) {
attribute.expression = null; attribute.expression = null;
} else if (expression) { } else if (expression) {
attribute.expression = /** @type {any} */ (to_expression(expression)); // the pattern's tables move to the expression made of it
this.js(/** @type {Node} */ (attribute.expression)); const converted = to_expression(expression);
keep_tables(converted, /** @type {Tables} */ (tables_of(expression)));
attribute.expression = /** @type {any} */ (converted);
} }
break; break;
} }
@ -495,115 +478,6 @@ class Finish {
); );
} }
/** @param {Node | Node[]} node a piece of JavaScript the parser read on its own */
js(node) {
this.roots.push(node);
}
}
/**
* The parser's tables cut to each piece of JavaScript, as the scope analysis reads them piece by
* piece: the scopes opened inside it, the bindings declared and the references made there, and
* first the scope around it.
* @param {Parsed<AST.Root>} answer
* @param {(Node | Node[])[]} roots
*/
function tables(answer, roots) {
const scopes = /** @type {Scope[]} */ (answer.scopes);
const bindings = /** @type {import('@teasel/parser').Binding[]} */ (answer.bindings);
const references = /** @type {import('@teasel/parser').Reference[]} */ (answer.references);
/** @type {Scope} */
const nowhere = { kind: 'fragment', node: null, parent: null, topLevelAwait: false };
// each table in source order, so a root's entries are one run of it; a fragment has no span
// and holds JavaScript rather than sitting in it
const opening = scopes
.filter((scope) => typeof (/** @type {any} */ (scope.node)?.start) === 'number')
.sort((a, b) => /** @type {any} */ (a.node).start - /** @type {any} */ (b.node).start);
const named = scopes.filter((scope) => scope.node === null);
const declaring = bindings
.filter((binding) => binding.node !== null)
.sort((a, b) => /** @type {any} */ (a.node).start - /** @type {any} */ (b.node).start);
const nameless = bindings.filter((binding) => binding.node === null);
const referring = [...references].sort(
(a, b) => /** @type {number} */ (a.node.start) - /** @type {number} */ (b.node.start)
);
/**
* The entries of a sorted table inside a span.
* @template T
* @param {T[]} table
* @param {(entry: T) => any} node
* @param {number} start
* @param {number} end
*/
const within = (table, node, start, end) => {
let low = 0;
let high = table.length;
while (low < high) {
const mid = (low + high) >> 1;
if (node(table[mid]).start < start) low = mid + 1;
else high = mid;
}
/** @type {T[]} */
const found = [];
for (let i = low; i < table.length && node(table[i]).start < end; i += 1) {
if (node(table[i]).end <= end) found.push(table[i]);
}
return found;
};
for (const root of roots) {
const list = Array.isArray(root) ? root : [root];
if (list.length === 0) continue;
const start = /** @type {number} */ (list[0].start);
const end = /** @type {number} */ (list[list.length - 1].end);
// a script's program is the scope itself; any other piece sits in the scope around it
const own =
/** @type {any} */ (root).type === 'Program'
? scopes.find((scope) => scope.node === root)
: undefined;
const inside = within(opening, (scope) => scope.node, start, end).filter(
(scope) => scope !== own
);
// a function-name scope has no node of its own; it sits between a scope and the function it names
const parents = new Set(inside.map((scope) => scope.parent));
const opened =
named.length === 0
? inside
: scopes.filter(
(scope) =>
scope !== own && (scope.node === null ? parents.has(scope) : inside.includes(scope))
);
const declared = within(declaring, (binding) => binding.node, start, end);
if (nameless.length > 0) {
for (const binding of nameless) if (opened.includes(binding.scope)) declared.push(binding);
}
const made = within(referring, (reference) => reference.node, start, end);
const outermost =
own ??
[...opened.map((scope) => scope.parent), ...declared, ...made]
.map((entry) => (entry && 'scope' in entry ? entry.scope : entry))
.find((scope) => scope !== null && !opened.includes(/** @type {Scope} */ (scope))) ??
around(list[0]) ??
nowhere;
keep_tables(root, {
node: root,
end,
scopes: [outermost, ...opened],
bindings: declared,
references: made
});
}
}
/**
* The scope a piece of JavaScript sits in when nothing in it says: the nearest ancestor that opens one.
* @param {Node} node
*/
function around(node) {
for (let parent = parentOf(node); parent !== undefined; parent = parentOf(parent)) {
const scope = scopeOf(parent);
if (scope !== undefined) return scope;
}
return undefined;
} }
/** /**

@ -29,7 +29,14 @@ export function parse(source, comments, typescript) {
dedent(comment, source); dedent(comment, source);
comments.push(/** @type {AST.JSComment} */ (comment)); comments.push(/** @type {AST.JSComment} */ (comment));
} }
keep_tables(answer.node, answer); // a module read on its own is one piece: its program, in its own scope
keep_tables(answer.node, {
node: answer.node,
scope: answer.scopes[0],
scopes: answer.scopes.slice(1),
bindings: answer.bindings,
references: answer.references
});
return answer.node; return answer.node;
} }

@ -1078,12 +1078,12 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
* @param {boolean} template the answer's declarations are a const tag's * @param {boolean} template the answer's declarations are a const tag's
*/ */
function from_tables(scope, answer, path, template) { function from_tables(scope, answer, path, template) {
const outermost = answer.scopes[0]; const outermost = answer.scope;
// most of a template's expressions declare nothing and open no scope: their references, // most of a template's expressions declare nothing and open no scope: their references,
// in source order already, are all there is // in source order already, are all there is
if ( if (
outermost.kind === 'fragment' && outermost.kind === 'fragment' &&
answer.scopes.length === 1 && answer.scopes.length === 0 &&
answer.bindings.length === 0 answer.bindings.length === 0
) { ) {
for (const reference of answer.references) { for (const reference of answer.references) {
@ -1124,8 +1124,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
declare_parsed(scope, of(outermost), false, template); declare_parsed(scope, of(outermost), false, template);
for (let i = 1; i < answer.scopes.length; i++) { for (const parsed of answer.scopes) {
const parsed = answer.scopes[i];
const node = /** @type {any} */ (parsed.node); const node = /** @type {any} */ (parsed.node);
const parent = at( const parent = at(
/** @type {import('@teasel/parser').Scope} */ (parsed.parent), /** @type {import('@teasel/parser').Scope} */ (parsed.parent),
@ -1204,7 +1203,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
node && node &&
node.start <= position && node.start <= position &&
position < node.end && position < node.end &&
parsed !== outermost &&
ours.has(parsed) ours.has(parsed)
) { ) {
// a fragment has no span of its own: anything inside it is closer // a fragment has no span of its own: anything inside it is closer
@ -1249,7 +1247,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// a script comes entirely from the parser's tables: nothing in it is walked // a script comes entirely from the parser's tables: nothing in it is walked
const program = ast.type === 'Program' ? tables_of(ast) : undefined; const program = ast.type === 'Program' ? tables_of(ast) : undefined;
if (program !== undefined) { if (program !== undefined) {
has_await = program.scopes[0].topLevelAwait; has_await = program.scope.topLevelAwait;
for (const node of /** @type {Program} */ (ast).body) { for (const node of /** @type {Program} */ (ast).body) {
if ( if (
allow_reactive_declarations && allow_reactive_declarations &&
@ -1355,7 +1353,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
_(node, context) { _(node, context) {
const answer = tables_of(node); const answer = tables_of(node);
if (answer === undefined) return context.next(); if (answer === undefined) return context.next();
has_await ||= answer.scopes[0].topLevelAwait; has_await ||= answer.scope.topLevelAwait;
from_tables( from_tables(
context.state.scope, context.state.scope,
answer, answer,
@ -1538,8 +1536,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
} }
} }
const params = tables_of(node.parameters); for (const param of node.parameters) {
if (params !== undefined) from_tables(child_scope, params, [...context.path, node], false); const params = tables_of(param);
if (params !== undefined) from_tables(child_scope, params, [...context.path, node], false);
}
for (const child of node.body.nodes) { for (const child of node.body.nodes) {
context.visit(child, { scope: child_scope }); context.visit(child, { scope: child_scope });
} }

@ -4,8 +4,8 @@ import { walk } from 'zimmerframe';
import { bindingOf } from '@teasel/parser'; import { bindingOf } from '@teasel/parser';
/** /**
* @typedef {Required<Pick<import('@teasel/parser').Parsed<unknown>, 'scopes' | 'bindings' | 'references'>>} Tables * @typedef {import('@teasel/parser').Root} Tables
* the parser's scope, binding and reference tables for one answer, parsed with `scopes` * the parser's tables for one piece of JavaScript: the scope it sits in, the scopes opened, the bindings declared and the references made inside it
*/ */
// a symbol on the root: a property load where the walk asks every node, not a map lookup // a symbol on the root: a property load where the walk asks every node, not a map lookup

Loading…
Cancel
Save