diff --git a/packages/babel-generator/src/buffer.js b/packages/babel-generator/src/buffer.js index 14bd4d2e07..a1390c1762 100644 --- a/packages/babel-generator/src/buffer.js +++ b/packages/babel-generator/src/buffer.js @@ -58,6 +58,9 @@ export default class Buffer { */ queue(str: string): void { + // Drop trailing spaces when a newline is inserted. + if (str === "\n") while (this._queue.length > 0 && SPACES_RE.test(this._queue[0][0])) this._queue.shift(); + const { line, column, filename } = this._sourcePosition; this._queue.unshift([str, line, column, filename]); } @@ -86,10 +89,6 @@ export default class Buffer { } } - removeTrailingSpaces(): void { - while (this._queue.length > 0 && SPACES_RE.test(this._queue[0][0])) this._queue.shift(); - } - removeTrailingNewline(): void { if (this._queue.length > 0 && this._queue[0][0] === "\n") this._queue.shift(); } @@ -98,10 +97,23 @@ export default class Buffer { if (this._queue.length > 0 && this._queue[0][0] === ";") this._queue.shift(); } - endsWith(str: string): boolean { + endsWith(suffix: string): boolean { + // Fast path to avoid iterating over this._queue. + if (suffix.length === 1) { + let last; + if (this._queue.length > 0) { + const str = this._queue[0][0]; + last = str[str.length - 1]; + } else { + last = this._last; + } + + return last === suffix; + } + const end = this._last + this._queue.reduce((acc, item) => item[0] + acc, ""); - if (str.length <= end.length) { - return end.slice(-str.length) === str; + if (suffix.length <= end.length) { + return end.slice(-suffix.length) === suffix; } // We assume that everything being matched is at most a single token plus some whitespace, @@ -109,15 +121,6 @@ export default class Buffer { return false; } - getLast(): string { - if (this._queue.length > 0) { - const last = this._queue[0][0]; - return last[last.length - 1]; - } - - return this._last; - } - hasContent(): boolean { return this._queue.length > 0 || !!this._last; } diff --git a/packages/babel-generator/src/generators/base.js b/packages/babel-generator/src/generators/base.js index bceaf00521..a7d1bd35b2 100644 --- a/packages/babel-generator/src/generators/base.js +++ b/packages/babel-generator/src/generators/base.js @@ -21,9 +21,12 @@ export function BlockStatement(node: Object) { if (node.directives && node.directives.length) this.newline(); this.printSequence(node.body, node, { indent: true }); - if (!this.format.retainLines && !this.format.concise) this.removeTrailingNewline(); + this.removeTrailingNewline(); this.source("end", node.loc); + + if (!this.endsWith("\n")) this.newline(); + this.rightBrace(); } else { this.source("end", node.loc); diff --git a/packages/babel-generator/src/generators/classes.js b/packages/babel-generator/src/generators/classes.js index f7b75a321e..841473324a 100644 --- a/packages/babel-generator/src/generators/classes.js +++ b/packages/babel-generator/src/generators/classes.js @@ -42,6 +42,8 @@ export function ClassBody(node: Object) { this.printSequence(node.body, node); this.dedent(); + if (!this.endsWith("\n")) this.newline(); + this.rightBrace(); } } diff --git a/packages/babel-generator/src/generators/expressions.js b/packages/babel-generator/src/generators/expressions.js index 38f4973665..a6cc4548d0 100644 --- a/packages/babel-generator/src/generators/expressions.js +++ b/packages/babel-generator/src/generators/expressions.js @@ -1,13 +1,9 @@ /* eslint max-len: 0 */ -import isInteger from "lodash/isInteger"; import isNumber from "lodash/isNumber"; import * as t from "babel-types"; import * as n from "../node"; -const SCIENTIFIC_NOTATION = /e/i; -const ZERO_DECIMAL_INTEGER = /\.0+$/; -const NON_DECIMAL_LITERAL = /^0[box]/; export function UnaryExpression(node: Object) { if (node.operator === "void" || node.operator === "delete" || node.operator === "typeof") { @@ -89,15 +85,16 @@ export function Decorator(node: Object) { function commaSeparatorNewline() { this.token(","); this.newline(); + + if (!this.endsWith("\n")) this.space(); } export function CallExpression(node: Object) { this.print(node.callee, node); - if (node.loc) this.printAuxAfterComment(); this.token("("); - let isPrettyCall = node._prettyCall && !this.format.retainLines && !this.format.compact; + let isPrettyCall = node._prettyCall; let separator; if (isPrettyCall) { @@ -208,17 +205,6 @@ export function MemberExpression(node: Object) { this.print(node.property, node); this.token("]"); } else { - if (t.isNumericLiteral(node.object)) { - let val = this.getPossibleRaw(node.object) || node.object.value; - if (isInteger(+val) && - !NON_DECIMAL_LITERAL.test(val) && - !SCIENTIFIC_NOTATION.test(val) && - !ZERO_DECIMAL_INTEGER.test(val) && - !this.endsWith(".")) { - this.token("."); - } - } - this.token("."); this.print(node.property, node); } diff --git a/packages/babel-generator/src/generators/flow.js b/packages/babel-generator/src/generators/flow.js index eb4845ce1c..bd3748e51f 100644 --- a/packages/babel-generator/src/generators/flow.js +++ b/packages/babel-generator/src/generators/flow.js @@ -251,6 +251,7 @@ export function ObjectTypeAnnotation(node: Object) { this.printJoin(props, node, { indent: true, + statement: true, iterator: () => { if (props.length !== 1) { this.semicolon(); diff --git a/packages/babel-generator/src/generators/statements.js b/packages/babel-generator/src/generators/statements.js index c3229db035..3a31ccc84d 100644 --- a/packages/babel-generator/src/generators/statements.js +++ b/packages/babel-generator/src/generators/statements.js @@ -1,7 +1,8 @@ import * as t from "babel-types"; export function WithStatement(node: Object) { - this.keyword("with"); + this.word("with"); + this.space(); this.token("("); this.print(node.object, node); this.token(")"); @@ -9,7 +10,8 @@ export function WithStatement(node: Object) { } export function IfStatement(node: Object) { - this.keyword("if"); + this.word("if"); + this.space(); this.token("("); this.print(node.test, node); this.token(")"); @@ -45,7 +47,8 @@ function getLastStatement(statement) { } export function ForStatement(node: Object) { - this.keyword("for"); + this.word("for"); + this.space(); this.token("("); this.inForStatementInitCounter++; @@ -69,7 +72,8 @@ export function ForStatement(node: Object) { } export function WhileStatement(node: Object) { - this.keyword("while"); + this.word("while"); + this.space(); this.token("("); this.print(node.test, node); this.token(")"); @@ -78,7 +82,8 @@ export function WhileStatement(node: Object) { let buildForXStatement = function (op) { return function (node: Object) { - this.keyword("for"); + this.word("for"); + this.space(); this.token("("); this.print(node.left, node); this.space(); @@ -98,7 +103,8 @@ export function DoWhileStatement(node: Object) { this.space(); this.print(node.body, node); this.space(); - this.keyword("while"); + this.word("while"); + this.space(); this.token("("); this.print(node.test, node); this.token(")"); @@ -135,7 +141,8 @@ export function LabeledStatement(node: Object) { } export function TryStatement(node: Object) { - this.keyword("try"); + this.word("try"); + this.space(); this.print(node.block, node); this.space(); @@ -157,7 +164,8 @@ export function TryStatement(node: Object) { } export function CatchClause(node: Object) { - this.keyword("catch"); + this.word("catch"); + this.space(); this.token("("); this.print(node.param, node); this.token(")"); @@ -166,7 +174,8 @@ export function CatchClause(node: Object) { } export function SwitchStatement(node: Object) { - this.keyword("switch"); + this.word("switch"); + this.space(); this.token("("); this.print(node.discriminant, node); this.token(")"); @@ -209,14 +218,14 @@ function variableDeclarationIdent() { // "let " or "var " indentation. this.token(","); this.newline(); - for (let i = 0; i < 4; i++) this.space(true); + if (this.endsWith("\n")) for (let i = 0; i < 4; i++) this.space(true); } function constDeclarationIdent() { // "const " indentation. this.token(","); this.newline(); - for (let i = 0; i < 6; i++) this.space(true); + if (this.endsWith("\n")) for (let i = 0; i < 6; i++) this.space(true); } export function VariableDeclaration(node: Object, parent: Object) { @@ -247,7 +256,7 @@ export function VariableDeclaration(node: Object, parent: Object) { // let separator; - if (!this.format.compact && !this.format.concise && hasInits && !this.format.retainLines) { + if (hasInits) { separator = node.kind === "const" ? constDeclarationIdent : variableDeclarationIdent; } diff --git a/packages/babel-generator/src/generators/types.js b/packages/babel-generator/src/generators/types.js index ccca3ba6dc..9d7ecde738 100644 --- a/packages/babel-generator/src/generators/types.js +++ b/packages/babel-generator/src/generators/types.js @@ -38,7 +38,7 @@ export function ObjectExpression(node: Object) { if (props.length) { this.space(); - this.printList(props, node, { indent: true }); + this.printList(props, node, { indent: true, statement: true }); this.space(); } @@ -124,12 +124,8 @@ export function NullLiteral() { export function NumericLiteral(node: Object) { let raw = this.getPossibleRaw(node); - if (raw != null) { - this.word(raw); - return; - } - this.word(node.value + ""); + this.number(raw == null ? node.value + "" : raw); } export function StringLiteral(node: Object, parent: Object) { diff --git a/packages/babel-generator/src/index.js b/packages/babel-generator/src/index.js index f660233c6e..5b106a401b 100644 --- a/packages/babel-generator/src/index.js +++ b/packages/babel-generator/src/index.js @@ -1,8 +1,8 @@ import detectIndent from "detect-indent"; -import Whitespace from "./whitespace"; import SourceMap from "./source-map"; import * as messages from "babel-messages"; import Printer from "./printer"; +import type {Format} from "./printer"; /** * Babel's code generator, turns an ast into code, maintaining sourcemaps, @@ -14,119 +14,15 @@ class Generator extends Printer { opts = opts || {}; const tokens = ast.tokens || []; - let format = Generator.normalizeOptions(code, opts, tokens); + let format = normalizeOptions(code, opts, tokens); + let map = opts.sourceMaps ? new SourceMap(opts, code) : null; + super(format, map, tokens); - let map = opts.sourceMaps ? new SourceMap(opts, code) : null; - - super(format, map); - - this.ast = ast; - - this._whitespace = tokens.length > 0 ? new Whitespace(tokens) : null; + this.ast = ast; } - format: { - shouldPrintComment: (comment: string) => boolean; - retainLines: boolean; - comments: boolean; - auxiliaryCommentBefore: string; - auxiliaryCommentAfter: string; - compact: boolean | "auto"; - minified: boolean; - quotes: "single" | "double"; - concise: boolean; - indent: { - adjustMultilineComment: boolean; - style: string; - base: number; - } - }; - - _whitespace: Whitespace; ast: Object; - /** - * Normalize generator options, setting defaults. - * - * - Detects code indentation. - * - If `opts.compact = "auto"` and the code is over 100KB, `compact` will be set to `true`. - */ - - static normalizeOptions(code, opts, tokens) { - let style = " "; - if (code && typeof code === "string") { - let indent = detectIndent(code).indent; - if (indent && indent !== " ") style = indent; - } - - let format = { - auxiliaryCommentBefore: opts.auxiliaryCommentBefore, - auxiliaryCommentAfter: opts.auxiliaryCommentAfter, - shouldPrintComment: opts.shouldPrintComment, - retainLines: opts.retainLines, - comments: opts.comments == null || opts.comments, - compact: opts.compact, - minified: opts.minified, - concise: opts.concise, - quotes: opts.quotes || Generator.findCommonStringDelimiter(code, tokens), - indent: { - adjustMultilineComment: true, - style: style, - base: 0 - } - }; - - if (format.minified) { - format.compact = true; - } - - if (format.compact === "auto") { - format.compact = code.length > 100000; // 100KB - - if (format.compact) { - console.error("[BABEL] " + messages.get("codeGeneratorDeopt", opts.filename, "100KB")); - } - } - - if (format.compact) { - format.indent.adjustMultilineComment = false; - } - - return format; - } - - /** - * Determine if input code uses more single or double quotes. - */ - static findCommonStringDelimiter(code, tokens) { - let occurences = { - single: 0, - double: 0 - }; - - let checked = 0; - - for (let i = 0; i < tokens.length; i++) { - let token = tokens[i]; - if (token.type.label !== "string") continue; - - let raw = code.slice(token.start, token.end); - if (raw[0] === "'") { - occurences.single++; - } else { - occurences.double++; - } - - checked++; - if (checked >= 3) break; - } - if (occurences.single > occurences.double) { - return "single"; - } else { - return "double"; - } - } - /** * Generate code and sourcemap from ast. * @@ -134,13 +30,96 @@ class Generator extends Printer { */ generate() { - this.print(this.ast); - this.printAuxAfterComment(); - - return this._buf.get(); + return super.generate(this.ast); } } +/** + * Normalize generator options, setting defaults. + * + * - Detects code indentation. + * - If `opts.compact = "auto"` and the code is over 100KB, `compact` will be set to `true`. + */ + +function normalizeOptions(code, opts, tokens): Format { + let style = " "; + if (code && typeof code === "string") { + let indent = detectIndent(code).indent; + if (indent && indent !== " ") style = indent; + } + + let format = { + auxiliaryCommentBefore: opts.auxiliaryCommentBefore, + auxiliaryCommentAfter: opts.auxiliaryCommentAfter, + shouldPrintComment: opts.shouldPrintComment, + retainLines: opts.retainLines, + comments: opts.comments == null || opts.comments, + compact: opts.compact, + minified: opts.minified, + concise: opts.concise, + quotes: opts.quotes || findCommonStringDelimiter(code, tokens), + indent: { + adjustMultilineComment: true, + style: style, + base: 0 + } + }; + + if (format.minified) { + format.compact = true; + + format.shouldPrintComment = format.shouldPrintComment || (() => format.comments); + } else { + format.shouldPrintComment = format.shouldPrintComment || ((value) => format.comments || + (value.indexOf("@license") >= 0 || value.indexOf("@preserve") >= 0)); + } + + if (format.compact === "auto") { + format.compact = code.length > 100000; // 100KB + + if (format.compact) { + console.error("[BABEL] " + messages.get("codeGeneratorDeopt", opts.filename, "100KB")); + } + } + + if (format.compact) { + format.indent.adjustMultilineComment = false; + } + + return format; +} + +/** + * Determine if input code uses more single or double quotes. + */ +function findCommonStringDelimiter(code, tokens) { + let occurences = { + single: 0, + double: 0 + }; + + let checked = 0; + + for (let i = 0; i < tokens.length; i++) { + let token = tokens[i]; + if (token.type.label !== "string") continue; + + let raw = code.slice(token.start, token.end); + if (raw[0] === "'") { + occurences.single++; + } else { + occurences.double++; + } + + checked++; + if (checked >= 3) break; + } + if (occurences.single > occurences.double) { + return "single"; + } else { + return "double"; + } +} /** * We originally exported the Generator class above, but to make it extra clear that it is a private API, diff --git a/packages/babel-generator/src/node/index.js b/packages/babel-generator/src/node/index.js index 4f1d48c42b..971f478a3d 100644 --- a/packages/babel-generator/src/node/index.js +++ b/packages/babel-generator/src/node/index.js @@ -53,11 +53,6 @@ function isOrHasCallExpression(node) { } } - -export function isUserWhitespacable(node) { - return t.isUserWhitespacable(node); -} - export function needsWhitespace(node, parent, type) { if (!node) return 0; diff --git a/packages/babel-generator/src/printer.js b/packages/babel-generator/src/printer.js index 6ecd82530d..fea799595f 100644 --- a/packages/babel-generator/src/printer.js +++ b/packages/babel-generator/src/printer.js @@ -1,25 +1,63 @@ /* eslint max-len: 0 */ +import find from "lodash/find"; +import findLast from "lodash/findLast"; +import isInteger from "lodash/isInteger"; import repeat from "lodash/repeat"; import Buffer from "./buffer"; import * as n from "./node"; +import Whitespace from "./whitespace"; import * as t from "babel-types"; +const SCIENTIFIC_NOTATION = /e/i; +const ZERO_DECIMAL_INTEGER = /\.0+$/; +const NON_DECIMAL_LITERAL = /^0[box]/; + +export type Format = { + shouldPrintComment: (comment: string) => boolean; + retainLines: boolean; + comments: boolean; + auxiliaryCommentBefore: string; + auxiliaryCommentAfter: string; + compact: boolean | "auto"; + minified: boolean; + quotes: "single" | "double"; + concise: boolean; + indent: { + adjustMultilineComment: boolean; + style: string; + base: number; + } +}; + export default class Printer { - constructor(format, map) { + constructor(format, map, tokens) { this.format = format || {}; this._buf = new Buffer(map); - this.insideAux = false; - this._printAuxAfterOnNextUserNode = false; - this._printStack = []; - this._printedCommentStarts = {}; - this._parenPushNewlineState = null; - this._indent = 0; - this.inForStatementInitCounter = 0; + this._whitespace = tokens.length > 0 ? new Whitespace(tokens) : null; } - _printedCommentStarts: Object; - _parenPushNewlineState: ?Object; + format: Format; + inForStatementInitCounter: number = 0; + + _buf: Buffer; + _whitespace: Whitespace; + _printStack: Array = []; + _indent: number = 0; + _insideAux: boolean = false; + _printedCommentStarts: Object = {}; + _parenPushNewlineState: ?Object = null; + _printAuxAfterOnNextUserNode: boolean = false; + _printedComments: WeakSet = new WeakSet(); + _endsWithInteger = false; + _endsWithWord = false; + + generate(ast) { + this.print(ast); + this._maybeAddAuxComment(); + + return this._buf.get(); + } /** * Increment indent size. @@ -46,6 +84,7 @@ export default class Printer { */ semicolon(force: boolean = false): void { + this._maybeAddAuxComment(); this._append(";", !force /* queue */); } @@ -54,23 +93,12 @@ export default class Printer { */ rightBrace(): void { - if (!this.endsWith("\n")) this.newline(); - if (this.format.minified) { this._buf.removeLastSemicolon(); } this.token("}"); } - /** - * Add a keyword to the buffer. - */ - - keyword(name: string): void { - this.word(name); - this.space(); - } - /** * Add a space to the buffer unless it is compact. */ @@ -90,27 +118,48 @@ export default class Printer { word(str: string): void { if (this._endsWithWord) this._space(); + this._maybeAddAuxComment(); this._append(str); this._endsWithWord = true; } + /** + * Writes a number token so that we can validate if it is an integer. + */ + + number(str: string): void { + this.word(str); + + // Integer tokens need special handling because they cannot have '.'s inserted + // immediately after them. + this._endsWithInteger = + isInteger(+str) && + !NON_DECIMAL_LITERAL.test(str) && + !SCIENTIFIC_NOTATION.test(str) && + !ZERO_DECIMAL_INTEGER.test(str) && + str[str.length - 1] !== "."; + } + /** * Writes a simple token. */ token(str: string): void { - const last = this._buf.getLast(); // space is mandatory to avoid outputting