Overhaul comment attachment (#13521)

* refactor: inline pushComment

* chore: add benchmark cases

* perf: overhaul comment attachment

* cleanup

* update test fixtures

They are all bugfixes.

* fix: merge HTMLComment parsing to skipSpace

* perf: remove unattachedCommentStack

baseline 128 nested leading comments: 11_034 ops/sec ±50.64% (0.091ms)
baseline 256 nested leading comments: 6_037 ops/sec ±11.46% (0.166ms)
baseline 512 nested leading comments: 3_077 ops/sec ±2.31% (0.325ms)
baseline 1024 nested leading comments: 1_374 ops/sec ±3.22% (0.728ms)
current 128 nested leading comments: 11_027 ops/sec ±37.41% (0.091ms)
current 256 nested leading comments: 6_736 ops/sec ±1.39% (0.148ms)
current 512 nested leading comments: 3_306 ops/sec ±0.69% (0.302ms)
current 1024 nested leading comments: 1_579 ops/sec ±2.09% (0.633ms)

baseline 128 nested trailing comments: 10_073 ops/sec ±42.95% (0.099ms)
baseline 256 nested trailing comments: 6_294 ops/sec ±2.19% (0.159ms)
baseline 512 nested trailing comments: 3_041 ops/sec ±0.8% (0.329ms)
baseline 1024 nested trailing comments: 1_530 ops/sec ±1.18% (0.654ms)
current 128 nested trailing comments: 11_461 ops/sec ±44.89% (0.087ms)
current 256 nested trailing comments: 7_212 ops/sec ±1.6% (0.139ms)
current 512 nested trailing comments: 3_403 ops/sec ±1% (0.294ms)
current 1024 nested trailing comments: 1_539 ops/sec ±1.49% (0.65ms)

* fix: do not expose CommentWhitespace type

* add comments on CommentWhitespace

* add test case for #11576

* fix: mark containerNode be the innermost node containing commentWS

* fix: adjust trailing comma comments for Record/Tuple/OptionalCall

* fix: drain comment stacks in parseExpression

* docs: update comments

* add a new benchmark

* chore: containerNode => containingNode

* add more benchmark cases

* fix: avoid finishNodeAt in stmtToDirective

* finalize comment right after containerNode is set

* add testcase about directive

* fix: finish SequenceExpression at current pos and adjust later

* chore: rename test cases

* add new test case on switch statement

* fix: adjust comments after trailing comma of function params

* add comment attachment design doc

* misc fix

* fix: reset previous trailing comments when parsing async method/accessor

* chore: add more comment testcases

* fix flow errors

* fix: handle comments when parsing async arrow

* fix: handle comments when "static" is a class modifier

* fix flow errors

* fix: handle comments when parsing async function/do

* refactor: simplify resetPreviousNodeTrailingComments

* update test fixtures
This commit is contained in:
Huáng Jùnliàng
2021-07-07 11:51:40 -04:00
committed by GitHub
parent 8a3e0fd960
commit 79d3276f61
76 changed files with 4822 additions and 498 deletions

View File

@@ -1,288 +1,241 @@
// @flow
/**
* Based on the comment attachment algorithm used in espree and estraverse.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*:: declare var invariant; */
import BaseParser from "./base";
import type { Comment, Node } from "../types";
import * as charCodes from "charcodes";
function last<T>(stack: $ReadOnlyArray<T>): T {
return stack[stack.length - 1];
/**
* A whitespace token containing comments
* @typedef CommentWhitespace
* @type {object}
* @property {number} start - the start of the whitespace token.
* @property {number} end - the end of the whitespace token.
* @property {Array<Comment>} comments - the containing comments
* @property {Node | null} leadingNode - the immediately preceding AST node of the whitespace token
* @property {Node | null} trailingNode - the immediately following AST node of the whitespace token
* @property {Node | null} containingNode - the innermost AST node containing the whitespace
* with minimal size (|end - start|)
*/
export type CommentWhitespace = {
start: number,
end: number,
comments: Array<Comment>,
leadingNode: Node | null,
trailingNode: Node | null,
containingNode: Node | null,
};
/**
* Merge comments with node's trailingComments or assign comments to be
* trailingComments. New comments will be placed before old comments
* because the commentStack is enumerated reversely.
*
* @param {Node} node
* @param {Array<Comment>} comments
*/
function setTrailingComments(node: Node, comments: Array<Comment>) {
if (node.trailingComments === undefined) {
node.trailingComments = comments;
} else {
node.trailingComments.unshift(...comments);
}
}
/**
* Merge comments with node's innerComments or assign comments to be
* innerComments. New comments will be placed before old comments
* because the commentStack is enumerated reversely.
*
* @param {Node} node
* @param {Array<Comment>} comments
*/
export function setInnerComments(node: Node, comments: Array<Comment> | void) {
if (node.innerComments === undefined) {
node.innerComments = comments;
} else if (comments !== undefined) {
node.innerComments.unshift(...comments);
}
}
/**
* Given node and elements array, if elements has non-null element,
* merge comments to its trailingComments, otherwise merge comments
* to node's innerComments
*
* @param {Node} node
* @param {Array<Node>} elements
* @param {Array<Comment>} comments
*/
function adjustInnerComments(
node: Node,
elements: Array<Node>,
commentWS: CommentWhitespace,
) {
let lastElement = null;
let i = elements.length;
while (lastElement === null && i > 0) {
lastElement = elements[--i];
}
if (lastElement === null || lastElement.start > commentWS.start) {
setInnerComments(node, commentWS.comments);
} else {
setTrailingComments(lastElement, commentWS.comments);
}
}
/** @class CommentsParser */
export default class CommentsParser extends BaseParser {
addComment(comment: Comment): void {
if (this.filename) comment.loc.filename = this.filename;
this.state.trailingComments.push(comment);
this.state.leadingComments.push(comment);
}
adjustCommentsAfterTrailingComma(
node: Node,
elements: (Node | null)[],
// When the current node is followed by a token which hasn't a respective AST node, we
// need to take all the trailing comments to prevent them from being attached to an
// unrelated node. e.g. in
// var { x } /* cmt */ = { y }
// we don't want /* cmt */ to be attached to { y }.
// On the other hand, in
// fn(x) [new line] /* cmt */ [new line] y
// /* cmt */ is both a trailing comment of fn(x) and a leading comment of y
takeAllComments?: boolean,
) {
if (this.state.leadingComments.length === 0) {
return;
}
let lastElement = null;
let i = elements.length;
while (lastElement === null && i > 0) {
lastElement = elements[--i];
}
if (lastElement === null) {
return;
}
for (let j = 0; j < this.state.leadingComments.length; j++) {
if (
this.state.leadingComments[j].end < this.state.commentPreviousNode.end
) {
this.state.leadingComments.splice(j, 1);
j--;
}
}
const newTrailingComments = [];
for (let i = 0; i < this.state.leadingComments.length; i++) {
const leadingComment = this.state.leadingComments[i];
if (leadingComment.end < node.end) {
newTrailingComments.push(leadingComment);
// Perf: we don't need to splice if we are going to reset the array anyway
if (!takeAllComments) {
this.state.leadingComments.splice(i, 1);
i--;
}
} else {
if (node.trailingComments === undefined) {
node.trailingComments = [];
}
node.trailingComments.push(leadingComment);
}
}
if (takeAllComments) this.state.leadingComments = [];
if (newTrailingComments.length > 0) {
lastElement.trailingComments = newTrailingComments;
} else if (lastElement.trailingComments !== undefined) {
lastElement.trailingComments = [];
}
this.state.comments.push(comment);
}
/**
* Given a newly created AST node _n_, attach _n_ to a comment whitespace _w_ if applicable
* {@see {@link CommentWhitespace}}
*
* @param {Node} node
* @returns {void}
* @memberof CommentsParser
*/
processComment(node: Node): void {
if (node.type === "Program" && node.body.length > 0) return;
const { commentStack } = this.state;
const commentStackLength = commentStack.length;
if (commentStackLength === 0) return;
let i = commentStackLength - 1;
const lastCommentWS = commentStack[i];
const stack = this.state.commentStack;
if (lastCommentWS.start === node.end) {
lastCommentWS.leadingNode = node;
i--;
}
let firstChild, lastChild, trailingComments, i, j;
if (this.state.trailingComments.length > 0) {
// If the first comment in trailingComments comes after the
// current node, then we're good - all comments in the array will
// come after the node and so it's safe to add them as official
// trailingComments.
if (this.state.trailingComments[0].start >= node.end) {
trailingComments = this.state.trailingComments;
this.state.trailingComments = [];
const { start: nodeStart } = node;
// invariant: for all 0 <= j <= i, let c = commentStack[j], c must satisfy c.end < node.end
for (; i >= 0; i--) {
const commentWS = commentStack[i];
const commentEnd = commentWS.end;
if (commentEnd > nodeStart) {
// by definition of commentWhiteSpace, this implies commentWS.start > nodeStart
// so node can be a containingNode candidate. At this time we can finalize the comment
// whitespace, because
// 1) its leadingNode or trailingNode, if exists, will not change
// 2) its containingNode have been assigned and will not change because it is the
// innermost minimal-sized AST node
commentWS.containingNode = node;
this.finalizeComment(commentWS);
commentStack.splice(i, 1);
} else {
// Otherwise, if the first comment doesn't come after the
// current node, that means we have a mix of leading and trailing
// comments in the array and that leadingComments contains the
// same items as trailingComments. Reset trailingComments to
// zero items and we'll handle this by evaluating leadingComments
// later.
this.state.trailingComments.length = 0;
}
} else if (stack.length > 0) {
const lastInStack = last(stack);
if (
lastInStack.trailingComments &&
lastInStack.trailingComments[0].start >= node.end
) {
trailingComments = lastInStack.trailingComments;
delete lastInStack.trailingComments;
}
}
// Eating the stack.
if (stack.length > 0 && last(stack).start >= node.start) {
firstChild = stack.pop();
}
while (stack.length > 0 && last(stack).start >= node.start) {
lastChild = stack.pop();
}
if (!lastChild && firstChild) lastChild = firstChild;
// Adjust comments that follow a trailing comma on the last element in a
// comma separated list of nodes to be the trailing comments on the last
// element
if (firstChild) {
switch (node.type) {
case "ObjectExpression":
this.adjustCommentsAfterTrailingComma(node, node.properties);
break;
case "ObjectPattern":
this.adjustCommentsAfterTrailingComma(node, node.properties, true);
break;
case "CallExpression":
this.adjustCommentsAfterTrailingComma(node, node.arguments);
break;
case "ArrayExpression":
this.adjustCommentsAfterTrailingComma(node, node.elements);
break;
case "ArrayPattern":
this.adjustCommentsAfterTrailingComma(node, node.elements, true);
break;
}
} else if (
this.state.commentPreviousNode &&
((this.state.commentPreviousNode.type === "ImportSpecifier" &&
node.type !== "ImportSpecifier") ||
(this.state.commentPreviousNode.type === "ExportSpecifier" &&
node.type !== "ExportSpecifier"))
) {
this.adjustCommentsAfterTrailingComma(node, [
this.state.commentPreviousNode,
]);
}
if (lastChild) {
if (lastChild.leadingComments) {
if (
lastChild !== node &&
lastChild.leadingComments.length > 0 &&
last(lastChild.leadingComments).end <= node.start
) {
node.leadingComments = lastChild.leadingComments;
delete lastChild.leadingComments;
} else {
// A leading comment for an anonymous class had been stolen by its first ClassMethod,
// so this takes back the leading comment.
// See also: https://github.com/eslint/espree/issues/158
for (i = lastChild.leadingComments.length - 2; i >= 0; --i) {
if (lastChild.leadingComments[i].end <= node.start) {
node.leadingComments = lastChild.leadingComments.splice(0, i + 1);
break;
}
}
if (commentEnd === nodeStart) {
commentWS.trailingNode = node;
}
// stop the loop when commentEnd <= nodeStart
break;
}
} else if (this.state.leadingComments.length > 0) {
if (last(this.state.leadingComments).end <= node.start) {
if (this.state.commentPreviousNode) {
for (j = 0; j < this.state.leadingComments.length; j++) {
if (
this.state.leadingComments[j].end <
this.state.commentPreviousNode.end
) {
this.state.leadingComments.splice(j, 1);
j--;
}
}
}
if (this.state.leadingComments.length > 0) {
node.leadingComments = this.state.leadingComments;
this.state.leadingComments = [];
}
} else {
// https://github.com/eslint/espree/issues/2
//
// In special cases, such as return (without a value) and
// debugger, all comments will end up as leadingComments and
// will otherwise be eliminated. This step runs when the
// commentStack is empty and there are comments left
// in leadingComments.
//
// This loop figures out the stopping point between the actual
// leading and trailing comments by finding the location of the
// first comment that comes after the given node.
for (i = 0; i < this.state.leadingComments.length; i++) {
if (this.state.leadingComments[i].end > node.start) {
}
}
/**
* Assign the comments of comment whitespaces to related AST nodes.
* Also adjust innerComments following trailing comma.
*
* @memberof CommentsParser
*/
finalizeComment(commentWS: CommentWhitespace) {
const { comments } = commentWS;
if (commentWS.leadingNode !== null || commentWS.trailingNode !== null) {
if (commentWS.leadingNode !== null) {
setTrailingComments(commentWS.leadingNode, comments);
}
if (commentWS.trailingNode !== null) {
commentWS.trailingNode.leadingComments = comments;
}
} else {
/*:: invariant(commentWS.containingNode !== null) */
const { containingNode: node, start: commentStart } = commentWS;
if (this.input.charCodeAt(commentStart - 1) === charCodes.comma) {
// If a commentWhitespace follows a comma and the containingNode allows
// list structures with trailing comma, merge it to the trailingComment
// of the last non-null list element
switch (node.type) {
case "ObjectExpression":
case "ObjectPattern":
case "RecordExpression":
adjustInnerComments(node, node.properties, commentWS);
break;
case "CallExpression":
case "OptionalCallExpression":
adjustInnerComments(node, node.arguments, commentWS);
break;
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
case "ObjectMethod":
case "ClassMethod":
case "ClassPrivateMethod":
adjustInnerComments(node, node.params, commentWS);
break;
case "ArrayExpression":
case "ArrayPattern":
case "TupleExpression":
adjustInnerComments(node, node.elements, commentWS);
break;
case "ExportNamedDeclaration":
case "ImportDeclaration":
adjustInnerComments(node, node.specifiers, commentWS);
break;
default: {
setInnerComments(node, comments);
}
}
// Split the array based on the location of the first comment
// that comes after the node. Keep in mind that this could
// result in an empty array, and if so, the array must be
// deleted.
const leadingComments = this.state.leadingComments.slice(0, i);
if (leadingComments.length) {
node.leadingComments = leadingComments;
}
// Similarly, trailing comments are attached later. The variable
// must be reset to null if there are no trailing comments.
trailingComments = this.state.leadingComments.slice(i);
if (trailingComments.length === 0) {
trailingComments = null;
}
}
}
this.state.commentPreviousNode = node;
if (trailingComments) {
if (
trailingComments.length &&
trailingComments[0].start >= node.start &&
last(trailingComments).end <= node.end
) {
node.innerComments = trailingComments;
} else {
// TrailingComments maybe contain innerComments
const firstTrailingCommentIndex = trailingComments.findIndex(
comment => comment.end >= node.end,
);
if (firstTrailingCommentIndex > 0) {
node.innerComments = trailingComments.slice(
0,
firstTrailingCommentIndex,
);
node.trailingComments = trailingComments.slice(
firstTrailingCommentIndex,
);
} else {
node.trailingComments = trailingComments;
}
setInnerComments(node, comments);
}
}
}
stack.push(node);
/**
* Drains remaning commentStack and applies finalizeComment
* to each comment whitespace. Used only in parseExpression
* where the top level AST node is _not_ Program
* {@see {@link CommentsParser#finalizeComment}}
*
* @memberof CommentsParser
*/
finalizeRemainingComments() {
const { commentStack } = this.state;
for (let i = commentStack.length - 1; i >= 0; i--) {
this.finalizeComment(commentStack[i]);
}
this.state.commentStack = [];
}
/**
* Reset previous node trailing comments. Used in object / class
* property parsing. We parse `async`, `static`, `set` and `get`
* as an identifier but may reinterepret it into an async/static/accessor
* method later. In this case the identifier is not part of the AST and we
* should sync the knowledge to commentStacks
*
* For example, when parsing */
// async /* 1 */ function f() {}
/*
* the comment whitespace "* 1 *" has leading node Identifier(async). When
* we see the function token, we create a Function node and mark "* 1 *" as
* inner comments. So "* 1 *" should be detached from the Identifier node.
*
* @param {N.Node} node the last finished AST node _before_ current token
* @returns
* @memberof CommentsParser
*/
resetPreviousNodeTrailingComments(node: Node) {
const { commentStack } = this.state;
const { length } = commentStack;
if (length === 0) return;
const commentWS = commentStack[length - 1];
if (commentWS.leadingNode === node) {
commentWS.leadingNode = null;
}
}
}

View File

@@ -56,6 +56,7 @@ import {
} from "../util/expression-scope";
import { Errors, SourceTypeModuleErrors } from "./error";
import type { ParsingError } from "./error";
import { setInnerComments } from "./comments";
/*::
import type { SourceType } from "../options";
@@ -161,6 +162,9 @@ export default class ExpressionParser extends LValParser {
if (!this.match(tt.eof)) {
this.unexpected();
}
// Unlike parseTopLevel, we need to drain remaining commentStacks
// because the top level node is _not_ Program.
this.finalizeRemainingComments();
expr.comments = this.state.comments;
expr.errors = this.state.errors;
if (this.options.tokens) {
@@ -938,6 +942,7 @@ export default class ExpressionParser extends LValParser {
node: N.ArrowFunctionExpression,
call: N.CallExpression,
): N.ArrowFunctionExpression {
this.resetPreviousNodeTrailingComments(call);
this.expect(tt.arrow);
this.parseArrowExpression(
node,
@@ -945,6 +950,10 @@ export default class ExpressionParser extends LValParser {
true,
call.extra?.trailingComma,
);
// mark inner comments of `async()` as inner comments of `async () =>`
setInnerComments(node, call.innerComments);
// mark trailing comments of `async` to be inner comments
setInnerComments(node, call.callee.trailingComments);
return node;
}
@@ -999,6 +1008,7 @@ export default class ExpressionParser extends LValParser {
if (!containsEsc && id.name === "async" && !this.canInsertSemicolon()) {
if (this.match(tt._function)) {
this.resetPreviousNodeTrailingComments(id);
this.next();
return this.parseFunction(
this.startNodeAtNode(id),
@@ -1010,13 +1020,19 @@ export default class ExpressionParser extends LValParser {
// arrow function. (Peeking ahead for "=" lets us avoid a more
// expensive full-token lookahead on this common path.)
if (this.lookaheadCharCode() === charCodes.equalsTo) {
return this.parseAsyncArrowUnaryFunction(id);
// although `id` is not used in async arrow unary function,
// we don't need to reset `async`'s trailing comments because
// it will be attached to the upcoming async arrow binding identifier
return this.parseAsyncArrowUnaryFunction(
this.startNodeAtNode(id),
);
} else {
// Otherwise, treat "async" as an identifier and let calling code
// deal with the current tt.name token.
return id;
}
} else if (this.match(tt._do)) {
this.resetPreviousNodeTrailingComments(id);
return this.parseDo(this.startNodeAtNode(id), true);
}
}
@@ -1189,8 +1205,7 @@ export default class ExpressionParser extends LValParser {
}
// async [no LineTerminator here] AsyncArrowBindingIdentifier[?Yield] [no LineTerminator here] => AsyncConciseBody[?In]
parseAsyncArrowUnaryFunction(id: N.Expression): N.ArrowFunctionExpression {
const node = this.startNodeAtNode(id);
parseAsyncArrowUnaryFunction(node: N.Node): N.ArrowFunctionExpression {
// We don't need to push a new ParameterDeclarationScope here since we are sure
// 1) it is an async arrow, 2) no biding pattern is allowed in params
this.prodParam.enter(functionFlags(true, this.prodParam.hasYield));
@@ -1509,7 +1524,10 @@ export default class ExpressionParser extends LValParser {
if (exprList.length > 1) {
val = this.startNodeAt(innerStartPos, innerStartLoc);
val.expressions = exprList;
this.finishNodeAt(val, "SequenceExpression", innerEndPos, innerEndLoc);
// finish node at current location so it can pick up comments after `)`
this.finishNode(val, "SequenceExpression");
val.end = innerEndPos;
val.loc.end = innerEndLoc;
} else {
val = exprList[0];
}
@@ -1782,6 +1800,7 @@ export default class ExpressionParser extends LValParser {
// https://tc39.es/ecma262/#prod-AsyncGeneratorMethod
if (keyName === "async" && !this.hasPrecedingLineBreak()) {
isAsync = true;
this.resetPreviousNodeTrailingComments(key);
isGenerator = this.eat(tt.star);
this.parsePropertyName(prop, /* isPrivateNameAllowed */ false);
}
@@ -1789,6 +1808,7 @@ export default class ExpressionParser extends LValParser {
// set PropertyName[?Yield, ?Await] ( PropertySetParameterList ) { FunctionBody[~Yield, ~Await] }
if (keyName === "get" || keyName === "set") {
isAccessor = true;
this.resetPreviousNodeTrailingComments(key);
prop.kind = keyName;
if (this.match(tt.star)) {
isGenerator = true;

View File

@@ -130,26 +130,27 @@ export default class StatementParser extends ExpressionParser {
// TODO
/**
* cast a Statement to a Directive. This method mutates input statement.
*
* @param {N.Statement} stmt
* @returns {N.Directive}
* @memberof StatementParser
*/
stmtToDirective(stmt: N.Statement): N.Directive {
const expr = stmt.expression;
const directive = (stmt: any);
directive.type = "Directive";
directive.value = directive.expression;
delete directive.expression;
const directiveLiteral = this.startNodeAt(expr.start, expr.loc.start);
const directive = this.startNodeAt(stmt.start, stmt.loc.start);
const raw = this.input.slice(expr.start, expr.end);
const directiveLiteral = directive.value;
const raw = this.input.slice(directiveLiteral.start, directiveLiteral.end);
const val = (directiveLiteral.value = raw.slice(1, -1)); // remove quotes
this.addExtra(directiveLiteral, "raw", raw);
this.addExtra(directiveLiteral, "rawValue", val);
directive.value = this.finishNodeAt(
directiveLiteral,
"DirectiveLiteral",
expr.end,
expr.loc.end,
);
return this.finishNodeAt(directive, "Directive", stmt.end, stmt.loc.end);
directiveLiteral.type = "DirectiveLiteral";
return directive;
}
parseInterpreterDirective(): N.InterpreterDirective | null {
@@ -1374,6 +1375,7 @@ export default class StatementParser extends ExpressionParser {
classBody.body.push(this.parseClassProperty(prop));
return true;
}
this.resetPreviousNodeTrailingComments(key);
return false;
}
@@ -1494,6 +1496,7 @@ export default class StatementParser extends ExpressionParser {
!this.isLineTerminator()
) {
// an async method
this.resetPreviousNodeTrailingComments(key);
const isGenerator = this.eat(tt.star);
if (publicMember.optional) {
@@ -1535,6 +1538,7 @@ export default class StatementParser extends ExpressionParser {
) {
// `get\n*` is an uninitialized property named 'get' followed by a generator.
// a getter or setter
this.resetPreviousNodeTrailingComments(key);
method.kind = key.name;
// The so-called parsed name would have been "get/set": get the real name.
const isPrivate = this.match(tt.privateName);

View File

@@ -131,8 +131,8 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}
stmtToDirective(stmt: N.Statement): N.Directive {
const directive = super.stmtToDirective(stmt);
const value = stmt.expression.value;
const directive = super.stmtToDirective(stmt);
// Record the expression value as in estree mode we want
// the stmt to have the real value e.g. ("use strict") and

View File

@@ -3212,7 +3212,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
return fileNode;
}
skipBlockComment(): void {
skipBlockComment(): N.CommentBlock | void {
if (this.hasPlugin("flowComments") && this.skipFlowComment()) {
if (this.state.hasFlowComment) {
this.unexpected(null, FlowErrors.NestedFlowComment);
@@ -3232,7 +3232,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
return;
}
super.skipBlockComment();
return super.skipBlockComment();
}
skipFlowComment(): number | boolean {

View File

@@ -4,7 +4,6 @@
import type { Options } from "../options";
import * as N from "../types";
import type { Position } from "../util/location";
import * as charCodes from "charcodes";
import { isIdentifierStart, isIdentifierChar } from "../util/identifier";
import { types as tt, keywords as keywordTypes, type TokenType } from "./types";
@@ -304,28 +303,7 @@ export default class Tokenizer extends ParserErrors {
}
}
pushComment(
block: boolean,
text: string,
start: number,
end: number,
startLoc: Position,
endLoc: Position,
): void {
const comment = {
type: block ? "CommentBlock" : "CommentLine",
value: text,
start: start,
end: end,
loc: new SourceLocation(startLoc, endLoc),
};
if (this.options.tokens) this.pushToken(comment);
this.state.comments.push(comment);
this.addComment(comment);
}
skipBlockComment(): void {
skipBlockComment(): N.CommentBlock | void {
let startLoc;
if (!this.isLookahead) startLoc = this.state.curPosition();
const start = this.state.pos;
@@ -348,17 +326,19 @@ export default class Tokenizer extends ParserErrors {
if (this.isLookahead) return;
/*:: invariant(startLoc) */
this.pushComment(
true,
this.input.slice(start + 2, end),
start,
this.state.pos,
startLoc,
this.state.curPosition(),
);
const value = this.input.slice(start + 2, end);
const comment = {
type: "CommentBlock",
value: value,
start: start,
end: end + 2,
loc: new SourceLocation(startLoc, this.state.curPosition()),
};
if (this.options.tokens) this.pushToken(comment);
return comment;
}
skipLineComment(startSkip: number): void {
skipLineComment(startSkip: number): N.CommentLine | void {
const start = this.state.pos;
let startLoc;
if (!this.isLookahead) startLoc = this.state.curPosition();
@@ -374,20 +354,26 @@ export default class Tokenizer extends ParserErrors {
if (this.isLookahead) return;
/*:: invariant(startLoc) */
this.pushComment(
false,
this.input.slice(start + startSkip, this.state.pos),
const end = this.state.pos;
const value = this.input.slice(start + startSkip, end);
const comment = {
type: "CommentLine",
value,
start,
this.state.pos,
startLoc,
this.state.curPosition(),
);
end,
loc: new SourceLocation(startLoc, this.state.curPosition()),
};
if (this.options.tokens) this.pushToken(comment);
return comment;
}
// Called at the start of the parse and after every token. Skips
// whitespace and comments, and.
skipSpace(): void {
const spaceStart = this.state.pos;
const comments = [];
loop: while (this.state.pos < this.length) {
const ch = this.input.charCodeAt(this.state.pos);
switch (ch) {
@@ -413,13 +399,23 @@ export default class Tokenizer extends ParserErrors {
case charCodes.slash:
switch (this.input.charCodeAt(this.state.pos + 1)) {
case charCodes.asterisk:
this.skipBlockComment();
case charCodes.asterisk: {
const comment = this.skipBlockComment();
if (comment !== undefined) {
this.addComment(comment);
comments.push(comment);
}
break;
}
case charCodes.slash:
this.skipLineComment(2);
case charCodes.slash: {
const comment = this.skipLineComment(2);
if (comment !== undefined) {
this.addComment(comment);
comments.push(comment);
}
break;
}
default:
break loop;
@@ -429,11 +425,56 @@ export default class Tokenizer extends ParserErrors {
default:
if (isWhitespace(ch)) {
++this.state.pos;
} else if (ch === charCodes.dash && !this.inModule) {
const pos = this.state.pos;
if (
this.input.charCodeAt(pos + 1) === charCodes.dash &&
this.input.charCodeAt(pos + 2) === charCodes.greaterThan &&
(spaceStart === 0 || this.state.lineStart > spaceStart)
) {
// A `-->` line comment
const comment = this.skipLineComment(3);
if (comment !== undefined) {
this.addComment(comment);
comments.push(comment);
}
} else {
break loop;
}
} else if (ch === charCodes.lessThan && !this.inModule) {
const pos = this.state.pos;
if (
this.input.charCodeAt(pos + 1) === charCodes.exclamationMark &&
this.input.charCodeAt(pos + 2) === charCodes.dash &&
this.input.charCodeAt(pos + 3) === charCodes.dash
) {
// `<!--`, an XML-style comment that should be interpreted as a line comment
const comment = this.skipLineComment(4);
if (comment !== undefined) {
this.addComment(comment);
comments.push(comment);
}
} else {
break loop;
}
} else {
break loop;
}
}
}
if (comments.length > 0) {
const end = this.state.pos;
const CommentWhitespace = {
start: spaceStart,
end,
comments,
leadingNode: null,
trailingNode: null,
containingNode: null,
};
this.state.commentStack.push(CommentWhitespace);
}
}
// Called at the end of every token. Sets `end`, `val`, and
@@ -661,18 +702,6 @@ export default class Tokenizer extends ParserErrors {
const next = this.input.charCodeAt(this.state.pos + 1);
if (next === code) {
if (
next === charCodes.dash &&
!this.inModule &&
this.input.charCodeAt(this.state.pos + 2) === charCodes.greaterThan &&
(this.state.lastTokEnd === 0 || this.hasPrecedingLineBreak())
) {
// A `-->` line comment
this.skipLineComment(3);
this.skipSpace();
this.nextToken();
return;
}
this.finishOp(tt.incDec, 2);
return;
}
@@ -703,20 +732,6 @@ export default class Tokenizer extends ParserErrors {
return;
}
if (
next === charCodes.exclamationMark &&
code === charCodes.lessThan &&
!this.inModule &&
this.input.charCodeAt(this.state.pos + 2) === charCodes.dash &&
this.input.charCodeAt(this.state.pos + 3) === charCodes.dash
) {
// `<!--`, an XML-style comment that should be interpreted as a line comment
this.skipLineComment(4);
this.skipSpace();
this.nextToken();
return;
}
if (next === charCodes.equalsTo) {
// <= | >=
size = 2;

View File

@@ -2,6 +2,7 @@
import type { Options } from "../options";
import * as N from "../types";
import type { CommentWhitespace } from "../parser/comments";
import { Position } from "../util/location";
import { types as ct, type TokContext } from "./context";
@@ -89,20 +90,11 @@ export default class State {
// where @foo belongs to the outer class and @bar to the inner
decoratorStack: Array<Array<N.Decorator>> = [[]];
// Comment store.
// Comment store for Program.comments
comments: Array<N.Comment> = [];
// Comment attachment store
trailingComments: Array<N.Comment> = [];
leadingComments: Array<N.Comment> = [];
commentStack: Array<{
start: number,
leadingComments: ?Array<N.Comment>,
trailingComments: ?Array<N.Comment>,
type: string,
}> = [];
// $FlowIgnore this is initialized when the parser starts.
commentPreviousNode: N.Node = null;
commentStack: Array<CommentWhitespace> = [];
// The current position of the tokenizer in the input.
pos: number = 0;

View File

@@ -16,7 +16,7 @@ import type { ParsingError } from "./parser/error";
* - packages/babel-generators/src/generators
*/
export type Comment = {
type CommentBase = {
type: "CommentBlock" | "CommentLine",
value: string,
start: number,
@@ -24,6 +24,26 @@ export type Comment = {
loc: SourceLocation,
};
export type CommentBlock = CommentBase & {
type: "CommentBlock",
};
export type CommentLine = CommentBase & {
type: "CommentLine",
};
export type Comment = CommentBlock | CommentLine;
// A whitespace containing comments
export type CommentWhitespace = {
start: number,
end: number,
comments: Array<Comment>,
leadingNode: Node | null,
trailingNode: Node | null,
containerNode: Node | null,
};
export interface NodeBase {
start: number;
end: number;