scripts parse in place, typescript erased by the parser

goodbye-acorn
Nic 2 days ago
parent 0a61385aa9
commit f3b399f9b1

@ -4,7 +4,6 @@
import { walk as zimmerframe_walk } from 'zimmerframe'; import { walk as zimmerframe_walk } from 'zimmerframe';
import { convert } from './legacy.js'; import { convert } from './legacy.js';
import { parse as _parse, Parser } from './phases/1-parse/index.js'; import { parse as _parse, Parser } from './phases/1-parse/index.js';
import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js';
import { parse_stylesheet } from './phases/1-parse/read/style.js'; import { parse_stylesheet } from './phases/1-parse/read/style.js';
import { analyze_component, analyze_module } from './phases/2-analyze/index.js'; import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
import { transform_component, transform_module } from './phases/3-transform/index.js'; import { transform_component, transform_module } from './phases/3-transform/index.js';
@ -26,7 +25,7 @@ export function compile(source, options) {
const validated = validate_component_options(options, ''); const validated = validate_component_options(options, '');
let parsed = _parse(source); const parsed = _parse(source, false, true);
const { customElement: customElementOptions, ...parsed_options } = parsed.options || {}; const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
@ -39,20 +38,6 @@ export function compile(source, options) {
runes: 'runes' in parsed_options ? () => parsed_options.runes : validated.runes runes: 'runes' in parsed_options ? () => parsed_options.runes : validated.runes
}; };
if (parsed.metadata.ts) {
parsed = {
...parsed,
fragment: parsed.fragment && remove_typescript_nodes(parsed.fragment),
instance: parsed.instance && remove_typescript_nodes(parsed.instance),
module: parsed.module && remove_typescript_nodes(parsed.module)
};
if (combined_options.customElementOptions?.extend) {
combined_options.customElementOptions.extend = remove_typescript_nodes(
combined_options.customElementOptions?.extend
);
}
}
const analysis = analyze_component(parsed, source, combined_options); const analysis = analyze_component(parsed, source, combined_options);
const result = transform_component(analysis, source, combined_options); const result = transform_component(analysis, source, combined_options);
result.ast = to_public_ast(source, parsed, options.modernAst); result.ast = to_public_ast(source, parsed, options.modernAst);

