This commit is contained in:
chrosey
2017-09-13 07:52:34 +02:00
parent a1f16c37f4
commit 2340b0226b
24621 changed files with 2912161 additions and 149 deletions
+2
View File
@@ -0,0 +1,2 @@
[*.js]
indent_style = space
+2
View File
@@ -0,0 +1,2 @@
/node_modules
/test
+18
View File
@@ -0,0 +1,18 @@
language: node_js
node_js:
- "7.0"
- "6.0"
- "5.0"
- "4.0"
- "iojs"
- "0.12"
- "0.11"
- "0.10"
- "0.8"
matrix:
allow_failures:
- node_js: "0.8"
# Allow Travis tests to run in containers.
sudo: false
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2012 Ben Newman <bn@cs.stanford.edu>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+148
View File
@@ -0,0 +1,148 @@
# recast, _v_. [![Build Status](https://travis-ci.org/benjamn/recast.svg?branch=master)](https://travis-ci.org/benjamn/recast) [![Join the chat at https://gitter.im/benjamn/recast](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/benjamn/recast?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
1. to give (a metal object) a different form by melting it down and reshaping it.
1. to form, fashion, or arrange again.
1. to remodel or reconstruct (a literary work, document, sentence, etc.).
1. to supply (a theater or opera work) with a new cast.
Installation
---
From NPM:
npm install recast
From GitHub:
cd path/to/node_modules
git clone git://github.com/benjamn/recast.git
cd recast
npm install .
Usage
---
In less poetic terms, Recast exposes two essential interfaces, one for parsing JavaScript code (`require("recast").parse`) and the other for reprinting modified syntax trees (`require("recast").print`).
Here's a simple but non-trivial example of how you might use `.parse` and `.print`:
```js
var recast = require("recast");
// Let's turn this function declaration into a variable declaration.
var code = [
"function add(a, b) {",
" return a +",
" // Weird formatting, huh?",
" b;",
"}"
].join("\n");
// Parse the code using an interface similar to require("esprima").parse.
var ast = recast.parse(code);
```
Now do *whatever* you want to `ast`. Really, anything at all!
See [ast-types](https://github.com/benjamn/ast-types) (especially the [def/core.js](https://github.com/benjamn/ast-types/blob/master/def/core.js)) module for a thorough overview of the `ast` api.
```js
// Grab a reference to the function declaration we just parsed.
var add = ast.program.body[0];
// Make sure it's a FunctionDeclaration (optional).
var n = recast.types.namedTypes;
n.FunctionDeclaration.assert(add);
// If you choose to use recast.builders to construct new AST nodes, all builder
// arguments will be dynamically type-checked against the Mozilla Parser API.
var b = recast.types.builders;
// This kind of manipulation should seem familiar if you've used Esprima or the
// Mozilla Parser API before.
ast.program.body[0] = b.variableDeclaration("var", [
b.variableDeclarator(add.id, b.functionExpression(
null, // Anonymize the function expression.
add.params,
add.body
))
]);
// Just for fun, because addition is commutative:
add.params.push(add.params.shift());
```
When you finish manipulating the AST, let `recast.print` work its magic:
```js
var output = recast.print(ast).code;
```
The `output` string now looks exactly like this, weird formatting and all:
```js
var add = function(b, a) {
return a +
// Weird formatting, huh?
b;
}
```
The magic of Recast is that it reprints only those parts of the syntax tree that you modify. In other words, the following identity is guaranteed:
```js
recast.print(recast.parse(source)).code === source
```
Whenever Recast cannot reprint a modified node using the original source code, it falls back to using a generic pretty printer. So the worst that can happen is that your changes trigger some harmless reformatting of your code.
If you really don't care about preserving the original formatting, you can access the pretty printer directly:
```js
var output = recast.prettyPrint(ast, { tabWidth: 2 }).code;
```
And here's the exact `output`:
```js
var add = function(b, a) {
return a + b;
}
```
Note that the weird formatting was discarded, yet the behavior and abstract structure of the code remain the same.
Source maps
---
One of the coolest consequences of tracking and reusing original source code during reprinting is that it's pretty easy to generate a high-resolution mapping between the original code and the generated code—completely automatically!
With every `slice`, `join`, and re-`indent`-ation, the reprinting process maintains exact knowledge of which character sequences are original, and where in the original source they came from.
All you have to think about is how to manipulate the syntax tree, and Recast will give you a [source map](https://github.com/mozilla/source-map) in exchange for specifying the names of your source file(s) and the desired name of the map:
```js
var result = recast.print(transform(recast.parse(source, {
sourceFileName: "source.js"
})), {
sourceMapName: "map.json"
});
console.log(result.code); // Resulting string of code.
console.log(result.map); // JSON source map.
var SourceMapConsumer = require("source-map").SourceMapConsumer;
var smc = new SourceMapConsumer(result.map);
console.log(smc.originalPositionFor({
line: 3,
column: 15
})); // { source: 'source.js',
// line: 2,
// column: 10,
// name: null }
```
Note that you are free to mix and match syntax trees parsed from different source files, and the resulting source map will automatically keep track of the separate file origins for you.
Note also that the source maps generated by Recast are character-by-character maps, so meaningful identifier names are not recorded at this time. This approach leads to higher-resolution debugging in modern browsers, at the expense of somewhat larger map sizes. Striking the perfect balance here is an area for future exploration, but such improvements will not require any breaking changes to the interface demonstrated above.
Options
---
All Recast API functions take second parameter with configuration options, documented in
[options.js](https://github.com/benjamn/recast/blob/master/lib/options.js)
Motivation
---
The more code you have, the harder it becomes to make big, sweeping changes quickly and confidently. Even if you trust yourself not to make too many mistakes, and no matter how proficient you are with your text editor, changing tens of thousands of lines of code takes precious, non-refundable time.
Is there a better way? Not always! When a task requires you to alter the semantics of many different pieces of code in subtly different ways, your brain inevitably becomes the bottleneck, and there is little hope of completely automating the process. Your best bet is to plan carefully, buckle down, and get it right the first time. Love it or loathe it, that's the way programming goes sometimes.
What I hope to eliminate are the brain-wasting tasks, the tasks that are bottlenecked by keystrokes, the tasks that can be expressed as operations on the _syntactic structure_ of your code. Specifically, my goal is to make it possible for you to run your code through a parser, manipulate the abstract syntax tree directly, subject only to the constraints of your imagination, and then automatically translate those modifications back into source code, without upsetting the formatting of unmodified code.
And here's the best part: when you're done running a Recast script, if you're not completely satisfied with the results, blow them away with `git reset --hard`, tweak the script, and just run it again. Change your mind as many times as you like. Instead of typing yourself into a nasty case of [RSI](http://en.wikipedia.org/wiki/Repetitive_strain_injury), gaze upon your new wells of free time and ask yourself: what next?
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env node
var recast = require("recast");
var types = recast.types;
var n = types.namedTypes;
var b = types.builders;
require("recast").run(function(ast, callback) {
recast.visit(ast, {
visitIfStatement: function(path) {
var stmt = path.node;
stmt.consequent = fix(stmt.consequent);
var alt = stmt.alternate;
if (!n.IfStatement.check(alt)) {
stmt.alternate = fix(alt);
}
this.traverse(path);
},
visitWhileStatement: visitLoop,
visitForStatement: visitLoop,
visitForInStatement: visitLoop
});
callback(ast);
});
function visitLoop(path) {
var loop = path.node;
loop.body = fix(loop.body);
this.traverse(path);
}
function fix(clause) {
if (clause) {
if (!n.BlockStatement.check(clause)) {
clause = b.blockStatement([clause]);
}
}
return clause;
}
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env node
// This script should reprint the contents of the given file without
// reusing the original source, but with identical AST structure.
var recast = require("recast");
recast.run(function(ast, callback) {
recast.visit(ast, {
visitNode: function(path) {
this.traverse(path);
path.node.original = null;
}
});
callback(ast);
});
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env node
// This script should echo the contents of the given file without
// modification.
require("recast").run(function(ast, callback) {
callback(ast);
});
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env node
// This script converts for and do-while loops into equivalent while loops.
// Note that for-in statements are left unmodified, as they do not have a
// simple analogy to while loops. Also note that labeled continue statements
// are not correctly handled at this point, and will trigger an assertion
// failure if encountered.
var assert = require("assert");
var recast = require("recast");
var types = recast.types;
var n = types.namedTypes;
var b = types.builders;
recast.run(function(ast, callback) {
recast.visit(ast, {
visitForStatement: function(path) {
var fst = path.node;
path.replace(
fst.init,
b.whileStatement(
fst.test,
insertBeforeLoopback(fst, fst.update)
)
);
this.traverse(path);
},
visitDoWhileStatement: function(path) {
var dwst = path.node;
return b.whileStatement(
b.literal(true),
insertBeforeLoopback(
dwst,
b.ifStatement(
dwst.test,
b.breakStatement()
)
)
);
}
});
callback(ast);
});
function insertBeforeLoopback(loop, toInsert) {
var body = loop.body;
if (!n.Statement.check(toInsert)) {
toInsert = b.expressionStatement(toInsert);
}
if (n.BlockStatement.check(body)) {
body.body.push(toInsert);
} else {
body = b.blockStatement([body, toInsert]);
loop.body = body;
}
recast.visit(body, {
visitContinueStatement: function(path) {
var cst = path.node;
assert.equal(
cst.label, null,
"Labeled continue statements are not yet supported."
);
path.replace(toInsert, path.node);
return false;
},
// Do not descend into nested loops.
visitWhileStatement: function() {},
visitForStatement: function() {},
visitForInStatement: function() {},
visitDoWhileStatement: function() {}
});
return body;
}
+351
View File
@@ -0,0 +1,351 @@
var assert = require("assert");
var types = require("./types");
var n = types.namedTypes;
var isArray = types.builtInTypes.array;
var isObject = types.builtInTypes.object;
var linesModule = require("./lines");
var fromString = linesModule.fromString;
var Lines = linesModule.Lines;
var concat = linesModule.concat;
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("\n");
} 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("\n");
}
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);
return concat(leadingParts);
};
+486
View File
@@ -0,0 +1,486 @@
var assert = require("assert");
var types = require("./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(n.CallExpression.check(parent) && name === 'callee') {
return true;
}
if(n.MemberExpression.check(parent) && name === 'object') {
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;
};
+895
View File
@@ -0,0 +1,895 @@
var assert = require("assert");
var sourceMap = require("source-map");
var normalizeOptions = require("./options").normalize;
var secretKey = require("private").makeUniqueKey();
var types = require("./types");
var isString = types.builtInTypes.string;
var comparePos = require("./util").comparePos;
var Mapping = require("./mapping");
// Goals:
// 1. Minimize new string creation.
// 2. Keep (de)identation O(lines) time.
// 3. Permit negative indentations.
// 4. Enforce immutability.
// 5. No newline characters.
function getSecret(lines) {
return lines[secretKey];
}
function Lines(infos, sourceFileName) {
assert.ok(this instanceof Lines);
assert.ok(infos.length > 0);
if (sourceFileName) {
isString.assert(sourceFileName);
} else {
sourceFileName = null;
}
Object.defineProperty(this, secretKey, {
value: {
infos: infos,
mappings: [],
name: sourceFileName,
cachedSourceMap: null
}
});
if (sourceFileName) {
getSecret(this).mappings.push(new Mapping(this, {
start: this.firstPos(),
end: this.lastPos()
}));
}
}
// Exposed for instanceof checks. The fromString function should be used
// to create new Lines objects.
exports.Lines = Lines;
var Lp = Lines.prototype;
// These properties used to be assigned to each new object in the Lines
// constructor, but we can more efficiently stuff them into the secret and
// let these lazy accessors compute their values on-the-fly.
Object.defineProperties(Lp, {
length: {
get: function() {
return getSecret(this).infos.length;
}
},
name: {
get: function() {
return getSecret(this).name;
}
}
});
function copyLineInfo(info) {
return {
line: info.line,
indent: info.indent,
locked: info.locked,
sliceStart: info.sliceStart,
sliceEnd: info.sliceEnd
};
}
var fromStringCache = {};
var hasOwn = fromStringCache.hasOwnProperty;
var maxCacheKeyLen = 10;
function countSpaces(spaces, tabWidth) {
var count = 0;
var len = spaces.length;
for (var i = 0; i < len; ++i) {
switch (spaces.charCodeAt(i)) {
case 9: // '\t'
assert.strictEqual(typeof tabWidth, "number");
assert.ok(tabWidth > 0);
var next = Math.ceil(count / tabWidth) * tabWidth;
if (next === count) {
count += tabWidth;
} else {
count = next;
}
break;
case 11: // '\v'
case 12: // '\f'
case 13: // '\r'
case 0xfeff: // zero-width non-breaking space
// These characters contribute nothing to indentation.
break;
case 32: // ' '
default: // Treat all other whitespace like ' '.
count += 1;
break;
}
}
return count;
}
exports.countSpaces = countSpaces;
var leadingSpaceExp = /^\s*/;
// As specified here: http://www.ecma-international.org/ecma-262/6.0/#sec-line-terminators
var lineTerminatorSeqExp =
/\u000D\u000A|\u000D(?!\u000A)|\u000A|\u2028|\u2029/;
/**
* @param {Object} options - Options object that configures printing.
*/
function fromString(string, options) {
if (string instanceof Lines)
return string;
string += "";
var tabWidth = options && options.tabWidth;
var tabless = string.indexOf("\t") < 0;
var locked = !! (options && options.locked);
var cacheable = !options && tabless && (string.length <= maxCacheKeyLen);
assert.ok(tabWidth || tabless, "No tab width specified but encountered tabs in string\n" + string);
if (cacheable && hasOwn.call(fromStringCache, string))
return fromStringCache[string];
var lines = new Lines(string.split(lineTerminatorSeqExp).map(function(line) {
var spaces = leadingSpaceExp.exec(line)[0];
return {
line: line,
indent: countSpaces(spaces, tabWidth),
// Boolean indicating whether this line can be reindented.
locked: locked,
sliceStart: spaces.length,
sliceEnd: line.length
};
}), normalizeOptions(options).sourceFileName);
if (cacheable)
fromStringCache[string] = lines;
return lines;
}
exports.fromString = fromString;
function isOnlyWhitespace(string) {
return !/\S/.test(string);
}
Lp.toString = function(options) {
return this.sliceString(this.firstPos(), this.lastPos(), options);
};
Lp.getSourceMap = function(sourceMapName, sourceRoot) {
if (!sourceMapName) {
// Although we could make up a name or generate an anonymous
// source map, instead we assume that any consumer who does not
// provide a name does not actually want a source map.
return null;
}
var targetLines = this;
function updateJSON(json) {
json = json || {};
isString.assert(sourceMapName);
json.file = sourceMapName;
if (sourceRoot) {
isString.assert(sourceRoot);
json.sourceRoot = sourceRoot;
}
return json;
}
var secret = getSecret(targetLines);
if (secret.cachedSourceMap) {
// Since Lines objects are immutable, we can reuse any source map
// that was previously generated. Nevertheless, we return a new
// JSON object here to protect the cached source map from outside
// modification.
return updateJSON(secret.cachedSourceMap.toJSON());
}
var smg = new sourceMap.SourceMapGenerator(updateJSON());
var sourcesToContents = {};
secret.mappings.forEach(function(mapping) {
var sourceCursor = mapping.sourceLines.skipSpaces(
mapping.sourceLoc.start
) || mapping.sourceLines.lastPos();
var targetCursor = targetLines.skipSpaces(
mapping.targetLoc.start
) || targetLines.lastPos();
while (comparePos(sourceCursor, mapping.sourceLoc.end) < 0 &&
comparePos(targetCursor, mapping.targetLoc.end) < 0) {
var sourceChar = mapping.sourceLines.charAt(sourceCursor);
var targetChar = targetLines.charAt(targetCursor);
assert.strictEqual(sourceChar, targetChar);
var sourceName = mapping.sourceLines.name;
// Add mappings one character at a time for maximum resolution.
smg.addMapping({
source: sourceName,
original: { line: sourceCursor.line,
column: sourceCursor.column },
generated: { line: targetCursor.line,
column: targetCursor.column }
});
if (!hasOwn.call(sourcesToContents, sourceName)) {
var sourceContent = mapping.sourceLines.toString();
smg.setSourceContent(sourceName, sourceContent);
sourcesToContents[sourceName] = sourceContent;
}
targetLines.nextPos(targetCursor, true);
mapping.sourceLines.nextPos(sourceCursor, true);
}
});
secret.cachedSourceMap = smg;
return smg.toJSON();
};
Lp.bootstrapCharAt = function(pos) {
assert.strictEqual(typeof pos, "object");
assert.strictEqual(typeof pos.line, "number");
assert.strictEqual(typeof pos.column, "number");
var line = pos.line,
column = pos.column,
strings = this.toString().split(lineTerminatorSeqExp),
string = strings[line - 1];
if (typeof string === "undefined")
return "";
if (column === string.length &&
line < strings.length)
return "\n";
if (column >= string.length)
return "";
return string.charAt(column);
};
Lp.charAt = function(pos) {
assert.strictEqual(typeof pos, "object");
assert.strictEqual(typeof pos.line, "number");
assert.strictEqual(typeof pos.column, "number");
var line = pos.line,
column = pos.column,
secret = getSecret(this),
infos = secret.infos,
info = infos[line - 1],
c = column;
if (typeof info === "undefined" || c < 0)
return "";
var indent = this.getIndentAt(line);
if (c < indent)
return " ";
c += info.sliceStart - indent;
if (c === info.sliceEnd &&
line < this.length)
return "\n";
if (c >= info.sliceEnd)
return "";
return info.line.charAt(c);
};
Lp.stripMargin = function(width, skipFirstLine) {
if (width === 0)
return this;
assert.ok(width > 0, "negative margin: " + width);
if (skipFirstLine && this.length === 1)
return this;
var secret = getSecret(this);
var lines = new Lines(secret.infos.map(function(info, i) {
if (info.line && (i > 0 || !skipFirstLine)) {
info = copyLineInfo(info);
info.indent = Math.max(0, info.indent - width);
}
return info;
}));
if (secret.mappings.length > 0) {
var newMappings = getSecret(lines).mappings;
assert.strictEqual(newMappings.length, 0);
secret.mappings.forEach(function(mapping) {
newMappings.push(mapping.indent(width, skipFirstLine, true));
});
}
return lines;
};
Lp.indent = function(by) {
if (by === 0)
return this;
var secret = getSecret(this);
var lines = new Lines(secret.infos.map(function(info) {
if (info.line && ! info.locked) {
info = copyLineInfo(info);
info.indent += by;
}
return info
}));
if (secret.mappings.length > 0) {
var newMappings = getSecret(lines).mappings;
assert.strictEqual(newMappings.length, 0);
secret.mappings.forEach(function(mapping) {
newMappings.push(mapping.indent(by));
});
}
return lines;
};
Lp.indentTail = function(by) {
if (by === 0)
return this;
if (this.length < 2)
return this;
var secret = getSecret(this);
var lines = new Lines(secret.infos.map(function(info, i) {
if (i > 0 && info.line && ! info.locked) {
info = copyLineInfo(info);
info.indent += by;
}
return info;
}));
if (secret.mappings.length > 0) {
var newMappings = getSecret(lines).mappings;
assert.strictEqual(newMappings.length, 0);
secret.mappings.forEach(function(mapping) {
newMappings.push(mapping.indent(by, true));
});
}
return lines;
};
Lp.lockIndentTail = function () {
if (this.length < 2) {
return this;
}
var infos = getSecret(this).infos;
return new Lines(infos.map(function (info, i) {
info = copyLineInfo(info);
info.locked = i > 0;
return info;
}));
};
Lp.getIndentAt = function(line) {
assert.ok(line >= 1, "no line " + line + " (line numbers start from 1)");
var secret = getSecret(this),
info = secret.infos[line - 1];
return Math.max(info.indent, 0);
};
Lp.guessTabWidth = function() {
var secret = getSecret(this);
if (hasOwn.call(secret, "cachedTabWidth")) {
return secret.cachedTabWidth;
}
var counts = []; // Sparse array.
var lastIndent = 0;
for (var line = 1, last = this.length; line <= last; ++line) {
var info = secret.infos[line - 1];
var sliced = info.line.slice(info.sliceStart, info.sliceEnd);
// Whitespace-only lines don't tell us much about the likely tab
// width of this code.
if (isOnlyWhitespace(sliced)) {
continue;
}
var diff = Math.abs(info.indent - lastIndent);
counts[diff] = ~~counts[diff] + 1;
lastIndent = info.indent;
}
var maxCount = -1;
var result = 2;
for (var tabWidth = 1;
tabWidth < counts.length;
tabWidth += 1) {
if (hasOwn.call(counts, tabWidth) &&
counts[tabWidth] > maxCount) {
maxCount = counts[tabWidth];
result = tabWidth;
}
}
return secret.cachedTabWidth = result;
};
// Determine if the list of lines has a first line that starts with a //
// or /* comment. If this is the case, the code may need to be wrapped in
// parens to avoid ASI issues.
Lp.startsWithComment = function () {
var secret = getSecret(this);
if (secret.infos.length === 0) {
return false;
}
var firstLineInfo = secret.infos[0],
sliceStart = firstLineInfo.sliceStart,
sliceEnd = firstLineInfo.sliceEnd,
firstLine = firstLineInfo.line.slice(sliceStart, sliceEnd).trim();
return firstLine.length === 0 ||
firstLine.slice(0, 2) === "//" ||
firstLine.slice(0, 2) === "/*";
};
Lp.isOnlyWhitespace = function() {
return isOnlyWhitespace(this.toString());
};
Lp.isPrecededOnlyByWhitespace = function(pos) {
var secret = getSecret(this);
var info = secret.infos[pos.line - 1];
var indent = Math.max(info.indent, 0);
var diff = pos.column - indent;
if (diff <= 0) {
// If pos.column does not exceed the indentation amount, then
// there must be only whitespace before it.
return true;
}
var start = info.sliceStart;
var end = Math.min(start + diff, info.sliceEnd);
var prefix = info.line.slice(start, end);
return isOnlyWhitespace(prefix);
};
Lp.getLineLength = function(line) {
var secret = getSecret(this),
info = secret.infos[line - 1];
return this.getIndentAt(line) + info.sliceEnd - info.sliceStart;
};
Lp.nextPos = function(pos, skipSpaces) {
var l = Math.max(pos.line, 0),
c = Math.max(pos.column, 0);
if (c < this.getLineLength(l)) {
pos.column += 1;
return skipSpaces
? !!this.skipSpaces(pos, false, true)
: true;
}
if (l < this.length) {
pos.line += 1;
pos.column = 0;
return skipSpaces
? !!this.skipSpaces(pos, false, true)
: true;
}
return false;
};
Lp.prevPos = function(pos, skipSpaces) {
var l = pos.line,
c = pos.column;
if (c < 1) {
l -= 1;
if (l < 1)
return false;
c = this.getLineLength(l);
} else {
c = Math.min(c - 1, this.getLineLength(l));
}
pos.line = l;
pos.column = c;
return skipSpaces
? !!this.skipSpaces(pos, true, true)
: true;
};
Lp.firstPos = function() {
// Trivial, but provided for completeness.
return { line: 1, column: 0 };
};
Lp.lastPos = function() {
return {
line: this.length,
column: this.getLineLength(this.length)
};
};
Lp.skipSpaces = function(pos, backward, modifyInPlace) {
if (pos) {
pos = modifyInPlace ? pos : {
line: pos.line,
column: pos.column
};
} else if (backward) {
pos = this.lastPos();
} else {
pos = this.firstPos();
}
if (backward) {
while (this.prevPos(pos)) {
if (!isOnlyWhitespace(this.charAt(pos)) &&
this.nextPos(pos)) {
return pos;
}
}
return null;
} else {
while (isOnlyWhitespace(this.charAt(pos))) {
if (!this.nextPos(pos)) {
return null;
}
}
return pos;
}
};
Lp.trimLeft = function() {
var pos = this.skipSpaces(this.firstPos(), false, true);
return pos ? this.slice(pos) : emptyLines;
};
Lp.trimRight = function() {
var pos = this.skipSpaces(this.lastPos(), true, true);
return pos ? this.slice(this.firstPos(), pos) : emptyLines;
};
Lp.trim = function() {
var start = this.skipSpaces(this.firstPos(), false, true);
if (start === null)
return emptyLines;
var end = this.skipSpaces(this.lastPos(), true, true);
assert.notStrictEqual(end, null);
return this.slice(start, end);
};
Lp.eachPos = function(callback, startPos, skipSpaces) {
var pos = this.firstPos();
if (startPos) {
pos.line = startPos.line,
pos.column = startPos.column
}
if (skipSpaces && !this.skipSpaces(pos, false, true)) {
return; // Encountered nothing but spaces.
}
do callback.call(this, pos);
while (this.nextPos(pos, skipSpaces));
};
Lp.bootstrapSlice = function(start, end) {
var strings = this.toString().split(
lineTerminatorSeqExp
).slice(
start.line - 1,
end.line
);
strings.push(strings.pop().slice(0, end.column));
strings[0] = strings[0].slice(start.column);
return fromString(strings.join("\n"));
};
Lp.slice = function(start, end) {
if (!end) {
if (!start) {
// The client seems to want a copy of this Lines object, but
// Lines objects are immutable, so it's perfectly adequate to
// return the same object.
return this;
}
// Slice to the end if no end position was provided.
end = this.lastPos();
}
var secret = getSecret(this);
var sliced = secret.infos.slice(start.line - 1, end.line);
if (start.line === end.line) {
sliced[0] = sliceInfo(sliced[0], start.column, end.column);
} else {
assert.ok(start.line < end.line);
sliced[0] = sliceInfo(sliced[0], start.column);
sliced.push(sliceInfo(sliced.pop(), 0, end.column));
}
var lines = new Lines(sliced);
if (secret.mappings.length > 0) {
var newMappings = getSecret(lines).mappings;
assert.strictEqual(newMappings.length, 0);
secret.mappings.forEach(function(mapping) {
var sliced = mapping.slice(this, start, end);
if (sliced) {
newMappings.push(sliced);
}
}, this);
}
return lines;
};
function sliceInfo(info, startCol, endCol) {
var sliceStart = info.sliceStart;
var sliceEnd = info.sliceEnd;
var indent = Math.max(info.indent, 0);
var lineLength = indent + sliceEnd - sliceStart;
if (typeof endCol === "undefined") {
endCol = lineLength;
}
startCol = Math.max(startCol, 0);
endCol = Math.min(endCol, lineLength);
endCol = Math.max(endCol, startCol);
if (endCol < indent) {
indent = endCol;
sliceEnd = sliceStart;
} else {
sliceEnd -= lineLength - endCol;
}
lineLength = endCol;
lineLength -= startCol;
if (startCol < indent) {
indent -= startCol;
} else {
startCol -= indent;
indent = 0;
sliceStart += startCol;
}
assert.ok(indent >= 0);
assert.ok(sliceStart <= sliceEnd);
assert.strictEqual(lineLength, indent + sliceEnd - sliceStart);
if (info.indent === indent &&
info.sliceStart === sliceStart &&
info.sliceEnd === sliceEnd) {
return info;
}
return {
line: info.line,
indent: indent,
// A destructive slice always unlocks indentation.
locked: false,
sliceStart: sliceStart,
sliceEnd: sliceEnd
};
}
Lp.bootstrapSliceString = function(start, end, options) {
return this.slice(start, end).toString(options);
};
Lp.sliceString = function(start, end, options) {
if (!end) {
if (!start) {
// The client seems to want a copy of this Lines object, but
// Lines objects are immutable, so it's perfectly adequate to
// return the same object.
return this;
}
// Slice to the end if no end position was provided.
end = this.lastPos();
}
options = normalizeOptions(options);
var infos = getSecret(this).infos;
var parts = [];
var tabWidth = options.tabWidth;
for (var line = start.line; line <= end.line; ++line) {
var info = infos[line - 1];
if (line === start.line) {
if (line === end.line) {
info = sliceInfo(info, start.column, end.column);
} else {
info = sliceInfo(info, start.column);
}
} else if (line === end.line) {
info = sliceInfo(info, 0, end.column);
}
var indent = Math.max(info.indent, 0);
var before = info.line.slice(0, info.sliceStart);
if (options.reuseWhitespace &&
isOnlyWhitespace(before) &&
countSpaces(before, options.tabWidth) === indent) {
// Reuse original spaces if the indentation is correct.
parts.push(info.line.slice(0, info.sliceEnd));
continue;
}
var tabs = 0;
var spaces = indent;
if (options.useTabs) {
tabs = Math.floor(indent / tabWidth);
spaces -= tabs * tabWidth;
}
var result = "";
if (tabs > 0) {
result += new Array(tabs + 1).join("\t");
}
if (spaces > 0) {
result += new Array(spaces + 1).join(" ");
}
result += info.line.slice(info.sliceStart, info.sliceEnd);
parts.push(result);
}
return parts.join(options.lineTerminator);
};
Lp.isEmpty = function() {
return this.length < 2 && this.getLineLength(1) < 1;
};
Lp.join = function(elements) {
var separator = this;
var separatorSecret = getSecret(separator);
var infos = [];
var mappings = [];
var prevInfo;
function appendSecret(secret) {
if (secret === null)
return;
if (prevInfo) {
var info = secret.infos[0];
var indent = new Array(info.indent + 1).join(" ");
var prevLine = infos.length;
var prevColumn = Math.max(prevInfo.indent, 0) +
prevInfo.sliceEnd - prevInfo.sliceStart;
prevInfo.line = prevInfo.line.slice(
0, prevInfo.sliceEnd) + indent + info.line.slice(
info.sliceStart, info.sliceEnd);
// If any part of a line is indentation-locked, the whole line
// will be indentation-locked.
prevInfo.locked = prevInfo.locked || info.locked;
prevInfo.sliceEnd = prevInfo.line.length;
if (secret.mappings.length > 0) {
secret.mappings.forEach(function(mapping) {
mappings.push(mapping.add(prevLine, prevColumn));
});
}
} else if (secret.mappings.length > 0) {
mappings.push.apply(mappings, secret.mappings);
}
secret.infos.forEach(function(info, i) {
if (!prevInfo || i > 0) {
prevInfo = copyLineInfo(info);
infos.push(prevInfo);
}
});
}
function appendWithSeparator(secret, i) {
if (i > 0)
appendSecret(separatorSecret);
appendSecret(secret);
}
elements.map(function(elem) {
var lines = fromString(elem);
if (lines.isEmpty())
return null;
return getSecret(lines);
}).forEach(separator.isEmpty()
? appendSecret
: appendWithSeparator);
if (infos.length < 1)
return emptyLines;
var lines = new Lines(infos);
getSecret(lines).mappings = mappings;
return lines;
};
exports.concat = function(elements) {
return emptyLines.join(elements);
};
Lp.concat = function(other) {
var args = arguments,
list = [this];
list.push.apply(list, args);
assert.strictEqual(list.length, args.length + 1);
return emptyLines.join(list);
};
// The emptyLines object needs to be created all the way down here so that
// Lines.prototype will be fully populated.
var emptyLines = fromString("");
+277
View File
@@ -0,0 +1,277 @@
var assert = require("assert");
var types = require("./types");
var isString = types.builtInTypes.string;
var isNumber = types.builtInTypes.number;
var SourceLocation = types.namedTypes.SourceLocation;
var Position = types.namedTypes.Position;
var linesModule = require("./lines");
var comparePos = require("./util").comparePos;
function Mapping(sourceLines, sourceLoc, targetLoc) {
assert.ok(this instanceof Mapping);
assert.ok(sourceLines instanceof linesModule.Lines);
SourceLocation.assert(sourceLoc);
if (targetLoc) {
// In certain cases it's possible for targetLoc.{start,end}.column
// values to be negative, which technically makes them no longer
// valid SourceLocation nodes, so we need to be more forgiving.
assert.ok(
isNumber.check(targetLoc.start.line) &&
isNumber.check(targetLoc.start.column) &&
isNumber.check(targetLoc.end.line) &&
isNumber.check(targetLoc.end.column)
);
} else {
// Assume identity mapping if no targetLoc specified.
targetLoc = sourceLoc;
}
Object.defineProperties(this, {
sourceLines: { value: sourceLines },
sourceLoc: { value: sourceLoc },
targetLoc: { value: targetLoc }
});
}
var Mp = Mapping.prototype;
module.exports = Mapping;
Mp.slice = function(lines, start, end) {
assert.ok(lines instanceof linesModule.Lines);
Position.assert(start);
if (end) {
Position.assert(end);
} else {
end = lines.lastPos();
}
var sourceLines = this.sourceLines;
var sourceLoc = this.sourceLoc;
var targetLoc = this.targetLoc;
function skip(name) {
var sourceFromPos = sourceLoc[name];
var targetFromPos = targetLoc[name];
var targetToPos = start;
if (name === "end") {
targetToPos = end;
} else {
assert.strictEqual(name, "start");
}
return skipChars(
sourceLines, sourceFromPos,
lines, targetFromPos, targetToPos
);
}
if (comparePos(start, targetLoc.start) <= 0) {
if (comparePos(targetLoc.end, end) <= 0) {
targetLoc = {
start: subtractPos(targetLoc.start, start.line, start.column),
end: subtractPos(targetLoc.end, start.line, start.column)
};
// The sourceLoc can stay the same because the contents of the
// targetLoc have not changed.
} else if (comparePos(end, targetLoc.start) <= 0) {
return null;
} else {
sourceLoc = {
start: sourceLoc.start,
end: skip("end")
};
targetLoc = {
start: subtractPos(targetLoc.start, start.line, start.column),
end: subtractPos(end, start.line, start.column)
};
}
} else {
if (comparePos(targetLoc.end, start) <= 0) {
return null;
}
if (comparePos(targetLoc.end, end) <= 0) {
sourceLoc = {
start: skip("start"),
end: sourceLoc.end
};
targetLoc = {
// Same as subtractPos(start, start.line, start.column):
start: { line: 1, column: 0 },
end: subtractPos(targetLoc.end, start.line, start.column)
};
} else {
sourceLoc = {
start: skip("start"),
end: skip("end")
};
targetLoc = {
// Same as subtractPos(start, start.line, start.column):
start: { line: 1, column: 0 },
end: subtractPos(end, start.line, start.column)
};
}
}
return new Mapping(this.sourceLines, sourceLoc, targetLoc);
};
Mp.add = function(line, column) {
return new Mapping(this.sourceLines, this.sourceLoc, {
start: addPos(this.targetLoc.start, line, column),
end: addPos(this.targetLoc.end, line, column)
});
};
function addPos(toPos, line, column) {
return {
line: toPos.line + line - 1,
column: (toPos.line === 1)
? toPos.column + column
: toPos.column
};
}
Mp.subtract = function(line, column) {
return new Mapping(this.sourceLines, this.sourceLoc, {
start: subtractPos(this.targetLoc.start, line, column),
end: subtractPos(this.targetLoc.end, line, column)
});
};
function subtractPos(fromPos, line, column) {
return {
line: fromPos.line - line + 1,
column: (fromPos.line === line)
? fromPos.column - column
: fromPos.column
};
}
Mp.indent = function(by, skipFirstLine, noNegativeColumns) {
if (by === 0) {
return this;
}
var targetLoc = this.targetLoc;
var startLine = targetLoc.start.line;
var endLine = targetLoc.end.line;
if (skipFirstLine && startLine === 1 && endLine === 1) {
return this;
}
targetLoc = {
start: targetLoc.start,
end: targetLoc.end
};
if (!skipFirstLine || startLine > 1) {
var startColumn = targetLoc.start.column + by;
targetLoc.start = {
line: startLine,
column: noNegativeColumns
? Math.max(0, startColumn)
: startColumn
};
}
if (!skipFirstLine || endLine > 1) {
var endColumn = targetLoc.end.column + by;
targetLoc.end = {
line: endLine,
column: noNegativeColumns
? Math.max(0, endColumn)
: endColumn
};
}
return new Mapping(this.sourceLines, this.sourceLoc, targetLoc);
};
function skipChars(
sourceLines, sourceFromPos,
targetLines, targetFromPos, targetToPos
) {
assert.ok(sourceLines instanceof linesModule.Lines);
assert.ok(targetLines instanceof linesModule.Lines);
Position.assert(sourceFromPos);
Position.assert(targetFromPos);
Position.assert(targetToPos);
var targetComparison = comparePos(targetFromPos, targetToPos);
if (targetComparison === 0) {
// Trivial case: no characters to skip.
return sourceFromPos;
}
if (targetComparison < 0) {
// Skipping forward.
var sourceCursor = sourceLines.skipSpaces(sourceFromPos);
var targetCursor = targetLines.skipSpaces(targetFromPos);
var lineDiff = targetToPos.line - targetCursor.line;
sourceCursor.line += lineDiff;
targetCursor.line += lineDiff;
if (lineDiff > 0) {
// If jumping to later lines, reset columns to the beginnings
// of those lines.
sourceCursor.column = 0;
targetCursor.column = 0;
} else {
assert.strictEqual(lineDiff, 0);
}
while (comparePos(targetCursor, targetToPos) < 0 &&
targetLines.nextPos(targetCursor, true)) {
assert.ok(sourceLines.nextPos(sourceCursor, true));
assert.strictEqual(
sourceLines.charAt(sourceCursor),
targetLines.charAt(targetCursor)
);
}
} else {
// Skipping backward.
var sourceCursor = sourceLines.skipSpaces(sourceFromPos, true);
var targetCursor = targetLines.skipSpaces(targetFromPos, true);
var lineDiff = targetToPos.line - targetCursor.line;
sourceCursor.line += lineDiff;
targetCursor.line += lineDiff;
if (lineDiff < 0) {
// If jumping to earlier lines, reset columns to the ends of
// those lines.
sourceCursor.column = sourceLines.getLineLength(sourceCursor.line);
targetCursor.column = targetLines.getLineLength(targetCursor.line);
} else {
assert.strictEqual(lineDiff, 0);
}
while (comparePos(targetToPos, targetCursor) < 0 &&
targetLines.prevPos(targetCursor, true)) {
assert.ok(sourceLines.prevPos(sourceCursor, true));
assert.strictEqual(
sourceLines.charAt(sourceCursor),
targetLines.charAt(targetCursor)
);
}
}
return sourceCursor;
}
+127
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"),
};
};
+158
View File
@@ -0,0 +1,158 @@
var assert = require("assert");
var types = require("./types");
var n = types.namedTypes;
var b = types.builders;
var isObject = types.builtInTypes.object;
var isArray = types.builtInTypes.array;
var isFunction = types.builtInTypes.function;
var Patcher = require("./patcher").Patcher;
var normalizeOptions = require("./options").normalize;
var fromString = require("./lines").fromString;
var attachComments = require("./comments").attach;
var util = require("./util");
exports.parse = function parse(source, options) {
options = normalizeOptions(options);
var lines = fromString(source, options);
var sourceWithoutTabs = lines.toString({
tabWidth: options.tabWidth,
reuseWhitespace: false,
useTabs: false
});
var comments = [];
var program = options.parser.parse(sourceWithoutTabs, {
jsx: true,
loc: true,
locations: true,
range: options.range,
comment: true,
onComment: comments,
tolerant: options.tolerant,
ecmaVersion: 6,
sourceType: 'module'
});
// If the source was empty, some parsers give loc.{start,end}.line
// values of 0, instead of the minimum of 1.
util.fixFaultyLocations(program, lines);
program.loc = program.loc || {
start: lines.firstPos(),
end: lines.lastPos()
};
program.loc.lines = lines;
program.loc.indent = 0;
// Expand the Program node's .loc to include all comments, since
// typically its .loc.start and .loc.end will coincide with those of the
// first and last statements, respectively, excluding any comments that
// fall outside that region.
var trueProgramLoc = util.getTrueLoc(program, lines);
program.loc.start = trueProgramLoc.start;
program.loc.end = trueProgramLoc.end;
if (program.comments) {
comments = program.comments;
delete program.comments;
}
// In order to ensure we reprint leading and trailing program comments,
// wrap the original Program node with a File node.
var file = program;
if (file.type === "Program") {
var file = b.file(program, options.sourceFileName || null);
file.loc = {
lines: lines,
indent: 0,
start: lines.firstPos(),
end: lines.lastPos()
};
} else if (file.type === "File") {
program = file.program;
}
// Passing file.program here instead of just file means that initial
// comments will be attached to program.body[0] instead of program.
attachComments(
comments,
program.body.length ? file.program : file,
lines
);
// Return a copy of the original AST so that any changes made may be
// compared to the original.
return new TreeCopier(lines).copy(file);
};
function TreeCopier(lines) {
assert.ok(this instanceof TreeCopier);
this.lines = lines;
this.indent = 0;
}
var TCp = TreeCopier.prototype;
TCp.copy = function(node) {
if (isArray.check(node)) {
return node.map(this.copy, this);
}
if (!isObject.check(node)) {
return node;
}
util.fixFaultyLocations(node, this.lines);
var copy = Object.create(Object.getPrototypeOf(node), {
original: { // Provide a link from the copy to the original.
value: node,
configurable: false,
enumerable: false,
writable: true
}
});
var loc = node.loc;
var oldIndent = this.indent;
var newIndent = oldIndent;
if (loc) {
// When node is a comment, we set node.loc.indent to
// node.loc.start.column so that, when/if we print the comment by
// itself, we can strip that much whitespace from the left margin of
// the comment. This only really matters for multiline Block comments,
// but it doesn't hurt for Line comments.
if (node.type === "Block" || node.type === "Line" ||
node.type === "CommentBlock" || node.type === "CommentLine" ||
this.lines.isPrecededOnlyByWhitespace(loc.start)) {
newIndent = this.indent = loc.start.column;
}
loc.lines = this.lines;
loc.indent = newIndent;
}
var keys = Object.keys(node);
var keyCount = keys.length;
for (var i = 0; i < keyCount; ++i) {
var key = keys[i];
if (key === "loc") {
copy[key] = node[key];
} else if (key === "tokens" &&
node.type === "File") {
// Preserve file.tokens (uncopied) in case client code cares about
// it, even though Recast ignores it when reprinting.
copy[key] = node[key];
} else {
copy[key] = this.copy(node[key]);
}
}
this.indent = oldIndent;
return copy;
};
+535
View File
@@ -0,0 +1,535 @@
var assert = require("assert");
var linesModule = require("./lines");
var types = require("./types");
var getFieldValue = types.getFieldValue;
var Printable = types.namedTypes.Printable;
var Expression = types.namedTypes.Expression;
var ReturnStatement = types.namedTypes.ReturnStatement;
var SourceLocation = types.namedTypes.SourceLocation;
var util = require("./util");
var comparePos = util.comparePos;
var FastPath = require("./fast-path");
var isObject = types.builtInTypes.object;
var isArray = types.builtInTypes.array;
var isString = types.builtInTypes.string;
var riskyAdjoiningCharExp = /[0-9a-z_$]/i;
function Patcher(lines) {
assert.ok(this instanceof Patcher);
assert.ok(lines instanceof linesModule.Lines);
var self = this,
replacements = [];
self.replace = function(loc, lines) {
if (isString.check(lines))
lines = linesModule.fromString(lines);
replacements.push({
lines: lines,
start: loc.start,
end: loc.end
});
};
self.get = function(loc) {
// If no location is provided, return the complete Lines object.
loc = loc || {
start: { line: 1, column: 0 },
end: { line: lines.length,
column: lines.getLineLength(lines.length) }
};
var sliceFrom = loc.start,
toConcat = [];
function pushSlice(from, to) {
assert.ok(comparePos(from, to) <= 0);
toConcat.push(lines.slice(from, to));
}
replacements.sort(function(a, b) {
return comparePos(a.start, b.start);
}).forEach(function(rep) {
if (comparePos(sliceFrom, rep.start) > 0) {
// Ignore nested replacement ranges.
} else {
pushSlice(sliceFrom, rep.start);
toConcat.push(rep.lines);
sliceFrom = rep.end;
}
});
pushSlice(sliceFrom, loc.end);
return linesModule.concat(toConcat);
};
}
exports.Patcher = Patcher;
var Pp = Patcher.prototype;
Pp.tryToReprintComments = function(newNode, oldNode, print) {
var patcher = this;
if (!newNode.comments &&
!oldNode.comments) {
// We were (vacuously) able to reprint all the comments!
return true;
}
var newPath = FastPath.from(newNode);
var oldPath = FastPath.from(oldNode);
newPath.stack.push("comments", getSurroundingComments(newNode));
oldPath.stack.push("comments", getSurroundingComments(oldNode));
var reprints = [];
var ableToReprintComments =
findArrayReprints(newPath, oldPath, reprints);
// No need to pop anything from newPath.stack or oldPath.stack, since
// newPath and oldPath are fresh local variables.
if (ableToReprintComments && reprints.length > 0) {
reprints.forEach(function(reprint) {
var oldComment = reprint.oldPath.getValue();
assert.ok(oldComment.leading || oldComment.trailing);
patcher.replace(
oldComment.loc,
// Comments can't have .comments, so it doesn't matter
// whether we print with comments or without.
print(reprint.newPath).indentTail(oldComment.loc.indent)
);
});
}
return ableToReprintComments;
};
// Get all comments that are either leading or trailing, ignoring any
// comments that occur inside node.loc. Returns an empty array for nodes
// with no leading or trailing comments.
function getSurroundingComments(node) {
var result = [];
if (node.comments &&
node.comments.length > 0) {
node.comments.forEach(function(comment) {
if (comment.leading || comment.trailing) {
result.push(comment);
}
});
}
return result;
}
Pp.deleteComments = function(node) {
if (!node.comments) {
return;
}
var patcher = this;
node.comments.forEach(function(comment) {
if (comment.leading) {
// Delete leading comments along with any trailing whitespace
// they might have.
patcher.replace({
start: comment.loc.start,
end: node.loc.lines.skipSpaces(
comment.loc.end, false, false)
}, "");
} else if (comment.trailing) {
// Delete trailing comments along with any leading whitespace
// they might have.
patcher.replace({
start: node.loc.lines.skipSpaces(
comment.loc.start, true, false),
end: comment.loc.end
}, "");
}
});
};
exports.getReprinter = function(path) {
assert.ok(path instanceof FastPath);
// Make sure that this path refers specifically to a Node, rather than
// some non-Node subproperty of a Node.
var node = path.getValue();
if (!Printable.check(node))
return;
var orig = node.original;
var origLoc = orig && orig.loc;
var lines = origLoc && origLoc.lines;
var reprints = [];
if (!lines || !findReprints(path, reprints))
return;
return function(print) {
var patcher = new Patcher(lines);
reprints.forEach(function(reprint) {
var newNode = reprint.newPath.getValue();
var oldNode = reprint.oldPath.getValue();
SourceLocation.assert(oldNode.loc, true);
var needToPrintNewPathWithComments =
!patcher.tryToReprintComments(newNode, oldNode, print)
if (needToPrintNewPathWithComments) {
// Since we were not able to preserve all leading/trailing
// comments, we delete oldNode's comments, print newPath
// with comments, and then patch the resulting lines where
// oldNode used to be.
patcher.deleteComments(oldNode);
}
var newLines = print(
reprint.newPath,
needToPrintNewPathWithComments
).indentTail(oldNode.loc.indent);
var nls = needsLeadingSpace(lines, oldNode.loc, newLines);
var nts = needsTrailingSpace(lines, oldNode.loc, newLines);
// If we try to replace the argument of a ReturnStatement like
// return"asdf" with e.g. a literal null expression, we run
// the risk of ending up with returnnull, so we need to add an
// extra leading space in situations where that might
// happen. Likewise for "asdf"in obj. See #170.
if (nls || nts) {
var newParts = [];
nls && newParts.push(" ");
newParts.push(newLines);
nts && newParts.push(" ");
newLines = linesModule.concat(newParts);
}
patcher.replace(oldNode.loc, newLines);
});
// Recall that origLoc is the .loc of an ancestor node that is
// guaranteed to contain all the reprinted nodes and comments.
return patcher.get(origLoc).indentTail(-orig.loc.indent);
};
};
// If the last character before oldLoc and the first character of newLines
// are both identifier characters, they must be separated by a space,
// otherwise they will most likely get fused together into a single token.
function needsLeadingSpace(oldLines, oldLoc, newLines) {
var posBeforeOldLoc = util.copyPos(oldLoc.start);
// The character just before the location occupied by oldNode.
var charBeforeOldLoc =
oldLines.prevPos(posBeforeOldLoc) &&
oldLines.charAt(posBeforeOldLoc);
// First character of the reprinted node.
var newFirstChar = newLines.charAt(newLines.firstPos());
return charBeforeOldLoc &&
riskyAdjoiningCharExp.test(charBeforeOldLoc) &&
newFirstChar &&
riskyAdjoiningCharExp.test(newFirstChar);
}
// If the last character of newLines and the first character after oldLoc
// are both identifier characters, they must be separated by a space,
// otherwise they will most likely get fused together into a single token.
function needsTrailingSpace(oldLines, oldLoc, newLines) {
// The character just after the location occupied by oldNode.
var charAfterOldLoc = oldLines.charAt(oldLoc.end);
var newLastPos = newLines.lastPos();
// Last character of the reprinted node.
var newLastChar = newLines.prevPos(newLastPos) &&
newLines.charAt(newLastPos);
return newLastChar &&
riskyAdjoiningCharExp.test(newLastChar) &&
charAfterOldLoc &&
riskyAdjoiningCharExp.test(charAfterOldLoc);
}
function findReprints(newPath, reprints) {
var newNode = newPath.getValue();
Printable.assert(newNode);
var oldNode = newNode.original;
Printable.assert(oldNode);
assert.deepEqual(reprints, []);
if (newNode.type !== oldNode.type) {
return false;
}
var oldPath = new FastPath(oldNode);
var canReprint = findChildReprints(newPath, oldPath, reprints);
if (!canReprint) {
// Make absolutely sure the calling code does not attempt to reprint
// any nodes.
reprints.length = 0;
}
return canReprint;
}
function findAnyReprints(newPath, oldPath, reprints) {
var newNode = newPath.getValue();
var oldNode = oldPath.getValue();
if (newNode === oldNode)
return true;
if (isArray.check(newNode))
return findArrayReprints(newPath, oldPath, reprints);
if (isObject.check(newNode))
return findObjectReprints(newPath, oldPath, reprints);
return false;
}
function findArrayReprints(newPath, oldPath, reprints) {
var newNode = newPath.getValue();
var oldNode = oldPath.getValue();
isArray.assert(newNode);
var len = newNode.length;
if (!(isArray.check(oldNode) &&
oldNode.length === len))
return false;
for (var i = 0; i < len; ++i) {
newPath.stack.push(i, newNode[i]);
oldPath.stack.push(i, oldNode[i]);
var canReprint = findAnyReprints(newPath, oldPath, reprints);
newPath.stack.length -= 2;
oldPath.stack.length -= 2;
if (!canReprint) {
return false;
}
}
return true;
}
function findObjectReprints(newPath, oldPath, reprints) {
var newNode = newPath.getValue();
isObject.assert(newNode);
if (newNode.original === null) {
// If newNode.original node was set to null, reprint the node.
return false;
}
var oldNode = oldPath.getValue();
if (!isObject.check(oldNode))
return false;
if (Printable.check(newNode)) {
if (!Printable.check(oldNode)) {
return false;
}
// Here we need to decide whether the reprinted code for newNode
// is appropriate for patching into the location of oldNode.
if (newNode.type === oldNode.type) {
var childReprints = [];
if (findChildReprints(newPath, oldPath, childReprints)) {
reprints.push.apply(reprints, childReprints);
} else if (oldNode.loc) {
// If we have no .loc information for oldNode, then we
// won't be able to reprint it.
reprints.push({
oldPath: oldPath.copy(),
newPath: newPath.copy()
});
} else {
return false;
}
return true;
}
if (Expression.check(newNode) &&
Expression.check(oldNode) &&
// If we have no .loc information for oldNode, then we won't
// be able to reprint it.
oldNode.loc) {
// If both nodes are subtypes of Expression, then we should be
// able to fill the location occupied by the old node with
// code printed for the new node with no ill consequences.
reprints.push({
oldPath: oldPath.copy(),
newPath: newPath.copy()
});
return true;
}
// The nodes have different types, and at least one of the types
// is not a subtype of the Expression type, so we cannot safely
// assume the nodes are syntactically interchangeable.
return false;
}
return findChildReprints(newPath, oldPath, reprints);
}
// This object is reused in hasOpeningParen and hasClosingParen to avoid
// having to allocate a temporary object.
var reusablePos = { line: 1, column: 0 };
var nonSpaceExp = /\S/;
function hasOpeningParen(oldPath) {
var oldNode = oldPath.getValue();
var loc = oldNode.loc;
var lines = loc && loc.lines;
if (lines) {
var pos = reusablePos;
pos.line = loc.start.line;
pos.column = loc.start.column;
while (lines.prevPos(pos)) {
var ch = lines.charAt(pos);
if (ch === "(") {
// If we found an opening parenthesis but it occurred before
// the start of the original subtree for this reprinting, then
// we must not return true for hasOpeningParen(oldPath).
return comparePos(oldPath.getRootValue().loc.start, pos) <= 0;
}
if (nonSpaceExp.test(ch)) {
return false;
}
}
}
return false;
}
function hasClosingParen(oldPath) {
var oldNode = oldPath.getValue();
var loc = oldNode.loc;
var lines = loc && loc.lines;
if (lines) {
var pos = reusablePos;
pos.line = loc.end.line;
pos.column = loc.end.column;
do {
var ch = lines.charAt(pos);
if (ch === ")") {
// If we found a closing parenthesis but it occurred after the
// end of the original subtree for this reprinting, then we
// must not return true for hasClosingParen(oldPath).
return comparePos(pos, oldPath.getRootValue().loc.end) <= 0;
}
if (nonSpaceExp.test(ch)) {
return false;
}
} while (lines.nextPos(pos));
}
return false;
}
function hasParens(oldPath) {
// This logic can technically be fooled if the node has parentheses
// but there are comments intervening between the parentheses and the
// node. In such cases the node will be harmlessly wrapped in an
// additional layer of parentheses.
return hasOpeningParen(oldPath) && hasClosingParen(oldPath);
}
function findChildReprints(newPath, oldPath, reprints) {
var newNode = newPath.getValue();
var oldNode = oldPath.getValue();
isObject.assert(newNode);
isObject.assert(oldNode);
if (newNode.original === null) {
// If newNode.original node was set to null, reprint the node.
return false;
}
// If this type of node cannot come lexically first in its enclosing
// statement (e.g. a function expression or object literal), and it
// seems to be doing so, then the only way we can ignore this problem
// and save ourselves from falling back to the pretty printer is if an
// opening parenthesis happens to precede the node. For example,
// (function(){ ... }()); does not need to be reprinted, even though
// the FunctionExpression comes lexically first in the enclosing
// ExpressionStatement and fails the hasParens test, because the
// parent CallExpression passes the hasParens test. If we relied on
// the path.needsParens() && !hasParens(oldNode) check below, the
// absence of a closing parenthesis after the FunctionExpression would
// trigger pretty-printing unnecessarily.
if (!newPath.canBeFirstInStatement() &&
newPath.firstInStatement() &&
!hasOpeningParen(oldPath))
return false;
// If this node needs parentheses and will not be wrapped with
// parentheses when reprinted, then return false to skip reprinting
// and let it be printed generically.
if (newPath.needsParens(true) && !hasParens(oldPath)) {
return false;
}
var keys = util.getUnionOfKeys(oldNode, newNode);
if (oldNode.type === "File" ||
newNode.type === "File") {
// Don't bother traversing file.tokens, an often very large array
// returned by Babylon, and useless for our purposes.
delete keys.tokens;
}
// Don't bother traversing .loc objects looking for reprintable nodes.
delete keys.loc;
var originalReprintCount = reprints.length;
for (var k in keys) {
newPath.stack.push(k, types.getFieldValue(newNode, k));
oldPath.stack.push(k, types.getFieldValue(oldNode, k));
var canReprint = findAnyReprints(newPath, oldPath, reprints);
newPath.stack.length -= 2;
oldPath.stack.length -= 2;
if (!canReprint) {
return false;
}
}
// Return statements might end up running into ASI issues due to comments
// inserted deep within the tree, so reprint them if anything changed
// within them.
if (ReturnStatement.check(newPath.getNode()) &&
reprints.length > originalReprintCount) {
return false;
}
return true;
}
+2010
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
// This module was originally created so that Recast could add its own
// custom types to the AST type system (in particular, the File type), but
// those types are now incorporated into ast-types, so this module doesn't
// have much to do anymore. Still, it might prove useful in the future.
module.exports = require("ast-types");
+313
View File
@@ -0,0 +1,313 @@
var assert = require("assert");
var types = require("./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 === "File") {
// Babylon returns File nodes whose .loc.{start,end} do not include
// leading or trailing whitespace.
loc.start = lines.firstPos();
loc.end = lines.lastPos();
}
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;
};
+99
View File
@@ -0,0 +1,99 @@
var types = require("./lib/types");
var parse = require("./lib/parser").parse;
var Printer = require("./lib/printer").Printer;
function print(node, options) {
return new Printer(options).print(node);
}
function prettyPrint(node, options) {
return new Printer(options).printGenerically(node);
}
function run(transformer, options) {
return runFile(process.argv[2], transformer, options);
}
function runFile(path, transformer, options) {
require("fs").readFile(path, "utf-8", function(err, code) {
if (err) {
console.error(err);
return;
}
runString(code, transformer, options);
});
}
function defaultWriteback(output) {
process.stdout.write(output);
}
function runString(code, transformer, options) {
var writeback = options && options.writeback || defaultWriteback;
transformer(parse(code, options), function(node) {
writeback(print(node, options).code);
});
}
Object.defineProperties(exports, {
/**
* Parse a string of code into an augmented syntax tree suitable for
* arbitrary modification and reprinting.
*/
parse: {
enumerable: true,
value: parse
},
/**
* Traverse and potentially modify an abstract syntax tree using a
* convenient visitor syntax:
*
* recast.visit(ast, {
* names: [],
* visitIdentifier: function(path) {
* var node = path.value;
* this.visitor.names.push(node.name);
* this.traverse(path);
* }
* });
*/
visit: {
enumerable: true,
value: types.visit
},
/**
* Reprint a modified syntax tree using as much of the original source
* code as possible.
*/
print: {
enumerable: true,
value: print
},
/**
* Print without attempting to reuse any original source code.
*/
prettyPrint: {
enumerable: false,
value: prettyPrint
},
/**
* Customized version of require("ast-types").
*/
types: {
enumerable: false,
value: types
},
/**
* Convenient command-line interface (see e.g. example/add-braces).
*/
run: {
enumerable: false,
value: run
}
});
+110
View File
@@ -0,0 +1,110 @@
{
"_args": [
[
{
"raw": "recast@~0.11.12",
"scope": null,
"escapedName": "recast",
"name": "recast",
"rawSpec": "~0.11.12",
"spec": ">=0.11.12 <0.12.0",
"type": "range"
},
"c:\\xampp\\htdocs\\laravel\\node_modules\\es6-templates"
]
],
"_from": "recast@>=0.11.12 <0.12.0",
"_id": "recast@0.11.23",
"_inCache": true,
"_location": "/recast",
"_nodeVersion": "4.7.3",
"_npmOperationalInternal": {
"host": "packages-18-east.internal.npmjs.com",
"tmp": "tmp/recast-0.11.23.tgz_1489336675946_0.5593111158814281"
},
"_npmUser": {
"name": "benjamn",
"email": "bn@cs.stanford.edu"
},
"_npmVersion": "4.1.2",
"_phantomChildren": {},
"_requested": {
"raw": "recast@~0.11.12",
"scope": null,
"escapedName": "recast",
"name": "recast",
"rawSpec": "~0.11.12",
"spec": ">=0.11.12 <0.12.0",
"type": "range"
},
"_requiredBy": [
"/es6-templates"
],
"_resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz",
"_shasum": "451fd3004ab1e4df9b4e4b66376b2a21912462d3",
"_shrinkwrap": null,
"_spec": "recast@~0.11.12",
"_where": "c:\\xampp\\htdocs\\laravel\\node_modules\\es6-templates",
"author": {
"name": "Ben Newman",
"email": "bn@cs.stanford.edu"
},
"browser": {
"fs": false
},
"bugs": {
"url": "https://github.com/benjamn/recast/issues"
},
"dependencies": {
"ast-types": "0.9.6",
"esprima": "~3.1.0",
"private": "~0.1.5",
"source-map": "~0.5.0"
},
"description": "JavaScript syntax tree transformer, nondestructive pretty-printer, and automatic source map generator",
"devDependencies": {
"babylon": "~6.15.0",
"esprima-fb": "^15001.1001.0-dev-harmony-fb",
"mocha": "~3.1.2"
},
"directories": {},
"dist": {
"shasum": "451fd3004ab1e4df9b4e4b66376b2a21912462d3",
"tarball": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz"
},
"engines": {
"node": ">= 0.8"
},
"gitHead": "3a155b9610c9c3b813980934a5910a7edffc6667",
"homepage": "http://github.com/benjamn/recast",
"keywords": [
"ast",
"rewriting",
"refactoring",
"codegen",
"syntax",
"transformation",
"parsing",
"pretty-printing"
],
"license": "MIT",
"main": "main.js",
"maintainers": [
{
"name": "benjamn",
"email": "bn@cs.stanford.edu"
}
],
"name": "recast",
"optionalDependencies": {},
"readme": "ERROR: No README data found!",
"repository": {
"type": "git",
"url": "git://github.com/benjamn/recast.git"
},
"scripts": {
"debug": "node ./node_modules/mocha/bin/mocha --debug-brk --reporter spec",
"test": "node ./node_modules/mocha/bin/mocha --reporter spec --full-trace"
},
"version": "0.11.23"
}