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