@ -73,8 +73,9 @@ export class Parser {
/** /**
* @param {string} template * @param {string} template
* @param {boolean} loose * @param {boolean} loose
* @param {boolean} [erase] hand back JavaScript for TypeScript input, as the compiler wants it
*/ */
constructor(template, loose) { constructor(template, loose, erase = false) {
if (typeof template !== 'string') { if (typeof template !== 'string') {
throw new TypeError('Template must be a string'); throw new TypeError('Template must be a string');
} }
@ -92,9 +93,11 @@ export class Parser {
this.ts = match_lang?.[2] === 'ts'; this.ts = match_lang?.[2] === 'ts';
this.js = new Source(this.template, { this.js = new Source(this.template, {
sourceType: 'module', sourceType: 'module',
typescript: this.ts, typescript: this.ts && (erase ? 'erase' : true),
comments: true, comments: true,
locations: true locations: true,
// a script may export what the component declares elsewhere
allowUndeclaredExports: true
}); });
this.root = { this.root = {
@ -337,12 +340,13 @@ export class Parser {
/** /**
* @param {string} template * @param {string} template
* @param {boolean} [loose] * @param {boolean} [loose]
* @param {boolean} [erase] hand back JavaScript for TypeScript input, as the compiler wants it
* @returns {AST.Root} * @returns {AST.Root}
*/ */
export function parse(template, loose = false) { export function parse(template, loose = false, erase = false) {
state.set_source(template); state.set_source(template);
const parser = new Parser(template, loose); const parser = new Parser(template, loose, erase);
return parser.root; return parser.root;
} }

@ -5,45 +5,73 @@ import * as teasel from '@teasel/parser';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
/** /**
* A standalone module, as `analyze_module` reads one.
* @param {string} source * @param {string} source
* @param {AST.JSComment[]} comments * @param {AST.JSComment[]} comments
* @param {boolean} typescript * @param {boolean} typescript
* @param {boolean} [is_script] a `<script>` may export names the component declares elsewhere
* @returns {Program} * @returns {Program}
*/ */
export function parse(source, comments, typescript, is_script) { export function parse(source, comments, typescript) {
let ast;
try { try {
const ast = teasel.parse(source, { ast = teasel.parse(source, {
sourceType: 'module', sourceType: 'module',
typescript, typescript,
comments: true, comments: true,
locations: true, locations: true
allowUndeclaredExports: is_script
}); });
} catch (err) {
return handle_parse_error(err);
}
add_comments(source, comments, /** @type {teasel.Comment[]} */ (ast.comments)); add_comments(source, comments, /** @type {teasel.Comment[]} */ (ast.comments));
delete ast.comments; delete ast.comments;
return ast; return ast;
}
/**
* The program inside a `<script>`, with the positions of the whole template.
* @param {Parser} parser
* @param {number} start
* @param {number} end
* @returns {Program}
*/
export function parse_script(parser, start, end) {
let ast;
try {
ast = parser.js.parse(start, end);
} catch (err) { } catch (err) {
return handle_parse_error(err); return handle_parse_error(err);
} }
add_comments(
parser.template,
parser.root.comments,
/** @type {teasel.Comment[]} */ (ast.comments)
);
delete ast.comments;
unsupported(ast.typescript);
delete ast.typescript;
return ast;
} }
/** /**
* @param {Parser} parser * @param {Parser} parser
* @param {number} index * @param {number} index
* @param {'as' | 'in'} [until] a word operator the expression stops before, at the top level * @param {'as'} [until] the host's `as` follows the expression, as an each block's item does
* @returns {{ node: Expression, end: number }} * @returns {{ node: Expression, end: number }}
*/ */
export function parse_expression_at(parser, index, until) { export function parse_expression_at(parser, index, until) {
let answer;
try { try {
const { node, end, comments } = parser.js.parseExpressionAt(index, until); answer = parser.js.parseExpressionAt(index, until);
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
return { node, end };
} catch (err) { } catch (err) {
return handle_parse_error(err); return handle_parse_error(err);
} }
return accept(parser, answer, answer.node);
} }
/** /**
@ -52,29 +80,30 @@ export function parse_expression_at(parser, index, until) {
* @returns {{ node: Pattern, end: number }} * @returns {{ node: Pattern, end: number }}
*/ */
export function parse_pattern_at(parser, index) { export function parse_pattern_at(parser, index) {
let answer;
try { try {
const { node, end, comments } = parser.js.parsePatternAt(index); answer = parser.js.parsePatternAt(index);
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
return { node, end };
} catch (err) { } catch (err) {
return handle_parse_error(err); return handle_parse_error(err);
} }
return accept(parser, answer, answer.node);
} }
/** /**
* @param {Parser} parser * @param {Parser} parser
* @param {string} source
* @param {number} index the opening paren * @param {number} index the opening paren
* @returns {{ params: Pattern[], end: number }} * @returns {{ node: Pattern[], end: number }}
*/ */
export function parse_params_at(parser, index) { export function parse_params_at(parser, index) {
let answer;
try { try {
const { params, end, comments } = parser.js.parseParamsAt(index); answer = parser.js.parseParamsAt(index);
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
return { params, end };
} catch (err) { } catch (err) {
return handle_parse_error(err); return handle_parse_error(err);
} }
return accept(parser, answer, answer.params);
} }
/** /**
@ -83,10 +112,9 @@ export function parse_params_at(parser, index) {
* @returns {{ node: Statement, end: number }} * @returns {{ node: Statement, end: number }}
*/ */
export function parse_statement_at(parser, index) { export function parse_statement_at(parser, index) {
let answer;
try { try {
const { node, end, comments } = parser.js.parseStatementAt(index); answer = parser.js.parseStatementAt(index);
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
return { node, end };
} catch (err) { } catch (err) {
// A statement that runs to the end of the source (e.g. an unterminated declaration tag) // A statement that runs to the end of the source (e.g. an unterminated declaration tag)
// is an EOF, not a stray token; preserve the friendlier `unexpected_eof` diagnostic. // is an EOF, not a stray token; preserve the friendlier `unexpected_eof` diagnostic.
@ -94,6 +122,48 @@ export function parse_statement_at(parser, index) {
e.unexpected_eof(parser.template.length); e.unexpected_eof(parser.template.length);
return handle_parse_error(err); return handle_parse_error(err);
} }
return accept(parser, answer, answer.node);
}
/**
* Keeps an answer's comments, rejects what erasure could not express, and hands back the node
* with the offset the parse stopped at.
* @template T
* @param {Parser} parser
* @param {{ end: number, comments?: teasel.Comment[], typescript?: teasel.Kept[] }} answer
* @param {T} node
* @returns {{ node: T, end: number }}
*/
function accept(parser, answer, node) {
add_comments(
parser.template,
parser.root.comments,
/** @type {teasel.Comment[]} */ (answer.comments)
);
unsupported(answer.typescript);
return { node, end: answer.end };
}
/** What erasure leaves in place needs a compiler, not this one */
const UNSUPPORTED = {
TSEnumDeclaration: 'enums',
TSModuleDeclaration: 'namespaces with non-type nodes',
TSParameterProperty: 'accessibility modifiers on constructor parameters',
Decorator: 'decorators (related TSC proposal is not stage 4 yet)',
TSExportAssignment: 'export assignments',
TSImportEqualsDeclaration: 'import assignments'
};
/** @param {teasel.Kept[] | undefined} kept */
function unsupported(kept) {
const node = kept?.[0];
if (node) {
e.typescript_invalid_feature(
node,
UNSUPPORTED[/** @type {keyof typeof UNSUPPORTED} */ (node.type)] ?? node.type
);
}
} }
const regex_position_indicator = / \(\d+:\d+\)$/; const regex_position_indicator = / \(\d+:\d+\)$/;

@ -33,7 +33,7 @@ export function get_loose_identifier(parser, opening_token) {
* @param {Parser} parser * @param {Parser} parser
* @param {string} [opening_token] * @param {string} [opening_token]
* @param {boolean} [disallow_loose] * @param {boolean} [disallow_loose]
* @param {'as' | 'in'} [until] a word operator the expression stops before, at the top level * @param {'as'} [until] the host's `as` follows the expression, as an each block's item does
* @returns {Expression} * @returns {Expression}
*/ */
export default function read_expression(parser, opening_token, disallow_loose, until) { export default function read_expression(parser, opening_token, disallow_loose, until) {

@ -1,8 +1,7 @@
/** @import { Program } from 'estree' */ /** @import { Program } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import { parse } from '../js.js'; import { parse_script } from '../js.js';
import { regex_not_newline_characters } from '../../patterns.js';
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 { is_text_attribute } from '../../../utils/ast.js'; import { is_text_attribute } from '../../../utils/ast.js';
@ -22,19 +21,14 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
*/ */
export function read_script(parser, start, attributes) { export function read_script(parser, start, attributes) {
const script_start = parser.index; const script_start = parser.index;
const data = parser.read_until_regex(regex_closing_script_tag); parser.read_until_regex(regex_closing_script_tag);
if (parser.index >= parser.template.length) { if (parser.index >= parser.template.length) {
e.element_unclosed(parser.template.length, 'script'); e.element_unclosed(parser.template.length, 'script');
} }
const source = const ast = parse_script(parser, script_start, parser.index);
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_starts_with_closing_script_tag); parser.read(regex_starts_with_closing_script_tag);
const ast = parse(source, parser.root.comments, parser.ts, true);
ast.start = script_start;
if (ast.loc) { if (ast.loc) {
// Acorn always uses `0` as the start of a `Program`, but for sourcemap purposes // Acorn always uses `0` as the start of a `Program`, but for sourcemap purposes
// we need it to be the start of the `<script>` contents // we need it to be the start of the `<script>` contents

@ -1,188 +0,0 @@
/** @import { Context, Visitors } from 'zimmerframe' */
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '#compiler/builders';
import * as e from '../../errors.js';
/**
* @param {FunctionExpression | FunctionDeclaration} node
* @param {Context<any, any>} context
*/
function remove_this_param(node, context) {
if (node.params[0]?.type === 'Identifier' && node.params[0].name === 'this') {
node.params.shift();
}
return context.next();
}
/** @type {Visitors<any, null>} */
const visitors = {
_(node, context) {
const n = context.next() ?? node;
// TODO there may come a time when we decide to preserve type annotations.
// until that day comes, we just delete them so they don't confuse esrap
delete n.typeAnnotation;
delete n.typeParameters;
delete n.typeArguments;
delete n.returnType;
delete n.accessibility;
delete n.readonly;
delete n.definite;
delete n.override;
// `optional` is reused by JS optional chaining (`a?.b`, `a?.()`), so only
// strip the TypeScript optional marker (`x?: T`, `m?(): T`, `x?: T` fields)
if (n.type !== 'MemberExpression' && n.type !== 'CallExpression') {
delete n.optional;
}
},
Decorator(node) {
e.typescript_invalid_feature(node, 'decorators (related TSC proposal is not stage 4 yet)');
},
ImportDeclaration(node) {
if (node.importKind === 'type') return b.empty;
if (node.specifiers?.length > 0) {
const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.importKind !== 'type');
if (specifiers.length === 0) return b.empty;
return { ...node, specifiers };
}
return node;
},
ExportNamedDeclaration(node, context) {
if (node.exportKind === 'type') return b.empty;
if (node.declaration) {
const result = context.next();
if (result?.declaration?.type === 'EmptyStatement') {
return b.empty;
}
return result;
}
if (node.specifiers) {
const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.exportKind !== 'type');
if (specifiers.length === 0) return b.empty;
return { ...node, specifiers };
}
return node;
},
ExportDefaultDeclaration(node) {
if (node.exportKind === 'type') return b.empty;
return node;
},
ExportAllDeclaration(node) {
if (node.exportKind === 'type') return b.empty;
return node;
},
PropertyDefinition(node, { next }) {
if (node.accessor) {
e.typescript_invalid_feature(
node,
'accessor fields (related TSC proposal is not stage 4 yet)'
);
}
return next();
},
TSAsExpression(node, context) {
return context.visit(node.expression);
},
TSSatisfiesExpression(node, context) {
return context.visit(node.expression);
},
TSNonNullExpression(node, context) {
return context.visit(node.expression);
},
TSInterfaceDeclaration() {
return b.empty;
},
TSTypeAliasDeclaration() {
return b.empty;
},
TSTypeAssertion(node, context) {
return context.visit(node.expression);
},
TSEnumDeclaration(node) {
e.typescript_invalid_feature(node, 'enums');
},
TSParameterProperty(node, context) {
if ((node.readonly || node.accessibility) && context.path.at(-2)?.kind === 'constructor') {
e.typescript_invalid_feature(node, 'accessibility modifiers on constructor parameters');
}
return context.visit(node.parameter);
},
TSInstantiationExpression(node, context) {
return context.visit(node.expression);
},
FunctionExpression: remove_this_param,
FunctionDeclaration: remove_this_param,
TSDeclareFunction() {
return b.empty;
},
ClassBody(node, context) {
const body = [];
for (const _child of node.body) {
const child = context.visit(_child);
if (child.type !== 'PropertyDefinition' || !child.declare) {
body.push(child);
}
}
return {
...node,
body
};
},
ClassDeclaration(node, context) {
if (node.declare) {
return b.empty;
}
delete node.abstract;
delete node.implements;
delete node.superTypeArguments;
delete node.superTypeParameters;
return context.next();
},
ClassExpression(node, context) {
delete node.implements;
delete node.superTypeArguments;
delete node.superTypeParameters;
return context.next();
},
MethodDefinition(node, context) {
if (node.abstract) {
return b.empty;
}
return context.next();
},
VariableDeclaration(node, context) {
if (node.declare) {
return b.empty;
}
return context.next();
},
TSModuleDeclaration(node, context) {
if (!node.body) return b.empty;
// namespaces can contain non-type nodes
const cleaned = /** @type {any[]} */ (node.body.body).map((entry) => context.visit(entry));
if (cleaned.some((entry) => entry !== b.empty)) {
e.typescript_invalid_feature(node, 'namespaces with non-type nodes');
}
return b.empty;
}
};
/**
* @template T
* @param {T} ast
* @returns {T}
*/
export function remove_typescript_nodes(ast) {
return walk(ast, null, visitors);
}

@ -417,8 +417,8 @@ function open(parser) {
e.expected_token(parser.template.length, ')'); e.expected_token(parser.template.length, ')');
} }
const { params, end } = parse_params_at(parser, open); const { node, end } = parse_params_at(parser, open);
parameters = params; parameters = node;
parser.index = end; parser.index = end;
} }

@ -260,7 +260,7 @@ export function analyze_module(source, options) {
const comments = []; const comments = [];
state.set_source(source); state.set_source(source);
const ast = parse(source, comments, false, false); const ast = parse(source, comments, false);
const { scope, scopes, has_await } = create_scopes(ast, new ScopeRoot(), false, null); const { scope, scopes, has_await } = create_scopes(ast, new ScopeRoot(), false, null);

@ -8,6 +8,10 @@ import { get_name } from '../../nodes.js';
* @param {Context} context * @param {Context} context
*/ */
export function PropertyDefinition(node, context) { export function PropertyDefinition(node, context) {
if (/** @type {any} */ (node).accessor) {
e.typescript_invalid_feature(node, 'accessor fields (related TSC proposal is not stage 4 yet)');
}
const name = get_name(node.key); const name = get_name(node.key);
const field = name && context.state.state_fields.get(name); const field = name && context.state.state_fields.get(name);

Loading…
Cancel
Save