/
githubmirror
/
babel
Обзор
Документация
Войти
/
githubmirror
/
babel
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/babel-generator/src/printer.ts
1 442 строки
39 KB
Nicolò Ribaudo
Catchup source map position in preserveFormat (#18011)
25 май 2026, 12:20
Не верифицирован
25 май 2026, 12:20
5e917ed
Код
Авторство
О чём код?
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */ import Buffer from "./buffer.ts"; import type { Loc, Pos } from "./buffer.ts"; import { isLastChild, parentNeedsParens } from "./node/index.ts"; import { generatorInfosMap } from "./nodes.ts"; import type * as t from "@babel/types"; import { isExpression, isFunction, isStatement, isClassBody, isTSInterfaceBody, isTSEnumMember, } from "@babel/types"; import type { Opts as jsescOptions } from "jsesc"; import { TokenMap } from "./token-map.ts"; import type { GeneratorOptions } from "./index.ts"; import type SourceMap from "./source-map.ts"; import type { TraceMap } from "@jridgewell/trace-mapping"; import type { Token } from "@babel/parser"; import * as charCodes from "charcodes"; const SCIENTIFIC_NOTATION = /e/i; const ZERO_DECIMAL_INTEGER = /\.0+$/; const HAS_NEWLINE = /[\n\r\u2028\u2029]/; const HAS_NEWLINE_OR_BlOCK_COMMENT_END = /[\n\r\u2028\u2029]|\*\//; function commentIsNewline(c: t.Comment) { return c.type === "CommentLine" || HAS_NEWLINE.test(c.value); } import { TokenContext } from "./node/index.ts"; import { _getRawIdentifier } from "./generators/types.ts"; const enum COMMENT_TYPE { LEADING, INNER, TRAILING, } const enum COMMENT_SKIP_NEWLINE { DEFAULT, ALL, LEADING, TRAILING, } const enum PRINT_COMMENT_HINT { SKIP, ALLOW, DEFER, } const enum PRINTER_FLAGS { EMPTY = 0, PRESERVE_FORMAT = 1 << 0, COMPACT = 1 << 1, CONCISE = 1 << 2, RETAIN_LINES = 1 << 3, RETAIN_FUNCTION_PARENS = 1 << 4, AUX_COMMENTS = 1 << 5, } const enum LAST_CHAR_KINDS { EMPTY = 0, NORMAL = -1, INTEGER = -2, WORD = -3, } const enum INNER_COMMENTS_STATE { DISALLOWED = 0, ALLOWED = 1, PRINTED = 2, WITH_INDENT = 4, MASK = 3, } const enum PRINT_COMMENTS_RESULT { PRINTED_NONE = 0, PRINTED_SOME = 1, PRINTED_ALL = 2, } export type Format = { shouldPrintComment: (comment: string) => boolean; preserveFormat: boolean | undefined; retainLines: boolean | undefined; retainFunctionParens: boolean | undefined; comments: boolean | undefined; auxiliaryCommentBefore: string | undefined; auxiliaryCommentAfter: string | undefined; compact: boolean | "auto" | undefined; minified: boolean | undefined; concise: boolean | undefined; indent: { adjustMultilineComment: boolean; style: string; }; jsescOption: jsescOptions; /** * For use with the Hack-style pipe operator. * Changes what token is used for pipe bodies’ topic references. */ topicToken?: GeneratorOptions["topicToken"]; }; interface PrintSequenceOptions { statement?: boolean; indent?: boolean; trailingCommentsLineOffset?: number; } interface PrintListOptions { separator?: (this: Printer, occurrenceCount: number, last: boolean) => void; statement?: boolean; indent?: boolean; printTrailingSeparator?: boolean; } export type PrintJoinOptions = PrintListOptions & PrintSequenceOptions; class Printer { constructor( format: Format, map: SourceMap | null, tokens: Token[] | null = null, originalCode: string | null = null, ) { this.format = format; this._tokens = tokens; this._originalCode = originalCode; this._indentRepeat = format.indent.style.length; this._inputMap = map?._inputMap || null; this._buf = new Buffer(map, format.indent.style[0]); const { preserveFormat, compact, concise, retainLines, retainFunctionParens, } = format; if (preserveFormat) { this._flags |= PRINTER_FLAGS.PRESERVE_FORMAT; } if (compact) { this._flags |= PRINTER_FLAGS.COMPACT; } if (concise) { this._flags |= PRINTER_FLAGS.CONCISE; } if (retainLines) { this._flags |= PRINTER_FLAGS.RETAIN_LINES; } if (retainFunctionParens) { this._flags |= PRINTER_FLAGS.RETAIN_FUNCTION_PARENS; } if (format.auxiliaryCommentBefore || format.auxiliaryCommentAfter) { this._flags |= PRINTER_FLAGS.AUX_COMMENTS; } } declare _inputMap: TraceMap | null; declare format: Format; enterDelimited() { const oldNoLineTerminatorAfterNode = this._noLineTerminatorAfterNode; if (oldNoLineTerminatorAfterNode !== null) { this._noLineTerminatorAfterNode = null; } return oldNoLineTerminatorAfterNode; } tokenContext: number = TokenContext.normal; _tokens: Token[] | null = null; _originalCode: string | null = null; declare _buf: Buffer; _currentNode: t.Node | null = null; _currentTypeId: number | null = null; _indent: number = 0; _indentRepeat: number = 0; _insideAux: boolean = false; _noLineTerminator: boolean = false; _noLineTerminatorAfterNode: t.Node | null = null; _printAuxAfterOnNextUserNode: boolean = false; _printedComments = new Set<t.Comment>(); _lastCommentLine = 0; _innerCommentsState = INNER_COMMENTS_STATE.DISALLOWED; _flags = PRINTER_FLAGS.EMPTY; tokenMap: TokenMap | null = null; _boundGetRawIdentifier: ((node: t.Identifier) => string) | null = null; generate(ast: t.Node) { if (this.format.preserveFormat) { this.tokenMap = new TokenMap(ast, this._tokens!, this._originalCode!); this._boundGetRawIdentifier = _getRawIdentifier.bind(this); } this.print(ast); this._maybeAddAuxComment(); return this._buf.get(); } /** * Increment indent size. */ indent(flags = this._flags): void { if ( flags & (PRINTER_FLAGS.PRESERVE_FORMAT | PRINTER_FLAGS.COMPACT | PRINTER_FLAGS.CONCISE) ) { return; } this._indent += this._indentRepeat; } /** * Decrement indent size. */ dedent(flags = this._flags): void { if ( flags & (PRINTER_FLAGS.PRESERVE_FORMAT | PRINTER_FLAGS.COMPACT | PRINTER_FLAGS.CONCISE) ) { return; } this._indent -= this._indentRepeat; } /** * If the next token is on the same line, we must first print a semicolon. * This option is only used in `preserveFormat` node, for semicolons that * might have omitted due to them being absent in the original code (thanks * to ASI). * * We need both *NextToken and *NextNode because we only want to insert the * semicolon when the next token starts a new node, and not in cases like * foo} (where } is not starting a new node). So we first set *NextNode, and * then the print() method will move it to *NextToken. */ _printSemicolonBeforeNextNode: number = -1; _printSemicolonBeforeNextToken: number = -1; /** * Add a semicolon to the buffer. */ semicolon(force: boolean = false): void { const flags = this._flags; if (flags & PRINTER_FLAGS.AUX_COMMENTS) { this._maybeAddAuxComment(); } if (flags & PRINTER_FLAGS.PRESERVE_FORMAT) { const node = this._currentNode!; if (node.start != null && node.end != null) { if (!this.tokenMap!.endMatches(node, ";")) { // no semicolon this._printSemicolonBeforeNextNode = this._buf.getCurrentLine(); return; } const indexes = this.tokenMap!.getIndexes(this._currentNode!)!; this._catchUpTo(this._tokens![indexes[indexes.length - 1]].loc.start); } } if (force) { this._appendChar(charCodes.semicolon); } else { this._queue(charCodes.semicolon); } this._noLineTerminator = false; } /** * Add a right brace to the buffer. */ rightBrace(node: t.Node): void { if (this.format.minified) { this._buf.removeLastSemicolon(); } this.sourceWithOffset("end", node.loc, -1); this.token("}"); } rightParens(node: t.Node): void { this.sourceWithOffset("end", node.loc, -1); this.token(")"); } /** * Add a space to the buffer unless it is compact. */ space(force: boolean = false): void { if (this._flags & (PRINTER_FLAGS.PRESERVE_FORMAT | PRINTER_FLAGS.COMPACT)) { return; } if (force) { this._space(); } else { const lastCp = this.getLastChar(true); if ( lastCp !== 0 && lastCp !== charCodes.space && lastCp !== charCodes.lineFeed ) { this._space(); } } } /** * Writes a token that can't be safely parsed without taking whitespace into account. */ word(str: string, noLineTerminatorAfter: boolean = false): void { this.tokenContext &= TokenContext.forInOrInitHeadAccumulatePassThroughMask; this._maybePrintInnerComments(str); const flags = this._flags; if (flags & PRINTER_FLAGS.AUX_COMMENTS) { this._maybeAddAuxComment(); } if (flags & PRINTER_FLAGS.PRESERVE_FORMAT) this._catchUpToCurrentToken(str); const lastChar = this.getLastChar(); if ( lastChar === LAST_CHAR_KINDS.INTEGER || lastChar === LAST_CHAR_KINDS.WORD || // prevent concatenating words and creating // comment out of division and regex (lastChar === charCodes.slash && str.charCodeAt(0) === charCodes.slash) ) { this._space(); } this._append(str, false); this.setLastChar(-3); this._noLineTerminator = noLineTerminatorAfter; } /** * Writes a number token so that we can validate if it is an integer. */ number(str: string, number?: number): void { // const NON_DECIMAL_LITERAL = /^0[box]/; function isNonDecimalLiteral(str: string) { if (str.length > 2 && str.charCodeAt(0) === charCodes.digit0) { const secondChar = str.charCodeAt(1); return ( secondChar === charCodes.lowercaseB || secondChar === charCodes.lowercaseO || secondChar === charCodes.lowercaseX ); } return false; } this.word(str); // Integer tokens need special handling because they cannot have '.'s inserted immediately after them. if ( Number.isInteger(number) && !isNonDecimalLiteral(str) && !SCIENTIFIC_NOTATION.test(str) && !ZERO_DECIMAL_INTEGER.test(str) && str.charCodeAt(str.length - 1) !== charCodes.dot ) { this.setLastChar(LAST_CHAR_KINDS.INTEGER); } } /** * Writes a simple token. * * @param str The string to append. * @param [maybeNewline=false] Whether `str` might potentially * contain a line terminator or not. * @param [occurrenceCount=0] The occurrence count of this token in * the current node. This is used when printing in `preserveFormat` mode, * to know which token we should map to (for example, to disambiguate the * commas in an array literal). * @param [mayNeedSpace=false] Check whether a whitespace might be * needed after this token, and if so, add a whitespace after it. */ token( str: string, maybeNewline = false, occurrenceCount = 0, mayNeedSpace: boolean = false, ): void { this.tokenContext &= TokenContext.forInOrInitHeadAccumulatePassThroughMask; this._maybePrintInnerComments(str, occurrenceCount); const flags = this._flags; if (flags & PRINTER_FLAGS.AUX_COMMENTS) { this._maybeAddAuxComment(); } if (flags & PRINTER_FLAGS.PRESERVE_FORMAT) { this._catchUpToCurrentToken(str, occurrenceCount); } if (mayNeedSpace) { const strFirst = str.charCodeAt(0); if ( // space is mandatory to avoid outputting <!-- // http://javascript.spec.whatwg.org/#comment-syntax (((strFirst === charCodes.dash && str === "--") || // Needs spaces to avoid changing a! == 0 to a!== 0 strFirst === charCodes.equalsTo) && this.getLastChar() === charCodes.exclamationMark) || // Need spaces for operators of the same kind to avoid: `a+++b` (strFirst === charCodes.plusSign && this.getLastChar() === charCodes.plusSign) || (strFirst === charCodes.dash && this.getLastChar() === charCodes.dash) || // Needs spaces to avoid changing '34' to '34.', which would still be a valid number. (strFirst === charCodes.dot && this.getLastChar() === LAST_CHAR_KINDS.INTEGER) ) { this._space(); } } this._append(str, maybeNewline); this._noLineTerminator = false; } tokenChar(char: number, occurrenceCount = 0): void { this.tokenContext &= TokenContext.forInOrInitHeadAccumulatePassThroughMask; this._maybePrintInnerComments(char, occurrenceCount); const flags = this._flags; if (flags & PRINTER_FLAGS.AUX_COMMENTS) { this._maybeAddAuxComment(); } if (flags & PRINTER_FLAGS.PRESERVE_FORMAT) { this._catchUpToCurrentToken(char, occurrenceCount); } if ( // Need spaces for operators of the same kind to avoid: `a+++b` (char === charCodes.plusSign && this.getLastChar() === charCodes.plusSign) || (char === charCodes.dash && this.getLastChar() === charCodes.dash) || // Needs spaces to avoid changing '34' to '34.', which would still be a valid number. (char === charCodes.dot && this.getLastChar() === LAST_CHAR_KINDS.INTEGER) ) { this._space(); } this._appendChar(char); this._noLineTerminator = false; } /** * Add a newline (or many newlines), maintaining formatting. * This function checks the number of newlines in the queue and subtracts them. * It currently has some limitations. * @see {Buffer#getNewlineCount} */ newline(i: number = 1, flags = this._flags): void { if (i <= 0) return; if (flags & (PRINTER_FLAGS.RETAIN_LINES | PRINTER_FLAGS.COMPACT)) { return; } if (flags & PRINTER_FLAGS.CONCISE) { this.space(); return; } if (i > 2) i = 2; // Max two lines i -= this._buf.getNewlineCount(); for (let j = 0; j < i; j++) { this._newline(); } } endsWith(char: number): boolean { return this.getLastChar(true) === char; } getLastChar(checkQueue?: boolean): number { return this._buf.getLastChar(checkQueue); } setLastChar(char: number) { this._buf._last = char; } exactSource(loc: Loc | null | undefined, cb: () => void) { if (!loc) { cb(); return; } this._catchUp("start", loc); this._buf.exactSource(loc, cb); } source(prop: "start" | "end", loc: Loc | undefined): void { if (!loc) return; this._catchUp(prop, loc); this._buf.source(prop, loc); } sourceWithOffset( prop: "start" | "end", loc: Loc | null | undefined, columnOffset: number, ): void { if (!loc || this.format.preserveFormat) return; this._catchUp(prop, loc); this._buf.sourceWithOffset(prop, loc, columnOffset); } sourceIdentifierName(identifierName: string, pos?: Pos): void { if (!this._buf._canMarkIdName) return; const sourcePosition = this._buf._sourcePosition; sourcePosition.identifierNamePos = pos; sourcePosition.identifierName = identifierName; } _space(): void { this._queue(charCodes.space); } _newline(): void { // Drop trailing spaces when a newline is inserted. if (this._buf._queuedChar === charCodes.space) this._buf._queuedChar = 0; this._appendChar(charCodes.lineFeed, true); } _catchUpToCurrentToken( str: string | number, occurrenceCount: number = 0, ): void { // Assert: this.tokenMap const token = this.tokenMap!.findMatching( this._currentNode!, str, occurrenceCount, ); if (token) this._catchUpTo(token.loc.start); if ( this._printSemicolonBeforeNextToken !== -1 && this._printSemicolonBeforeNextToken === this._buf.getCurrentLine() ) { this._appendChar(charCodes.semicolon, true); } this._printSemicolonBeforeNextToken = -1; this._printSemicolonBeforeNextNode = -1; } _append(str: string, maybeNewline: boolean): void { this._maybeIndent(); this._buf.append(str, maybeNewline); } _appendChar(char: number, noIndent?: boolean): void { if (!noIndent) { this._maybeIndent(); } this._buf.appendChar(char); } _queue(char: typeof charCodes.space | typeof charCodes.semicolon): void { this._buf.queue(char); this.setLastChar(-1); } _maybeIndent(): void { const indent = this._shouldIndent(); if (indent > 0) { this._buf._appendChar(-1, indent, false); } } _shouldIndent() { // we've got a newline before us so prepend on the indentation return this.endsWith(charCodes.lineFeed) ? this._indent : 0; } catchUp(line: number) { if (!this.format.retainLines) return; // catch up to this nodes newline if we're behind const count = line - this._buf.getCurrentLine(); for (let i = 0; i < count; i++) { this._newline(); } } _catchUp(prop: "start" | "end", loc?: Loc) { const flags = this._flags; if ((flags & PRINTER_FLAGS.PRESERVE_FORMAT) === 0) { if (flags & PRINTER_FLAGS.RETAIN_LINES && loc?.[prop]) { this.catchUp(loc[prop].line); } return; } // catch up to this nodes newline if we're behind const pos = loc?.[prop]; if (pos != null) this._catchUpTo(pos); } _catchUpTo({ line, column, index }: Pos) { const count = line - this._buf.getCurrentLine(); if (count > 0 && this._noLineTerminator) { // We cannot inject new lines when _noLineTemrinator is set // to `true`, or we would generate invalid code. return; } for (let i = 0; i < count; i++) { this._newline(); } const spacesCount = count > 0 ? column : column - this._buf.getCurrentColumn(); if (spacesCount > 0) { const spaces = this._originalCode ? this._originalCode .slice(index - spacesCount, index) // https://tc39.es/ecma262/#sec-white-space .replace(/[^\t\v\f\uFEFF\p{Space_Separator}]/gu, " ") : " ".repeat(spacesCount); this._buf.append(spaces, false, true); this._buf.setSourcePosition(line, column); this.setLastChar(charCodes.space); } } printTerminatorless(node: t.Node) { /** * Set some state that will be modified if a newline has been inserted before any * non-space characters. * * This is to prevent breaking semantics for terminatorless separator nodes. eg: * * return foo; * * returns `foo`. But if we do: * * return * foo; * * `undefined` will be returned and not `foo` due to the terminator. */ this._noLineTerminator = true; this.print(node); } print( node: t.Node | null | undefined, noLineTerminatorAfter: boolean = false, resetTokenContext: boolean = false, // trailingCommentsLineOffset also used to check if called from printJoin // it will be ignored if `noLineTerminatorAfter||this._noLineTerminator` trailingCommentsLineOffset?: number, ) { if (!node) return; this._innerCommentsState = INNER_COMMENTS_STATE.DISALLOWED; const { type, loc, extra } = node; const flags = this._flags; let changedFlags = false; if ( // @ts-expect-error document _compact AST properties node._compact ) { this._flags |= PRINTER_FLAGS.CONCISE; changedFlags = true; } const nodeInfo = generatorInfosMap.get(type); if (nodeInfo === undefined) { throw new ReferenceError( `unknown node of type ${JSON.stringify( type, )} with constructor ${JSON.stringify(node.constructor.name)}`, ); } const [printMethod, nodeId, needsParens] = nodeInfo; const parent = this._currentNode; const parentId = this._currentTypeId; this._currentNode = node; this._currentTypeId = nodeId; if (flags & PRINTER_FLAGS.PRESERVE_FORMAT) { this._printSemicolonBeforeNextToken = this._printSemicolonBeforeNextNode; } let oldInAux; if (flags & PRINTER_FLAGS.AUX_COMMENTS) { oldInAux = this._insideAux; this._insideAux = loc == null; this._maybeAddAuxComment(this._insideAux && !oldInAux); } let oldTokenContext = 0; if (resetTokenContext) { oldTokenContext = this.tokenContext; if (oldTokenContext & TokenContext.forInOrInitHeadAccumulate) { this.tokenContext = 0; } else { oldTokenContext = 0; } } const parenthesized = extra != null && (extra.parenthesized as boolean | undefined); let shouldPrintParens = (parenthesized && flags & PRINTER_FLAGS.PRESERVE_FORMAT) || (parenthesized && flags & PRINTER_FLAGS.RETAIN_FUNCTION_PARENS && nodeId === __node("FunctionExpression")) || (parent && (parentNeedsParens(node, parent, parentId!) || (needsParens != null && needsParens( node, parent, parentId!, this.tokenContext, flags & PRINTER_FLAGS.PRESERVE_FORMAT ? this._boundGetRawIdentifier! : undefined, )))); if ( !shouldPrintParens && parenthesized && node.leadingComments?.length && node.leadingComments[0].type === "CommentBlock" ) { switch (parentId) { case __node("ExpressionStatement"): case __node("VariableDeclarator"): case __node("AssignmentExpression"): case __node("ReturnStatement"): break; case __node("CallExpression"): case __node("OptionalCallExpression"): case __node("NewExpression"): // @ts-expect-error checked by parentTypeId if (parent.callee !== node) break; // falls through default: shouldPrintParens = true; } } let indentParenthesized = false; if ( !shouldPrintParens && this._noLineTerminator && (node.leadingComments?.some(commentIsNewline) || (flags & PRINTER_FLAGS.RETAIN_LINES && loc && loc.start.line > this._buf.getCurrentLine())) ) { shouldPrintParens = true; indentParenthesized = true; } let oldNoLineTerminatorAfterNode; if (!shouldPrintParens) { noLineTerminatorAfter ||= !!parent && this._noLineTerminatorAfterNode === parent && isLastChild(parent, node); if (noLineTerminatorAfter) { if (node.trailingComments?.some(commentIsNewline)) { if (isExpression(node)) shouldPrintParens = true; } else { oldNoLineTerminatorAfterNode = this._noLineTerminatorAfterNode; this._noLineTerminatorAfterNode = node; } } } if (shouldPrintParens) { this.token("("); if (indentParenthesized) this.indent(); this._innerCommentsState = INNER_COMMENTS_STATE.DISALLOWED; if (!resetTokenContext) { oldTokenContext = this.tokenContext; } if (oldTokenContext & TokenContext.forInOrInitHeadAccumulate) { this.tokenContext = 0; } oldNoLineTerminatorAfterNode = this._noLineTerminatorAfterNode; this._noLineTerminatorAfterNode = null; } this._printLeadingComments(node, parent); this.exactSource( nodeId === __node("Program") || nodeId === __node("File") ? null : loc, printMethod.bind(this, node, parent), ); if (shouldPrintParens) { this._printTrailingComments(node, parent); if (indentParenthesized) { this.dedent(); this.newline(); } this.token(")"); this._noLineTerminator = noLineTerminatorAfter; } else if (noLineTerminatorAfter && !this._noLineTerminator) { this._noLineTerminator = true; this._printTrailingComments(node, parent); } else { this._printTrailingComments(node, parent, trailingCommentsLineOffset); } if (oldTokenContext) this.tokenContext = oldTokenContext; // end this._currentNode = parent; this._currentTypeId = parentId; if (changedFlags) { this._flags = flags; } if (flags & PRINTER_FLAGS.AUX_COMMENTS) { this._insideAux = oldInAux!; } if (oldNoLineTerminatorAfterNode != null) { this._noLineTerminatorAfterNode = oldNoLineTerminatorAfterNode; } this._innerCommentsState = INNER_COMMENTS_STATE.DISALLOWED; } _maybeAddAuxComment(enteredPositionlessNode?: boolean) { if (enteredPositionlessNode) this._printAuxBeforeComment(); if (!this._insideAux) this._printAuxAfterComment(); } _printAuxBeforeComment() { if (this._printAuxAfterOnNextUserNode) return; this._printAuxAfterOnNextUserNode = true; const comment = this.format.auxiliaryCommentBefore; if (comment) { this._printComment( { type: "CommentBlock", value: comment, }, COMMENT_SKIP_NEWLINE.DEFAULT, ); } } _printAuxAfterComment() { if (!this._printAuxAfterOnNextUserNode) return; this._printAuxAfterOnNextUserNode = false; const comment = this.format.auxiliaryCommentAfter; if (comment) { this._printComment( { type: "CommentBlock", value: comment, }, COMMENT_SKIP_NEWLINE.DEFAULT, ); } } getPossibleRaw( node: | t.StringLiteral | t.NumericLiteral | t.BigIntLiteral | t.DirectiveLiteral | t.JSXText, ): string | undefined { const extra = node.extra; if ( extra?.raw != null && extra.rawValue != null && node.value === extra.rawValue ) { // @ts-expect-error: The extra.raw of these AST node types must be a string return extra.raw; } } printJoin( nodes: t.Node[] | undefined | null, statement?: boolean, indent?: boolean, separator?: PrintJoinOptions["separator"] | null, printTrailingSeparator?: boolean | null, resetTokenContext?: boolean, trailingCommentsLineOffset?: number, ) { if (!nodes?.length) return; const flags = this._flags; if (indent == null && flags & PRINTER_FLAGS.RETAIN_LINES) { const startLine = nodes[0].loc?.start.line; if (startLine != null && startLine !== this._buf.getCurrentLine()) { indent = true; } } if (indent) this.indent(flags); const len = nodes.length; for (let i = 0; i < len; i++) { const node = nodes[i]; if (!node) continue; // don't add newlines at the beginning of the file if (statement && i === 0 && this._buf.hasContent()) { this.newline(1, flags); } this.print( node, false, resetTokenContext, trailingCommentsLineOffset || 0, ); if (separator != null) { if (i < len - 1) separator.call(this, i, false); else if (printTrailingSeparator) separator.call(this, i, true); } if (statement) { if (i + 1 === len) { this.newline(1, flags); } else { const lastCommentLine = this._lastCommentLine; if (lastCommentLine > 0) { const offset = (nodes[i + 1].loc?.start.line || 0) - lastCommentLine; if (offset >= 0) { this.newline(offset || 1, flags); continue; } } this.newline(1, flags); } } } if (indent) this.dedent(flags); } printAndIndentOnComments(node: t.Node) { const indent = node.leadingComments && node.leadingComments.length > 0; if (indent) this.indent(); this.print(node); if (indent) this.dedent(); } printBlock(body: t.Statement) { if (body.type !== "EmptyStatement") { this.space(); } this.print(body); } _printTrailingComments( node: t.Node, parent?: t.Node | null, lineOffset?: number, ) { const { innerComments, trailingComments } = node; // We print inner comments here, so that if for some reason they couldn't // be printed in earlier locations they are still printed *somewhere*, // even if at the end of the node. if (innerComments?.length) { this._printComments( COMMENT_TYPE.TRAILING, innerComments, node, parent, lineOffset, ); } if (trailingComments?.length) { this._printComments( COMMENT_TYPE.TRAILING, trailingComments, node, parent, lineOffset, ); } else { this._lastCommentLine = 0; } } _printLeadingComments(node: t.Node, parent: t.Node | null) { const comments = node.leadingComments; if (!comments?.length) return; this._printComments(COMMENT_TYPE.LEADING, comments, node, parent); } _maybePrintInnerComments( nextTokenStr: string | number, nextTokenOccurrenceCount?: number, ) { const state = this._innerCommentsState; switch (state & INNER_COMMENTS_STATE.MASK) { case INNER_COMMENTS_STATE.DISALLOWED: this._innerCommentsState = INNER_COMMENTS_STATE.ALLOWED | INNER_COMMENTS_STATE.WITH_INDENT; return; case INNER_COMMENTS_STATE.ALLOWED: this.printInnerComments( (state & INNER_COMMENTS_STATE.WITH_INDENT) > 0, this.tokenMap?.findMatching( this._currentNode!, nextTokenStr, nextTokenOccurrenceCount, ), ); } } printInnerComments(indent = true, nextToken?: Token | null) { const node = this._currentNode!; const comments = node.innerComments; if (!comments?.length) { this._innerCommentsState = INNER_COMMENTS_STATE.PRINTED; return; } const hasSpace = this.endsWith(charCodes.space); if (indent) this.indent(); switch ( this._printComments( COMMENT_TYPE.INNER, comments, node, undefined, undefined, nextToken, ) ) { case PRINT_COMMENTS_RESULT.PRINTED_ALL: this._innerCommentsState = INNER_COMMENTS_STATE.PRINTED; // falls through case PRINT_COMMENTS_RESULT.PRINTED_SOME: if (hasSpace) this.space(); } if (indent) this.dedent(); } noIndentInnerCommentsHere() { this._innerCommentsState &= ~INNER_COMMENTS_STATE.WITH_INDENT; } printSequence( nodes: t.Node[], indent?: boolean, resetTokenContext?: boolean, trailingCommentsLineOffset?: number, ) { this.printJoin( nodes, true, indent ?? false, undefined, undefined, resetTokenContext, trailingCommentsLineOffset, ); } printList( items: t.Node[] | null | undefined, printTrailingSeparator?: boolean | null, statement?: boolean, indent?: boolean, separator?: PrintListOptions["separator"], resetTokenContext?: boolean, ) { this.printJoin( items, statement, indent, separator ?? commaSeparator, printTrailingSeparator, resetTokenContext, ); } shouldPrintTrailingComma(listEnd: string | number): boolean | null { if (!this.tokenMap) return null; const listEndIndex = this.tokenMap.findLastIndex( this._currentNode!, token => this.tokenMap!.matchesOriginal( token, typeof listEnd === "number" ? String.fromCharCode(listEnd) : listEnd, ), ); if (listEndIndex <= 0) return null; return this.tokenMap.matchesOriginal(this._tokens![listEndIndex - 1], ","); } // Returns `PRINT_COMMENT_HINT.DEFER` if the comment cannot be printed in this position due to // line terminators, signaling that the print comments loop can stop and // resume printing comments at the next possible position. This happens when // printing inner comments, since if we have an inner comment with a multiline // there is at least one inner position where line terminators are allowed. _shouldPrintComment( comment: t.Comment, nextToken?: Token | null, ): PRINT_COMMENT_HINT { // Some plugins (such as flow-strip-types) use this to mark comments as removed using the AST-root 'comments' property, // where they can't manually mutate the AST node comment lists. if (comment.ignore) return PRINT_COMMENT_HINT.SKIP; if (this._printedComments.has(comment)) return PRINT_COMMENT_HINT.SKIP; if ( this._noLineTerminator && HAS_NEWLINE_OR_BlOCK_COMMENT_END.test(comment.value) ) { return PRINT_COMMENT_HINT.DEFER; } if (nextToken && this.tokenMap) { const commentTok = this.tokenMap.find( this._currentNode!, token => token.value === comment.value, ); if (commentTok && commentTok.start > nextToken.start) { return PRINT_COMMENT_HINT.DEFER; } } this._printedComments.add(comment); if (!this.format.shouldPrintComment(comment.value)) { return PRINT_COMMENT_HINT.SKIP; } return PRINT_COMMENT_HINT.ALLOW; } _printComment(comment: t.Comment, skipNewLines: COMMENT_SKIP_NEWLINE) { const noLineTerminator = this._noLineTerminator; const isBlockComment = comment.type === "CommentBlock"; // Add a newline before and after a block comment, unless explicitly // disallowed const printNewLines = isBlockComment && skipNewLines !== COMMENT_SKIP_NEWLINE.ALL && !noLineTerminator; if ( printNewLines && this._buf.hasContent() && skipNewLines !== COMMENT_SKIP_NEWLINE.LEADING ) { this.newline(1); } switch (this.getLastChar(true)) { // Avoid converting a / operator into a line comment by appending /* to it case charCodes.slash: this._space(); // falls through case charCodes.leftSquareBracket: case charCodes.leftCurlyBrace: case charCodes.leftParenthesis: break; default: this.space(); } let val; if (isBlockComment) { val = `/*${comment.value}*/`; if (this.format.indent.adjustMultilineComment) { const offset = comment.loc?.start.column; if (offset) { const newlineRegex = new RegExp("\\n\\s{1," + offset + "}", "g"); val = val.replace(newlineRegex, "\n"); } if (this._flags & PRINTER_FLAGS.CONCISE) { val = val.replace(/\n(?!$)/g, `\n`); } else { let indentSize = this.format.retainLines ? 0 : this._buf.getCurrentColumn(); if (this._shouldIndent() || this.format.retainLines) { indentSize += this._indent; } val = val.replace(/\n(?!$)/g, `\n${" ".repeat(indentSize)}`); } } } else if (!noLineTerminator) { val = `//${comment.value}`; } else { // It was a single-line comment, so it's guaranteed to not // contain newlines and it can be safely printed as a block // comment. val = `/*${comment.value}*/`; } this.source("start", comment.loc); this._append(val, isBlockComment); if (!isBlockComment && !noLineTerminator) { this._newline(); } if (printNewLines && skipNewLines !== COMMENT_SKIP_NEWLINE.TRAILING) { this.newline(1); } } _printComments( type: COMMENT_TYPE, comments: t.Comment[], node: t.Node, parent?: t.Node | null, lineOffset: number = 0, nextToken?: Token | null, ): PRINT_COMMENTS_RESULT { const nodeLoc = node.loc; const len = comments.length; let hasLoc = !!nodeLoc; const nodeStartLine = hasLoc ? nodeLoc!.start.line : 0; const nodeEndLine = hasLoc ? nodeLoc!.end.line : 0; let lastLine = 0; let leadingCommentNewline = 0; const { _noLineTerminator, _flags } = this; for (let i = 0; i < len; i++) { const comment = comments[i]; const shouldPrint = this._shouldPrintComment(comment, nextToken); if (shouldPrint === PRINT_COMMENT_HINT.DEFER) { return i === 0 ? PRINT_COMMENTS_RESULT.PRINTED_NONE : PRINT_COMMENTS_RESULT.PRINTED_SOME; } if (hasLoc && comment.loc && shouldPrint === PRINT_COMMENT_HINT.ALLOW) { const commentStartLine = comment.loc.start.line; const commentEndLine = comment.loc.end.line; if (type === COMMENT_TYPE.LEADING) { let offset = 0; if (i === 0) { // Because currently we cannot handle blank lines before leading comments, // we always wrap before and after multi-line comments. if ( this._buf.hasContent() && (comment.type === "CommentLine" || commentStartLine !== commentEndLine) ) { offset = leadingCommentNewline = 1; } } else { offset = commentStartLine - lastLine; } lastLine = commentEndLine; if (offset > 0 && !_noLineTerminator) { this.newline(offset, _flags); } this._printComment(comment, COMMENT_SKIP_NEWLINE.ALL); if (i + 1 === len) { const count = Math.max( nodeStartLine - lastLine, leadingCommentNewline, ); if (count > 0 && !_noLineTerminator) { this.newline(count, _flags); } lastLine = nodeStartLine; } } else if (type === COMMENT_TYPE.INNER) { const offset = commentStartLine - (i === 0 ? nodeStartLine : lastLine); lastLine = commentEndLine; if (offset > 0 && !_noLineTerminator) { this.newline(offset, _flags); } this._printComment(comment, COMMENT_SKIP_NEWLINE.ALL); if (i + 1 === len) { const count = Math.min(1, nodeEndLine - lastLine); if (count > 0 && !_noLineTerminator) { this.newline(count, _flags); } lastLine = nodeEndLine; } } else { const offset = commentStartLine - (i === 0 ? nodeEndLine - lineOffset : lastLine); lastLine = commentEndLine; if (offset > 0 && !_noLineTerminator) { this.newline(offset, _flags); } this._printComment(comment, COMMENT_SKIP_NEWLINE.ALL); } } else { hasLoc = false; if (shouldPrint !== PRINT_COMMENT_HINT.ALLOW) { continue; } if (len === 1) { const singleLine = comment.loc ? comment.loc.start.line === comment.loc.end.line : !HAS_NEWLINE.test(comment.value); const shouldSkipNewline = singleLine && !isStatement(node) && !isClassBody(parent) && !isTSInterfaceBody(parent) && !isTSEnumMember(node); if (type === COMMENT_TYPE.LEADING) { this._printComment( comment, (shouldSkipNewline && node.type !== "ObjectExpression") || (singleLine && isFunction(parent) && parent.body === node) ? COMMENT_SKIP_NEWLINE.ALL : COMMENT_SKIP_NEWLINE.DEFAULT, ); } else if (shouldSkipNewline && type === COMMENT_TYPE.TRAILING) { this._printComment(comment, COMMENT_SKIP_NEWLINE.ALL); } else { this._printComment(comment, COMMENT_SKIP_NEWLINE.DEFAULT); } } else if ( type === COMMENT_TYPE.INNER && !(node.type === "ObjectExpression" && node.properties.length > 1) && node.type !== "ClassBody" && node.type !== "TSInterfaceBody" ) { // class X { // /*:: a: number*/ // /*:: b: ?string*/ // } this._printComment( comment, i === 0 ? COMMENT_SKIP_NEWLINE.LEADING : i === len - 1 ? COMMENT_SKIP_NEWLINE.TRAILING : COMMENT_SKIP_NEWLINE.DEFAULT, ); } else { this._printComment(comment, COMMENT_SKIP_NEWLINE.DEFAULT); } } } if (type === COMMENT_TYPE.TRAILING && hasLoc && lastLine) { this._lastCommentLine = lastLine; } return PRINT_COMMENTS_RESULT.PRINTED_ALL; } } export default Printer; function commaSeparator(this: Printer, occurrenceCount: number, last: boolean) { this.tokenChar(charCodes.comma, occurrenceCount); if (!last) this.space(); }