From 726b764b2bfb4c9cb1cd5aa07623676a55652e09 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Apr 2026 09:35:21 -0400 Subject: [PATCH] WIP --- .../src/compiler/phases/1-parse/acorn.js | 102 ++++++++---------- .../src/compiler/phases/1-parse/index.js | 85 ++++++++++++++- .../compiler/phases/1-parse/read/context.js | 16 ++- .../src/compiler/phases/1-parse/types.d.ts | 6 ++ .../samples/comment-in-tag/output.json | 42 ++++++++ .../samples/comment-with-brackets/output.json | 88 +++++++++------ 6 files changed, 242 insertions(+), 97 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/1-parse/types.d.ts diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 26ddf8826f..cbdab3a8d2 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -163,62 +163,52 @@ function get_comment_handlers(source, comments, index = 0) { /** @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)); - } + // 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)); + // } } }; } diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 844705121b..2e874b9615 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -1,6 +1,7 @@ /** @import { AST } from '#compiler' */ /** @import { Location } from 'locate-character' */ /** @import * as ESTree from 'estree' */ +/** @import { CommentWithLocation } from './types' */ // @ts-expect-error acorn type definitions are borked in the release we use import { isIdentifierStart, isIdentifierChar } from 'acorn'; import fragment from './state/fragment.js'; @@ -11,6 +12,7 @@ import { is_reserved } from '../../../utils.js'; import { disallow_children } from '../2-analyze/visitors/shared/special-element.js'; import * as state from '../../state.js'; import { find_end } from './utils/find.js'; +import { walk } from 'zimmerframe'; /** @param {number} cc */ function is_whitespace(cc) { @@ -168,6 +170,8 @@ export class Parser { enumerable: false }); } + + this.#attach_comments(); } current() { @@ -228,10 +232,23 @@ export class Parser { } if (code === 47) { + const start = i; const next = source.charCodeAt(i + 1); if (next === 47 || next === 42) { - i = find_end(source, next === 42 ? '*/' : '\n', i); + const is_block = next === 42; + i = find_end(source, is_block ? '*/' : '\n', i); + + const end = is_block ? i : i - 1; // line comments don't include the '\n' + + this.root.comments.push({ + type: is_block ? 'Block' : 'Line', + start, + end, + value: source.slice(start + 2, end - (is_block ? 2 : 1)), + loc: state.get_loc(start, end) + }); + continue; } } @@ -343,6 +360,72 @@ export class Parser { this.fragments.at(-1)?.nodes.push(node); return node; } + + #attach_comments() { + const source = this.template; + const comments = this.root.comments.map(({ type, value, start, end }) => ({ + type, + value, + start, + end + })); + + const root = + /** @type {AST.BaseNode & { leadingComments: CommentWithLocation[], trailingComments: CommentWithLocation[] }} */ ( + /** @type {unknown} */ (this.root) + ); + + const nodes = [this.root.module, this.root.instance, this.root.css, ...this.root.fragment.nodes] + .filter((node) => !!node) + .sort((a, b) => a.start - b.start); + + for (const node of nodes) { + walk(node, 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())]; + } + } + } + } + }); + } + } } /** diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index 84a8e38114..b4b87fa63f 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -84,7 +84,7 @@ function read_object_pattern(parser) { const property = { type: 'Property', start, - end: -1, + end: key.end, key, value: /** @type {Identifier} */ (key), method: false, @@ -98,6 +98,7 @@ function read_object_pattern(parser) { if (parser.eat(':', computed)) { property.value = read_pattern(parser); property.shorthand = false; + property.end = property.value.end; } parser.advance(); @@ -123,14 +124,17 @@ function read_object_pattern(parser) { right, loc: get_loc(property.value.start, right.end) }; + + property.end = property.value.end; } if (parser.ts) { property.typeAnnotation = read_type_annotation(parser); + if (property.typeAnnotation) { + property.end = property.typeAnnotation.end; + } } - property.end = parser.index; - property.loc = get_loc(start, property.end); properties.push(property); @@ -227,9 +231,11 @@ function read_array_pattern(parser) { if (parser.ts) { element.typeAnnotation = read_type_annotation(parser); - } - element.end = parser.index; + if (element.typeAnnotation) { + element.end = element.typeAnnotation.end; + } + } element.loc = get_loc(start, element.end); diff --git a/packages/svelte/src/compiler/phases/1-parse/types.d.ts b/packages/svelte/src/compiler/phases/1-parse/types.d.ts new file mode 100644 index 0000000000..1d5ceb0174 --- /dev/null +++ b/packages/svelte/src/compiler/phases/1-parse/types.d.ts @@ -0,0 +1,6 @@ +import type { Comment } from 'estree'; + +export type CommentWithLocation = Comment & { + start: number; + end: number; +}; diff --git a/packages/svelte/tests/parser-modern/samples/comment-in-tag/output.json b/packages/svelte/tests/parser-modern/samples/comment-in-tag/output.json index 45a57d3049..84e79b13aa 100644 --- a/packages/svelte/tests/parser-modern/samples/comment-in-tag/output.json +++ b/packages/svelte/tests/parser-modern/samples/comment-in-tag/output.json @@ -77,6 +77,14 @@ "raw": "2", "data": "2" } + ], + "leadingComments": [ + { + "type": "Line", + "value": " this is a line comment", + "start": 20, + "end": 45 + } ] }, { @@ -104,6 +112,26 @@ "raw": "3", "data": "3" } + ], + "leadingComments": [ + { + "type": "Block", + "value": " this is a\n\t\tm\n\t\tu\n\t\tl\n\t\tt\n\t\ti\n\t\tl\n\t\ti\n\t\tn\n\t\te\n\t\tcomment\n\t", + "start": 61, + "end": 123 + }, + { + "type": "Line", + "value": " oh look another line comment", + "start": 125, + "end": 156 + }, + { + "type": "Line", + "value": " (two, in fact)", + "start": 158, + "end": 175 + } ] } ], @@ -162,6 +190,20 @@ "raw": "1", "data": "1" } + ], + "leadingComments": [ + { + "type": "Block", + "value": " inline ", + "start": 207, + "end": 219 + }, + { + "type": "Block", + "value": " another inline ", + "start": 220, + "end": 240 + } ] } ], diff --git a/packages/svelte/tests/parser-modern/samples/comment-with-brackets/output.json b/packages/svelte/tests/parser-modern/samples/comment-with-brackets/output.json index 20229a9de9..4fa211c203 100644 --- a/packages/svelte/tests/parser-modern/samples/comment-with-brackets/output.json +++ b/packages/svelte/tests/parser-modern/samples/comment-with-brackets/output.json @@ -92,11 +92,13 @@ "loc": { "start": { "line": 1, - "column": 16 + "column": 16, + "character": 16 }, "end": { "line": 1, - "column": 17 + "column": 17, + "character": 17 } }, "name": "x" @@ -108,11 +110,13 @@ "loc": { "start": { "line": 1, - "column": 16 + "column": 16, + "character": 16 }, "end": { "line": 1, - "column": 17 + "column": 17, + "character": 17 } }, "name": "x" @@ -190,11 +194,11 @@ "loc": { "start": { "line": 2, - "column": 15 + "column": 14 }, "end": { "line": 2, - "column": 29 + "column": 28 } }, "properties": [ @@ -205,11 +209,11 @@ "loc": { "start": { "line": 2, - "column": 17 + "column": 16 }, "end": { "line": 2, - "column": 18 + "column": 17 } }, "method": false, @@ -222,11 +226,13 @@ "loc": { "start": { "line": 2, - "column": 17 + "column": 16, + "character": 55 }, "end": { "line": 2, - "column": 18 + "column": 17, + "character": 56 } }, "name": "x" @@ -238,11 +244,13 @@ "loc": { "start": { "line": 2, - "column": 17 + "column": 16, + "character": 55 }, "end": { "line": 2, - "column": 18 + "column": 17, + "character": 56 } }, "name": "x" @@ -320,11 +328,11 @@ "loc": { "start": { "line": 3, - "column": 15 + "column": 14 }, "end": { "line": 3, - "column": 28 + "column": 27 } }, "properties": [ @@ -335,11 +343,11 @@ "loc": { "start": { "line": 3, - "column": 17 + "column": 16 }, "end": { "line": 3, - "column": 18 + "column": 17 } }, "method": false, @@ -352,11 +360,13 @@ "loc": { "start": { "line": 3, - "column": 17 + "column": 16, + "character": 95 }, "end": { "line": 3, - "column": 18 + "column": 17, + "character": 96 } }, "name": "x" @@ -368,11 +378,13 @@ "loc": { "start": { "line": 3, - "column": 17 + "column": 16, + "character": 95 }, "end": { "line": 3, - "column": 18 + "column": 17, + "character": 96 } }, "name": "x" @@ -450,11 +462,11 @@ "loc": { "start": { "line": 4, - "column": 15 + "column": 14 }, "end": { "line": 4, - "column": 29 + "column": 28 } }, "properties": [ @@ -465,11 +477,13 @@ "loc": { "start": { "line": 4, - "column": 17 + "column": 16, + "character": 134 }, "end": { "line": 4, - "column": 18 + "column": 17, + "character": 135 } }, "method": false, @@ -482,11 +496,13 @@ "loc": { "start": { "line": 4, - "column": 17 + "column": 16, + "character": 134 }, "end": { "line": 4, - "column": 18 + "column": 17, + "character": 135 } }, "name": "x" @@ -498,11 +514,13 @@ "loc": { "start": { "line": 4, - "column": 17 + "column": 16, + "character": 134 }, "end": { "line": 4, - "column": 18 + "column": 17, + "character": 135 } }, "name": "x" @@ -548,11 +566,11 @@ "loc": { "start": { "line": 2, - "column": 19 + "column": 18 }, "end": { "line": 2, - "column": 27 + "column": 26 } } }, @@ -564,11 +582,11 @@ "loc": { "start": { "line": 3, - "column": 19 + "column": 18 }, "end": { "line": 3, - "column": 26 + "column": 25 } } }, @@ -580,13 +598,13 @@ "loc": { "start": { "line": 4, - "column": 19 + "column": 18 }, "end": { "line": 4, - "column": 27 + "column": 26 } } } ] -} \ No newline at end of file +}