diff --git a/src/common/ast-path.js b/src/common/ast-path.js index 8d2f16c6e..7f44a3780 100644 --- a/src/common/ast-path.js +++ b/src/common/ast-path.js @@ -61,6 +61,10 @@ class AstPath { return this.stack[0]; } + get ancestors() { + return [...this.#getAncestors()]; + } + // The name of the current property is always the penultimate element of // this.stack, and always a string/number/symbol. getName() { @@ -208,30 +212,43 @@ class AstPath { * Traverses the ancestors of the current node heading toward the tree root * until it finds a node that matches the provided predicate function. Will * return the first matching ancestor. If no such node exists, returns undefined. - * @param {(node: any, name: string, number: number | null) => boolean} predicate + * @param {(node: any) => boolean} predicate * @internal Unstable API. Don't use in plugins for now. */ findAncestor(predicate) { - let stackPointer = this.stack.length - 1; - - let name = null; - let node = this.stack[stackPointer--]; - - while (node) { - // skip index/array - let number = null; - if (typeof name === "number") { - number = name; - name = this.stack[stackPointer--]; - node = this.stack[stackPointer--]; - } - - if (name !== null && predicate(node, name, number)) { + for (const node of this.#getAncestors()) { + if (predicate(node)) { return node; } + } + } - name = this.stack[stackPointer--]; - node = this.stack[stackPointer--]; + /** + * Traverses the ancestors of the current node heading toward the tree root + * until it finds a node that matches the provided predicate function. + * returns true if matched node found. + * @param {(node: any) => boolean} predicate + * @returns {boolean} + * @internal Unstable API. Don't use in plugins for now. + */ + hasAncestor(predicate) { + for (const node of this.#getAncestors()) { + if (predicate(node)) { + return true; + } + } + + return false; + } + + *#getAncestors() { + const { stack } = this; + + for (let index = stack.length - 3; index >= 0; index -= 2) { + const value = stack[index]; + if (!Array.isArray(value)) { + yield value; + } } } } diff --git a/src/language-css/printer-postcss.js b/src/language-css/printer-postcss.js index 5d4110148..a1baafc2d 100644 --- a/src/language-css/printer-postcss.js +++ b/src/language-css/printer-postcss.js @@ -26,7 +26,6 @@ import { insertPragma } from "./pragma.js"; import getVisitorKeys from "./get-visitor-keys.js"; import { - getAncestorNode, getPropOfDeclNode, maybeToLowerCase, insideValueFunctionNode, @@ -356,7 +355,11 @@ function genericPrint(path, options, print) { case "selector-root": return group([ insideAtRuleNode(path, "custom-selector") - ? [getAncestorNode(path, "css-atrule").customSelector, line] + ? [ + path.findAncestor((node) => node.type === "css-atrule") + .customSelector, + line, + ] : "", join( [ @@ -464,7 +467,9 @@ function genericPrint(path, options, print) { return node.value; case "selector-unknown": { - const ruleAncestorNode = getAncestorNode(path, "css-rule"); + const ruleAncestorNode = path.findAncestor( + (node) => node.type === "css-rule" + ); // Nested SCSS property if (ruleAncestorNode?.isSCSSNesterProperty) { @@ -516,7 +521,9 @@ function genericPrint(path, options, print) { parentNode.type === "value-value" && (declAncestorProp === "grid" || declAncestorProp.startsWith("grid-template")); - const atRuleAncestorNode = getAncestorNode(path, "css-atrule"); + const atRuleAncestorNode = path.findAncestor( + (node) => node.type === "css-atrule" + ); const isControlDirective = atRuleAncestorNode && isSCSSControlDirectiveNode(atRuleAncestorNode, options); diff --git a/src/language-css/utils/index.js b/src/language-css/utils/index.js index 70b1ea60a..4514e4da8 100644 --- a/src/language-css/utils/index.js +++ b/src/language-css/utils/index.js @@ -26,14 +26,10 @@ const colorAdjusterFunctions = new Set([ "hwba", ]); -function getAncestorNode(path, type) { - return path.findAncestor((node) => node.type === type); -} - function getPropOfDeclNode(path) { - const declAncestorNode = getAncestorNode(path, "css-decl"); - - return declAncestorNode?.prop?.toLowerCase(); + return path + .findAncestor((node) => node.type === "css-decl") + ?.prop?.toLowerCase(); } const wideKeywords = new Set(["initial", "inherit", "unset", "revert"]); @@ -42,7 +38,9 @@ function isWideKeywords(value) { } function isKeyframeAtRuleKeywords(path, value) { - const atRuleAncestorNode = getAncestorNode(path, "css-atrule"); + const atRuleAncestorNode = path.findAncestor( + (node) => node.type === "css-atrule" + ); return ( atRuleAncestorNode?.name?.toLowerCase().endsWith("keyframes") && ["from", "to"].includes(value.toLowerCase()) @@ -62,12 +60,16 @@ function maybeToLowerCase(value) { } function insideValueFunctionNode(path, functionName) { - const funcAncestorNode = getAncestorNode(path, "value-func"); + const funcAncestorNode = path.findAncestor( + (node) => node.type === "value-func" + ); return funcAncestorNode?.value?.toLowerCase() === functionName; } function insideICSSRuleNode(path) { - const ruleAncestorNode = getAncestorNode(path, "css-rule"); + const ruleAncestorNode = path.findAncestor( + (node) => node.type === "css-rule" + ); const selector = ruleAncestorNode?.raws?.selector; return ( @@ -80,7 +82,9 @@ function insideAtRuleNode(path, atRuleNameOrAtRuleNames) { const atRuleNames = Array.isArray(atRuleNameOrAtRuleNames) ? atRuleNameOrAtRuleNames : [atRuleNameOrAtRuleNames]; - const atRuleAncestorNode = getAncestorNode(path, "css-atrule"); + const atRuleAncestorNode = path.findAncestor( + (node) => node.type === "css-atrule" + ); return ( atRuleAncestorNode && @@ -90,12 +94,10 @@ function insideAtRuleNode(path, atRuleNameOrAtRuleNames) { function insideURLFunctionInImportAtRuleNode(path) { const { node } = path; - const atRuleAncestorNode = getAncestorNode(path, "css-atrule"); - return ( - atRuleAncestorNode?.name === "import" && node.groups[0].value === "url" && - node.groups.length === 2 + node.groups.length === 2 && + path.findAncestor((node) => node.type === "css-atrule")?.name === "import" ); } @@ -271,7 +273,7 @@ function isSCSSMapItemNode(path, options) { return false; } - const declNode = getAncestorNode(path, "css-decl"); + const declNode = path.findAncestor((node) => node.type === "css-decl"); // SCSS map declaration (i.e. `$map: (key: value, other-key: other-value)`) if (declNode?.prop?.startsWith("$")) { @@ -384,7 +386,6 @@ function isParenGroupNode(node) { } export { - getAncestorNode, getPropOfDeclNode, maybeToLowerCase, insideValueFunctionNode, diff --git a/src/language-handlebars/printer-glimmer.js b/src/language-handlebars/printer-glimmer.js index c98adb99e..f35fa90b3 100644 --- a/src/language-handlebars/printer-glimmer.js +++ b/src/language-handlebars/printer-glimmer.js @@ -708,20 +708,14 @@ function printStringLiteral(stringLiteral, favoriteQuote) { } function needsOppositeQuote(path) { - let index = 0; - let ancestor = path.getParentNode(index); - while (ancestor?.type === "SubExpression") { - index++; - ancestor = path.getParentNode(index); - } - if ( - ancestor && - path.getParentNode(index + 1).type === "ConcatStatement" && - path.getParentNode(index + 2).type === "AttrNode" - ) { - return true; - } - return false; + const { ancestors } = path; + const level = ancestors.findIndex((node) => node.type !== "SubExpression"); + + return ( + level !== -1 && + ancestors[level + 1].type === "ConcatStatement" && + ancestors[level + 2].type === "AttrNode" + ); } /* SubExpression print helpers */ diff --git a/src/language-html/print/element.js b/src/language-html/print/element.js index 2375120d2..0b0723755 100644 --- a/src/language-html/print/element.js +++ b/src/language-html/print/element.js @@ -14,7 +14,6 @@ import { shouldPreserveContent, isScriptLikeTag, isVueCustomBlock, - countParents, forceBreakContent, } from "../utils/index.js"; import { @@ -139,13 +138,7 @@ function printElement(path, options, print) { node.isWhitespaceSensitive && node.isIndentationSensitive)) && new RegExp( - `\\n[\\t ]{${ - options.tabWidth * - countParents( - path, - (node) => node.parent && node.parent.type !== "root" - ) - }}$` + `\\n[\\t ]{${options.tabWidth * (path.ancestors.length - 1)}}$` ).test(node.lastChild.value) ) { return ""; diff --git a/src/language-html/utils/index.js b/src/language-html/utils/index.js index a120716c1..de6f4b73f 100644 --- a/src/language-html/utils/index.js +++ b/src/language-html/utils/index.js @@ -443,26 +443,6 @@ function isPreLikeNode(node) { return getNodeCssStyleWhiteSpace(node).startsWith("pre"); } -/** - * @param {AstPath} path - * @param {(any) => boolean} predicate - */ -function countParents(path, predicate) { - let counter = 0; - for (let i = path.stack.length - 1; i >= 0; i--) { - const value = path.stack[i]; - if ( - value && - typeof value === "object" && - !Array.isArray(value) && - predicate(value) - ) { - counter++; - } - } - return counter; -} - function hasParent(node, fn) { let current = node; @@ -646,7 +626,6 @@ export { getLeadingAndTrailingHtmlWhitespace, canHaveInterpolation, countChars, - countParents, dedentString, forceBreakChildren, forceBreakContent, diff --git a/src/language-js/print/member.js b/src/language-js/print/member.js index 30b0b21c2..912e43e66 100644 --- a/src/language-js/print/member.js +++ b/src/language-js/print/member.js @@ -7,22 +7,12 @@ import { import { printOptionalToken } from "./misc.js"; function printMemberExpression(path, options, print) { - const { node } = path; - - const { parent } = path; - let firstNonMemberParent; - let i = 0; - do { - firstNonMemberParent = path.getParentNode(i); - i++; - } while ( - firstNonMemberParent && - (isMemberExpression(firstNonMemberParent) || - firstNonMemberParent.type === "TSNonNullExpression") - ); - const objectDoc = print("object"); const lookupDoc = printMemberLookup(path, options, print); + const { node, parent } = path; + const firstNonMemberParent = path.findAncestor( + (node) => !(isMemberExpression(node) || node.type === "TSNonNullExpression") + ); const shouldInline = (firstNonMemberParent && diff --git a/src/language-markdown/print-whitespace.js b/src/language-markdown/print-whitespace.js index 7a865b055..00172b689 100644 --- a/src/language-markdown/print-whitespace.js +++ b/src/language-markdown/print-whitespace.js @@ -4,7 +4,6 @@ import { KIND_CJ_LETTER, KIND_K_LETTER, KIND_NON_CJK, - getAncestorNode, } from "./utils.js"; /** @@ -18,7 +17,12 @@ import { * `word`. */ -const SINGLE_LINE_NODE_TYPES = ["heading", "tableCell", "link", "wikiLink"]; +const SINGLE_LINE_NODE_TYPES = new Set([ + "heading", + "tableCell", + "link", + "wikiLink", +]); /** * These characters must not immediately precede a line break. @@ -218,7 +222,10 @@ function isNonCJKOrKoreanLetter(kind) { * @returns {boolean} */ function isBreakable(path, value, proseWrap, isLink, canBeSpace) { - if (proseWrap !== "always" || getAncestorNode(path, SINGLE_LINE_NODE_TYPES)) { + if ( + proseWrap !== "always" || + path.hasAncestor((node) => SINGLE_LINE_NODE_TYPES.has(node.type)) + ) { return false; } diff --git a/src/language-markdown/printer-markdown.js b/src/language-markdown/printer-markdown.js index f571f6e03..09130ab1c 100644 --- a/src/language-markdown/printer-markdown.js +++ b/src/language-markdown/printer-markdown.js @@ -35,8 +35,6 @@ import { INLINE_NODE_TYPES, INLINE_NODE_WRAPPER_TYPES, isAutolink, - getAncestorNode, - getAncestorCounter, } from "./utils.js"; import getVisitorKeys from "./get-visitor-keys.js"; import { printWhitespace } from "./print-whitespace.js"; @@ -151,7 +149,10 @@ function genericPrint(path, options, print) { next.children[0]?.type === "word" && !next.children[0].hasLeadingPunctuation); style = - hasPrevOrNextWord || getAncestorNode(path, "emphasis") ? "*" : "_"; + hasPrevOrNextWord || + path.hasAncestor((node) => node.type === "emphasis") + ? "*" + : "_"; } return [style, printChildren(path, options, print), style]; } @@ -321,13 +322,14 @@ function genericPrint(path, options, print) { }); } case "thematicBreak": { - const counter = getAncestorCounter(path, "list"); + const { ancestors } = path; + const counter = ancestors.findIndex((node) => node.type === "list"); if (counter === -1) { return "---"; } const nthSiblingIndex = getNthListSiblingIndex( - path.getParentNode(counter), - path.getParentNode(counter + 1) + ancestors[counter], + ancestors[counter + 1] ); return nthSiblingIndex % 2 === 0 ? "***" : "---"; } @@ -762,15 +764,11 @@ function shouldPrePrintTripleHardline({ node, previous }) { } function shouldRemainTheSameContent(path) { - const ancestorNode = getAncestorNode(path, [ - "linkReference", - "imageReference", - ]); - + const node = path.findAncestor( + (node) => node.type === "linkReference" || node.type === "imageReference" + ); return ( - ancestorNode && - (ancestorNode.type !== "linkReference" || - ancestorNode.referenceType !== "full") + node && (node.type !== "linkReference" || node.referenceType !== "full") ); } diff --git a/src/language-markdown/utils.js b/src/language-markdown/utils.js index 6d54336c2..495d073a2 100644 --- a/src/language-markdown/utils.js +++ b/src/language-markdown/utils.js @@ -238,26 +238,6 @@ function isAutolink(node) { return locStart(node) === locStart(child) && locEnd(node) === locEnd(child); } -function getAncestorCounter(path, typeOrTypes) { - const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; - - let counter = -1; - let ancestorNode; - - while ((ancestorNode = path.getParentNode(++counter))) { - if (types.includes(ancestorNode.type)) { - return counter; - } - } - - return -1; -} - -function getAncestorNode(path, typeOrTypes) { - const counter = getAncestorCounter(path, typeOrTypes); - return counter === -1 ? null : path.getParentNode(counter); -} - export { mapAst, splitText, @@ -272,6 +252,4 @@ export { KIND_CJ_LETTER, KIND_K_LETTER, KIND_CJK_PUNCTUATION, - getAncestorCounter, - getAncestorNode, }; diff --git a/src/language-yaml/print/block.js b/src/language-yaml/print/block.js index b9c078516..056d4752d 100644 --- a/src/language-yaml/print/block.js +++ b/src/language-yaml/print/block.js @@ -11,19 +11,17 @@ import { markAsRoot, } from "../../document/builders.js"; import { - getAncestorCount, getBlockValueLineContents, hasIndicatorComment, isLastDescendantNode, - isNode, } from "../utils.js"; import { alignWithSpaces } from "./misc.js"; function printBlock(path, print, options) { const { node } = path; - const parentIndent = getAncestorCount(path, (ancestorNode) => - isNode(ancestorNode, ["sequence", "mapping"]) - ); + const parentIndent = path.ancestors.filter( + (node) => node.type === "sequence" || node.type === "mapping" + ).length; const isLastDescendant = isLastDescendantNode(path); /** @type {Doc[]} */ const parts = [node.type === "blockFolded" ? ">" : "|"]; diff --git a/src/language-yaml/utils.js b/src/language-yaml/utils.js index cc417de71..8981844ab 100644 --- a/src/language-yaml/utils.js +++ b/src/language-yaml/utils.js @@ -1,17 +1,5 @@ import { isNonEmptyArray } from "../common/util.js"; -function getAncestorCount(path, filter) { - let counter = 0; - const pathStackLength = path.stack.length - 1; - for (let i = 0; i < pathStackLength; i++) { - const value = path.stack[i]; - if (isNode(value) && filter(value)) { - counter++; - } - } - return counter; -} - /** * @param {any} value * @param {string[]=} types @@ -345,7 +333,6 @@ function isInlineNode(node) { } export { - getAncestorCount, isNode, isEmptyNode, isInlineNode,