UNPKG

typescript

Version:

TypeScript is a language for application scale JavaScript development

596 lines 24.6 kB
import { NodeFlags } from "#enums/nodeFlags"; import { SyntaxKind } from "#enums/syntaxKind"; import { createToken } from "./factory.generated.js"; import { isJSDocNodeKind, isKeywordKind, isPrivateIdentifier, isPropertyNameLiteral, isTokenKind, } from "./is.js"; import { createScanner, skipTrivia, } from "./scanner.js"; export function getTokenAtPosition(sourceFile, position) { return getTokenAtPositionImpl(sourceFile, position, /*allowPositionInLeadingTrivia*/ true, /*includePrecedingTokenAtEndPosition*/ undefined); } export function getTouchingPropertyName(sourceFile, position) { return getTokenAtPositionImpl(sourceFile, position, /*allowPositionInLeadingTrivia*/ false, node => isPropertyNameLiteral(node) || isKeywordKind(node.kind) || isPrivateIdentifier(node)); } export function getTouchingToken(sourceFile, position) { return getTokenAtPositionImpl(sourceFile, position, /*allowPositionInLeadingTrivia*/ false, /*includePrecedingTokenAtEndPosition*/ undefined); } /** * Finds the token that starts immediately after `previousToken` ends, searching * within `parent`. Returns `undefined` if no such token exists. */ export function findNextToken(previousToken, parent, sourceFile) { return find(parent); function find(n) { if (isTokenKind(n.kind) && n.pos === previousToken.end) { // This is the token that starts at the end of previousToken – return it. return n; } // Find the child node that contains `previousToken` or starts immediately after it. let foundNode; const visitChild = (node) => { if (node.flags & NodeFlags.Reparsed) { return undefined; } if (node.pos <= previousToken.end && node.end > previousToken.end) { foundNode = node; } return undefined; }; // Visit JSDoc children first (mirrors Go's VisitEachChildAndJSDoc). if (n.jsDoc) { for (const jsdoc of n.jsDoc) { visitChild(jsdoc); } } n.forEachChild(visitChild, nodes => { if (nodes.length > 0 && foundNode === undefined) { for (const node of nodes) { if (node.flags & NodeFlags.Reparsed) continue; if (node.pos > previousToken.end) break; if (node.end > previousToken.end) { foundNode = node; break; } } } return undefined; }); // Recurse into the found child. if (foundNode !== undefined) { return find(foundNode); } // No AST child covers the position; use the scanner to find the syntactic token. // The scanner is initialized at `previousToken.end`, so tokenFullStart === previousToken.end. const startPos = previousToken.end; if (startPos >= n.pos && startPos < n.end) { const scanner = getScannerForSourceFile(sourceFile, startPos); const token = scanner.getToken(); const tokenFullStart = scanner.getTokenFullStart(); const tokenEnd = scanner.getTokenEnd(); const flags = scanner.getTokenFlags(); return getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, n, flags); } return undefined; } } /** * Finds the leftmost token satisfying `position < token.end`. * If the position is in the trivia of that leftmost token, or the token is invalid, * returns the rightmost valid token with `token.end <= position`. * Excludes `JsxText` tokens containing only whitespace. */ export function findPrecedingToken(sourceFile, position) { return findPrecedingTokenImpl(sourceFile, position, sourceFile); } function getTokenAtPositionImpl(sourceFile, position, allowPositionInLeadingTrivia, includePrecedingTokenAtEndPosition) { let current = sourceFile; let nodeAfterLeft; const state = { next: undefined, prevSubtree: undefined, left: 0, }; const testNode = (node) => { if (node.kind !== SyntaxKind.EndOfFile && node.end === position && includePrecedingTokenAtEndPosition !== undefined) { state.prevSubtree = node; } // A node "contains" the position if position < end, except nodes at the file end // treat end as inclusive (there's nowhere else to look). This applies to the EOF // token itself, and to JSDoc nodes reaching EOF (e.g. unterminated JSDoc comments). if (node.end < position || node.end === position && node.kind !== SyntaxKind.EndOfFile && (!isJSDocNodeKind(node.kind) || node.end !== sourceFile.endOfFileToken.end)) { return -1; } const nodePos = getPosition(node, sourceFile, allowPositionInLeadingTrivia); if (nodePos > position) { return 1; } return 0; }; while (true) { // Visit each child of current to find the one containing the position. state.next = undefined; nodeAfterLeft = undefined; // In Strada, JSDoc nodes with a single comment represent that comment as a string // property (not a child node), so forEachChild does not visit it. We replicate this // by detecting single-comment NodeLists in visitList and skipping their elements in visitNode. let skipSingleCommentChildren = false; const visitNode = (node) => { if (node.flags & NodeFlags.Reparsed) { return undefined; } if (skipSingleCommentChildren && isJSDocCommentChildKind(node.kind)) { return undefined; } if (nodeAfterLeft === undefined) { nodeAfterLeft = node; } if (state.next === undefined) { const result = testNode(node); switch (result) { case -1: if (!isJSDocNodeKind(node.kind)) { state.left = node.end; } nodeAfterLeft = undefined; break; case 0: state.next = node; break; } } return undefined; }; // Visit JSDoc children first, then regular children (mirrors Go's VisitEachChildAndJSDoc). if (current.jsDoc) { for (const jsdoc of current.jsDoc) { visitNode(jsdoc); } } current.forEachChild(visitNode, nodes => { // Track whether this NodeList is a single-comment list that should be skipped. // The flag affects the subsequent forEachNode(visitNode) calls for this NodeList. skipSingleCommentChildren = isJSDocSingleCommentNodeList(nodes); if (nodes.length === 0 || skipSingleCommentChildren) { return undefined; } if (nodeAfterLeft === undefined) { for (const node of nodes) { if (!(node.flags & NodeFlags.Reparsed)) { nodeAfterLeft = node; break; } } } if (state.next === undefined) { if (nodes.end === position && includePrecedingTokenAtEndPosition !== undefined) { state.left = nodes.end; nodeAfterLeft = undefined; state.prevSubtree = nodes[nodes.length - 1]; } else if (nodes.end <= position) { state.left = nodes.end; nodeAfterLeft = undefined; } else if (nodes.pos <= position) { binarySearchNodeList(nodes, testNode, (node, middle, arr) => { state.left = node.end; nodeAfterLeft = undefined; for (let i = middle + 1; i < arr.length; i++) { if (!(arr[i].flags & NodeFlags.Reparsed)) { nodeAfterLeft = arr[i]; break; } } }, found => { state.next = found; }); } } return undefined; }); // If prevSubtree was set, check if the rightmost token of prevSubtree should be returned. if (state.prevSubtree !== undefined) { const child = findPrecedingTokenImpl(sourceFile, position, state.prevSubtree); if (child !== undefined && child.end === position && includePrecedingTokenAtEndPosition(child)) { return child; } state.prevSubtree = undefined; } if (state.next === undefined) { if (isTokenKind(current.kind) || shouldSkipChild(current)) { return current; } // Use the scanner to find a token not stored in the AST. const scanner = getScannerForSourceFile(sourceFile, state.left); let end = current.end; const afterLeft = nodeAfterLeft; if (afterLeft !== undefined) { end = afterLeft.pos; } while (state.left < end) { const token = scanner.getToken(); const tokenFullStart = scanner.getTokenFullStart(); const tokenStart = allowPositionInLeadingTrivia ? tokenFullStart : scanner.getTokenStart(); const tokenEnd = scanner.getTokenEnd(); const flags = scanner.getTokenFlags(); if (tokenEnd > end) { break; } if (tokenStart <= position && position < tokenEnd) { if (token === SyntaxKind.Identifier || !isTokenKind(token)) { if (isJSDocNodeKind(current.kind)) { return current; } throw new Error(`did not expect ${SyntaxKind[current.kind]} to have ${SyntaxKind[token]} in its trivia`); } return getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, current, flags); } if (includePrecedingTokenAtEndPosition !== undefined && tokenEnd === position) { const prevToken = getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, current, flags); if (includePrecedingTokenAtEndPosition(prevToken)) { return prevToken; } } state.left = tokenEnd; scanner.scan(); } return current; } current = state.next; state.left = current.pos; nodeAfterLeft = undefined; } } function getPosition(node, sourceFile, allowPositionInLeadingTrivia) { if (allowPositionInLeadingTrivia) { return node.pos; } return getTokenPosOfNode(node, sourceFile, /*includeJSDoc*/ true); } /** @internal */ export function getTokenPosOfNode(node, sourceFile, includeJSDoc) { // With nodes that have no width (i.e. 'Missing' nodes), we actually *don't* // want to skip trivia because this will launch us forward to the next token. if (nodeIsMissing(node)) { return node.pos; } if (isJSDocNodeKind(node.kind) || node.kind === SyntaxKind.JsxText) { return skipTrivia(sourceFile.text, node.pos, /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); } if (includeJSDoc && node.jsDoc && node.jsDoc.length > 0) { return getTokenPosOfNode(node.jsDoc[0], sourceFile, /*includeJSDoc*/ false); } return skipTrivia(sourceFile.text, node.pos, /*stopAfterLineBreak*/ false, /*stopAtComments*/ false, /*inJSDoc*/ !!(node.flags & NodeFlags.JSDoc)); } function nodeIsMissing(node) { return node.pos === node.end && node.pos >= 0 && node.kind !== SyntaxKind.EndOfFile; } function findPrecedingTokenImpl(sourceFile, position, startNode) { const find = (n) => { if (isTokenKind(n.kind) && n.kind !== SyntaxKind.EndOfFile) { return n; } let foundChild; let prevChild; // Visit JSDoc nodes first (mirrors Go's VisitEachChildAndJSDoc). if (n.jsDoc) { for (const jsdoc of n.jsDoc) { if (jsdoc.flags & NodeFlags.Reparsed) continue; if (foundChild !== undefined) break; if (position < jsdoc.end && (prevChild === undefined || prevChild.end <= position)) { foundChild = jsdoc; } else { prevChild = jsdoc; } } } let skipSingleCommentChildrenImpl = false; n.forEachChild(node => { if (node.flags & NodeFlags.Reparsed) { return undefined; } if (skipSingleCommentChildrenImpl && isJSDocCommentChildKind(node.kind)) { return undefined; } if (foundChild !== undefined) { return undefined; } if (position < node.end && (prevChild === undefined || prevChild.end <= position)) { foundChild = node; } else { prevChild = node; } return undefined; }, nodes => { skipSingleCommentChildrenImpl = isJSDocSingleCommentNodeList(nodes); if (foundChild !== undefined) { return undefined; } if (nodes.length > 0 && !skipSingleCommentChildrenImpl) { const index = binarySearchForPrecedingToken(nodes, position); if (index >= 0 && !(nodes[index].flags & NodeFlags.Reparsed)) { foundChild = nodes[index]; } const lookupIndex = index >= 0 ? index - 1 : nodes.length - 1; for (let i = lookupIndex; i >= 0; i--) { if (!(nodes[i].flags & NodeFlags.Reparsed)) { if (prevChild === undefined) { prevChild = nodes[i]; } break; } } } return undefined; }); if (foundChild !== undefined) { const start = getTokenPosOfNode(foundChild, sourceFile, /*includeJSDoc*/ true); if (start >= position) { if (position >= foundChild.pos) { // We are in the leading trivia of foundChild. Check for JSDoc nodes of n // preceding foundChild, mirroring Go's findPrecedingToken logic. let jsDoc; if (n.jsDoc) { for (let i = n.jsDoc.length - 1; i >= 0; i--) { if (n.jsDoc[i].pos >= foundChild.pos) { jsDoc = n.jsDoc[i]; break; } } } if (jsDoc !== undefined) { if (position < jsDoc.end) { return find(jsDoc); } return findRightmostValidToken(sourceFile, jsDoc.end, n, position); } return findRightmostValidToken(sourceFile, foundChild.pos, n, -1); } // Answer is in tokens between two visited children. return findRightmostValidToken(sourceFile, foundChild.pos, n, position); } return find(foundChild); } if (position >= n.end) { return findRightmostValidToken(sourceFile, n.end, n, -1); } return findRightmostValidToken(sourceFile, n.end, n, position); }; return find(startNode); } function findRightmostValidToken(sourceFile, endPos, containingNode, position) { if (position === -1) { position = containingNode.end; } const find = (n, endPos) => { if (isTokenKind(n.kind) && n.kind !== SyntaxKind.EndOfFile) { return n; } let rightmostValidNode; let hasChildren = false; // Visit JSDoc nodes first (mirrors Go's VisitEachChildAndJSDoc). if (n.jsDoc) { hasChildren = true; for (const jsdoc of n.jsDoc) { if (jsdoc.flags & NodeFlags.Reparsed) continue; if (jsdoc.end > endPos || getTokenPosOfNode(jsdoc, sourceFile) >= position) continue; if (isValidPrecedingNode(jsdoc, sourceFile)) { rightmostValidNode = jsdoc; } } } let skipSingleCommentChildren = false; n.forEachChild(node => { if (node.flags & NodeFlags.Reparsed) { return undefined; } if (skipSingleCommentChildren && isJSDocCommentChildKind(node.kind)) { return undefined; } hasChildren = true; if (node.end > endPos || getTokenPosOfNode(node, sourceFile) >= position) { return undefined; } if (isValidPrecedingNode(node, sourceFile)) { rightmostValidNode = node; } return undefined; }, nodes => { // Skip single-comment JSDoc NodeLists (e.g. JSDocText children of a JSDoc node): // In Go, these are stored as string properties and are never visited as children. skipSingleCommentChildren = isJSDocSingleCommentNodeList(nodes); if (nodes.length > 0 && !skipSingleCommentChildren) { hasChildren = true; for (let i = nodes.length - 1; i >= 0; i--) { const node = nodes[i]; if (node.flags & NodeFlags.Reparsed) continue; if (node.end > endPos || getTokenPosOfNode(node, sourceFile) >= position) continue; if (isValidPrecedingNode(node, sourceFile)) { rightmostValidNode = node; break; } } } return undefined; }); // Scan for syntactic tokens (e.g. `{`, `,`) between AST nodes, matching Go's // findRightmostValidToken scanner step. if (!shouldSkipChild(n)) { const startPos = rightmostValidNode !== undefined ? rightmostValidNode.end : n.pos; const targetEnd = Math.min(endPos, position); if (startPos < targetEnd) { const scanner = getScannerForSourceFile(sourceFile, startPos); let pos = startPos; let lastScannedToken; while (pos < targetEnd) { const tokenStart = scanner.getTokenStart(); if (tokenStart >= position) break; const tokenFullStart = scanner.getTokenFullStart(); const tokenEnd = scanner.getTokenEnd(); const token = scanner.getToken(); const flags = scanner.getTokenFlags(); lastScannedToken = getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, n, flags); pos = tokenEnd; scanner.scan(); } if (lastScannedToken !== undefined) { return lastScannedToken; } } } if (!hasChildren) { if (n !== containingNode) { return n; } return undefined; } if (rightmostValidNode !== undefined) { return find(rightmostValidNode, rightmostValidNode.end); } return undefined; }; return find(containingNode, endPos); } function isValidPrecedingNode(node, sourceFile) { if (node.kind === SyntaxKind.EndOfFile) { return false; } const start = getTokenPosOfNode(node, sourceFile); const width = node.end - start; return width > 0; } function shouldSkipChild(node) { return node.kind === SyntaxKind.JSDoc || node.kind === SyntaxKind.JSDocText || node.kind === SyntaxKind.JSDocTypeLiteral || node.kind === SyntaxKind.JSDocSignature || node.kind === SyntaxKind.JSDocLink || node.kind === SyntaxKind.JSDocLinkCode || node.kind === SyntaxKind.JSDocLinkPlain || isJSDocTag(node); } function isJSDocTag(node) { return node.kind >= SyntaxKind.FirstJSDocTagNode && node.kind <= SyntaxKind.LastJSDocTagNode; } // In Strada, if a JSDoc node has a single comment, that comment is represented as a string // property as a simplification, and therefore that comment is not visited by forEachChild. // To match, we skip single-element comment NodeLists within JSDoc/JSDocTag nodes. function isJSDocCommentChildKind(kind) { switch (kind) { case SyntaxKind.JSDocText: case SyntaxKind.JSDocLink: case SyntaxKind.JSDocLinkCode: case SyntaxKind.JSDocLinkPlain: return true; default: return false; } } function isJSDocSingleCommentNodeList(nodes) { return nodes.length === 1 && isJSDocCommentChildKind(nodes[0].kind); } function getScannerForSourceFile(sourceFile, pos) { const scanner = createScanner(/*skipTrivia*/ true, sourceFile.languageVariant, sourceFile.text); scanner.resetTokenState(pos); scanner.scan(); return scanner; } function getOrCreateToken(sourceFile, kind, pos, end, parent, _flags) { const key = `${pos}_${end}`; if (!sourceFile.tokenCache) { sourceFile.tokenCache = new Map(); } const existing = sourceFile.tokenCache.get(key); if (existing !== undefined) { return existing; } const token = createToken(kind); token.pos = pos; token.end = end; token.parent = parent; sourceFile.tokenCache.set(key, token); return token; } /** Binary search a node list for the node containing position. */ function binarySearchNodeList(nodes, testNode, onLeft, onMatch) { let lo = 0; let hi = nodes.length - 1; while (lo <= hi) { const mid = (lo + hi) >>> 1; const node = nodes[mid]; if (node.flags & NodeFlags.Reparsed) { // Skip reparsed nodes: try to find a non-reparsed node nearby let found = false; for (let i = mid + 1; i <= hi; i++) { if (!(nodes[i].flags & NodeFlags.Reparsed)) { const cmp = testNode(nodes[i]); if (cmp < 0) { onLeft(nodes[i], i, nodes); lo = i + 1; } else if (cmp > 0) { hi = i - 1; } else { onMatch(nodes[i]); return; } found = true; break; } } if (!found) { hi = mid - 1; } continue; } const cmp = testNode(node); if (cmp < 0) { onLeft(node, mid, nodes); lo = mid + 1; } else if (cmp > 0) { hi = mid - 1; } else { onMatch(node); return; } } } function binarySearchForPrecedingToken(nodes, position) { let lo = 0; let hi = nodes.length - 1; let result = -1; while (lo <= hi) { const mid = (lo + hi) >>> 1; const node = nodes[mid]; if (node.flags & NodeFlags.Reparsed) { lo = mid + 1; continue; } if (position < node.end) { if (mid === 0 || position >= nodes[mid - 1].end) { result = mid; break; } hi = mid - 1; } else { lo = mid + 1; } } return result; } //# sourceMappingURL=astnav.js.map