"use strict"; function isExportDeclaration(node) { if (node) { switch (node.type) { case "ExportDeclaration": 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) { 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 isNextLineEmpty(text, node) { let oldIdx = null; let idx = locEnd(node); 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 getNextNonSpaceNonCommentCharacter(text, node) { let oldIdx = null; let idx = locEnd(node); while (idx !== oldIdx) { oldIdx = idx; idx = skipSpaces(text, idx); idx = skipInlineComment(text, idx); idx = skipTrailingComment(text, idx); idx = skipNewline(text, idx); } return text.charAt(idx); } function hasSpaces(text, index, opts) { opts = opts || {}; const idx = skipSpaces(text, opts.backwards ? index - 1 : index, opts); return idx !== index; } function locStart(node) { if (node.decorators && node.decorators.length > 0) { return locStart(node.decorators[0]); } if (node.range) { return node.range[0]; } if (typeof node.start === "number") { return node.start; } if (node.source) { return lineColumnToIndex(node.source.start, node.source.input.css) - 1; } } function locEnd(node) { if (node.range) { return node.range[1]; } if (typeof node.end === "number") { return node.end; } if (node.source) { return lineColumnToIndex(node.source.end, node.source.input.css); } } // Super inefficient, needs to be cached. function lineColumnToIndex(lineColumn, text) { let index = 0; for (let i = 0; i < lineColumn.line - 1; ++i) { index = text.indexOf("\n", index) + 1; if (index === -1) { return -1; } } return index + lineColumn.column; } 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]; } // Tests if an expression starts with `{`, or (if forbidFunctionAndClass holds) `function` or `class`. // Will be overzealous if there's already necessary grouping parentheses. function startsWithNoLookaheadToken(node, forbidFunctionAndClass) { node = getLeftMost(node); switch (node.type) { case "FunctionExpression": case "ClassExpression": return forbidFunctionAndClass; case "ObjectExpression": return true; case "MemberExpression": return startsWithNoLookaheadToken(node.object, forbidFunctionAndClass); case "TaggedTemplateExpression": if (node.tag.type === "FunctionExpression") { // IIFEs are always already parenthesized return false; } return startsWithNoLookaheadToken(node.tag, forbidFunctionAndClass); case "CallExpression": if (node.callee.type === "FunctionExpression") { // IIFEs are always already parenthesized return false; } return startsWithNoLookaheadToken(node.callee, forbidFunctionAndClass); case "ConditionalExpression": return startsWithNoLookaheadToken(node.test, forbidFunctionAndClass); case "UpdateExpression": return ( !node.prefix && startsWithNoLookaheadToken(node.argument, forbidFunctionAndClass) ); case "BindExpression": return ( node.object && startsWithNoLookaheadToken(node.object, forbidFunctionAndClass) ); case "SequenceExpression": return startsWithNoLookaheadToken( node.expressions[0], forbidFunctionAndClass ); default: return false; } } function getLeftMost(node) { if (node.left) { return getLeftMost(node.left); } else { return node; } } function hasBlockComments(node) { return node.comments && node.comments.some(isBlockComment); } function isBlockComment(comment) { return comment.type === "Block" || comment.type === "CommentBlock"; } 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; } module.exports = { getPrecedence, isExportDeclaration, getParentExportDeclaration, getPenultimate, getLast, getNextNonSpaceNonCommentCharacter, skipWhitespace, skipSpaces, skipNewline, isNextLineEmpty, isPreviousLineEmpty, hasNewline, hasNewlineInRange, hasSpaces, locStart, locEnd, setLocStart, setLocEnd, startsWithNoLookaheadToken, hasBlockComments, isBlockComment, getAlignmentSize };