Merge in forked recast printer that uses Wadler's algorithm

master
James Long 2016-12-23 13:38:10 -05:00
parent 35d8546d27
commit 9b4535e9f8
15 changed files with 3587 additions and 8 deletions

146
README.md
View File

@ -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.

View File

@ -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});
});

View File

@ -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;
}
};

View File

@ -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"
}
}

356
src/comments.js Normal file
View File

@ -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);
};

484
src/fast-path.js Normal file
View File

@ -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;
};

127
src/options.js Normal file
View File

@ -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"),
};
};

254
src/pp.js Normal file
View File

@ -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<n; i++) {
s += " ";
}
return s;
}
const MODE_BREAK = 1;
const MODE_FLAT = 2;
function fits(next, restCommands, width) {
let restIdx = restCommands.length;
const cmds = [next];
while(width >= 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
};

1852
src/printer.js Normal file

File diff suppressed because it is too large Load Diff

306
src/util.js Normal file
View File

@ -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;
};

6
test/fixtures/assignment.expected.js vendored Normal file
View File

@ -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;

6
test/fixtures/assignment.js vendored Normal file
View File

@ -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;

6
test/fixtures/binaryexpr.expected.js vendored Normal file
View File

@ -0,0 +1,6 @@
x && y || z;
x | y & z;
x + y + z + w;
reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally_long_var ||
foo ||
bar + z;

5
test/fixtures/binaryexpr.js vendored Normal file
View File

@ -0,0 +1,5 @@
x && y || z
x | y & z
x + y + z +w
reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally_long_var || foo || bar + z

29
test/index.js Normal file
View File

@ -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");
}
});
}