From a22b7bfd04049c7f28052336f6b57c5fc9fbdfae Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Sat, 1 Feb 2025 15:31:09 -0800 Subject: [PATCH] WIP: detect embedded references to a narrowable type parameter --- src/compiler/checker.ts | 172 +++++++++++++++++++++++++++++++++------- 1 file changed, 143 insertions(+), 29 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3b74a8ec4a0..7d4faf5e8af 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -45988,59 +45988,54 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const declaration = typeParam.symbol.declarations[0]; const container = isJSDocTemplateTag(declaration.parent) ? getJSDocHost(declaration.parent) : declaration.parent; if (!isFunctionLike(container)) continue; - let reference: Identifier | undefined; + let paramReference: ParameterDeclaration | undefined; + let referencePath: Name[] | undefined; let hasInvalidReference = false; for (const paramDecl of container.parameters) { const typeNode = getEffectiveTypeAnnotationNode(paramDecl); if (!typeNode) continue; - if (isTypeParameterReferenced(typeParam, typeNode)) { - let candidateReference; - if ( - isTypeReferenceNode(typeNode) && - isReferenceToTypeParameter(typeParam, typeNode) && - (candidateReference = getValidParameterReference(paramDecl, constraint)) - ) { - // Type parameter has more than one valid reference. - if (reference) { - hasInvalidReference = true; - break; - } - reference = candidateReference; - } - else { // Type parameter has invalid reference. + const result = getValidTypeParameterReference(typeNode, typeParam, []); + if (!result) { // Invalid reference to type parameter. + hasInvalidReference = true; + break; + } + if (isArray(result)) { + if (referencePath) { // More than one valid reference to type parameter. hasInvalidReference = true; break; } + referencePath = result; + paramReference = paramDecl; } } - if (!hasInvalidReference && reference) { - const symbol = getResolvedSymbol(reference); + if (!hasInvalidReference && referencePath) { // Valid type parameter reference: type parameter is narrowable + const symbol = getResolvedSymbol(reference); // >> TODO: construct reference; get its symbol if (symbol !== unknownSymbol) narrowableParams.push([typeParam, symbol, reference]); } } } return narrowableParams; - // For a parameter of declared type `T` to be a valid reference for narrowing, it must satisfy: - // - the parameter name is an identifier - // - if the parameter is optional, then `T`'s constraint must allow for undefined - function getValidParameterReference(paramDecl: ParameterDeclaration, constraint: Type): Identifier | undefined { - if (!isIdentifier(paramDecl.name)) return; - const isOptional = !!paramDecl.questionToken || isJSDocOptionalParameter(paramDecl); - if (isOptional && !containsUndefinedType(constraint)) return; - return paramDecl.name; - } + // // For a parameter of declared type `T` to be a valid reference for narrowing, it must satisfy: + // // - the parameter name is an identifier + // // - if the parameter is optional, then `T`'s constraint must allow for undefined + // function getValidParameterReference(paramDecl: ParameterDeclaration, constraint: Type): Identifier | undefined { + // if (!isIdentifier(paramDecl.name)) return; + // const isOptional = !!paramDecl.questionToken || isJSDocOptionalParameter(paramDecl); + // if (isOptional && !containsUndefinedType(constraint)) return; + // return paramDecl.name; + // } function isReferenceToTypeParameter(typeParam: TypeParameter, node: TypeReferenceNode) { return getTypeFromTypeReference(node) === typeParam; } - function isTypeParameterReferenced(typeParam: TypeParameter, node: TypeNode) { + function isTypeParameterReferenced(typeParam: TypeParameter, node: Node) { return isReferenced(node); function isReferenced(node: Node): boolean { if (isTypeReferenceNode(node)) { - return isReferenceToTypeParameter(typeParam, node); + return isReferenceToTypeParameter(typeParam, node) || some(node.typeArguments, isReferenced); } if (isTypeQueryNode(node)) { return isTypeParameterPossiblyReferenced(typeParam, node); @@ -46048,9 +46043,128 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return !!forEachChild(node, isReferenced); } } + + // Given a type node and a type parameter `T`, this function does two things: + // (1) validates all syntactic occurrences of `T` + // (2) collects a path to a valid (i.e. narrowable) occurrence to `T` + // Returns: + // `true` if no references to `T` were found; + // `false` if invalid or multiple valid references to `T` were found; + // an array of names that corresponds to the valid reference to `T`, if exactly one valid reference was found. + // `path` is initially is empty; it's an accumulator for the path through valid property accesses. + type Name = Identifier | StringLiteral; + function getValidTypeParameterReference(typeNode: Node, typeParam: TypeParameter, path: Name[]): Name[] | boolean { + switch (typeNode.kind) { + case SyntaxKind.TypeReference: + const type = getTypeFromTypeReference((typeNode as TypeReferenceNode)); + if (type === typeParam) { // `T` + return path; + } + const typeArgs = (typeNode as TypeReferenceNode).typeArguments; + // Type arguments that reference `T` + const typeArgsReferenced = typeArgs?.filter(node => isTypeParameterReferenced(typeParam, node)) + if (!typeArgsReferenced || typeArgsReferenced.length == 0) return true; // Type reference unrelated to `T` + if (typeArgsReferenced && typeArgsReferenced.length > 1) return false; // e.g. `Foo` + const typeArg = typeArgsReferenced[0]; + if (!(typeArg.kind & SyntaxKind.TypeReference)) return false; // e.g. `Foo, ...>` + if (!type.symbol || !type.symbol.declarations || type.symbol.declarations.length != 1) return false; + const typeDeclaration = type.symbol.declarations[0]; + let aliasDeclaration; + if (isTypeLiteralNode(typeDeclaration)) { + aliasDeclaration = walkUpParenthesizedTypes(typeDeclaration.parent); // `type Foo = { ... }` + if (!isTypeAliasDeclaration(aliasDeclaration)) return false; + } else if (isInterfaceDeclaration(typeDeclaration)) { // `interface Foo { ... }` + aliasDeclaration = typeDeclaration; + } else { + return false; // Unsuported case, e.g. `class Foo<...> ...`, `type Foo<...> = { [P in Foo]: ... }`, etc. + } + const typeArgIndex = typeArgs!.findIndex(arg => arg === typeArg); + const matchingTypeParamDecl = aliasDeclaration.typeParameters?.[typeArgIndex]; + if (!matchingTypeParamDecl) return false; // Shouldn't happen, unless there is an error in the input program. + const matchingTypeParam = getTypeOfSymbol(getSymbolOfDeclaration(matchingTypeParamDecl)) as TypeParameter; // >> TODO: better way? + return getValidTypeParameterReference(typeDeclaration, matchingTypeParam, path); + case SyntaxKind.InterfaceDeclaration: + const extendsTypes = flatMap((typeNode as InterfaceDeclaration).heritageClauses, clause => clause.types); + const relevantExtendsTypes = extendsTypes.filter(node => isTypeParameterReferenced(typeParam, node)); + if (relevantExtendsTypes && relevantExtendsTypes.length > 1) { + return false; // Unsupported: `interface Foo extends Bar, Baz { ... }` + } + const result = getValidTypeParameterReferenceFromTypeElements((typeNode as InterfaceDeclaration).members, typeParam, path); + if (relevantExtendsTypes && relevantExtendsTypes.length === 1) { // `interface Foo extends Bar, Baz { ... }` + if (result !== true) { + return false; // e.g. `interface Foo extends Bar { ... otherRef: T; ...}` + } + return getValidTypeParameterReference(relevantExtendsTypes[0], typeParam, path) + } + return result; + case SyntaxKind.TypeLiteral: + return getValidTypeParameterReferenceFromTypeElements((typeNode as TypeLiteralNode).members, typeParam, path); + case SyntaxKind.IntersectionType: + let validPath: Name[] | undefined; + for (const type of (typeNode as IntersectionTypeNode).types) { + const result = getValidTypeParameterReference(type, typeParam, path); + if (!result) { + return false; + } + if (isArray(result)) { + if (validPath) { + return false; + } + validPath = result; + } + } + return validPath ?? true; + default: + // If we see a reference to `T` in the type node here, invalidate the whole thing. + return !isTypeParameterReferenced(typeParam, typeNode); + } + } + + function getValidTypeParameterReferenceFromTypeElements(members: NodeArray, typeParam: TypeParameter, path: Name[]): Name[] | boolean { + let validPath: Name[] | undefined; + for (const member of members) { + if (!isTypeParameterReferenced(typeParam, member)) { + continue; + } + if (!isPropertySignature(member)) { + return false; // Unsupported reference to `T`, e.g. `[s: string]: T`. + } + if (!isIdentifier(member.name) && !isStringLiteral(member.name)) { // >> TODO: support number literal? others? + return false; // Unsupported property name, e.g. `[c]: T` + } + if (member.questionToken) { + // >> TODO: account for property optionality + } + const result = getValidTypeParameterReference(member.type!, typeParam, [member.name, ...path]) + if (!result) { + return false; + } + if (isArray(result)) { + if (validPath) { + return false; // Invalid case: multiple valid references + } + validPath = result; + } + } + return validPath ?? true; + } + + // Given a parameter declaration, and a name path to a reference of type parameter `T` in the type of the parameter, + // construct a reference to the parameter property that corresponds to the `T` reference in the types. + // Examples: + // `constructNarrowableReference(`param: { b: T }`, [`b`])` ==> `param.b` + // `constructNarrowableReference(`param: { a: { b: T } }`, [`a`, `b`])` ==> `param.ab` + // `constructNarrowableReference(`param: { "a b": T }`, ["a b"])` ==> `param["a b"]` + // `constructNarrowableReference(`{ b }: { b: T }`, [`b`])` ==> `b` + // `constructNarrowableReference(`{ a }: { a: { b: T } }`, [`a`, `b`])` ==> `a.b` + function constructNarrowableReference(paramDecl: ParameterDeclaration, path: Name[]): PropertyAccessExpression { + // >> TODO: impl + // >> TODO: consider if we'll be able to get the symbol at this constructed location... + } } + /** * Determines if the type parameter constraint allows for narrowing of that type parameter. * This is true if: