diff --git a/README.md b/README.md index 6b9a727a..57ad63bc 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,153 @@ +# jscodefmt + +This is a JavaScript pretty-printer that is opinionated. All is takes +a width to format the code to and it does the rest. Zero config: it +just works! Integrate this into your editor to get immediate feedback, +or run it across an entire project to format all your files. + +## Details + +This is a fork of [recast](https://github.com/benjamn/recast)'s +printer because it already handles a lot of edge cases like handling +comments. The core algorithm has been rewritten to be based on +Wadler's "[A prettier +printer](http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf)" +paper, however. Recast also supported only re-printing nodes that +changed from a transformation, but we avoid that and always +pretty-print the entire AST so it's always consistent. + +That paper allows a flexible formatting that will break expressions +across lines if they get too big. This means you can sloppily write +code as you need and just format it, and it will always produce +consistent output. + +The core of the algorithm is implemented in pp.js. The printer should +use the basic formatting abstractions provided to construct a format +when printing a node. Parts of the API only exist to be compatible +with recast's previous API to ease migration, but over time we can +clean it up. + +The following commands are available: + +* **concat** + +Combine an array into a single string. + +* **group** + +Mark a group of items which the printer should try to fit on one line. +This is the basic command to tell the printer when to break. Groups +are usually nested, and the printer will try to fit everything on one +line, but if it doesn't fit it will break the outermost group first +and try again. It will continue breaking groups until everything fits +(or there are no more groups to break). + +* **multilineGroup** + +This is the same as `group`, but with an additional behavior: if this +group spans any other groups that have hard breaks (see below) this +group *always* breaks. Otherwise it acts the same as `group`. + +For example, an array with try to fit on one line: + +```js +[1, "foo", { bar: 2 }] +``` + +However, if any of the items inside the array have a hard break, the +array will *always* break as well: + +```js +[ + 1, + function() { + return 2 + }, + 3 +] + +Functions always break after the opening curly brace no matter what, +so the array breaks as well for consistent formatting. See the +implementation of `ArrayExpression` for an example. + +* **join** + +Join an array of items with a separator. + +* **line** + +Specify a line break. If an expression fits on one line, the line +break will be replaced with a space. Line breaks always indent the +next line with the current level of indentation. + +* **softline** + +Specify a line break. The difference from `line` is that if the +expression fits on one line, it will be replaced with nothing. + +* **hardline** + +Specify a line break that is **always** included in the output, no +matter if the expression fits on one line or not. + +* **literalline** + +Specify a line break that is **always** included in the output, and +don't indent the next line. This is used for template literals. + +* **indent** + +Increase the level of indentation. + +### Example + +For an example, here's the implementation of the `ArrayExpression` node type: + +```js +return multilineGroup(concat([ + "[", + indent(options.tabWidth, + concat([ + line, + join(concat([",", line]), + path.map(print, "elements")) + ])), + line, + "]" +])); +``` + +This is a group with opening and closing brackets, and possibly +indented contents. Because it's a `multilineGroup` it will always be +broken up if any of the sub-expressions are broken. + +## TODO + +There is a lot to do: + +1. Remove any cruft leftover from recast that we don't need +2. Polish the API (it was currently designed to be "usable" and compatible with what recast did before) +3. Many node types have not been converted from recast's old ways of doing things, so need to finish converting them. +4. Better editor integration +5. Better CLI + +## Contributing + ``` $ git clone https://github.com/jlongster/jscodefmt.git $ cd jscodefmt -$ npm install -g . -$ jscodefmt file.js +$ npm install +$ ./bin/jscodefmt file.js ``` +## Tests + +A few snapshot tests are currently implemented. See `tests`. To run +the tests simply cd into the tests directory and run `node index.js`. + +## Editors + It's most useful when integrated with your editor, so see `editors` to for editor support. Atom and Emacs is currently supported. diff --git a/editors/jscodefmt-atom/lib/jscodefmt.js b/editors/jscodefmt-atom/lib/jscodefmt.js index 7d1588b5..60639777 100644 --- a/editors/jscodefmt-atom/lib/jscodefmt.js +++ b/editors/jscodefmt-atom/lib/jscodefmt.js @@ -85,7 +85,7 @@ module.exports = { if(editor.editorElement) { window.addEventListener("resize", e => { const { width } = window.document.body.getBoundingClientRect(); - const columns = (width / editor.editorElement.getDefaultCharacterWidth() | 0) - 10; + const columns = (width / editor.editorElement.getDefaultCharacterWidth() | 0); console.log(width, columns); this.format({selection: false, printWidth: columns}); }); diff --git a/index.js b/index.js index 6893c20d..67de1262 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ const recast = require("recast"); const babylon = require("babylon"); +const Printer = require("./src/printer").Printer; var babylonOptions = { sourceType: 'module', @@ -35,7 +36,7 @@ module.exports = { } }); - const result = recast.prettyPrint(ast, { tabWidth, wrapColumn: printWidth }); - return result.code; + const printer = new Printer({ tabWidth, wrapColumn: printWidth }); + return printer.printGenerically(ast).code; } }; diff --git a/package.json b/package.json index b92c97f7..4d840540 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,16 @@ { "name": "jscodefmt", "version": "0.0.1", - "bin": { "jscodefmt": "./bin/jscodefmt" }, + "bin": { + "jscodefmt": "./bin/jscodefmt" + }, "main": "./index.js", "dependencies": { - "babylon": "^6.14.1", + "babylon": "git+https://github.com/jlongster/babylon.git#published", "minimist": "^1.2.0", - "recast": "git+https://github.com/jlongster/recast.git#print-all!" + "recast": "^0.11.18" + }, + "devDependencies": { + "glob": "^7.1.1" } } diff --git a/src/comments.js b/src/comments.js new file mode 100644 index 00000000..930d33e7 --- /dev/null +++ b/src/comments.js @@ -0,0 +1,356 @@ +var assert = require("assert"); +var types = require("ast-types"); +var n = types.namedTypes; +var isArray = types.builtInTypes.array; +var isObject = types.builtInTypes.object; +var pp = require("./pp"); +var fromString = pp.fromString; +var concat = pp.concat; +var line = pp.line; +var util = require("./util"); +var comparePos = util.comparePos; +var childNodesCacheKey = require("private").makeUniqueKey(); + +// TODO Move a non-caching implementation of this function into ast-types, +// and implement a caching wrapper function here. +function getSortedChildNodes(node, lines, resultArray) { + if (!node) { + return; + } + + // The .loc checks below are sensitive to some of the problems that + // are fixed by this utility function. Specifically, if it decides to + // set node.loc to null, indicating that the node's .loc information + // is unreliable, then we don't want to add node to the resultArray. + util.fixFaultyLocations(node, lines); + + if (resultArray) { + if (n.Node.check(node) && + n.SourceLocation.check(node.loc)) { + // This reverse insertion sort almost always takes constant + // time because we almost always (maybe always?) append the + // nodes in order anyway. + for (var i = resultArray.length - 1; i >= 0; --i) { + if (comparePos(resultArray[i].loc.end, + node.loc.start) <= 0) { + break; + } + } + resultArray.splice(i + 1, 0, node); + return; + } + } else if (node[childNodesCacheKey]) { + return node[childNodesCacheKey]; + } + + var names; + if (isArray.check(node)) { + names = Object.keys(node); + } else if (isObject.check(node)) { + names = types.getFieldNames(node); + } else { + return; + } + + if (!resultArray) { + Object.defineProperty(node, childNodesCacheKey, { + value: resultArray = [], + enumerable: false + }); + } + + for (var i = 0, nameCount = names.length; i < nameCount; ++i) { + getSortedChildNodes(node[names[i]], lines, resultArray); + } + + return resultArray; +} + +// As efficiently as possible, decorate the comment object with +// .precedingNode, .enclosingNode, and/or .followingNode properties, at +// least one of which is guaranteed to be defined. +function decorateComment(node, comment, lines) { + var childNodes = getSortedChildNodes(node, lines); + + // Time to dust off the old binary search robes and wizard hat. + var left = 0, right = childNodes.length; + while (left < right) { + var middle = (left + right) >> 1; + var child = childNodes[middle]; + + if (comparePos(child.loc.start, comment.loc.start) <= 0 && + comparePos(comment.loc.end, child.loc.end) <= 0) { + // The comment is completely contained by this child node. + decorateComment(comment.enclosingNode = child, comment, lines); + return; // Abandon the binary search at this level. + } + + if (comparePos(child.loc.end, comment.loc.start) <= 0) { + // This child node falls completely before the comment. + // Because we will never consider this node or any nodes + // before it again, this node must be the closest preceding + // node we have encountered so far. + var precedingNode = child; + left = middle + 1; + continue; + } + + if (comparePos(comment.loc.end, child.loc.start) <= 0) { + // This child node falls completely after the comment. + // Because we will never consider this node or any nodes after + // it again, this node must be the closest following node we + // have encountered so far. + var followingNode = child; + right = middle; + continue; + } + + throw new Error("Comment location overlaps with node location"); + } + + if (precedingNode) { + comment.precedingNode = precedingNode; + } + + if (followingNode) { + comment.followingNode = followingNode; + } +} + +exports.attach = function(comments, ast, lines) { + if (!isArray.check(comments)) { + return; + } + + var tiesToBreak = []; + + comments.forEach(function(comment) { + comment.loc.lines = lines; + decorateComment(ast, comment, lines); + + var pn = comment.precedingNode; + var en = comment.enclosingNode; + var fn = comment.followingNode; + + if (pn && fn) { + var tieCount = tiesToBreak.length; + if (tieCount > 0) { + var lastTie = tiesToBreak[tieCount - 1]; + + assert.strictEqual( + lastTie.precedingNode === comment.precedingNode, + lastTie.followingNode === comment.followingNode + ); + + if (lastTie.followingNode !== comment.followingNode) { + breakTies(tiesToBreak, lines); + } + } + + tiesToBreak.push(comment); + + } else if (pn) { + // No contest: we have a trailing comment. + breakTies(tiesToBreak, lines); + addTrailingComment(pn, comment); + + } else if (fn) { + // No contest: we have a leading comment. + breakTies(tiesToBreak, lines); + addLeadingComment(fn, comment); + + } else if (en) { + // The enclosing node has no child nodes at all, so what we + // have here is a dangling comment, e.g. [/* crickets */]. + breakTies(tiesToBreak, lines); + addDanglingComment(en, comment); + + } else { + throw new Error("AST contains no nodes at all?"); + } + }); + + breakTies(tiesToBreak, lines); + + comments.forEach(function(comment) { + // These node references were useful for breaking ties, but we + // don't need them anymore, and they create cycles in the AST that + // may lead to infinite recursion if we don't delete them here. + delete comment.precedingNode; + delete comment.enclosingNode; + delete comment.followingNode; + }); +}; + +function breakTies(tiesToBreak, lines) { + var tieCount = tiesToBreak.length; + if (tieCount === 0) { + return; + } + + var pn = tiesToBreak[0].precedingNode; + var fn = tiesToBreak[0].followingNode; + var gapEndPos = fn.loc.start; + + // Iterate backwards through tiesToBreak, examining the gaps + // between the tied comments. In order to qualify as leading, a + // comment must be separated from fn by an unbroken series of + // whitespace-only gaps (or other comments). + for (var indexOfFirstLeadingComment = tieCount; + indexOfFirstLeadingComment > 0; + --indexOfFirstLeadingComment) { + var comment = tiesToBreak[indexOfFirstLeadingComment - 1]; + assert.strictEqual(comment.precedingNode, pn); + assert.strictEqual(comment.followingNode, fn); + + var gap = lines.sliceString(comment.loc.end, gapEndPos); + if (/\S/.test(gap)) { + // The gap string contained something other than whitespace. + break; + } + + gapEndPos = comment.loc.start; + } + + while (indexOfFirstLeadingComment <= tieCount && + (comment = tiesToBreak[indexOfFirstLeadingComment]) && + // If the comment is a //-style comment and indented more + // deeply than the node itself, reconsider it as trailing. + (comment.type === "Line" || comment.type === "CommentLine") && + comment.loc.start.column > fn.loc.start.column) { + ++indexOfFirstLeadingComment; + } + + tiesToBreak.forEach(function(comment, i) { + if (i < indexOfFirstLeadingComment) { + addTrailingComment(pn, comment); + } else { + addLeadingComment(fn, comment); + } + }); + + tiesToBreak.length = 0; +} + +function addCommentHelper(node, comment) { + var comments = node.comments || (node.comments = []); + comments.push(comment); +} + +function addLeadingComment(node, comment) { + comment.leading = true; + comment.trailing = false; + addCommentHelper(node, comment); +} + +function addDanglingComment(node, comment) { + comment.leading = false; + comment.trailing = false; + addCommentHelper(node, comment); +} + +function addTrailingComment(node, comment) { + comment.leading = false; + comment.trailing = true; + addCommentHelper(node, comment); +} + +function printLeadingComment(commentPath, print) { + var comment = commentPath.getValue(); + n.Comment.assert(comment); + + var loc = comment.loc; + var lines = loc && loc.lines; + var parts = [print(commentPath)]; + + if (comment.trailing) { + // When we print trailing comments as leading comments, we don't + // want to bring any trailing spaces along. + parts.push(line); + + } + + // else if (lines instanceof Lines) { + // var trailingSpace = lines.slice( + // loc.end, + // lines.skipSpaces(loc.end) + // ); + + // if (trailingSpace.length === 1) { + // // If the trailing space contains no newlines, then we want to + // // preserve it exactly as we found it. + // parts.push(trailingSpace); + // } else { + // // If the trailing space contains newlines, then replace it + // // with just that many newlines, with all other spaces removed. + // parts.push(new Array(trailingSpace.length).join("\n")); + // } + + // } + else { + parts.push(line); + } + + return concat(parts); +} + +function printTrailingComment(commentPath, print) { + var comment = commentPath.getValue(commentPath); + n.Comment.assert(comment); + + var loc = comment.loc; + var lines = loc && loc.lines; + var parts = []; + + // if (lines instanceof Lines) { + // var fromPos = lines.skipSpaces(loc.start, true) || lines.firstPos(); + // var leadingSpace = lines.slice(fromPos, loc.start); + + // if (leadingSpace.length === 1) { + // // If the leading space contains no newlines, then we want to + // // preserve it exactly as we found it. + // parts.push(leadingSpace); + // } else { + // // If the leading space contains newlines, then replace it + // // with just that many newlines, sans all other spaces. + // parts.push(new Array(leadingSpace.length).join("\n")); + // } + // } + + parts.push(print(commentPath)); + + return concat(parts); +} + +exports.printComments = function(path, print) { + var value = path.getValue(); + var innerLines = print(path); + var comments = n.Node.check(value) && + types.getFieldValue(value, "comments"); + + if (!comments || comments.length === 0) { + return innerLines; + } + + var leadingParts = []; + var trailingParts = [innerLines]; + + path.each(function(commentPath) { + var comment = commentPath.getValue(); + var leading = types.getFieldValue(comment, "leading"); + var trailing = types.getFieldValue(comment, "trailing"); + + if (leading || (trailing && !(n.Statement.check(value) || + comment.type === "Block" || + comment.type === "CommentBlock"))) { + leadingParts.push(printLeadingComment(commentPath, print)); + } else if (trailing) { + trailingParts.push(printTrailingComment(commentPath, print)); + } + }, "comments"); + + leadingParts.push.apply(leadingParts, trailingParts); + // TODO: Optimize this; if there are no comments we shouldn't + // touch the structure + return concat(leadingParts); +}; diff --git a/src/fast-path.js b/src/fast-path.js new file mode 100644 index 00000000..8d631d74 --- /dev/null +++ b/src/fast-path.js @@ -0,0 +1,484 @@ +var assert = require("assert"); +var types = require("ast-types"); +var n = types.namedTypes; +var Node = n.Node; +var isArray = types.builtInTypes.array; +var isNumber = types.builtInTypes.number; + +function FastPath(value) { + assert.ok(this instanceof FastPath); + this.stack = [value]; +} + +var FPp = FastPath.prototype; +module.exports = FastPath; + +// Static convenience function for coercing a value to a FastPath. +FastPath.from = function(obj) { + if (obj instanceof FastPath) { + // Return a defensive copy of any existing FastPath instances. + return obj.copy(); + } + + if (obj instanceof types.NodePath) { + // For backwards compatibility, unroll NodePath instances into + // lightweight FastPath [..., name, value] stacks. + var copy = Object.create(FastPath.prototype); + var stack = [obj.value]; + for (var pp; (pp = obj.parentPath); obj = pp) + stack.push(obj.name, pp.value); + copy.stack = stack.reverse(); + return copy; + } + + // Otherwise use obj as the value of the new FastPath instance. + return new FastPath(obj); +}; + +FPp.copy = function copy() { + var copy = Object.create(FastPath.prototype); + copy.stack = this.stack.slice(0); + return copy; +}; + +// The name of the current property is always the penultimate element of +// this.stack, and always a String. +FPp.getName = function getName() { + var s = this.stack; + var len = s.length; + if (len > 1) { + return s[len - 2]; + } + // Since the name is always a string, null is a safe sentinel value to + // return if we do not know the name of the (root) value. + return null; +}; + +// The value of the current property is always the final element of +// this.stack. +FPp.getValue = function getValue() { + var s = this.stack; + return s[s.length - 1]; +}; + +function getNodeHelper(path, count) { + var s = path.stack; + + for (var i = s.length - 1; i >= 0; i -= 2) { + var value = s[i]; + if (n.Node.check(value) && --count < 0) { + return value; + } + } + + return null; +} + +FPp.getNode = function getNode(count) { + return getNodeHelper(this, ~~count); +}; + +FPp.getParentNode = function getParentNode(count) { + return getNodeHelper(this, ~~count + 1); +}; + +// The length of the stack can be either even or odd, depending on whether +// or not we have a name for the root value. The difference between the +// index of the root value and the index of the final value is always +// even, though, which allows us to return the root value in constant time +// (i.e. without iterating backwards through the stack). +FPp.getRootValue = function getRootValue() { + var s = this.stack; + if (s.length % 2 === 0) { + return s[1]; + } + return s[0]; +}; + +// Temporarily push properties named by string arguments given after the +// callback function onto this.stack, then call the callback with a +// reference to this (modified) FastPath object. Note that the stack will +// be restored to its original state after the callback is finished, so it +// is probably a mistake to retain a reference to the path. +FPp.call = function call(callback/*, name1, name2, ... */) { + var s = this.stack; + var origLen = s.length; + var value = s[origLen - 1]; + var argc = arguments.length; + for (var i = 1; i < argc; ++i) { + var name = arguments[i]; + value = value[name]; + s.push(name, value); + } + var result = callback(this); + s.length = origLen; + return result; +}; + +// Similar to FastPath.prototype.call, except that the value obtained by +// accessing this.getValue()[name1][name2]... should be array-like. The +// callback will be called with a reference to this path object for each +// element of the array. +FPp.each = function each(callback/*, name1, name2, ... */) { + var s = this.stack; + var origLen = s.length; + var value = s[origLen - 1]; + var argc = arguments.length; + + for (var i = 1; i < argc; ++i) { + var name = arguments[i]; + value = value[name]; + s.push(name, value); + } + + for (var i = 0; i < value.length; ++i) { + if (i in value) { + s.push(i, value[i]); + // If the callback needs to know the value of i, call + // path.getName(), assuming path is the parameter name. + callback(this); + s.length -= 2; + } + } + + s.length = origLen; +}; + +// Similar to FastPath.prototype.each, except that the results of the +// callback function invocations are stored in an array and returned at +// the end of the iteration. +FPp.map = function map(callback/*, name1, name2, ... */) { + var s = this.stack; + var origLen = s.length; + var value = s[origLen - 1]; + var argc = arguments.length; + + for (var i = 1; i < argc; ++i) { + var name = arguments[i]; + value = value[name]; + s.push(name, value); + } + + var result = new Array(value.length); + + for (var i = 0; i < value.length; ++i) { + if (i in value) { + s.push(i, value[i]); + result[i] = callback(this, i); + s.length -= 2; + } + } + + s.length = origLen; + + return result; +}; + +// Inspired by require("ast-types").NodePath.prototype.needsParens, but +// more efficient because we're iterating backwards through a stack. +FPp.needsParens = function(assumeExpressionContext) { + var parent = this.getParentNode(); + if (!parent) { + return false; + } + + var name = this.getName(); + var node = this.getNode(); + + // If the value of this path is some child of a Node and not a Node + // itself, then it doesn't need parentheses. Only Node objects (in + // fact, only Expression nodes) need parentheses. + if (this.getValue() !== node) { + return false; + } + + // Only statements don't need parentheses. + if (n.Statement.check(node)) { + return false; + } + + // Identifiers never need parentheses. + if (node.type === "Identifier") { + return false; + } + + if (parent.type === "ParenthesizedExpression") { + return false; + } + + switch (node.type) { + case "UnaryExpression": + case "SpreadElement": + case "SpreadProperty": + return parent.type === "MemberExpression" + && name === "object" + && parent.object === node; + + case "BinaryExpression": + case "LogicalExpression": + switch (parent.type) { + case "CallExpression": + return name === "callee" + && parent.callee === node; + + case "UnaryExpression": + case "SpreadElement": + case "SpreadProperty": + return true; + + case "MemberExpression": + return name === "object" + && parent.object === node; + + case "BinaryExpression": + case "LogicalExpression": + var po = parent.operator; + var pp = PRECEDENCE[po]; + var no = node.operator; + var np = PRECEDENCE[no]; + + if (pp > np) { + return true; + } + + if (pp === np && name === "right") { + assert.strictEqual(parent.right, node); + return true; + } + + default: + return false; + } + + case "SequenceExpression": + switch (parent.type) { + case "ReturnStatement": + return false; + + case "ForStatement": + // Although parentheses wouldn't hurt around sequence + // expressions in the head of for loops, traditional style + // dictates that e.g. i++, j++ should not be wrapped with + // parentheses. + return false; + + case "ExpressionStatement": + return name !== "expression"; + + default: + // Otherwise err on the side of overparenthesization, adding + // explicit exceptions above if this proves overzealous. + return true; + } + + case "YieldExpression": + switch (parent.type) { + case "BinaryExpression": + case "LogicalExpression": + case "UnaryExpression": + case "SpreadElement": + case "SpreadProperty": + case "CallExpression": + case "MemberExpression": + case "NewExpression": + case "ConditionalExpression": + case "YieldExpression": + return true; + + default: + return false; + } + + case "IntersectionTypeAnnotation": + case "UnionTypeAnnotation": + return parent.type === "NullableTypeAnnotation"; + + case "Literal": + return parent.type === "MemberExpression" + && isNumber.check(node.value) + && name === "object" + && parent.object === node; + + case "AssignmentExpression": + case "ConditionalExpression": + switch (parent.type) { + case "UnaryExpression": + case "SpreadElement": + case "SpreadProperty": + case "BinaryExpression": + case "LogicalExpression": + return true; + + case "CallExpression": + return name === "callee" + && parent.callee === node; + + case "ConditionalExpression": + return name === "test" + && parent.test === node; + + case "MemberExpression": + return name === "object" + && parent.object === node; + + default: + return false; + } + + case "ArrowFunctionExpression": + if(parent.type === 'CallExpression' && + name === 'callee') { + return true; + }; + + return isBinary(parent); + + case "ObjectExpression": + if (parent.type === "ArrowFunctionExpression" && + name === "body") { + return true; + } + + default: + if (parent.type === "NewExpression" && + name === "callee" && + parent.callee === node) { + return containsCallExpression(node); + } + } + + if (assumeExpressionContext !== true && + !this.canBeFirstInStatement() && + this.firstInStatement()) + return true; + + return false; +}; + +function isBinary(node) { + return n.BinaryExpression.check(node) + || n.LogicalExpression.check(node); +} + +function isUnaryLike(node) { + return n.UnaryExpression.check(node) + // I considered making SpreadElement and SpreadProperty subtypes + // of UnaryExpression, but they're not really Expression nodes. + || (n.SpreadElement && n.SpreadElement.check(node)) + || (n.SpreadProperty && n.SpreadProperty.check(node)); +} + +var PRECEDENCE = {}; +[["||"], + ["&&"], + ["|"], + ["^"], + ["&"], + ["==", "===", "!=", "!=="], + ["<", ">", "<=", ">=", "in", "instanceof"], + [">>", "<<", ">>>"], + ["+", "-"], + ["*", "/", "%", "**"] +].forEach(function(tier, i) { + tier.forEach(function(op) { + PRECEDENCE[op] = i; + }); +}); + +function containsCallExpression(node) { + if (n.CallExpression.check(node)) { + return true; + } + + if (isArray.check(node)) { + return node.some(containsCallExpression); + } + + if (n.Node.check(node)) { + return types.someField(node, function(name, child) { + return containsCallExpression(child); + }); + } + + return false; +} + +FPp.canBeFirstInStatement = function() { + var node = this.getNode(); + return !n.FunctionExpression.check(node) + && !n.ObjectExpression.check(node); +}; + +FPp.firstInStatement = function() { + var s = this.stack; + var parentName, parent; + var childName, child; + + for (var i = s.length - 1; i >= 0; i -= 2) { + if (n.Node.check(s[i])) { + childName = parentName; + child = parent; + parentName = s[i - 1]; + parent = s[i]; + } + + if (!parent || !child) { + continue; + } + + if (n.BlockStatement.check(parent) && + parentName === "body" && + childName === 0) { + assert.strictEqual(parent.body[0], child); + return true; + } + + if (n.ExpressionStatement.check(parent) && + childName === "expression") { + assert.strictEqual(parent.expression, child); + return true; + } + + if (n.SequenceExpression.check(parent) && + parentName === "expressions" && + childName === 0) { + assert.strictEqual(parent.expressions[0], child); + continue; + } + + if (n.CallExpression.check(parent) && + childName === "callee") { + assert.strictEqual(parent.callee, child); + continue; + } + + if (n.MemberExpression.check(parent) && + childName === "object") { + assert.strictEqual(parent.object, child); + continue; + } + + if (n.ConditionalExpression.check(parent) && + childName === "test") { + assert.strictEqual(parent.test, child); + continue; + } + + if (isBinary(parent) && + childName === "left") { + assert.strictEqual(parent.left, child); + continue; + } + + if (n.UnaryExpression.check(parent) && + !parent.prefix && + childName === "argument") { + assert.strictEqual(parent.argument, child); + continue; + } + + return false; + } + + return true; +}; diff --git a/src/options.js b/src/options.js new file mode 100644 index 00000000..9fffa59e --- /dev/null +++ b/src/options.js @@ -0,0 +1,127 @@ +var defaults = { + // If you want to use a different branch of esprima, or any other + // module that supports a .parse function, pass that module object to + // recast.parse as options.parser (legacy synonym: options.esprima). + parser: require("esprima"), + + // Number of spaces the pretty-printer should use per tab for + // indentation. If you do not pass this option explicitly, it will be + // (quite reliably!) inferred from the original code. + tabWidth: 4, + + // If you really want the pretty-printer to use tabs instead of + // spaces, make this option true. + useTabs: false, + + // The reprinting code leaves leading whitespace untouched unless it + // has to reindent a line, or you pass false for this option. + reuseWhitespace: true, + + // Override this option to use a different line terminator, e.g. \r\n. + lineTerminator: require("os").EOL, + + // Some of the pretty-printer code (such as that for printing function + // parameter lists) makes a valiant attempt to prevent really long + // lines. You can adjust the limit by changing this option; however, + // there is no guarantee that line length will fit inside this limit. + wrapColumn: 74, // Aspirational for now. + + // Pass a string as options.sourceFileName to recast.parse to tell the + // reprinter to keep track of reused code so that it can construct a + // source map automatically. + sourceFileName: null, + + // Pass a string as options.sourceMapName to recast.print, and + // (provided you passed options.sourceFileName earlier) the + // PrintResult of recast.print will have a .map property for the + // generated source map. + sourceMapName: null, + + // If provided, this option will be passed along to the source map + // generator as a root directory for relative source file paths. + sourceRoot: null, + + // If you provide a source map that was generated from a previous call + // to recast.print as options.inputSourceMap, the old source map will + // be composed with the new source map. + inputSourceMap: null, + + // If you want esprima to generate .range information (recast only + // uses .loc internally), pass true for this option. + range: false, + + // If you want esprima not to throw exceptions when it encounters + // non-fatal errors, keep this option true. + tolerant: true, + + // If you want to override the quotes used in string literals, specify + // either "single", "double", or "auto" here ("auto" will select the one + // which results in the shorter literal) + // Otherwise, double quotes are used. + quote: null, + + // Controls the printing of trailing commas in object literals, + // array expressions and function parameters. + // + // This option could either be: + // * Boolean - enable/disable in all contexts (objects, arrays and function params). + // * Object - enable/disable per context. + // + // Example: + // trailingComma: { + // objects: true, + // arrays: true, + // parameters: false, + // } + trailingComma: false, + + // Controls the printing of spaces inside array brackets. + // See: http://eslint.org/docs/rules/array-bracket-spacing + arrayBracketSpacing: false, + + // Controls the printing of spaces inside object literals, + // destructuring assignments, and import/export specifiers. + // See: http://eslint.org/docs/rules/object-curly-spacing + objectCurlySpacing: true, + + // If you want parenthesis to wrap single-argument arrow function parameter + // lists, pass true for this option. + arrowParensAlways: false, + + // There are 2 supported syntaxes (`,` and `;`) in Flow Object Types; + // The use of commas is in line with the more popular style and matches + // how objects are defined in JS, making it a bit more natural to write. + flowObjectCommas: true, +}, hasOwn = defaults.hasOwnProperty; + +// Copy options and fill in default values. +exports.normalize = function(options) { + options = options || defaults; + + function get(key) { + return hasOwn.call(options, key) + ? options[key] + : defaults[key]; + } + + return { + tabWidth: +get("tabWidth"), + useTabs: !!get("useTabs"), + reuseWhitespace: !!get("reuseWhitespace"), + lineTerminator: get("lineTerminator"), + wrapColumn: Math.max(get("wrapColumn"), 0), + sourceFileName: get("sourceFileName"), + sourceMapName: get("sourceMapName"), + sourceRoot: get("sourceRoot"), + inputSourceMap: get("inputSourceMap"), + parser: get("esprima") || get("parser"), + range: get("range"), + tolerant: get("tolerant"), + quote: get("quote"), + trailingComma: get("trailingComma"), + arrayBracketSpacing: get("arrayBracketSpacing"), + objectCurlySpacing: get("objectCurlySpacing"), + arrowParensAlways: get("arrowParensAlways"), + flowObjectCommas: get("flowObjectCommas"), + }; +}; diff --git a/src/pp.js b/src/pp.js new file mode 100644 index 00000000..3fe2f45e --- /dev/null +++ b/src/pp.js @@ -0,0 +1,254 @@ + +function fromString(text) { + if(typeof text !== "string") { + return text.toString(); + } + return text; +} + +function concat(parts) { + return { type: 'concat', parts }; +} + +function indent(n, contents) { + return { type: 'indent', contents, n }; +} + +function group(contents) { + return { type: 'group', contents }; +} + +function multilineGroup(doc) { + const shouldBreak = hasHardLine(doc); + return { type: 'group', contents: doc, break: shouldBreak }; +} + +function iterDoc(topDoc, func) { + const docs = [topDoc]; + + while(docs.length !== 0) { + const doc = docs.pop(); + let res = undefined; + + if(typeof doc === "string") { + const res = func("string", doc); + if(res) { + return res; + } + } + else { + const res = func(doc.type, doc); + if(res) { + return res; + } + + if(doc.type === "concat") { + for(var i = doc.parts.length - 1; i >= 0; i--) { + docs.push(doc.parts[i]); + } + } + else if(doc.type !== "line") { + docs.push(doc.contents); + } + } + } +} + +const line = { type: 'line' }; +const softline = { type: 'line', soft: true }; +const hardline = { type: 'line', hard: true }; +const literalline = { type: 'line', hard: true, literal: true }; + +function indentedLine(n) { + return { type: 'line', indent: n }; +} + +function isEmpty(n) { + return typeof n === "string" && n.length === 0; +} + +function join(sep, arr) { + var res = []; + for(var i=0; i < arr.length; i++) { + if(i !== 0) { + res.push(sep); + } + res.push(arr[i]); + } + return concat(res); +} + +function getFirstString(doc) { + return iterDoc(doc, (type, doc) => { + if(type === "string" && doc.trim().length !== 0) { + return doc; + } + }); +} + +function hasHardLine(doc) { + // TODO: If we hit a group, check if it's already marked as a + // multiline group because they should be marked bottom-up. + return !!iterDoc(doc, (type, doc) => { + switch(type) { + case "line": + if(doc.hard) { + return true; + } + break; + } + }); +} + +function _makeIndent(n) { + var s = ""; + for(var i=0; i= 0) { + if(cmds.length === 0) { + if(restIdx === 0) { + return true; + } + else { + cmds.push(restCommands[restIdx - 1]); + restIdx--; + continue; + } + } + const [ind, mode, doc] = cmds.pop(); + + if(typeof doc === "string") { + width -= doc.length; + } + else { + switch(doc.type) { + case "concat": + for(var i = doc.parts.length - 1; i >= 0; i--) { + cmds.push([ind, mode, doc.parts[i]]); + } + break; + case "indent": + cmds.push([ind + doc.n, mode, doc.contents]); + break; + case "group": + cmds.push([ind, doc.break ? MODE_BREAK : mode, doc.contents]); + break; + case "line": + switch(mode) { + case MODE_FLAT: + if(!doc.hard) { + if(!doc.soft) { + width -= 1; + } + break; + } + // fallthrough + case MODE_BREAK: + return true; + } + break; + } + } + } + + return false; +} + +function print(w, doc) { + let pos = 0; + // cmds is basically a stack. We've turned a recursive call into a + // while loop which is much faster. The while loop below adds new + // cmds to the array instead of recursively calling `print`. + let cmds = [[0, MODE_BREAK, doc]]; + let out = []; + + while(cmds.length !== 0) { + const [ind, mode, doc] = cmds.pop(); + + if(typeof doc === "string") { + out.push(doc); + pos += doc.length; + } + else { + switch(doc.type) { + case "concat": + for(var i = doc.parts.length - 1; i >= 0; i--) { + cmds.push([ind, mode, doc.parts[i]]); + } + break; + case "indent": + cmds.push([ind + doc.n, mode, doc.contents]); + break; + case "group": + switch(mode) { + case MODE_FLAT: + cmds.push([ind, doc.break ? MODE_BREAK : MODE_FLAT, doc.contents]); + break; + case MODE_BREAK: + const next = [ind, MODE_FLAT, doc.contents]; + let rem = w - pos; + if(!doc.break && fits(next, cmds, rem)) { + cmds.push(next); + } + else { + cmds.push([ind, MODE_BREAK, doc.contents]); + } + break; + } + break; + case "line": + switch(mode) { + case MODE_FLAT: + if(!doc.hard) { + if(!doc.soft) { + out.push(" "); + pos += 1; + } + break; + } + else { + // We need to switch everything back into + // the breaking mode because this is + // forcing a newline and everything needs + // to be re-measured. + cmds.forEach(cmd => { + cmd[1] = MODE_BREAK; + }); + } + // fallthrough + case MODE_BREAK: + if(doc.literal) { + out.push("\n"); + pos = 0; + } + else { + out.push("\n" + _makeIndent(ind)); + pos = ind; + } + break; + } + break; + default: + } + } + }; + + return out.join(""); +} + +module.exports = { + fromString, concat, isEmpty, join, + line, softline, hardline, literalline, group, multilineGroup, + hasHardLine, indent, print, getFirstString +}; diff --git a/src/printer.js b/src/printer.js new file mode 100644 index 00000000..c8943f73 --- /dev/null +++ b/src/printer.js @@ -0,0 +1,1852 @@ +var assert = require("assert"); +var sourceMap = require("source-map"); +var printComments = require("./comments").printComments; +var pp = require("./pp"); +var fromString = pp.fromString; +var concat = pp.concat; +var isEmpty = pp.isEmpty; +var join = pp.join; +var line = pp.line; +var hardline = pp.hardline; +var softline = pp.softline; +var literalline = pp.literalline; +var group = pp.group; +var multilineGroup = pp.multilineGroup; +var indent = pp.indent; +var getFirstString = pp.getFirstString; +var hasHardLine = pp.hasHardLine; +var normalizeOptions = require("./options").normalize; +var types = require("ast-types"); +var namedTypes = types.namedTypes; +var isString = types.builtInTypes.string; +var isObject = types.builtInTypes.object; +var FastPath = require("./fast-path"); +var util = require("./util"); + +function PrintResult(code, sourceMap) { + assert.ok(this instanceof PrintResult); + + isString.assert(code); + this.code = code; + + if (sourceMap) { + isObject.assert(sourceMap); + this.map = sourceMap; + } +} + +var PRp = PrintResult.prototype; +var warnedAboutToString = false; + +PRp.toString = function() { + if (!warnedAboutToString) { + console.warn( + "Deprecation warning: recast.print now returns an object with " + + "a .code property. You appear to be treating the object as a " + + "string, which might still work but is strongly discouraged." + ); + + warnedAboutToString = true; + } + + return this.code; +}; + +var emptyPrintResult = new PrintResult(""); + +function Printer(originalOptions) { + assert.ok(this instanceof Printer); + + var explicitTabWidth = originalOptions && originalOptions.tabWidth; + var options = normalizeOptions(originalOptions); + assert.notStrictEqual(options, originalOptions); + + // It's common for client code to pass the same options into both + // recast.parse and recast.print, but the Printer doesn't need (and + // can be confused by) options.sourceFileName, so we null it out. + options.sourceFileName = null; + + function printWithComments(path) { + assert.ok(path instanceof FastPath); + return printComments(path, print); + } + + function print(path, includeComments) { + if (includeComments) + return printWithComments(path); + + assert.ok(path instanceof FastPath); + + if (!explicitTabWidth) { + var oldTabWidth = options.tabWidth; + var loc = path.getNode().loc; + if (loc && loc.lines && loc.lines.guessTabWidth) { + options.tabWidth = loc.lines.guessTabWidth(); + var lines = maybeReprint(path); + options.tabWidth = oldTabWidth; + return lines; + } + } + + return maybeReprint(path); + } + + function maybeReprint(path) { + // TODO: remove this function entirely as we don't ever keep the + // previous formatting + return printRootGenerically(path); + } + + // Print the root node generically, but then resume reprinting its + // children non-generically. + function printRootGenerically(path, includeComments) { + return includeComments + ? printComments(path, printRootGenerically) + : genericPrint(path, options, printWithComments); + } + + // Print the entire AST generically. + function printGenerically(path) { + // return genericPrint(path, options, printGenerically); + return printComments(path, p => genericPrint(p, options, printGenerically)); + } + + this.print = function(ast) { + if (!ast) { + return emptyPrintResult; + } + + var lines = print(FastPath.from(ast), true); + + return new PrintResult( + lines.toString(options), + util.composeSourceMaps( + options.inputSourceMap, + lines.getSourceMap( + options.sourceMapName, + options.sourceRoot + ) + ) + ); + }; + + this.printGenerically = function(ast) { + if (!ast) { + return emptyPrintResult; + } + + var path = FastPath.from(ast); + var oldReuseWhitespace = options.reuseWhitespace; + + // Do not reuse whitespace (or anything else, for that matter) + // when printing generically. + options.reuseWhitespace = false; + + var res = printGenerically(path); + + var pr = new PrintResult(pp.print(options.wrapColumn, res)); + options.reuseWhitespace = oldReuseWhitespace; + return pr; + }; +} + +exports.Printer = Printer; + +function maybeAddParens(path, lines) { + return path.needsParens() ? concat(["(", lines, ")"]) : lines; +} + +function genericPrint(path, options, printPath) { + assert.ok(path instanceof FastPath); + + var node = path.getValue(); + var parts = []; + var needsParens = false; + var linesWithoutParens = + genericPrintNoParens(path, options, printPath); + + if (! node || isEmpty(linesWithoutParens)) { + return linesWithoutParens; + } + + if (node.decorators && + node.decorators.length > 0 && + // If the parent node is an export declaration, it will be + // responsible for printing node.decorators. + ! util.getParentExportDeclaration(path)) { + + path.each(function(decoratorPath) { + parts.push(printPath(decoratorPath), line); + }, "decorators"); + + } else if (util.isExportDeclaration(node) && + node.declaration && + node.declaration.decorators) { + // Export declarations are responsible for printing any decorators + // that logically apply to node.declaration. + path.each(function(decoratorPath) { + parts.push(printPath(decoratorPath), line); + }, "declaration", "decorators"); + + } else { + // Nodes with decorators can't have parentheses, so we can avoid + // computing path.needsParens() except in this case. + needsParens = path.needsParens(); + } + + if (needsParens) { + parts.unshift("("); + } + + parts.push(linesWithoutParens); + + if (needsParens) { + parts.push(")"); + } + + return concat(parts); +} + +function genericPrintNoParens(path, options, print) { + var n = path.getValue(); + + if (!n) { + return fromString(""); + } + + if (typeof n === "string") { + return fromString(n, options); + } + + namedTypes.Printable.assert(n); + + var parts = []; + + switch (n.type) { + case "File": + return path.call(print, "program"); + + case "Program": + // Babel 6 + if (n.directives) { + path.each(function(childPath) { + parts.push(print(childPath), ";", line); + }, "directives"); + } + + parts.push(path.call(function(bodyPath) { + return printStatementSequence(bodyPath, options, print); + }, "body")); + + // Make sure the file always ends with a newline + parts.push(hardline); + + return concat(parts); + + case "Noop": // Babel extension. + case "EmptyStatement": + return fromString(""); + + case "ExpressionStatement": + return concat([path.call(print, "expression"), ";"]); + + case "ParenthesizedExpression": // Babel extension. + return concat(["(", path.call(print, "expression"), ")"]); + + case "AssignmentExpression": + return group(concat([ + path.call(print, "left"), + " ", + n.operator, + " ", + path.call(print, "right") + ])); + + case "BinaryExpression": + case "LogicalExpression": + return group(concat([ + path.call(print, "left"), + " ", + n.operator, + indent(options.tabWidth, concat([line, path.call(print, "right")])) + ])); + + case "AssignmentPattern": + return concat([ + path.call(print, "left"), + " = ", + path.call(print, "right") + ]); + + case "MemberExpression": + parts.push(path.call(print, "object")); + + var property = path.call(print, "property"); + if (n.computed) { + parts.push("[", property, "]"); + } else { + parts.push(".", property); + } + + return concat(parts); + + case "MetaProperty": + return concat([ + path.call(print, "meta"), + ".", + path.call(print, "property") + ]); + + case "BindExpression": + if (n.object) { + parts.push(path.call(print, "object")); + } + + parts.push("::", path.call(print, "callee")); + + return concat(parts); + + case "Path": + return fromString(".").join(n.body); + + case "Identifier": + return concat([ + fromString(n.name, options), + path.call(print, "typeAnnotation") + ]); + + case "SpreadElement": + case "SpreadElementPattern": + case "RestProperty": // Babel 6 for ObjectPattern + case "SpreadProperty": + case "SpreadPropertyPattern": + case "RestElement": + return concat(["...", path.call(print, "argument")]); + + case "FunctionDeclaration": + case "FunctionExpression": + if (n.async) + parts.push("async "); + + parts.push("function"); + + if (n.generator) + parts.push("*"); + + if (n.id) { + parts.push( + " ", + path.call(print, "id"), + path.call(print, "typeParameters") + ); + } + + parts.push( + group(concat([ + "(", + indent(options.tabWidth, + concat([ + softline, + printFunctionParams(path, options, print) + ])), + softline, + ")" + ])), + path.call(print, "returnType"), + " ", + path.call(print, "body") + ); + + return group(concat(parts)); + + case "ArrowFunctionExpression": + if (n.async) + parts.push("async "); + + if (n.typeParameters) { + parts.push(path.call(print, "typeParameters")); + } + + if ( + !options.arrowParensAlways && + n.params.length === 1 && + !n.rest && + n.params[0].type === 'Identifier' && + !n.params[0].typeAnnotation && + !n.returnType + ) { + parts.push(path.call(print, "params", 0)); + } else { + parts.push( + "(", + printFunctionParams(path, options, print), + ")", + path.call(print, "returnType") + ); + } + + parts.push(" => ", path.call(print, "body")); + + return concat(parts); + + case "MethodDefinition": + if (n.static) { + parts.push("static "); + } + + parts.push(printMethod(path, options, print)); + + return concat(parts); + + case "YieldExpression": + parts.push("yield"); + + if (n.delegate) + parts.push("*"); + + if (n.argument) + parts.push(" ", path.call(print, "argument")); + + return concat(parts); + + case "AwaitExpression": + parts.push("await"); + + if (n.all) + parts.push("*"); + + if (n.argument) + parts.push(" ", path.call(print, "argument")); + + return concat(parts); + + case "ModuleDeclaration": + parts.push("module", path.call(print, "id")); + + if (n.source) { + assert.ok(!n.body); + parts.push("from", path.call(print, "source")); + } else { + parts.push(path.call(print, "body")); + } + + return fromString(" ").join(parts); + + case "ImportSpecifier": + if (n.imported) { + parts.push(path.call(print, "imported")); + if (n.local && + n.local.name !== n.imported.name) { + parts.push(" as ", path.call(print, "local")); + } + } else if (n.id) { + parts.push(path.call(print, "id")); + if (n.name) { + parts.push(" as ", path.call(print, "name")); + } + } + + return concat(parts); + + case "ExportSpecifier": + if (n.local) { + parts.push(path.call(print, "local")); + if (n.exported && + n.exported.name !== n.local.name) { + parts.push(" as ", path.call(print, "exported")); + } + } else if (n.id) { + parts.push(path.call(print, "id")); + if (n.name) { + parts.push(" as ", path.call(print, "name")); + } + } + + return concat(parts); + + case "ExportBatchSpecifier": + return fromString("*"); + + case "ImportNamespaceSpecifier": + parts.push("* as "); + if (n.local) { + parts.push(path.call(print, "local")); + } else if (n.id) { + parts.push(path.call(print, "id")); + } + return concat(parts); + + case "ImportDefaultSpecifier": + if (n.local) { + return path.call(print, "local"); + } + return path.call(print, "id"); + + case "ExportDeclaration": + case "ExportDefaultDeclaration": + case "ExportNamedDeclaration": + return printExportDeclaration(path, options, print); + + case "ExportAllDeclaration": + parts.push("export *"); + + if (n.exported) { + parts.push(" as ", path.call(print, "exported")); + } + + parts.push( + " from ", + path.call(print, "source") + ); + + return concat(parts); + + case "ExportNamespaceSpecifier": + return concat(["* as ", path.call(print, "exported")]); + + case "ExportDefaultSpecifier": + return path.call(print, "exported"); + + case "ImportDeclaration": + parts.push("import "); + + if (n.importKind && n.importKind !== "value") { + parts.push(n.importKind + " "); + } + + if (n.specifiers && + n.specifiers.length > 0) { + + var foundImportSpecifier = false; + + path.each(function(specifierPath) { + var i = specifierPath.getName(); + if (i > 0) { + parts.push(", "); + } + + var value = specifierPath.getValue(); + + if (namedTypes.ImportDefaultSpecifier.check(value) || + namedTypes.ImportNamespaceSpecifier.check(value)) { + assert.strictEqual(foundImportSpecifier, false); + } else { + namedTypes.ImportSpecifier.assert(value); + if (!foundImportSpecifier) { + foundImportSpecifier = true; + parts.push( + options.objectCurlySpacing ? "{ " : "{" + ); + } + } + + parts.push(print(specifierPath)); + }, "specifiers"); + + if (foundImportSpecifier) { + parts.push( + options.objectCurlySpacing ? " }" : "}" + ); + } + + parts.push(" from "); + } + + parts.push(path.call(print, "source"), ";"); + + return concat(parts); + + case "BlockStatement": + var naked = path.call(function(bodyPath) { + return printStatementSequence(bodyPath, options, print); + }, "body"); + + parts.push("{"); + // Babel 6 + if (n.directives) { + path.each(function(childPath) { + parts.push( + indent(options.tabWidth, concat([ + line, + print(childPath), + ";", + line + ])) + ); + }, "directives"); + } + parts.push(indent(options.tabWidth, concat([hardline, naked]))); + parts.push(hardline, "}"); + + return concat(parts); + + case "ReturnStatement": + parts.push("return"); + var arg = path.call(print, "argument"); + + if (n.argument) { + if (namedTypes.JSXElement && + namedTypes.JSXElement.check(n.argument) && + hasHardLine(arg)) { + parts.push( + " (", + indent(options.tabWidth, concat([hardline, arg])), + hardline, + ")" + ); + } else { + parts.push(" ", arg); + } + } + + parts.push(";"); + + return concat(parts); + + case "CallExpression": + return concat([ + path.call(print, "callee"), + printArgumentsList(path, options, print) + ]); + + case "ObjectExpression": + case "ObjectPattern": + case "ObjectTypeAnnotation": + var allowBreak = false; + var isTypeAnnotation = n.type === "ObjectTypeAnnotation"; + var separator = options.flowObjectCommas ? "," : (isTypeAnnotation ? ";" : ","); + var fields = []; + var leftBrace = n.exact ? "{|" : "{"; + var rightBrace = n.exact ? "|}" : "}"; + + if (isTypeAnnotation) { + fields.push("indexers", "callProperties"); + } + + fields.push("properties"); + + var i = 0; + var props = []; + fields.forEach(function(field) { + path.each(function(childPath) { + props.push(group(print(childPath))); + }, field); + }); + + if(props.length === 0) { + return "{}"; + } + else { + return multilineGroup(concat([ + leftBrace, + indent(options.tabWidth, + concat([ + line, + join(concat([",", line]), props) + ])), + line, + rightBrace + ])); + } + + case "PropertyPattern": + return concat([ + path.call(print, "key"), + ": ", + path.call(print, "pattern") + ]); + + case "ObjectProperty": // Babel 6 + case "Property": // Non-standard AST node type. + if (n.method || n.kind === "get" || n.kind === "set") { + return printMethod(path, options, print); + } + + var key = path.call(print, "key"); + if (n.computed) { + parts.push("[", key, "]"); + } else { + parts.push(key); + } + + if (! n.shorthand) { + parts.push(": ", path.call(print, "value")); + } + + return concat(parts); + + case "ClassMethod": // Babel 6 + if (n.static) { + parts.push("static "); + } + + return concat([parts, printObjectMethod(path, options, print)]); + + case "ObjectMethod": // Babel 6 + return printObjectMethod(path, options, print); + + case "Decorator": + return concat(["@", path.call(print, "expression")]); + + case "ArrayExpression": + case "ArrayPattern": + return multilineGroup(concat([ + "[", + indent(options.tabWidth, + concat([ + line, + join(concat([",", line]), + path.map(print, "elements")) + ])), + line, + "]" + ])); + + case "SequenceExpression": + return fromString(", ").join(path.map(print, "expressions")); + + case "ThisExpression": + return fromString("this"); + + case "Super": + return fromString("super"); + + case "NullLiteral": // Babel 6 Literal split + return fromString("null"); + + case "RegExpLiteral": // Babel 6 Literal split + return fromString(n.extra.raw); + + case "BooleanLiteral": // Babel 6 Literal split + case "NumericLiteral": // Babel 6 Literal split + case "StringLiteral": // Babel 6 Literal split + case "Literal": + if (typeof n.value !== "string") + return fromString(n.value, options); + + return fromString(nodeStr(n.value, options), options); + + case "Directive": // Babel 6 + return path.call(print, "value"); + + case "DirectiveLiteral": // Babel 6 + return fromString(nodeStr(n.value, options)); + + case "ModuleSpecifier": + if (n.local) { + throw new Error( + "The ESTree ModuleSpecifier type should be abstract" + ); + } + + // The Esprima ModuleSpecifier type is just a string-valued + // Literal identifying the imported-from module. + return fromString(nodeStr(n.value, options), options); + + case "UnaryExpression": + parts.push(n.operator); + if (/[a-z]$/.test(n.operator)) + parts.push(" "); + parts.push(path.call(print, "argument")); + return concat(parts); + + case "UpdateExpression": + parts.push( + path.call(print, "argument"), + n.operator + ); + + if (n.prefix) + parts.reverse(); + + return concat(parts); + + case "ConditionalExpression": + return concat([ + "(", path.call(print, "test"), + " ? ", path.call(print, "consequent"), + " : ", path.call(print, "alternate"), ")" + ]); + + case "NewExpression": + parts.push("new ", path.call(print, "callee")); + var args = n.arguments; + if (args) { + parts.push(printArgumentsList(path, options, print)); + } + + return concat(parts); + + case "VariableDeclaration": + var printed = path.map(function(childPath) { + return print(childPath); + }, "declarations"); + + parts = [ + n.kind, + " ", + printed[0], + indent(options.tabWidth, + join(concat(",", line), + printed.slice(1))) + ]; + + // We generally want to terminate all variable declarations with a + // semicolon, except when they are children of for loops. + var parentNode = path.getParentNode(); + if (!namedTypes.ForStatement.check(parentNode) && + !namedTypes.ForInStatement.check(parentNode) && + !(namedTypes.ForOfStatement && + namedTypes.ForOfStatement.check(parentNode)) && + !(namedTypes.ForAwaitStatement && + namedTypes.ForAwaitStatement.check(parentNode))) { + parts.push(";"); + } + + return concat(parts); + + case "VariableDeclarator": + return n.init ? concat([ + path.call(print, "id"), + " = ", + path.call(print, "init") + ]) : path.call(print, "id"); + + case "WithStatement": + return concat([ + "with (", + path.call(print, "object"), + ") ", + path.call(print, "body") + ]); + + case "IfStatement": + var con = adjustClause(path.call(print, "consequent"), options); + var parts = [ + "if (", + group(concat([ + indent(options.tabWidth, concat([ + softline, + path.call(print, "test"), + ])), + softline + ])), + ")", + con, + ]; + + if(n.alternate) { + const hasBraces = getFirstString(con) === "{"; + parts.push( + hasBraces ? " else" : "\nelse", + adjustClause(path.call(print, "alternate"), options) + ); + } + + return concat(parts); + + // var con = adjustClause(path.call(print, "consequent"), options), + // parts = ["if (", path.call(print, "test"), ")", con]; + + // if (n.alternate) + // parts.push( + // endsWithBrace(con) ? " else" : "\nelse", + // adjustClause(path.call(print, "alternate"), options)); + + // return concat(parts); + + case "ForStatement": + // TODO Get the for (;;) case right. + + return concat([ + "for (", + group(concat([ + indent(options.tabWidth, concat([ + softline, + path.call(print, "init"), + ";", + line, + path.call(print, "test"), + ";", + line, + path.call(print, "update") + ])), + softline + ])), + ")", + adjustClause(path.call(print, "body"), options) + ]); + + case "WhileStatement": + return concat([ + "while (", + path.call(print, "test"), + ")", + adjustClause(path.call(print, "body"), options) + ]); + + case "ForInStatement": + // Note: esprima can't actually parse "for each (". + return concat([ + n.each ? "for each (" : "for (", + path.call(print, "left"), + " in ", + path.call(print, "right"), + ")", + adjustClause(path.call(print, "body"), options) + ]); + + case "ForOfStatement": + return concat([ + "for (", + path.call(print, "left"), + " of ", + path.call(print, "right"), + ")", + adjustClause(path.call(print, "body"), options) + ]); + + case "ForAwaitStatement": + return concat([ + "for await (", + path.call(print, "left"), + " of ", + path.call(print, "right"), + ")", + adjustClause(path.call(print, "body"), options) + ]); + + case "DoWhileStatement": + var doBody = concat([ + "do", + adjustClause(path.call(print, "body"), options) + ]), parts = [doBody]; + + if (endsWithBrace(doBody)) + parts.push(" while"); + else + parts.push("\nwhile"); + + parts.push(" (", path.call(print, "test"), ");"); + + return concat(parts); + + case "DoExpression": + var statements = path.call(function(bodyPath) { + return printStatementSequence(bodyPath, options, print); + }, "body"); + + return concat([ + "do {\n", + statements.indent(options.tabWidth), + "\n}" + ]); + + case "BreakStatement": + parts.push("break"); + if (n.label) + parts.push(" ", path.call(print, "label")); + parts.push(";"); + return concat(parts); + + case "ContinueStatement": + parts.push("continue"); + if (n.label) + parts.push(" ", path.call(print, "label")); + parts.push(";"); + return concat(parts); + + case "LabeledStatement": + return concat([ + path.call(print, "label"), + ":\n", + path.call(print, "body") + ]); + + case "TryStatement": + parts.push( + "try ", + path.call(print, "block") + ); + + if (n.handler) { + parts.push(" ", path.call(print, "handler")); + } else if (n.handlers) { + path.each(function(handlerPath) { + parts.push(" ", print(handlerPath)); + }, "handlers"); + } + + if (n.finalizer) { + parts.push(" finally ", path.call(print, "finalizer")); + } + + return concat(parts); + + case "CatchClause": + parts.push("catch (", path.call(print, "param")); + + if (n.guard) + // Note: esprima does not recognize conditional catch clauses. + parts.push(" if ", path.call(print, "guard")); + + parts.push(") ", path.call(print, "body")); + + return concat(parts); + + case "ThrowStatement": + return concat(["throw ", path.call(print, "argument"), ";"]); + + case "SwitchStatement": + return concat([ + "switch (", + path.call(print, "discriminant"), + ") {\n", + fromString("\n").join(path.map(print, "cases")), + "\n}" + ]); + + // Note: ignoring n.lexical because it has no printing consequences. + + case "SwitchCase": + if (n.test) + parts.push("case ", path.call(print, "test"), ":"); + else + parts.push("default:"); + + if (n.consequent.length > 0) { + parts.push("\n", path.call(function(consequentPath) { + return printStatementSequence(consequentPath, options, print); + }, "consequent").indent(options.tabWidth)); + } + + return concat(parts); + + case "DebuggerStatement": + return fromString("debugger;"); + + // JSX extensions below. + + case "JSXAttribute": + parts.push(path.call(print, "name")); + if (n.value) + parts.push("=", path.call(print, "value")); + return concat(parts); + + case "JSXIdentifier": + return fromString(n.name, options); + + case "JSXNamespacedName": + return fromString(":").join([ + path.call(print, "namespace"), + path.call(print, "name") + ]); + + case "JSXMemberExpression": + return fromString(".").join([ + path.call(print, "object"), + path.call(print, "property") + ]); + + case "JSXSpreadAttribute": + return concat(["{...", path.call(print, "argument"), "}"]); + + case "JSXExpressionContainer": + return concat(["{", path.call(print, "expression"), "}"]); + + case "JSXElement": + var openingLines = path.call(print, "openingElement"); + + if (n.openingElement.selfClosing) { + assert.ok(!n.closingElement); + return openingLines; + } + + var children = path.map(function(childPath) { + var child = childPath.getValue(); + + if (namedTypes.Literal.check(child) && + typeof child.value === "string") { + if (/\S/.test(child.value)) { + return child.value.replace(/^\s+|\s+$/g, "").replace(/\n/, hardline); + } else if (/\n/.test(child.value)) { + return hardline; + } + } + + return print(childPath); + }, "children"); + + var mostChildren = children.slice(0, -1); + var lastChild = children[children.length - 1]; + var closingLines = path.call(print, "closingElement"); + + return concat([ + openingLines, + indent(options.tabWidth, concat(mostChildren)), + lastChild, + closingLines + ]); + + case "JSXOpeningElement": + return group(concat([ + "<", + path.call(print, "name"), + concat(path.map(attr => concat([" ", print(attr)]), "attributes")), + n.selfClosing ? "/>" : ">" + ])); + + case "JSXClosingElement": + return concat([""]); + + case "JSXText": + return fromString(n.value, options); + + case "JSXEmptyExpression": + return fromString(""); + + case "TypeAnnotatedIdentifier": + return concat([ + path.call(print, "annotation"), + " ", + path.call(print, "identifier") + ]); + + case "ClassBody": + if (n.body.length === 0) { + return fromString("{}"); + } + + return concat([ + "{", + indent( + options.tabWidth, + concat([ + hardline, + path.call(function(bodyPath) { + return printStatementSequence(bodyPath, options, print); + }, "body") + ]) + ), + hardline, + "}" + ]); + + case "ClassPropertyDefinition": + parts.push("static ", path.call(print, "definition")); + if (!namedTypes.MethodDefinition.check(n.definition)) + parts.push(";"); + return concat(parts); + + case "ClassProperty": + if (n.static) + parts.push("static "); + + var key = path.call(print, "key"); + if (n.computed) { + key = concat(["[", key, "]"]); + } else if (n.variance === "plus") { + key = concat(["+", key]); + } else if (n.variance === "minus") { + key = concat(["-", key]); + } + parts.push(key); + + if (n.typeAnnotation) + parts.push(path.call(print, "typeAnnotation")); + + if (n.value) + parts.push(" = ", path.call(print, "value")); + + parts.push(";"); + return concat(parts); + + case "ClassDeclaration": + case "ClassExpression": + parts.push("class"); + + if (n.id) { + parts.push( + " ", + path.call(print, "id"), + path.call(print, "typeParameters") + ); + } + + if (n.superClass) { + parts.push( + " extends ", + path.call(print, "superClass"), + path.call(print, "superTypeParameters") + ); + } + + if (n["implements"] && n['implements'].length > 0) { + parts.push( + " implements ", + fromString(", ").join(path.map(print, "implements")) + ); + } + + parts.push(" ", path.call(print, "body")); + + return concat(parts); + + case "TemplateElement": + return join(literalline, n.value.raw.split("\n")); + + case "TemplateLiteral": + var expressions = path.map(print, "expressions"); + parts.push("`"); + + path.each(function(childPath) { + var i = childPath.getName(); + parts.push(print(childPath)); + if (i < expressions.length) { + parts.push("${", expressions[i], "}"); + } + }, "quasis"); + + parts.push("`"); + + return concat(parts); + + case "TaggedTemplateExpression": + return concat([ + path.call(print, "tag"), + path.call(print, "quasi") + ]); + + // These types are unprintable because they serve as abstract + // supertypes for other (printable) types. + case "Node": + case "Printable": + case "SourceLocation": + case "Position": + case "Statement": + case "Function": + case "Pattern": + case "Expression": + case "Declaration": + case "Specifier": + case "NamedSpecifier": + case "Comment": // Supertype of Block and Line. + case "MemberTypeAnnotation": // Flow + case "TupleTypeAnnotation": // Flow + case "Type": // Flow + throw new Error("unprintable type: " + JSON.stringify(n.type)); + + case "CommentBlock": // Babel block comment. + case "Block": // Esprima block comment. + return concat(["/*", fromString(n.value, options), "*/"]); + + case "CommentLine": // Babel line comment. + case "Line": // Esprima line comment. + return concat(["//", fromString(n.value, options)]); + + // Type Annotations for Facebook Flow, typically stripped out or + // transformed away before printing. + case "TypeAnnotation": + if (n.typeAnnotation) { + if (n.typeAnnotation.type !== "FunctionTypeAnnotation") { + parts.push(": "); + } + parts.push(path.call(print, "typeAnnotation")); + return concat(parts); + } + + return fromString(""); + + case "ExistentialTypeParam": + case "ExistsTypeAnnotation": + return fromString("*", options); + + case "EmptyTypeAnnotation": + return fromString("empty", options); + + case "AnyTypeAnnotation": + return fromString("any", options); + + case "MixedTypeAnnotation": + return fromString("mixed", options); + + case "ArrayTypeAnnotation": + return concat([ + path.call(print, "elementType"), + "[]" + ]); + + case "BooleanTypeAnnotation": + return fromString("boolean", options); + + case "BooleanLiteralTypeAnnotation": + assert.strictEqual(typeof n.value, "boolean"); + return fromString("" + n.value, options); + + case "DeclareClass": + return printFlowDeclaration(path, [ + "class ", + path.call(print, "id"), + " ", + path.call(print, "body"), + ]); + + case "DeclareFunction": + return printFlowDeclaration(path, [ + "function ", + path.call(print, "id"), + ";" + ]); + + case "DeclareModule": + return printFlowDeclaration(path, [ + "module ", + path.call(print, "id"), + " ", + path.call(print, "body"), + ]); + + case "DeclareModuleExports": + return printFlowDeclaration(path, [ + "module.exports", + path.call(print, "typeAnnotation"), + ]); + + case "DeclareVariable": + return printFlowDeclaration(path, [ + "var ", + path.call(print, "id"), + ";" + ]); + + case "DeclareExportDeclaration": + return concat([ + "declare ", + printExportDeclaration(path, options, print) + ]); + + case "FunctionTypeAnnotation": + // FunctionTypeAnnotation is ambiguous: + // declare function(a: B): void; OR + // var A: (a: B) => void; + var parent = path.getParentNode(0); + var isArrowFunctionTypeAnnotation = !( + namedTypes.ObjectTypeCallProperty.check(parent) || + namedTypes.DeclareFunction.check(path.getParentNode(2)) + ); + + var needsColon = + isArrowFunctionTypeAnnotation && + !namedTypes.FunctionTypeParam.check(parent); + + if (needsColon) { + parts.push(": "); + } + + parts.push( + "(", + fromString(", ").join(path.map(print, "params")), + ")" + ); + + // The returnType is not wrapped in a TypeAnnotation, so the colon + // needs to be added separately. + if (n.returnType) { + parts.push( + isArrowFunctionTypeAnnotation ? " => " : ": ", + path.call(print, "returnType") + ); + } + + return concat(parts); + + case "FunctionTypeParam": + return concat([ + path.call(print, "name"), + n.optional ? '?' : '', + ": ", + path.call(print, "typeAnnotation"), + ]); + + case "GenericTypeAnnotation": + return concat([ + path.call(print, "id"), + path.call(print, "typeParameters") + ]); + + case "DeclareInterface": + parts.push("declare "); + + case "InterfaceDeclaration": + parts.push( + fromString("interface ", options), + path.call(print, "id"), + path.call(print, "typeParameters"), + " " + ); + + if (n["extends"]) { + parts.push( + "extends ", + fromString(", ").join(path.map(print, "extends")) + ); + } + + parts.push(" ", path.call(print, "body")); + + return concat(parts); + + case "ClassImplements": + case "InterfaceExtends": + return concat([ + path.call(print, "id"), + path.call(print, "typeParameters") + ]); + + case "IntersectionTypeAnnotation": + return fromString(" & ").join(path.map(print, "types")); + + case "NullableTypeAnnotation": + return concat([ + "?", + path.call(print, "typeAnnotation") + ]); + + case "NullLiteralTypeAnnotation": + return fromString("null", options); + + case "ThisTypeAnnotation": + return fromString("this", options); + + case "NumberTypeAnnotation": + return fromString("number", options); + + case "ObjectTypeCallProperty": + return path.call(print, "value"); + + case "ObjectTypeIndexer": + var variance = + n.variance === "plus" ? "+" : + n.variance === "minus" ? "-" : ""; + + return concat([ + variance, + "[", + path.call(print, "id"), + ": ", + path.call(print, "key"), + "]: ", + path.call(print, "value") + ]); + + case "ObjectTypeProperty": + var variance = + n.variance === "plus" ? "+" : + n.variance === "minus" ? "-" : ""; + + return concat([ + variance, + path.call(print, "key"), + n.optional ? "?" : "", + ": ", + path.call(print, "value") + ]); + + case "QualifiedTypeIdentifier": + return concat([ + path.call(print, "qualification"), + ".", + path.call(print, "id") + ]); + + case "StringLiteralTypeAnnotation": + return fromString(nodeStr(n.value, options), options); + + case "NumberLiteralTypeAnnotation": + assert.strictEqual(typeof n.value, "number"); + return fromString("" + n.value, options); + + case "StringTypeAnnotation": + return fromString("string", options); + + case "DeclareTypeAlias": + parts.push("declare "); + + case "TypeAlias": + return concat([ + "type ", + path.call(print, "id"), + path.call(print, "typeParameters"), + " = ", + path.call(print, "right"), + ";" + ]); + + case "TypeCastExpression": + return concat([ + "(", + path.call(print, "expression"), + path.call(print, "typeAnnotation"), + ")" + ]); + + case "TypeParameterDeclaration": + case "TypeParameterInstantiation": + return concat([ + "<", + fromString(", ").join(path.map(print, "params")), + ">" + ]); + case "TypeParameter": + switch (n.variance) { + case 'plus': + parts.push('+'); + break; + case 'minus': + parts.push('-'); + break; + default: + } + + parts.push(path.call(print, 'name')); + + if (n.bound) { + parts.push(path.call(print, 'bound')); + } + + if (n['default']) { + parts.push('=', path.call(print, 'default')); + } + + return concat(parts); + + case "TypeofTypeAnnotation": + return concat([ + fromString("typeof ", options), + path.call(print, "argument") + ]); + + case "UnionTypeAnnotation": + return fromString(" | ").join(path.map(print, "types")); + + case "VoidTypeAnnotation": + return fromString("void", options); + + case "NullTypeAnnotation": + return fromString("null", options); + + // Unhandled types below. If encountered, nodes of these types should + // be either left alone or desugared into AST types that are fully + // supported by the pretty-printer. + case "ClassHeritage": // TODO + case "ComprehensionBlock": // TODO + case "ComprehensionExpression": // TODO + case "Glob": // TODO + case "GeneratorExpression": // TODO + case "LetStatement": // TODO + case "LetExpression": // TODO + case "GraphExpression": // TODO + case "GraphIndexExpression": // TODO + + // XML types that nobody cares about or needs to print. + case "XMLDefaultDeclaration": + case "XMLAnyName": + case "XMLQualifiedIdentifier": + case "XMLFunctionQualifiedIdentifier": + case "XMLAttributeSelector": + case "XMLFilterExpression": + case "XML": + case "XMLElement": + case "XMLList": + case "XMLEscape": + case "XMLText": + case "XMLStartTag": + case "XMLEndTag": + case "XMLPointTag": + case "XMLName": + case "XMLAttribute": + case "XMLCdata": + case "XMLComment": + case "XMLProcessingInstruction": + default: + debugger; + throw new Error("unknown type: " + JSON.stringify(n.type)); + } + + return p; +} + +function printStatementSequence(path, options, print) { + var inClassBody = + namedTypes.ClassBody && + namedTypes.ClassBody.check(path.getParentNode()); + + var printed = []; + + path.map(function(stmtPath) { + var stmt = stmtPath.getValue(); + + // Just in case the AST has been modified to contain falsy + // "statements," it's safer simply to skip them. + if (!stmt) { + return; + } + + // Skip printing EmptyStatement nodes to avoid leaving stray + // semicolons lying around. + if (stmt.type === "EmptyStatement") { + return; + } + + printed.push(print(stmtPath)); + }); + + return join(hardline, printed); +} + +function maxSpace(s1, s2) { + if (!s1 && !s2) { + return fromString(""); + } + + if (!s1) { + return fromString(s2); + } + + if (!s2) { + return fromString(s1); + } + + var spaceLines1 = fromString(s1); + var spaceLines2 = fromString(s2); + + if (spaceLines2.length > spaceLines1.length) { + return spaceLines2; + } + + return spaceLines1; +} + +function printMethod(path, options, print) { + var node = path.getNode(); + var kind = node.kind; + var parts = []; + + if (node.type === "ObjectMethod" || node.type === "ClassMethod") { + node.value = node; + } else { + namedTypes.FunctionExpression.assert(node.value); + } + + if (node.value.async) { + parts.push("async "); + } + + if (!kind || kind === "init" || kind === "method" || kind === "constructor") { + if (node.value.generator) { + parts.push("*"); + } + } else { + assert.ok(kind === "get" || kind === "set"); + parts.push(kind, " "); + } + + var key = path.call(print, "key"); + if (node.computed) { + key = concat(["[", key, "]"]); + } + + parts.push( + key, + path.call(print, "value", "typeParameters"), + "(", + path.call(function(valuePath) { + return printFunctionParams(valuePath, options, print); + }, "value"), + ")", + path.call(print, "value", "returnType"), + " ", + path.call(print, "value", "body") + ); + + return concat(parts); +} + +function printArgumentsList(path, options, print) { + var printed = path.map(print, "arguments"); + var trailingComma = util.isTrailingCommaEnabled(options, "parameters"); + var args; + + if(printed.length === 0) { + args = ""; + } + else if(printed.length === 1 && getFirstString(printed[0]) === "{") { + // If the only argument is an object, don't force it to be on + // newline and keep the braces on the same line as the parens + args = printed[0]; + } + else { + args = concat([ + indent( + options.tabWidth, + concat([ + softline, + join(concat([",", line]), printed), + ]) + ), + softline + ]); + } + + return multilineGroup(concat(["(", args, ")"])); +} + +function printFunctionParams(path, options, print) { + var fun = path.getValue(); + + namedTypes.Function.assert(fun); + + var printed = path.map(print, "params"); + + if (fun.defaults) { + path.each(function(defExprPath) { + var i = defExprPath.getName(); + var p = printed[i]; + if (p && defExprPath.getValue()) { + printed[i] = concat([p, " = ", print(defExprPath)]); + } + }, "defaults"); + } + + if (fun.rest) { + printed.push(concat(["...", path.call(print, "rest")])); + } + + return group(join(concat([",", line]), printed)); +} + +function printObjectMethod(path, options, print) { + var objMethod = path.getValue(); + var parts = []; + + if (objMethod.async) + parts.push("async "); + + if (objMethod.generator) + parts.push("*"); + + if (objMethod.method || objMethod.kind === "get" || objMethod.kind === "set") { + return printMethod(path, options, print); + } + + var key = path.call(print, "key"); + if (objMethod.computed) { + parts.push("[", key, "]"); + } else { + parts.push(key); + } + + parts.push( + "(", + printFunctionParams(path, options, print), + ")", + path.call(print, "returnType"), + " ", + path.call(print, "body") + ); + + return concat(parts); +} + +function printExportDeclaration(path, options, print) { + var decl = path.getValue(); + var parts = ["export "]; + var shouldPrintSpaces = options.objectCurlySpacing; + + namedTypes.Declaration.assert(decl); + + if (decl["default"] || + decl.type === "ExportDefaultDeclaration") { + parts.push("default "); + } + + if (decl.declaration) { + parts.push(path.call(print, "declaration")); + + } else if (decl.specifiers && + decl.specifiers.length > 0) { + + if (decl.specifiers.length === 1 && + decl.specifiers[0].type === "ExportBatchSpecifier") { + parts.push("*"); + } else { + parts.push( + shouldPrintSpaces ? "{ " : "{", + fromString(", ").join(path.map(print, "specifiers")), + shouldPrintSpaces ? " }" : "}" + ); + } + + if (decl.source) { + parts.push(" from ", path.call(print, "source")); + } + } + + return concat(parts); +} + +function printFlowDeclaration(path, parts) { + var parentExportDecl = util.getParentExportDeclaration(path); + + if (parentExportDecl) { + assert.strictEqual( + parentExportDecl.type, + "DeclareExportDeclaration" + ); + } else { + // If the parent node has type DeclareExportDeclaration, then it + // will be responsible for printing the "declare" token. Otherwise + // it needs to be printed with this non-exported declaration node. + parts.unshift("declare "); + } + + return concat(parts); +} + +function adjustClause(clause, options) { + if(getFirstString(clause) === "{") { + return concat([" ", clause]); + } + + return indent(options.tabWidth, concat([hardline, clause])); +} + +function lastNonSpaceCharacter(lines) { + var pos = lines.lastPos(); + do { + var ch = lines.charAt(pos); + if (/\S/.test(ch)) + return ch; + } while (lines.prevPos(pos)); +} + +function swapQuotes(str) { + return str.replace(/['"]/g, function(m) { + return m === '"' ? '\'' : '"'; + }); +} + +function nodeStr(str, options) { + isString.assert(str); + switch (options.quote) { + case "auto": + var double = JSON.stringify(str); + var single = swapQuotes(JSON.stringify(swapQuotes(str))); + return double.length > single.length ? single : double; + case "single": + return swapQuotes(JSON.stringify(swapQuotes(str))); + case "double": + default: + return JSON.stringify(str); + } +} diff --git a/src/util.js b/src/util.js new file mode 100644 index 00000000..125fd3f6 --- /dev/null +++ b/src/util.js @@ -0,0 +1,306 @@ +var assert = require("assert"); +var types = require("ast-types"); +var getFieldValue = types.getFieldValue; +var n = types.namedTypes; +var sourceMap = require("source-map"); +var SourceMapConsumer = sourceMap.SourceMapConsumer; +var SourceMapGenerator = sourceMap.SourceMapGenerator; +var hasOwn = Object.prototype.hasOwnProperty; +var util = exports; + +function getUnionOfKeys() { + var result = {}; + var argc = arguments.length; + for (var i = 0; i < argc; ++i) { + var keys = Object.keys(arguments[i]); + var keyCount = keys.length; + for (var j = 0; j < keyCount; ++j) { + result[keys[j]] = true; + } + } + return result; +} +util.getUnionOfKeys = getUnionOfKeys; + +function comparePos(pos1, pos2) { + return (pos1.line - pos2.line) || (pos1.column - pos2.column); +} +util.comparePos = comparePos; + +function copyPos(pos) { + return { + line: pos.line, + column: pos.column + }; +} +util.copyPos = copyPos; + +util.composeSourceMaps = function(formerMap, latterMap) { + if (formerMap) { + if (!latterMap) { + return formerMap; + } + } else { + return latterMap || null; + } + + var smcFormer = new SourceMapConsumer(formerMap); + var smcLatter = new SourceMapConsumer(latterMap); + var smg = new SourceMapGenerator({ + file: latterMap.file, + sourceRoot: latterMap.sourceRoot + }); + + var sourcesToContents = {}; + + smcLatter.eachMapping(function(mapping) { + var origPos = smcFormer.originalPositionFor({ + line: mapping.originalLine, + column: mapping.originalColumn + }); + + var sourceName = origPos.source; + if (sourceName === null) { + return; + } + + smg.addMapping({ + source: sourceName, + original: copyPos(origPos), + generated: { + line: mapping.generatedLine, + column: mapping.generatedColumn + }, + name: mapping.name + }); + + var sourceContent = smcFormer.sourceContentFor(sourceName); + if (sourceContent && !hasOwn.call(sourcesToContents, sourceName)) { + sourcesToContents[sourceName] = sourceContent; + smg.setSourceContent(sourceName, sourceContent); + } + }); + + return smg.toJSON(); +}; + +util.getTrueLoc = function(node, lines) { + // It's possible that node is newly-created (not parsed by Esprima), + // in which case it probably won't have a .loc property (or an + // .original property for that matter). That's fine; we'll just + // pretty-print it as usual. + if (!node.loc) { + return null; + } + + var result = { + start: node.loc.start, + end: node.loc.end + }; + + function include(node) { + expandLoc(result, node.loc); + } + + // If the node has any comments, their locations might contribute to + // the true start/end positions of the node. + if (node.comments) { + node.comments.forEach(include); + } + + // If the node is an export declaration and its .declaration has any + // decorators, their locations might contribute to the true start/end + // positions of the export declaration node. + if (node.declaration && util.isExportDeclaration(node) && + node.declaration.decorators) { + node.declaration.decorators.forEach(include); + } + + if (comparePos(result.start, result.end) < 0) { + // Trim leading whitespace. + result.start = copyPos(result.start); + lines.skipSpaces(result.start, false, true); + + if (comparePos(result.start, result.end) < 0) { + // Trim trailing whitespace, if the end location is not already the + // same as the start location. + result.end = copyPos(result.end); + lines.skipSpaces(result.end, true, true); + } + } + + return result; +}; + +function expandLoc(parentLoc, childLoc) { + if (parentLoc && childLoc) { + if (comparePos(childLoc.start, parentLoc.start) < 0) { + parentLoc.start = childLoc.start; + } + + if (comparePos(parentLoc.end, childLoc.end) < 0) { + parentLoc.end = childLoc.end; + } + } +} + +util.fixFaultyLocations = function(node, lines) { + var loc = node.loc; + if (loc) { + if (loc.start.line < 1) { + loc.start.line = 1; + } + + if (loc.end.line < 1) { + loc.end.line = 1; + } + } + + if (node.type === "TemplateLiteral") { + fixTemplateLiteral(node, lines); + + } else if (loc && node.decorators) { + // Expand the .loc of the node responsible for printing the decorators + // (here, the decorated node) so that it includes node.decorators. + node.decorators.forEach(function (decorator) { + expandLoc(loc, decorator.loc); + }); + + } else if (node.declaration && util.isExportDeclaration(node)) { + // Nullify .loc information for the child declaration so that we never + // try to reprint it without also reprinting the export declaration. + node.declaration.loc = null; + + // Expand the .loc of the node responsible for printing the decorators + // (here, the export declaration) so that it includes node.decorators. + var decorators = node.declaration.decorators; + if (decorators) { + decorators.forEach(function (decorator) { + expandLoc(loc, decorator.loc); + }); + } + + } else if ((n.MethodDefinition && n.MethodDefinition.check(node)) || + (n.Property.check(node) && (node.method || node.shorthand))) { + // If the node is a MethodDefinition or a .method or .shorthand + // Property, then the location information stored in + // node.value.loc is very likely untrustworthy (just the {body} + // part of a method, or nothing in the case of shorthand + // properties), so we null out that information to prevent + // accidental reuse of bogus source code during reprinting. + node.value.loc = null; + + if (n.FunctionExpression.check(node.value)) { + // FunctionExpression method values should be anonymous, + // because their .id fields are ignored anyway. + node.value.id = null; + } + + } else if (node.type === "ObjectTypeProperty") { + var loc = node.loc; + var end = loc && loc.end; + if (end) { + end = copyPos(end); + if (lines.prevPos(end) && + lines.charAt(end) === ",") { + // Some parsers accidentally include trailing commas in the + // .loc.end information for ObjectTypeProperty nodes. + if ((end = lines.skipSpaces(end, true, true))) { + loc.end = end; + } + } + } + } +}; + +function fixTemplateLiteral(node, lines) { + assert.strictEqual(node.type, "TemplateLiteral"); + + if (node.quasis.length === 0) { + // If there are no quasi elements, then there is nothing to fix. + return; + } + + // First we need to exclude the opening ` from the .loc of the first + // quasi element, in case the parser accidentally decided to include it. + var afterLeftBackTickPos = copyPos(node.loc.start); + assert.strictEqual(lines.charAt(afterLeftBackTickPos), "`"); + assert.ok(lines.nextPos(afterLeftBackTickPos)); + var firstQuasi = node.quasis[0]; + if (comparePos(firstQuasi.loc.start, afterLeftBackTickPos) < 0) { + firstQuasi.loc.start = afterLeftBackTickPos; + } + + // Next we need to exclude the closing ` from the .loc of the last quasi + // element, in case the parser accidentally decided to include it. + var rightBackTickPos = copyPos(node.loc.end); + assert.ok(lines.prevPos(rightBackTickPos)); + assert.strictEqual(lines.charAt(rightBackTickPos), "`"); + var lastQuasi = node.quasis[node.quasis.length - 1]; + if (comparePos(rightBackTickPos, lastQuasi.loc.end) < 0) { + lastQuasi.loc.end = rightBackTickPos; + } + + // Now we need to exclude ${ and } characters from the .loc's of all + // quasi elements, since some parsers accidentally include them. + node.expressions.forEach(function (expr, i) { + // Rewind from expr.loc.start over any whitespace and the ${ that + // precedes the expression. The position of the $ should be the same + // as the .loc.end of the preceding quasi element, but some parsers + // accidentally include the ${ in the .loc of the quasi element. + var dollarCurlyPos = lines.skipSpaces(expr.loc.start, true, false); + if (lines.prevPos(dollarCurlyPos) && + lines.charAt(dollarCurlyPos) === "{" && + lines.prevPos(dollarCurlyPos) && + lines.charAt(dollarCurlyPos) === "$") { + var quasiBefore = node.quasis[i]; + if (comparePos(dollarCurlyPos, quasiBefore.loc.end) < 0) { + quasiBefore.loc.end = dollarCurlyPos; + } + } + + // Likewise, some parsers accidentally include the } that follows + // the expression in the .loc of the following quasi element. + var rightCurlyPos = lines.skipSpaces(expr.loc.end, false, false); + if (lines.charAt(rightCurlyPos) === "}") { + assert.ok(lines.nextPos(rightCurlyPos)); + // Now rightCurlyPos is technically the position just after the }. + var quasiAfter = node.quasis[i + 1]; + if (comparePos(quasiAfter.loc.start, rightCurlyPos) < 0) { + quasiAfter.loc.start = rightCurlyPos; + } + } + }); +} + +util.isExportDeclaration = function (node) { + if (node) switch (node.type) { + case "ExportDeclaration": + case "ExportDefaultDeclaration": + case "ExportDefaultSpecifier": + case "DeclareExportDeclaration": + case "ExportNamedDeclaration": + case "ExportAllDeclaration": + return true; + } + + return false; +}; + +util.getParentExportDeclaration = function (path) { + var parentNode = path.getParentNode(); + if (path.getName() === "declaration" && + util.isExportDeclaration(parentNode)) { + return parentNode; + } + + return null; +}; + +util.isTrailingCommaEnabled = function(options, context) { + var trailingComma = options.trailingComma; + if (typeof trailingComma === "object") { + return !!trailingComma[context]; + } + return !!trailingComma; +}; diff --git a/test/fixtures/assignment.expected.js b/test/fixtures/assignment.expected.js new file mode 100644 index 00000000..fbeff1a8 --- /dev/null +++ b/test/fixtures/assignment.expected.js @@ -0,0 +1,6 @@ +x = 10; +y = 20; +z = 30; +x = y = z = w = 50; +reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally_long_var = 10; +reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally_long_var = x = y = 10; diff --git a/test/fixtures/assignment.js b/test/fixtures/assignment.js new file mode 100644 index 00000000..c84ae2c9 --- /dev/null +++ b/test/fixtures/assignment.js @@ -0,0 +1,6 @@ +x = 10; +y = 20; +z=30; +x = y = z = w = 50; +reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally_long_var = 10; +reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally_long_var = x = y = 10; diff --git a/test/fixtures/binaryexpr.expected.js b/test/fixtures/binaryexpr.expected.js new file mode 100644 index 00000000..920c03cc --- /dev/null +++ b/test/fixtures/binaryexpr.expected.js @@ -0,0 +1,6 @@ +x && y || z; +x | y & z; +x + y + z + w; +reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally_long_var || + foo || + bar + z; diff --git a/test/fixtures/binaryexpr.js b/test/fixtures/binaryexpr.js new file mode 100644 index 00000000..fbcfe430 --- /dev/null +++ b/test/fixtures/binaryexpr.js @@ -0,0 +1,5 @@ +x && y || z +x | y & z +x + y + z +w + +reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally_long_var || foo || bar + z diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000..4a084051 --- /dev/null +++ b/test/index.js @@ -0,0 +1,29 @@ +const fs = require("fs"); +const { join } = require("path"); +const glob = require("glob"); + +const jscodefmt = require("../"); + +glob("./fixtures/**/*.js", function(err, files) { + if(err) { throw err }; + runFixtureTests(files.filter(file => !file.includes(".expected.js"))); +}); + +function runFixtureTests(files) { + files.forEach(file => { + const expectedFile = file.replace(/\.js$/, ".expected.js"); + const src = fs.readFileSync(file); + const formatted = jscodefmt.format(src, { printWidth: 60 }); + + if(fs.existsSync(expectedFile)) { + const expected = fs.readFileSync(expectedFile, "utf8"); + + if(formatted !== expected) { + throw new Error("Failure: " + file + "\n" + formatted + "\ndoes not equal\n" + expected); + } + } + else { + fs.writeFileSync(expectedFile, formatted, "utf8"); + } + }); +}