Merge in forked recast printer that uses Wadler's algorithm
parent
35d8546d27
commit
9b4535e9f8
146
README.md
146
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.
|
||||
|
||||
|
|
|
@ -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});
|
||||
});
|
||||
|
|
5
index.js
5
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;
|
||||
}
|
||||
};
|
||||
|
|
11
package.json
11
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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"),
|
||||
};
|
||||
};
|
|
@ -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
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,6 @@
|
|||
x && y || z;
|
||||
x | y & z;
|
||||
x + y + z + w;
|
||||
reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally_long_var ||
|
||||
foo ||
|
||||
bar + z;
|
|
@ -0,0 +1,5 @@
|
|||
x && y || z
|
||||
x | y & z
|
||||
x + y + z +w
|
||||
|
||||
reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally_long_var || foo || bar + z
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue