react-scripts
Version:
Configuration and scripts for Create React App.
369 lines (307 loc) • 11.6 kB
JavaScript
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 comparePos = require("./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, resultArray) {
if (!node) {
return;
}
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]], 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) {
var childNodes = getSortedChildNodes(node);
// 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);
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);
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);
Comments.forNode(pn).addTrailing(comment);
} else if (fn) {
// No contest: we have a leading comment.
breakTies(tiesToBreak, lines);
Comments.forNode(fn).addLeading(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);
Comments.forNode(en).addDangling(comment);
} else {
throw new Error("AST contains no nodes at all?");
}
});
breakTies(tiesToBreak, lines);
};
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.loc.start.column > fn.loc.start.column) {
++indexOfFirstLeadingComment;
}
tiesToBreak.forEach(function(comment, i) {
if (i < indexOfFirstLeadingComment) {
Comments.forNode(pn).addTrailing(comment);
} else {
Comments.forNode(fn).addLeading(comment);
}
});
tiesToBreak.length = 0;
}
function Comments() {
assert.ok(this instanceof Comments);
this.leading = [];
this.dangling = [];
this.trailing = [];
}
var Cp = Comments.prototype;
Comments.forNode = function forNode(node) {
var comments = node.comments;
if (!comments) {
Object.defineProperty(node, "comments", {
value: comments = new Comments,
enumerable: false
});
}
return comments;
};
Cp.forEach = function forEach(callback, context) {
this.leading.forEach(callback, context);
// this.dangling.forEach(callback, context);
this.trailing.forEach(callback, context);
};
Cp.addLeading = function addLeading(comment) {
this.leading.push(comment);
};
Cp.addDangling = function addDangling(comment) {
this.dangling.push(comment);
};
Cp.addTrailing = function addTrailing(comment) {
comment.trailing = true;
if (comment.type === "Block") {
this.trailing.push(comment);
} else {
this.leading.push(comment);
}
};
/**
* @param {Object} options - Options object that configures printing.
*/
function printLeadingComment(comment, options) {
var loc = comment.loc;
var lines = loc && loc.lines;
var parts = [];
if (comment.type === "Block") {
parts.push("/*", fromString(comment.value, options), "*/");
} else if (comment.type === "Line") {
parts.push("//", fromString(comment.value, options));
} else assert.fail(comment.type);
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).stripMargin(loc ? loc.start.column : 0);
}
/**
* @param {Object} options - Options object that configures printing.
*/
function printTrailingComment(comment, options) {
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"));
}
}
if (comment.type === "Block") {
parts.push("/*", fromString(comment.value, options), "*/");
} else if (comment.type === "Line") {
parts.push("//", fromString(comment.value, options), "\n");
} else assert.fail(comment.type);
return concat(parts).stripMargin(
loc ? loc.start.column : 0,
true // Skip the first line, in case there were leading spaces.
);
}
/**
* @param {Object} options - Options object that configures printing.
*/
exports.printComments = function(comments, innerLines, options) {
if (innerLines) {
assert.ok(innerLines instanceof Lines);
} else {
innerLines = fromString("");
}
if (!comments || !(comments.leading.length +
comments.trailing.length)) {
return innerLines;
}
var parts = [];
comments.leading.forEach(function(comment) {
parts.push(printLeadingComment(comment, options));
});
parts.push(innerLines);
comments.trailing.forEach(function(comment) {
assert.strictEqual(comment.type, "Block");
parts.push(printTrailingComment(comment, options));
});
return concat(parts);
};