From 77310c49ea01fa4b9aa3ba3e0f26f37561eff6d2 Mon Sep 17 00:00:00 2001 From: Alex Rattray Date: Sun, 17 Sep 2023 10:30:18 -0400 Subject: [PATCH] Refactor simple member expressions etc (#15423) --- src/language-js/print/assignment.js | 45 +------ src/language-js/print/estree.js | 3 +- src/language-js/print/literal.js | 20 +-- src/language-js/utils/index.js | 197 +++++++++++++++++++++------- 4 files changed, 154 insertions(+), 111 deletions(-) diff --git a/src/language-js/print/assignment.js b/src/language-js/print/assignment.js index b77519488..a12a45df2 100644 --- a/src/language-js/print/assignment.js +++ b/src/language-js/print/assignment.js @@ -16,15 +16,12 @@ import { isCallExpression, isMemberExpression, getCallArguments, - rawText, - hasComment, - isSignedNumericLiteral, + isLoneShortArgument, isObjectProperty, createTypeCheckFunction, } from "../utils/index.js"; import { shouldInlineLogicalExpression } from "./binaryish.js"; import { printCallExpression } from "./call-expression.js"; -import { isLiteral } from "./literal.js"; function printAssignment( path, @@ -369,46 +366,6 @@ function isPoorlyBreakableMemberOrCallChain( return deep && (node.type === "Identifier" || node.type === "ThisExpression"); } -const LONE_SHORT_ARGUMENT_THRESHOLD_RATE = 0.25; - -function isLoneShortArgument(node, { printWidth }) { - if (hasComment(node)) { - return false; - } - - const threshold = printWidth * LONE_SHORT_ARGUMENT_THRESHOLD_RATE; - - if ( - node.type === "ThisExpression" || - (node.type === "Identifier" && node.name.length <= threshold) || - (isSignedNumericLiteral(node) && !hasComment(node.argument)) - ) { - return true; - } - - const regexpPattern = - (node.type === "Literal" && "regex" in node && node.regex.pattern) || - (node.type === "RegExpLiteral" && node.pattern); - - if (regexpPattern) { - return regexpPattern.length <= threshold; - } - - if (isStringLiteral(node)) { - return rawText(node).length <= threshold; - } - - if (node.type === "TemplateLiteral") { - return ( - node.expressions.length === 0 && - node.quasis[0].value.raw.length <= threshold && - !node.quasis[0].value.raw.includes("\n") - ); - } - - return isLiteral(node); -} - function isObjectPropertyWithShortKey(node, keyDoc, options) { if (!isObjectProperty(node)) { return false; diff --git a/src/language-js/print/estree.js b/src/language-js/print/estree.js index 319a25091..6d872ce4c 100644 --- a/src/language-js/print/estree.js +++ b/src/language-js/print/estree.js @@ -19,6 +19,7 @@ import { isCallExpression, isMemberExpression, isArrayOrTupleExpression, + isLiteral, isObjectOrRecordExpression, startsWithNoLookaheadToken, } from "../utils/index.js"; @@ -67,7 +68,7 @@ import { printBinaryishExpression } from "./binaryish.js"; import { printStatementSequence } from "./statement.js"; import { printMemberExpression } from "./member.js"; import { printBlock, printBlockBody } from "./block.js"; -import { printLiteral, isLiteral } from "./literal.js"; +import { printLiteral } from "./literal.js"; import { printTypeAnnotationProperty } from "./type-annotation.js"; import { printExpressionStatement } from "./expression-statement.js"; import { printHtmlBinding } from "./html-binding.js"; diff --git a/src/language-js/print/literal.js b/src/language-js/print/literal.js index 3b61e61d7..533dcc0b3 100644 --- a/src/language-js/print/literal.js +++ b/src/language-js/print/literal.js @@ -1,7 +1,6 @@ import printString from "../../utils/print-string.js"; import printNumber from "../../utils/print-number.js"; import { replaceEndOfLine } from "../../document/utils.js"; -import { createTypeCheckFunction } from "../utils/index.js"; /** * @typedef {import("../types/estree.js").Node} Node @@ -92,21 +91,4 @@ function printDirective(rawText, options) { return enclosingQuote + rawContent + enclosingQuote; } -/** - * @param {Node} node - * @returns {boolean} - */ -const isLiteral = createTypeCheckFunction([ - "Literal", - // Babel, flow uses `BigIntLiteral` too - "BigIntLiteral", - "BooleanLiteral", - "DecimalLiteral", - "DirectiveLiteral", - "NullLiteral", - "NumericLiteral", - "RegExpLiteral", - "StringLiteral", -]); - -export { printLiteral, printBigInt, isLiteral }; +export { printLiteral, printBigInt }; diff --git a/src/language-js/utils/index.js b/src/language-js/utils/index.js index 9bbacc965..754c0a99e 100644 --- a/src/language-js/utils/index.js +++ b/src/language-js/utils/index.js @@ -189,6 +189,35 @@ function isRegExpLiteral(node) { ); } +/** + * @param {Node} node + * @returns {boolean} + */ +const isLiteral = createTypeCheckFunction([ + "Literal", + "BooleanLiteral", + "BigIntLiteral", // Babel, flow use `BigIntLiteral` too + "DecimalLiteral", + "DirectiveLiteral", + "NullLiteral", + "NumericLiteral", + "RegExpLiteral", + "StringLiteral", +]); + +/** + * @param {Node} node + * @returns {boolean} + */ +const isSingleWordType = createTypeCheckFunction([ + "Identifier", + "ThisExpression", + "Super", + "PrivateName", + "PrivateIdentifier", + "Import", +]); + /** * @param {Node} node * @returns {boolean} @@ -442,47 +471,127 @@ function isSimpleTemplateLiteral(node) { } return expressions.every((expr) => { - // Disallow comments since printDocToString can't print them here - if (hasComment(expr)) { - return false; - } - - // Allow `x` and `this` - if (expr.type === "Identifier" || expr.type === "ThisExpression") { + if (isSimpleAtomicExpression(expr) || isSimpleMemberExpression(expr)) { return true; } + }); +} - if (expr.type === "ChainExpression") { - expr = expr.expression; - } - - // Allow `a.b.c`, `a.b[c]`, and `this.x.y` - if (isMemberExpression(expr)) { - let head = expr; - while (isMemberExpression(head)) { - if ( - head.property.type !== "Identifier" && - head.property.type !== "Literal" && - head.property.type !== "StringLiteral" && - head.property.type !== "NumericLiteral" - ) { - return false; - } - head = head.object; - if (hasComment(head)) { - return false; - } - } - - if (head.type === "Identifier" || head.type === "ThisExpression") { - return true; - } +function isSimpleMemberExpression( + node, + { maxDepth = Number.POSITIVE_INFINITY } = {}, +) { + if (hasComment(node)) { + return false; + } + if (node.type === "ChainExpression") { + return isSimpleMemberExpression(node.expression, { maxDepth }); + } + if (!isMemberExpression(node)) { + return false; + } + let head = node; + let depth = 0; + while (isMemberExpression(head) && depth++ <= maxDepth) { + if (!isSimpleAtomicExpression(head.property)) { return false; } + head = head.object; + if (hasComment(head)) { + return false; + } + } + return isSimpleAtomicExpression(head); +} +/** + * This is intended to return true for small expressions + * which cannot be broken. + */ +function isSimpleAtomicExpression(node) { + if (hasComment(node)) { return false; - }); + } + return isLiteral(node) || isSingleWordType(node); +} + +/** + * Attempts to gauge the rough complexity of a node, for example + * to detect deeply-nested booleans, call expressions with lots of arguments, etc. + */ +function isSimpleExpressionByNodeCount(node, maxInnerNodeCount = 5) { + const count = getExpressionInnerNodeCount(node, maxInnerNodeCount); + return count <= maxInnerNodeCount; +} + +function getExpressionInnerNodeCount(node, maxCount) { + let count = 0; + for (const k in node) { + const prop = node[k]; + + if (prop && typeof prop === "object" && typeof prop.type === "string") { + count++; + count += getExpressionInnerNodeCount(prop, maxCount - count); + } + + // Bail early to protect against bad performance. + if (count > maxCount) { + return count; + } + } + return count; +} + +const LONE_SHORT_ARGUMENT_THRESHOLD_RATE = 0.25; +function isLoneShortArgument(node, { printWidth }) { + if (hasComment(node)) { + return false; + } + + const threshold = printWidth * LONE_SHORT_ARGUMENT_THRESHOLD_RATE; + + if ( + node.type === "ThisExpression" || + (node.type === "Identifier" && node.name.length <= threshold) || + (isSignedNumericLiteral(node) && !hasComment(node.argument)) + ) { + return true; + } + + const regexpPattern = + (node.type === "Literal" && "regex" in node && node.regex.pattern) || + (node.type === "RegExpLiteral" && node.pattern); + + if (regexpPattern) { + return regexpPattern.length <= threshold; + } + + if (isStringLiteral(node)) { + return rawText(node).length <= threshold; + } + + if (node.type === "TemplateLiteral") { + return ( + node.expressions.length === 0 && + node.quasis[0].value.raw.length <= threshold && + !node.quasis[0].value.raw.includes("\n") + ); + } + + if (node.type === "UnaryExpression") { + return isLoneShortArgument(node.argument, { printWidth }); + } + + if ( + node.type === "CallExpression" && + node.arguments.length === 0 && + node.callee.type === "Identifier" + ) { + return node.callee.name.length <= threshold - 2; + } + + return isLiteral(node); } /** @@ -654,20 +763,9 @@ function isSimpleCallArgument(node, depth = 2) { } if ( - node.type === "Literal" || - node.type === "BigIntLiteral" || - node.type === "DecimalLiteral" || - node.type === "BooleanLiteral" || - node.type === "NullLiteral" || - node.type === "NumericLiteral" || - node.type === "StringLiteral" || - node.type === "Identifier" || - node.type === "ThisExpression" || - node.type === "Super" || - node.type === "PrivateName" || - node.type === "PrivateIdentifier" || - node.type === "ArgumentPlaceholder" || - node.type === "Import" + isLiteral(node) || + isSingleWordType(node) || + node.type === "ArgumentPlaceholder" ) { return true; } @@ -1151,8 +1249,13 @@ export { isRegExpLiteral, isSimpleType, isSimpleNumber, + isSimpleAtomicExpression, + isSimpleMemberExpression, isSimpleTemplateLiteral, + isSimpleExpressionByNodeCount, + isLoneShortArgument, isStringLiteral, + isLiteral, isStringPropSafeToUnquote, isTemplateOnItsOwnLine, isTestCall,