mirror of https://github.com/sveltejs/svelte
parent
5895c637b0
commit
0a61385aa9
@ -1,289 +0,0 @@
|
||||
/** @import { Comment, Program, Statement } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from './index.js' */
|
||||
import * as acorn from 'acorn';
|
||||
import { walk } from 'zimmerframe';
|
||||
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
||||
import * as e from '../../errors.js';
|
||||
import { locator } from '../../state.js';
|
||||
|
||||
const JSParser = acorn.Parser;
|
||||
const TSParser = JSParser.extend(tsPlugin());
|
||||
|
||||
/**
|
||||
* @typedef {Comment & {
|
||||
* start: number;
|
||||
* end: number;
|
||||
* }} CommentWithLocation
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {AST.JSComment[]} comments
|
||||
* @param {boolean} typescript
|
||||
* @param {boolean} [is_script]
|
||||
*/
|
||||
export function parse(source, comments, typescript, is_script) {
|
||||
const acorn = typescript ? TSParser : JSParser;
|
||||
|
||||
const { onComment, add_comments } = get_comment_handlers(
|
||||
source,
|
||||
/** @type {CommentWithLocation[]} */ (comments)
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
const parse_statement = acorn.prototype.parseStatement;
|
||||
|
||||
// If we're dealing with a <script> then it might contain an export
|
||||
// for something that doesn't exist directly inside but is inside the
|
||||
// component instead, so we need to ensure that Acorn doesn't throw
|
||||
// an error in these cases
|
||||
if (is_script) {
|
||||
// @ts-ignore
|
||||
acorn.prototype.parseStatement = function (...args) {
|
||||
const v = parse_statement.call(this, ...args);
|
||||
// @ts-ignore
|
||||
this.undefinedExports = {};
|
||||
return v;
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const ast = acorn.parse(source, {
|
||||
onComment,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 16,
|
||||
locations: true
|
||||
});
|
||||
|
||||
add_comments(ast);
|
||||
|
||||
return /** @type {Program} */ (ast);
|
||||
} catch (err) {
|
||||
// TODO the `return` is necessary for TS<7 due to a bug; otherwise
|
||||
// the `finally` block is regarded as unreachable
|
||||
return handle_parse_error(err);
|
||||
} finally {
|
||||
if (is_script) {
|
||||
// @ts-expect-error
|
||||
acorn.prototype.parseStatement = parse_statement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {string} source
|
||||
* @param {number} index
|
||||
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
|
||||
*/
|
||||
export function parse_expression_at(parser, source, index) {
|
||||
const acorn = parser.ts ? TSParser : JSParser;
|
||||
|
||||
const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
|
||||
|
||||
try {
|
||||
const ast = acorn.parseExpressionAt(source, index, {
|
||||
onComment,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 16,
|
||||
locations: true,
|
||||
preserveParens: true,
|
||||
startLocation: start_location(parser, index)
|
||||
});
|
||||
|
||||
add_comments(ast);
|
||||
|
||||
return ast;
|
||||
} catch (e) {
|
||||
handle_parse_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {string} source
|
||||
* @param {number} index
|
||||
* @returns {Statement}
|
||||
*/
|
||||
export function parse_statement_at(parser, source, index) {
|
||||
// cast to `any`: acorn's Parser constructor and parseStatement/nextToken aren't in its public types
|
||||
const acorn = /** @type {any} */ (parser.ts ? TSParser : JSParser);
|
||||
const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
|
||||
|
||||
try {
|
||||
// This is like parseExpressionAt but for statements
|
||||
const p = new acorn(
|
||||
{
|
||||
onComment,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 16,
|
||||
locations: true,
|
||||
startLocation: start_location(parser, index)
|
||||
},
|
||||
source,
|
||||
index
|
||||
);
|
||||
p.nextToken();
|
||||
const statement = /** @type {Statement} */ (p.parseStatement(null, true, Object.create(null)));
|
||||
add_comments(/** @type {acorn.Node} */ (statement));
|
||||
return statement;
|
||||
} catch (err) {
|
||||
// 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.
|
||||
if (/** @type {any} */ (err).pos === source.length) e.unexpected_eof(source.length);
|
||||
handle_parse_error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const regex_non_lf_line_break = /\r(?!\n)|[\u2028\u2029]/;
|
||||
|
||||
let last_template = '';
|
||||
let lf_only = true;
|
||||
|
||||
/**
|
||||
* Without `startLocation`, acorn counts the lines before `index` on every call
|
||||
* @param {Parser} parser
|
||||
* @param {number} index
|
||||
*/
|
||||
function start_location(parser, index) {
|
||||
return has_lf_line_breaks_only(parser) ? locator(index) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* acorn breaks lines on bare `\r`, `\u2028` and `\u2029`, which the locator doesn't
|
||||
* @param {Parser} parser
|
||||
*/
|
||||
export function has_lf_line_breaks_only(parser) {
|
||||
if (parser.template !== last_template) {
|
||||
last_template = parser.template;
|
||||
lf_only = !regex_non_lf_line_break.test(last_template);
|
||||
}
|
||||
return lf_only;
|
||||
}
|
||||
|
||||
const regex_position_indicator = / \(\d+:\d+\)$/;
|
||||
|
||||
/**
|
||||
* @param {any} err
|
||||
* @returns {never}
|
||||
*/
|
||||
function handle_parse_error(err) {
|
||||
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {acorn.Expression} node
|
||||
* @returns {acorn.Expression}
|
||||
*/
|
||||
export function remove_parens(node) {
|
||||
return walk(node, null, {
|
||||
ParenthesizedExpression(node, context) {
|
||||
return context.visit(node.expression);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
|
||||
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
|
||||
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
|
||||
* @param {string} source
|
||||
* @param {CommentWithLocation[]} comments
|
||||
* @param {number} index
|
||||
*/
|
||||
function get_comment_handlers(source, comments, index = 0) {
|
||||
return {
|
||||
/**
|
||||
* @param {boolean} block
|
||||
* @param {string} value
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
* @param {import('acorn').Position} [start_loc]
|
||||
* @param {import('acorn').Position} [end_loc]
|
||||
*/
|
||||
onComment: (block, value, start, end, start_loc, end_loc) => {
|
||||
if (block && /\n/.test(value)) {
|
||||
let a = start;
|
||||
while (a > 0 && source[a - 1] !== '\n') a -= 1;
|
||||
|
||||
let b = a;
|
||||
while (/[ \t]/.test(source[b])) b += 1;
|
||||
|
||||
const indentation = source.slice(a, b);
|
||||
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
|
||||
}
|
||||
|
||||
comments.push({
|
||||
type: block ? 'Block' : 'Line',
|
||||
value,
|
||||
start,
|
||||
end,
|
||||
loc: {
|
||||
start: /** @type {import('acorn').Position} */ (start_loc),
|
||||
end: /** @type {import('acorn').Position} */ (end_loc)
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
|
||||
add_comments(ast) {
|
||||
if (comments.length === 0) return;
|
||||
|
||||
comments = comments
|
||||
.filter((comment) => comment.start >= index)
|
||||
.map(({ type, value, start, end }) => ({ type, value, start, end }));
|
||||
|
||||
walk(ast, null, {
|
||||
_(node, { next, path }) {
|
||||
let comment;
|
||||
|
||||
while (comments[0] && comments[0].start < node.start) {
|
||||
comment = /** @type {CommentWithLocation} */ (comments.shift());
|
||||
(node.leadingComments ||= []).push(comment);
|
||||
}
|
||||
|
||||
next();
|
||||
|
||||
if (comments[0]) {
|
||||
const parent = /** @type {any} */ (path.at(-1));
|
||||
|
||||
if (parent === undefined || node.end !== parent.end) {
|
||||
const slice = source.slice(node.end, comments[0].start);
|
||||
const is_last_in_body =
|
||||
((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
|
||||
parent.body.indexOf(node) === parent.body.length - 1) ||
|
||||
(parent?.type === 'ArrayExpression' &&
|
||||
parent.elements.indexOf(node) === parent.elements.length - 1) ||
|
||||
(parent?.type === 'ObjectExpression' &&
|
||||
parent.properties.indexOf(node) === parent.properties.length - 1);
|
||||
|
||||
if (is_last_in_body) {
|
||||
// Special case: There can be multiple trailing comments after the last node in a block,
|
||||
// and they can be separated by newlines
|
||||
let end = node.end;
|
||||
|
||||
while (comments.length) {
|
||||
const comment = comments[0];
|
||||
if (parent && comment.start >= parent.end) break;
|
||||
|
||||
(node.trailingComments ||= []).push(comment);
|
||||
comments.shift();
|
||||
end = comment.end;
|
||||
}
|
||||
} else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
|
||||
node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
|
||||
// Adding them ensures that we can later detect the end of the expression tag correctly.
|
||||
if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
|
||||
(ast.trailingComments ||= []).push(...comments.splice(0));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
/** @import { Expression, Pattern, Program, Statement } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from './index.js' */
|
||||
import * as teasel from '@teasel/parser';
|
||||
import * as e from '../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {AST.JSComment[]} comments
|
||||
* @param {boolean} typescript
|
||||
* @param {boolean} [is_script] a `<script>` may export names the component declares elsewhere
|
||||
* @returns {Program}
|
||||
*/
|
||||
export function parse(source, comments, typescript, is_script) {
|
||||
try {
|
||||
const ast = teasel.parse(source, {
|
||||
sourceType: 'module',
|
||||
typescript,
|
||||
comments: true,
|
||||
locations: true,
|
||||
allowUndeclaredExports: is_script
|
||||
});
|
||||
|
||||
add_comments(source, comments, /** @type {teasel.Comment[]} */ (ast.comments));
|
||||
delete ast.comments;
|
||||
|
||||
return ast;
|
||||
} catch (err) {
|
||||
return handle_parse_error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} index
|
||||
* @param {'as' | 'in'} [until] a word operator the expression stops before, at the top level
|
||||
* @returns {{ node: Expression, end: number }}
|
||||
*/
|
||||
export function parse_expression_at(parser, index, until) {
|
||||
try {
|
||||
const { node, end, comments } = parser.js.parseExpressionAt(index, until);
|
||||
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
|
||||
return { node, end };
|
||||
} catch (err) {
|
||||
return handle_parse_error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} index
|
||||
* @returns {{ node: Pattern, end: number }}
|
||||
*/
|
||||
export function parse_pattern_at(parser, index) {
|
||||
try {
|
||||
const { node, end, comments } = parser.js.parsePatternAt(index);
|
||||
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
|
||||
return { node, end };
|
||||
} catch (err) {
|
||||
return handle_parse_error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {string} source
|
||||
* @param {number} index the opening paren
|
||||
* @returns {{ params: Pattern[], end: number }}
|
||||
*/
|
||||
export function parse_params_at(parser, index) {
|
||||
try {
|
||||
const { params, end, comments } = parser.js.parseParamsAt(index);
|
||||
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
|
||||
return { params, end };
|
||||
} catch (err) {
|
||||
return handle_parse_error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} index
|
||||
* @returns {{ node: Statement, end: number }}
|
||||
*/
|
||||
export function parse_statement_at(parser, index) {
|
||||
try {
|
||||
const { node, end, comments } = parser.js.parseStatementAt(index);
|
||||
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
|
||||
return { node, end };
|
||||
} catch (err) {
|
||||
// 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.
|
||||
if (/** @type {any} */ (err).pos === parser.template.length)
|
||||
e.unexpected_eof(parser.template.length);
|
||||
return handle_parse_error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const regex_position_indicator = / \(\d+:\d+\)$/;
|
||||
|
||||
/**
|
||||
* @param {any} err
|
||||
* @returns {never}
|
||||
*/
|
||||
function handle_parse_error(err) {
|
||||
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Comments are needed in order to support `svelte-ignore` comments in JS code and so that
|
||||
* `prettier-plugin-svelte` doesn't remove all comments when formatting. A block comment loses
|
||||
* the indentation of the line it starts on.
|
||||
* @param {string} source
|
||||
* @param {AST.JSComment[]} comments
|
||||
* @param {teasel.Comment[]} parsed
|
||||
*/
|
||||
function add_comments(source, comments, parsed) {
|
||||
for (const comment of parsed) {
|
||||
if (comment.type === 'Block' && comment.value.includes('\n')) {
|
||||
let a = comment.start;
|
||||
while (a > 0 && source[a - 1] !== '\n') a -= 1;
|
||||
|
||||
let b = a;
|
||||
while (/[ \t]/.test(source[b])) b += 1;
|
||||
|
||||
const indentation = source.slice(a, b);
|
||||
comment.value = comment.value.replace(new RegExp(`^${indentation}`, 'gm'), '');
|
||||
}
|
||||
|
||||
comments.push(/** @type {AST.JSComment} */ (comment));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue