prettier/src/common/util.js

734 lines
18 KiB
JavaScript

"use strict";
const stringWidth = require("string-width");
const emojiRegex = require("emoji-regex")();
const escapeStringRegexp = require("escape-string-regexp");
// eslint-disable-next-line no-control-regex
const notAsciiRegex = /[^\x20-\x7F]/;
function isExportDeclaration(node) {
if (node) {
switch (node.type) {
case "ExportDefaultDeclaration":
case "ExportDefaultSpecifier":
case "DeclareExportDeclaration":
case "ExportNamedDeclaration":
case "ExportAllDeclaration":
return true;
}
}
return false;
}
function getParentExportDeclaration(path) {
const parentNode = path.getParentNode();
if (path.getName() === "declaration" && isExportDeclaration(parentNode)) {
return parentNode;
}
return null;
}
function getPenultimate(arr) {
if (arr.length > 1) {
return arr[arr.length - 2];
}
return null;
}
function getLast(arr) {
if (arr.length > 0) {
return arr[arr.length - 1];
}
return null;
}
function skip(chars) {
return (text, index, opts) => {
const backwards = opts && opts.backwards;
// Allow `skip` functions to be threaded together without having
// to check for failures (did someone say monads?).
if (index === false) {
return false;
}
const length = text.length;
let cursor = index;
while (cursor >= 0 && cursor < length) {
const c = text.charAt(cursor);
if (chars instanceof RegExp) {
if (!chars.test(c)) {
return cursor;
}
} else if (chars.indexOf(c) === -1) {
return cursor;
}
backwards ? cursor-- : cursor++;
}
if (cursor === -1 || cursor === length) {
// If we reached the beginning or end of the file, return the
// out-of-bounds cursor. It's up to the caller to handle this
// correctly. We don't want to indicate `false` though if it
// actually skipped valid characters.
return cursor;
}
return false;
};
}
const skipWhitespace = skip(/\s/);
const skipSpaces = skip(" \t");
const skipToLineEnd = skip(",; \t");
const skipEverythingButNewLine = skip(/[^\r\n]/);
function skipInlineComment(text, index) {
if (index === false) {
return false;
}
if (text.charAt(index) === "/" && text.charAt(index + 1) === "*") {
for (let i = index + 2; i < text.length; ++i) {
if (text.charAt(i) === "*" && text.charAt(i + 1) === "/") {
return i + 2;
}
}
}
return index;
}
function skipTrailingComment(text, index) {
if (index === false) {
return false;
}
if (text.charAt(index) === "/" && text.charAt(index + 1) === "/") {
return skipEverythingButNewLine(text, index);
}
return index;
}
// This one doesn't use the above helper function because it wants to
// test \r\n in order and `skip` doesn't support ordering and we only
// want to skip one newline. It's simple to implement.
function skipNewline(text, index, opts) {
const backwards = opts && opts.backwards;
if (index === false) {
return false;
}
const atIndex = text.charAt(index);
if (backwards) {
if (text.charAt(index - 1) === "\r" && atIndex === "\n") {
return index - 2;
}
if (
atIndex === "\n" ||
atIndex === "\r" ||
atIndex === "\u2028" ||
atIndex === "\u2029"
) {
return index - 1;
}
} else {
if (atIndex === "\r" && text.charAt(index + 1) === "\n") {
return index + 2;
}
if (
atIndex === "\n" ||
atIndex === "\r" ||
atIndex === "\u2028" ||
atIndex === "\u2029"
) {
return index + 1;
}
}
return index;
}
function hasNewline(text, index, opts) {
opts = opts || {};
const idx = skipSpaces(text, opts.backwards ? index - 1 : index, opts);
const idx2 = skipNewline(text, idx, opts);
return idx !== idx2;
}
function hasNewlineInRange(text, start, end) {
for (let i = start; i < end; ++i) {
if (text.charAt(i) === "\n") {
return true;
}
}
return false;
}
// Note: this function doesn't ignore leading comments unlike isNextLineEmpty
function isPreviousLineEmpty(text, node, locStart) {
let idx = locStart(node) - 1;
idx = skipSpaces(text, idx, { backwards: true });
idx = skipNewline(text, idx, { backwards: true });
idx = skipSpaces(text, idx, { backwards: true });
const idx2 = skipNewline(text, idx, { backwards: true });
return idx !== idx2;
}
function isNextLineEmptyAfterIndex(text, index) {
let oldIdx = null;
let idx = index;
while (idx !== oldIdx) {
// We need to skip all the potential trailing inline comments
oldIdx = idx;
idx = skipToLineEnd(text, idx);
idx = skipInlineComment(text, idx);
idx = skipSpaces(text, idx);
}
idx = skipTrailingComment(text, idx);
idx = skipNewline(text, idx);
return hasNewline(text, idx);
}
function isNextLineEmpty(text, node, locEnd) {
return isNextLineEmptyAfterIndex(text, locEnd(node));
}
function getNextNonSpaceNonCommentCharacterIndexWithStartIndex(text, idx) {
let oldIdx = null;
while (idx !== oldIdx) {
oldIdx = idx;
idx = skipSpaces(text, idx);
idx = skipInlineComment(text, idx);
idx = skipTrailingComment(text, idx);
idx = skipNewline(text, idx);
}
return idx;
}
function getNextNonSpaceNonCommentCharacterIndex(text, node, locEnd) {
return getNextNonSpaceNonCommentCharacterIndexWithStartIndex(
text,
locEnd(node)
);
}
function getNextNonSpaceNonCommentCharacter(text, node, locEnd) {
return text.charAt(
getNextNonSpaceNonCommentCharacterIndex(text, node, locEnd)
);
}
function hasSpaces(text, index, opts) {
opts = opts || {};
const idx = skipSpaces(text, opts.backwards ? index - 1 : index, opts);
return idx !== index;
}
function setLocStart(node, index) {
if (node.range) {
node.range[0] = index;
} else {
node.start = index;
}
}
function setLocEnd(node, index) {
if (node.range) {
node.range[1] = index;
} else {
node.end = index;
}
}
const PRECEDENCE = {};
[
["|>"],
["||", "??"],
["&&"],
["|"],
["^"],
["&"],
["==", "===", "!=", "!=="],
["<", ">", "<=", ">=", "in", "instanceof"],
[">>", "<<", ">>>"],
["+", "-"],
["*", "/", "%"],
["**"]
].forEach((tier, i) => {
tier.forEach(op => {
PRECEDENCE[op] = i;
});
});
function getPrecedence(op) {
return PRECEDENCE[op];
}
const equalityOperators = {
"==": true,
"!=": true,
"===": true,
"!==": true
};
const multiplicativeOperators = {
"*": true,
"/": true,
"%": true
};
const bitshiftOperators = {
">>": true,
">>>": true,
"<<": true
};
function shouldFlatten(parentOp, nodeOp) {
if (getPrecedence(nodeOp) !== getPrecedence(parentOp)) {
return false;
}
// ** is right-associative
// x ** y ** z --> x ** (y ** z)
if (parentOp === "**") {
return false;
}
// x == y == z --> (x == y) == z
if (equalityOperators[parentOp] && equalityOperators[nodeOp]) {
return false;
}
// x * y % z --> (x * y) % z
if (
(nodeOp === "%" && multiplicativeOperators[parentOp]) ||
(parentOp === "%" && multiplicativeOperators[nodeOp])
) {
return false;
}
// x * y / z --> (x * y) / z
// x / y * z --> (x / y) * z
if (
nodeOp !== parentOp &&
multiplicativeOperators[nodeOp] &&
multiplicativeOperators[parentOp]
) {
return false;
}
// x << y << z --> (x << y) << z
if (bitshiftOperators[parentOp] && bitshiftOperators[nodeOp]) {
return false;
}
return true;
}
function isBitwiseOperator(operator) {
return (
!!bitshiftOperators[operator] ||
operator === "|" ||
operator === "^" ||
operator === "&"
);
}
// Tests if an expression starts with `{`, or (if forbidFunctionClassAndDoExpr
// holds) `function`, `class`, or `do {}`. Will be overzealous if there's
// already necessary grouping parentheses.
function startsWithNoLookaheadToken(node, forbidFunctionClassAndDoExpr) {
node = getLeftMost(node);
switch (node.type) {
case "FunctionExpression":
case "ClassExpression":
case "DoExpression":
return forbidFunctionClassAndDoExpr;
case "ObjectExpression":
return true;
case "MemberExpression":
return startsWithNoLookaheadToken(
node.object,
forbidFunctionClassAndDoExpr
);
case "TaggedTemplateExpression":
if (node.tag.type === "FunctionExpression") {
// IIFEs are always already parenthesized
return false;
}
return startsWithNoLookaheadToken(node.tag, forbidFunctionClassAndDoExpr);
case "CallExpression":
if (node.callee.type === "FunctionExpression") {
// IIFEs are always already parenthesized
return false;
}
return startsWithNoLookaheadToken(
node.callee,
forbidFunctionClassAndDoExpr
);
case "ConditionalExpression":
return startsWithNoLookaheadToken(
node.test,
forbidFunctionClassAndDoExpr
);
case "UpdateExpression":
return (
!node.prefix &&
startsWithNoLookaheadToken(node.argument, forbidFunctionClassAndDoExpr)
);
case "BindExpression":
return (
node.object &&
startsWithNoLookaheadToken(node.object, forbidFunctionClassAndDoExpr)
);
case "SequenceExpression":
return startsWithNoLookaheadToken(
node.expressions[0],
forbidFunctionClassAndDoExpr
);
case "TSAsExpression":
return startsWithNoLookaheadToken(
node.expression,
forbidFunctionClassAndDoExpr
);
default:
return false;
}
}
function getLeftMost(node) {
if (node.left) {
return getLeftMost(node.left);
}
return node;
}
function getAlignmentSize(value, tabWidth, startIndex) {
startIndex = startIndex || 0;
let size = 0;
for (let i = startIndex; i < value.length; ++i) {
if (value[i] === "\t") {
// Tabs behave in a way that they are aligned to the nearest
// multiple of tabWidth:
// 0 -> 4, 1 -> 4, 2 -> 4, 3 -> 4
// 4 -> 8, 5 -> 8, 6 -> 8, 7 -> 8 ...
size = size + tabWidth - (size % tabWidth);
} else {
size++;
}
}
return size;
}
function getIndentSize(value, tabWidth) {
const lastNewlineIndex = value.lastIndexOf("\n");
if (lastNewlineIndex === -1) {
return 0;
}
return getAlignmentSize(
// All the leading whitespaces
value.slice(lastNewlineIndex + 1).match(/^[ \t]*/)[0],
tabWidth
);
}
function getPreferredQuote(raw, preferredQuote) {
// `rawContent` is the string exactly like it appeared in the input source
// code, without its enclosing quotes.
const rawContent = raw.slice(1, -1);
const double = { quote: '"', regex: /"/g };
const single = { quote: "'", regex: /'/g };
const preferred = preferredQuote === "'" ? single : double;
const alternate = preferred === single ? double : single;
let result = preferred.quote;
// If `rawContent` contains at least one of the quote preferred for enclosing
// the string, we might want to enclose with the alternate quote instead, to
// minimize the number of escaped quotes.
if (
rawContent.includes(preferred.quote) ||
rawContent.includes(alternate.quote)
) {
const numPreferredQuotes = (rawContent.match(preferred.regex) || []).length;
const numAlternateQuotes = (rawContent.match(alternate.regex) || []).length;
result =
numPreferredQuotes > numAlternateQuotes
? alternate.quote
: preferred.quote;
}
return result;
}
function printString(raw, options, isDirectiveLiteral) {
// `rawContent` is the string exactly like it appeared in the input source
// code, without its enclosing quotes.
const rawContent = raw.slice(1, -1);
// Check for the alternate quote, to determine if we're allowed to swap
// the quotes on a DirectiveLiteral.
const canChangeDirectiveQuotes =
!rawContent.includes('"') && !rawContent.includes("'");
const enclosingQuote =
options.parser === "json"
? '"'
: options.__isInHtmlAttribute
? "'"
: getPreferredQuote(raw, options.singleQuote ? "'" : '"');
// Directives are exact code unit sequences, which means that you can't
// change the escape sequences they use.
// See https://github.com/prettier/prettier/issues/1555
// and https://tc39.github.io/ecma262/#directive-prologue
if (isDirectiveLiteral) {
if (canChangeDirectiveQuotes) {
return enclosingQuote + rawContent + enclosingQuote;
}
return raw;
}
// It might sound unnecessary to use `makeString` even if the string already
// is enclosed with `enclosingQuote`, but it isn't. The string could contain
// unnecessary escapes (such as in `"\'"`). Always using `makeString` makes
// sure that we consistently output the minimum amount of escaped quotes.
return makeString(
rawContent,
enclosingQuote,
!(
options.parser === "css" ||
options.parser === "less" ||
options.parser === "scss" ||
options.parentParser === "html" ||
options.parentParser === "vue" ||
options.parentParser === "angular"
)
);
}
function makeString(rawContent, enclosingQuote, unescapeUnnecessaryEscapes) {
const otherQuote = enclosingQuote === '"' ? "'" : '"';
// Matches _any_ escape and unescaped quotes (both single and double).
const regex = /\\([\s\S])|(['"])/g;
// Escape and unescape single and double quotes as needed to be able to
// enclose `rawContent` with `enclosingQuote`.
const newContent = rawContent.replace(regex, (match, escaped, quote) => {
// If we matched an escape, and the escaped character is a quote of the
// other type than we intend to enclose the string with, there's no need for
// it to be escaped, so return it _without_ the backslash.
if (escaped === otherQuote) {
return escaped;
}
// If we matched an unescaped quote and it is of the _same_ type as we
// intend to enclose the string with, it must be escaped, so return it with
// a backslash.
if (quote === enclosingQuote) {
return "\\" + quote;
}
if (quote) {
return quote;
}
// Unescape any unnecessarily escaped character.
// Adapted from https://github.com/eslint/eslint/blob/de0b4ad7bd820ade41b1f606008bea68683dc11a/lib/rules/no-useless-escape.js#L27
return unescapeUnnecessaryEscapes &&
/^[^\\nrvtbfux\r\n\u2028\u2029"'0-7]$/.test(escaped)
? escaped
: "\\" + escaped;
});
return enclosingQuote + newContent + enclosingQuote;
}
function printNumber(rawNumber) {
return (
rawNumber
.toLowerCase()
// Remove unnecessary plus and zeroes from scientific notation.
.replace(/^([+-]?[\d.]+e)(?:\+|(-))?0*(\d)/, "$1$2$3")
// Remove unnecessary scientific notation (1e0).
.replace(/^([+-]?[\d.]+)e[+-]?0+$/, "$1")
// Make sure numbers always start with a digit.
.replace(/^([+-])?\./, "$10.")
// Remove extraneous trailing decimal zeroes.
.replace(/(\.\d+?)0+(?=e|$)/, "$1")
// Remove trailing dot.
.replace(/\.(?=e|$)/, "")
);
}
function getMaxContinuousCount(str, target) {
const results = str.match(
new RegExp(`(${escapeStringRegexp(target)})+`, "g")
);
if (results === null) {
return 0;
}
return results.reduce(
(maxCount, result) => Math.max(maxCount, result.length / target.length),
0
);
}
function getStringWidth(text) {
if (!text) {
return 0;
}
// shortcut to avoid needless string `RegExp`s, replacements, and allocations within `string-width`
if (!notAsciiRegex.test(text)) {
return text.length;
}
// emojis are considered 2-char width for consistency
// see https://github.com/sindresorhus/string-width/issues/11
// for the reason why not implemented in `string-width`
return stringWidth(text.replace(emojiRegex, " "));
}
function hasIgnoreComment(path) {
const node = path.getValue();
return hasNodeIgnoreComment(node);
}
function hasNodeIgnoreComment(node) {
return (
node &&
node.comments &&
node.comments.length > 0 &&
node.comments.some(comment => comment.value.trim() === "prettier-ignore")
);
}
function matchAncestorTypes(path, types, index) {
index = index || 0;
types = types.slice();
while (types.length) {
const parent = path.getParentNode(index);
const type = types.shift();
if (!parent || parent.type !== type) {
return false;
}
index++;
}
return true;
}
function addCommentHelper(node, comment) {
const comments = node.comments || (node.comments = []);
comments.push(comment);
comment.printed = false;
// For some reason, TypeScript parses `// x` inside of JSXText as a comment
// We already "print" it via the raw text, we don't need to re-print it as a
// comment
if (node.type === "JSXText") {
comment.printed = true;
}
}
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 isWithinParentArrayProperty(path, propertyName) {
const node = path.getValue();
const parent = path.getParentNode();
if (parent == null) {
return false;
}
if (!Array.isArray(parent[propertyName])) {
return false;
}
const key = path.getName();
return parent[propertyName][key] === node;
}
function replaceEndOfLineWith(text, replacement) {
const parts = [];
for (const part of text.split("\n")) {
if (parts.length !== 0) {
parts.push(replacement);
}
parts.push(part);
}
return parts;
}
module.exports = {
replaceEndOfLineWith,
getStringWidth,
getMaxContinuousCount,
getPrecedence,
shouldFlatten,
isBitwiseOperator,
isExportDeclaration,
getParentExportDeclaration,
getPenultimate,
getLast,
getNextNonSpaceNonCommentCharacterIndexWithStartIndex,
getNextNonSpaceNonCommentCharacterIndex,
getNextNonSpaceNonCommentCharacter,
skip,
skipWhitespace,
skipSpaces,
skipToLineEnd,
skipEverythingButNewLine,
skipInlineComment,
skipTrailingComment,
skipNewline,
isNextLineEmptyAfterIndex,
isNextLineEmpty,
isPreviousLineEmpty,
hasNewline,
hasNewlineInRange,
hasSpaces,
setLocStart,
setLocEnd,
startsWithNoLookaheadToken,
getAlignmentSize,
getIndentSize,
getPreferredQuote,
printString,
printNumber,
hasIgnoreComment,
hasNodeIgnoreComment,
makeString,
matchAncestorTypes,
addLeadingComment,
addDanglingComment,
addTrailingComment,
isWithinParentArrayProperty
};