diff --git a/src/nodes/typeGuards/index.ts b/src/nodes/typeGuards/index.ts index 5d602e52..7bcab266 100644 --- a/src/nodes/typeGuards/index.ts +++ b/src/nodes/typeGuards/index.ts @@ -1,3 +1,4 @@ export * from "./compound.js"; +export * from "./private.js"; export * from "./single.js"; export * from "./union.js"; diff --git a/src/nodes/typeGuards/private.ts b/src/nodes/typeGuards/private.ts new file mode 100644 index 00000000..6ec9718e --- /dev/null +++ b/src/nodes/typeGuards/private.ts @@ -0,0 +1,319 @@ +/** + * @file safe version of type guards that exists in TypeScript's private API. + */ + +import ts from "typescript"; + +import { isPartOfTypeNode } from "../utilities.js"; +import { + isAnyKeyword, + isBigIntKeyword, + isBooleanKeyword, + isFalseKeyword, + isImportKeyword, + isInKeyword, + isNeverKeyword, + isNullKeyword, + isNumberKeyword, + isObjectKeyword, + isStringKeyword, + isSuperKeyword, + isSymbolKeyword, + isThisKeyword, + isTrueKeyword, + isUndefinedKeyword, + isUnknownKeyword, + isVoidKeyword, +} from "./single.js"; + +export function isExpression(node: ts.Node): node is ts.Expression { + return ( + ts.isConditionalExpression(node) || + ts.isYieldExpression(node) || + ts.isArrowFunction(node) || + ts.isBinaryExpression(node) || + ts.isSpreadElement(node) || + ts.isAsExpression(node) || + ts.isOmittedExpression(node) || + ts.isCommaListExpression(node) || + ts.isPartiallyEmittedExpression(node) || + ts.isSatisfiesExpression(node) || + isUnaryExpression(node) + ); +} + +export function isExpressionNode(node: ts.Node): boolean { + if ( + isSuperKeyword(node) || + isNullKeyword(node) || + isTrueKeyword(node) || + isFalseKeyword(node) || + ts.isRegularExpressionLiteral(node) || + ts.isArrayLiteralExpression(node) || + ts.isObjectLiteralExpression(node) || + ts.isPropertyAccessExpression(node) || + ts.isElementAccessExpression(node) || + ts.isCallExpression(node) || + ts.isNewExpression(node) || + ts.isTaggedTemplateExpression(node) || + ts.isAsExpression(node) || + ts.isTypeAssertionExpression(node) || + ts.isSatisfiesExpression(node) || + ts.isNonNullExpression(node) || + ts.isParenthesizedExpression(node) || + ts.isFunctionExpression(node) || + ts.isClassExpression(node) || + ts.isArrowFunction(node) || + ts.isVoidExpression(node) || + ts.isDeleteExpression(node) || + ts.isTypeOfExpression(node) || + ts.isPrefixUnaryExpression(node) || + ts.isPostfixUnaryExpression(node) || + ts.isBinaryExpression(node) || + ts.isConditionalExpression(node) || + ts.isSpreadElement(node) || + ts.isTemplateExpression(node) || + ts.isOmittedExpression(node) || + ts.isJsxElement(node) || + ts.isJsxSelfClosingElement(node) || + ts.isJsxFragment(node) || + ts.isYieldExpression(node) || + ts.isAwaitExpression(node) || + ts.isMetaProperty(node) + ) { + return true; + } + + if (ts.SyntaxKind.ExpressionWithTypeArguments) { + return !ts.isHeritageClause(node.parent); + } + + if (ts.SyntaxKind.QualifiedName) { + while (ts.isQualifiedName(node.parent)) { + node = node.parent; + } + return ( + ts.isTypeQueryNode(node.parent) || + ts.isJSDocLinkLike(node.parent) || + ts.isJSDocNameReference(node.parent) || + ts.isJSDocMemberName(node.parent) || + isJSXTagName(node) + ); + } + + if (ts.SyntaxKind.JSDocMemberName) { + while (ts.isJSDocMemberName(node.parent)) { + node = node.parent; + } + return ( + ts.isTypeQueryNode(node.parent) || + ts.isJSDocLinkLike(node.parent) || + ts.isJSDocNameReference(node.parent) || + ts.isJSDocMemberName(node.parent) || + isJSXTagName(node) + ); + } + + if (ts.SyntaxKind.PrivateIdentifier) { + return ( + ts.isBinaryExpression(node.parent) && + node.parent.left === node && + isInKeyword(node.parent.operatorToken) + ); + } + + if (ts.SyntaxKind.Identifier) { + if ( + node.parent.kind === ts.SyntaxKind.TypeQuery || + ts.isJSDocLinkLike(node.parent) || + ts.isJSDocNameReference(node.parent) || + ts.isJSDocMemberName(node.parent) || + isJSXTagName(node) + ) { + return true; + } + } + + if ( + ts.isNumericLiteral(node) || + ts.isBigIntLiteral(node) || + ts.isStringLiteral(node) || + ts.isNoSubstitutionTemplateLiteral(node) || + isThisKeyword(node) + ) { + return isInExpressionContext(node); + } + + return false; +} + +export function isUnaryExpression(node: ts.Node): node is ts.UnaryExpression { + return ( + ts.isPrefixUnaryExpression(node) || + ts.isPostfixUnaryExpression(node) || + ts.isDeleteExpression(node) || + ts.isTypeOfExpression(node) || + ts.isVoidExpression(node) || + ts.isAwaitExpression(node) || + ts.isTypeAssertionExpression(node) || + isLeftHandSideExpression(node) + ); +} + +export function isLeftHandSideExpression( + node: ts.Node +): node is ts.LeftHandSideExpression { + if ( + ts.isPropertyAccessExpression(node) || + ts.isElementAccessExpression(node) || + ts.isNewExpression(node) || + ts.isCallExpression(node) || + ts.isJsxElement(node) || + ts.isJsxSelfClosingElement(node) || + ts.isJsxFragment(node) || + ts.isTaggedTemplateExpression(node) || + ts.isArrayLiteralExpression(node) || + ts.isParenthesizedExpression(node) || + ts.isObjectLiteralExpression(node) || + ts.isClassExpression(node) || + ts.isFunctionExpression(node) || + ts.isIdentifier(node) || + ts.isRegularExpressionLiteral(node) || + ts.isNumericLiteral(node) || + ts.isBigIntLiteral(node) || + ts.isStringLiteral(node) || + ts.isNoSubstitutionTemplateLiteral(node) || + ts.isTemplateExpression(node) || + isFalseKeyword(node) || + isNullKeyword(node) || + isThisKeyword(node) || + isTrueKeyword(node) || + isSuperKeyword(node) || + ts.isNonNullExpression(node) || + ts.isExpressionWithTypeArguments(node) || + ts.isMetaProperty(node) + ) { + return true; + } + + // PrivateIdentifier is only an Expression if it's in a `#field in expr` BinaryExpression + if (ts.isPrivateIdentifier(node)) { + return ( + ts.isBinaryExpression(node.parent) && + node.parent.left === node && + isInKeyword(node.parent.operatorToken) + ); + } + + // ImportKeyword is only an Expression if it's in a CallExpression + if (isImportKeyword(node)) { + return ts.isCallExpression(node.parent) && node.parent.expression === node; + } + + return false; +} + +export function isJSXTagName(node: ts.Node): node is ts.JsxTagNameExpression { + if ( + ts.isJsxOpeningElement(node.parent) || + ts.isJsxSelfClosingElement(node.parent) || + ts.isJsxClosingElement(node.parent) + ) { + return node.parent.tagName === node; + } + return false; +} + +function isInExpressionContext(node: ts.Node) { + if ( + ts.isVariableDeclaration(node.parent) || + ts.isParameter(node.parent) || + ts.isPropertyDeclaration(node.parent) || + ts.isEnumMember(node.parent) || + ts.isPropertyAssignment(node.parent) || + ts.isBindingElement(node.parent) + ) { + return node.parent.initializer === node; + } + + if ( + ts.isExpressionStatement(node.parent) || + ts.isIfStatement(node.parent) || + ts.isDoStatement(node.parent) || + ts.isWhileStatement(node.parent) || + ts.isReturnStatement(node.parent) || + ts.isWithStatement(node.parent) || + ts.isSwitchStatement(node.parent) || + ts.isCaseClause(node.parent) || + ts.isThrowStatement(node.parent) || + ts.isTypeAssertionExpression(node.parent) || + ts.isAsExpression(node.parent) || + ts.isTemplateSpan(node.parent) || + ts.isComputedPropertyName(node.parent) || + ts.isSatisfiesExpression(node.parent) + ) { + return node.parent.expression === node; + } + + if (ts.isForStatement(node.parent)) { + return ( + (node.parent.initializer === node && + ts.isVariableDeclarationList(node.parent.initializer)) || + node.parent.condition === node || + node.parent.incrementor === node + ); + } + + if (ts.isForInStatement(node.parent) || ts.isForOfStatement(node.parent)) { + return ( + (node.parent.initializer === node && + ts.isVariableDeclarationList(node.parent.initializer)) || + node.parent.expression === node + ); + } + + if ( + ts.isDecorator(node.parent) || + ts.isJsxExpression(node.parent) || + ts.isJsxSpreadAttribute(node.parent) || + ts.isSpreadAssignment(node.parent) + ) { + return true; + } + + if (ts.isExpressionWithTypeArguments(node.parent)) { + return node.parent.expression === node && !isPartOfTypeNode(node.parent); + } + + if (ts.isShorthandPropertyAssignment(node.parent)) { + return node.parent.objectAssignmentInitializer === node; + } + + return isExpressionNode(node.parent); +} + +export function isTypeNode(node: ts.Node): node is ts.TypeNode { + return ( + (node.kind >= ts.SyntaxKind.FirstTypeNode && + node.kind <= ts.SyntaxKind.LastTypeNode) || + isAnyKeyword(node) || + isUnknownKeyword(node) || + isNumberKeyword(node) || + isBigIntKeyword(node) || + isObjectKeyword(node) || + isBooleanKeyword(node) || + isStringKeyword(node) || + isSymbolKeyword(node) || + isVoidKeyword(node) || + isUndefinedKeyword(node) || + isNeverKeyword(node) || + ts.isExpressionWithTypeArguments(node) || + ts.isJSDocAllType(node) || + ts.isJSDocUnknownType(node) || + ts.isJSDocNullableType(node) || + ts.isJSDocNonNullableType(node) || + ts.isJSDocOptionalType(node) || + ts.isJSDocFunctionType(node) || + ts.isJSDocVariadicType(node) + ); +} diff --git a/src/nodes/utilities.ts b/src/nodes/utilities.ts index 340ff843..76c1d7a8 100644 --- a/src/nodes/utilities.ts +++ b/src/nodes/utilities.ts @@ -4,9 +4,20 @@ import * as ts from "typescript"; import { + isAnyKeyword, + isBigIntKeyword, + isBooleanKeyword, isConstAssertionExpression, isEntityNameExpression, + isNeverKeyword, + isNumberKeyword, isNumericOrStringLikeLiteral, + isObjectKeyword, + isStringKeyword, + isSymbolKeyword, + isUndefinedKeyword, + isUnknownKeyword, + isVoidKeyword, } from "./typeGuards/index.js"; /** Determines whether a call to `Object.defineProperty` is statically analyzable. */ @@ -62,3 +73,144 @@ export function isInConstContext(node: ts.Expression): boolean { } } } + +export function isPartOfTypeNode(node: ts.Node): boolean { + if ( + (node.kind >= ts.SyntaxKind.FirstTypeNode && + node.kind <= ts.SyntaxKind.LastTypeNode) || + isAnyKeyword(node) || + isUnknownKeyword(node) || + isNumberKeyword(node) || + isBigIntKeyword(node) || + isObjectKeyword(node) || + isBooleanKeyword(node) || + isStringKeyword(node) || + isSymbolKeyword(node) || + isUndefinedKeyword(node) || + isNeverKeyword(node) || + ts.isExpressionWithTypeArguments(node) + ) { + return true; + } + + if (isVoidKeyword(node)) { + return ts.isVoidExpression(node.parent); + } + + if (ts.isExpressionWithTypeArguments(node)) { + return ( + ts.isHeritageClause(node.parent) && + !isExpressionWithTypeArgumentsInClassExtendsClause(node) + ); + } + + if (ts.isTypeParameterDeclaration(node)) { + return ts.isMappedTypeNode(node.parent) || ts.isInferTypeNode(node.parent); + } + + if (ts.isIdentifier(node)) { + if (ts.isQualifiedName(node.parent) && node.parent.right === node) { + return isPartOfTypeNodeInternal(node.parent); + } else if ( + ts.isPropertyAccessExpression(node.parent) && + node.parent.name === node + ) { + return isPartOfTypeNodeInternal(node.parent); + } + + return isPartOfTypeNodeInternal(node); + } + + return false; +} + +function isPartOfTypeNodeInternal( + node: + | ts.TypeNode + | ts.Identifier + | ts.QualifiedName + | ts.PropertyAccessExpression +): boolean { + if (ts.isTypeQueryNode(node.parent)) { + return false; + } + + if (ts.isImportTypeNode(node.parent)) { + return !node.parent.isTypeOf; + } + + if ( + ts.SyntaxKind.FirstTypeNode <= node.parent.kind && + node.parent.kind <= ts.SyntaxKind.LastTypeNode + ) { + return true; + } + + if (ts.isExpressionWithTypeArguments(node.parent)) + return ( + ts.isHeritageClause(node.parent.parent) && + !isExpressionWithTypeArgumentsInClassExtendsClause(node.parent) + ); + + if (ts.isTypeParameterDeclaration(node.parent)) { + return node === node.parent.constraint; + } + + if (ts.isJSDocTemplateTag(node.parent)) { + return node === node.parent.constraint; + } + + if ( + ts.isPropertyDeclaration(node.parent) || + ts.isPropertySignature(node.parent) || + ts.isParameter(node.parent) || + ts.isVariableDeclaration(node.parent) + ) { + return node === node.parent.type; + } + + if ( + ts.isFunctionDeclaration(node.parent) || + ts.isFunctionExpression(node.parent) || + ts.isArrowFunction(node.parent) || + ts.isConstructorDeclaration(node.parent) || + ts.isMethodDeclaration(node.parent) || + ts.isMethodSignature(node.parent) || + ts.isGetAccessor(node.parent) || + ts.isSetAccessor(node.parent) + ) { + return node === node.parent.type; + } + + if ( + ts.isCallSignatureDeclaration(node.parent) || + ts.isConstructSignatureDeclaration(node.parent) || + ts.isIndexSignatureDeclaration(node.parent) + ) { + return node === node.parent.type; + } + + if (ts.isTypeAssertionExpression(node.parent)) { + return node === node.parent.type; + } + + if (ts.isCallExpression(node.parent) || ts.isNewExpression(node.parent)) { + return node.parent.typeArguments?.includes(node as ts.TypeNode) === true; + } + + return false; +} + +function isExpressionWithTypeArgumentsInClassExtendsClause( + node: ts.ExpressionWithTypeArguments +): boolean { + if ( + !ts.isExpressionWithTypeArguments(node) || + !ts.isHeritageClause(node.parent) || + !ts.isClassLike(node.parent.parent) + ) { + return false; + } + + return node.parent.token !== ts.SyntaxKind.ImplementsKeyword; +}