diff --git a/packages/pyright-internal/src/analyzer/typeCacheUtils.ts b/packages/pyright-internal/src/analyzer/typeCacheUtils.ts index c893f8cff83b..4288b41d0a27 100644 --- a/packages/pyright-internal/src/analyzer/typeCacheUtils.ts +++ b/packages/pyright-internal/src/analyzer/typeCacheUtils.ts @@ -36,9 +36,12 @@ export interface TypeResult { isIncomplete?: boolean; } -export interface SpeculativeTypeEntry { - typeResult: TypeResult; +export interface ContextualTypeCacheEntry { expectedType: Type | undefined; +} + +export interface SpeculativeTypeEntry extends ContextualTypeCacheEntry { + typeResult: TypeResult; incompleteGenerationCount: number; dependentTypes?: DependentType[]; } @@ -54,6 +57,33 @@ export interface SpeculativeModeOptions { allowDiagnostics?: boolean; } +const maxContextualTypeCacheEntriesPerNode = 8; + +export function contextualTypeCacheEntryMatches( + entry: ContextualTypeCacheEntry, + expectedType: Type | undefined +): boolean { + return expectedType ? !!entry.expectedType && isTypeSame(expectedType, entry.expectedType) : !entry.expectedType; +} + +export function addContextualTypeCacheEntry( + cacheEntries: readonly T[], + newEntry: T, + isEntryValid?: (entry: T) => boolean +): T[] { + let newCacheEntries = cacheEntries.filter( + (entry) => + (!isEntryValid || isEntryValid(entry)) && !contextualTypeCacheEntryMatches(entry, newEntry.expectedType) + ); + + newCacheEntries.push(newEntry); + if (newCacheEntries.length > maxContextualTypeCacheEntriesPerNode) { + newCacheEntries = newCacheEntries.slice(newCacheEntries.length - maxContextualTypeCacheEntriesPerNode); + } + + return newCacheEntries; +} + // This class maintains a stack of "speculative type contexts". When // a context is popped off the stack, all of the speculative type cache // entries that were created within that context are removed from the @@ -158,38 +188,6 @@ export class SpeculativeTypeTracker { ) { assert(this._speculativeContextStack.length > 0); - const maxCacheEntriesPerNode = 8; - let cacheEntries = this._speculativeTypeCache.get(node.id); - - if (!cacheEntries) { - cacheEntries = []; - } else { - cacheEntries = cacheEntries.filter((entry) => { - // Filter out any incomplete entries that no longer match the generation count. - // These are obsolete and cannot be used. - if (entry.typeResult.isIncomplete && entry.incompleteGenerationCount !== incompleteGenerationCount) { - return false; - } - - // Filter out any entries that match the expected type of the - // new entry. The new entry replaces the old in this case. - if (expectedType) { - if (!entry.expectedType) { - return true; - } - return !isTypeSame(entry.expectedType, expectedType); - } - - return !!entry.expectedType; - }); - - // Don't allow the cache to grow too large. - if (cacheEntries.length >= maxCacheEntriesPerNode) { - cacheEntries.slice(1); - } - } - - // Add the new entry. const newEntry: SpeculativeTypeEntry = { typeResult, expectedType, @@ -200,8 +198,11 @@ export class SpeculativeTypeTracker { newEntry.dependentTypes = Array.from(this._activeDependentTypes); } - cacheEntries.push(newEntry); - + const cacheEntries = addContextualTypeCacheEntry( + this._speculativeTypeCache.get(node.id) ?? [], + newEntry, + (entry) => !entry.typeResult.isIncomplete || entry.incompleteGenerationCount === incompleteGenerationCount + ); this._speculativeTypeCache.set(node.id, cacheEntries); } @@ -214,15 +215,7 @@ export class SpeculativeTypeTracker { const entries = this._speculativeTypeCache.get(node.id); if (entries) { for (const entry of entries) { - if (!expectedType) { - if (!entry.expectedType && this._dependentTypesMatch(entry)) { - return entry; - } - } else if ( - entry.expectedType && - isTypeSame(expectedType, entry.expectedType) && - this._dependentTypesMatch(entry) - ) { + if (contextualTypeCacheEntryMatches(entry, expectedType) && this._dependentTypesMatch(entry)) { return entry; } } diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 7f759ee2af06..9302ceab1e62 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -175,7 +175,13 @@ import { indeterminateSymbolId, Symbol, SymbolFlags, SynthesizedTypeInfo } from import { isConstantName, isPrivateName, isPrivateOrProtectedName } from './symbolNameUtils'; import { getLastTypedDeclarationForSymbol, isEffectivelyClassVar } from './symbolUtils'; import { assignTupleTypeArgs, expandTuple, getSlicedTupleType, getTypeOfTuple, makeTupleObject } from './tuples'; -import { SpeculativeModeOptions, SpeculativeTypeTracker } from './typeCacheUtils'; +import { + addContextualTypeCacheEntry, + ContextualTypeCacheEntry, + contextualTypeCacheEntryMatches, + SpeculativeModeOptions, + SpeculativeTypeTracker, +} from './typeCacheUtils'; import { assignToTypedDict, assignTypedDictToTypedDict, @@ -625,6 +631,8 @@ interface TypeCacheEntry { flags: EvalFlags | undefined; } +interface TypeFormTypeCacheEntry extends TypeCacheEntry, ContextualTypeCacheEntry {} + interface CodeFlowAnalyzerCacheEntry { typeAtStart: TypeResult | undefined; codeFlowAnalyzer: CodeFlowAnalyzer; @@ -659,6 +667,7 @@ export function createTypeEvaluator( let functionRecursionMap = new Map(); let codeFlowAnalyzerCache = new Map(); let typeCache = new Map(); + let typeFormTypeCache = new Map(); let effectiveTypeCache = new Map>(); let expectedTypeCache = new Map(); let asymmetricAccessorAssignmentCache = new Set(); @@ -668,6 +677,7 @@ export function createTypeEvaluator( let incompleteGenCount = 0; const returnTypeInferenceContextStack: ReturnTypeInferenceContext[] = []; let returnTypeInferenceTypeCache: Map | undefined; + let returnTypeInferenceTypeFormTypeCache: Map | undefined; const signatureTrackerStack: SignatureTrackerStackEntry[] = []; let prefetched: Partial | undefined; @@ -715,6 +725,7 @@ export function createTypeEvaluator( functionRecursionMap = new Map(); codeFlowAnalyzerCache = new Map(); typeCache = new Map(); + typeFormTypeCache = new Map(); effectiveTypeCache = new Map>(); expectedTypeCache = new Map(); asymmetricAccessorAssignmentCache = new Set(); @@ -730,7 +741,52 @@ export function createTypeEvaluator( } } + function getTypeFormTypeCache(node: ParseNode) { + if (returnTypeInferenceTypeFormTypeCache && isNodeInReturnTypeInferenceContext(node)) { + return returnTypeInferenceTypeFormTypeCache; + } + + return typeFormTypeCache; + } + + function readTypeFormTypeCacheEntry(node: ParseNode, expectedType: Type | undefined) { + return getTypeFormTypeCache(node) + .get(node.id) + ?.find((entry) => contextualTypeCacheEntryMatches(entry, expectedType)); + } + + // Contextual reads consult expectedTypeCache and prefer a matching TypeForm result. + // Runtime-only consumers must use readTypeCacheEntry so this precedence is not + // accidentally applied where an ordinary runtime type is required. + function readContextualTypeCacheEntryForNode(node: ParseNode) { + const expectedType = expectedTypeCache.get(node.id)?.type; + if (expectedType && expectedTypeWantsTypeForm(expectedType)) { + return ( + readTypeFormTypeCacheEntry(node, expectedType) ?? + readTypeFormTypeCacheEntry(node, /* expectedType */ undefined) ?? + readTypeCacheEntry(node) + ); + } + + return readTypeCacheEntry(node) ?? readTypeFormTypeCacheEntry(node, /* expectedType */ undefined); + } + + // Bumps the incomplete generation count using the same rules for both the + // regular type cache and the TypeForm type cache so the two cache-invalidation + // paths cannot drift. A complete result always bumps the count (invalidating + // dependent incomplete entries); an incomplete result bumps only when its type + // differs from the previously-cached value. + function updateIncompleteGenerationCount(typeResult: TypeResult, oldTypeResult: TypeResult | undefined) { + if (!typeResult.isIncomplete) { + incompleteGenCount++; + } else if (oldTypeResult !== undefined && !isTypeSame(typeResult.type, oldTypeResult.type)) { + incompleteGenCount++; + } + } + function isTypeCached(node: ParseNode) { + // This helper is used by runtime-evaluation guards. A contextual TypeForm + // entry does not prove that the ordinary runtime type was evaluated. const cacheEntry = readTypeCacheEntry(node); if (!cacheEntry) { return false; @@ -777,6 +833,32 @@ export function createTypeEvaluator( inferenceContext?: InferenceContext, allowSpeculativeCaching = false ) { + const useTypeFormCache = + (flags !== undefined && (flags & EvalFlags.TypeFormArg) !== 0) || + (!!inferenceContext && expectedTypeWantsTypeForm(inferenceContext.expectedType)); + + if (useTypeFormCache) { + const expectedType = inferenceContext?.expectedType; + + // Speculative TypeForm results are not retained, so they must not invalidate + // persistent incomplete entries through the global generation counter. + if (isSpeculativeModeInUse(node)) { + return; + } + + const typeFormCache = getTypeFormTypeCache(node); + const cacheEntries = typeFormCache.get(node.id) ?? []; + const oldEntry = cacheEntries.find((entry) => contextualTypeCacheEntryMatches(entry, expectedType)); + + updateIncompleteGenerationCount(typeResult, oldEntry?.typeResult); + + typeFormCache.set( + node.id, + addContextualTypeCacheEntry(cacheEntries, { typeResult, flags, incompleteGenCount, expectedType }) + ); + return; + } + // Should we use a temporary cache associated with a contextual // analysis of a function, contextualized based on call-site argument types? const typeCacheToUse = @@ -784,14 +866,8 @@ export function createTypeEvaluator( ? returnTypeInferenceTypeCache : typeCache; - if (!typeResult.isIncomplete) { - incompleteGenCount++; - } else { - const oldValue = typeCacheToUse.get(node.id); - if (oldValue !== undefined && !isTypeSame(typeResult.type, oldValue.typeResult.type)) { - incompleteGenCount++; - } - } + const oldValue = typeCacheToUse.get(node.id); + updateIncompleteGenerationCount(typeResult, oldValue?.typeResult); typeCacheToUse.set(node.id, { typeResult, flags, incompleteGenCount }); @@ -914,7 +990,7 @@ export function createTypeEvaluator( function getType(node: ExpressionNode): Type | undefined { initializePrefetchedTypes(node); - let type = evaluateTypeForSubnode(node, () => { + let type = evaluateContextualTypeForSubnode(node, () => { evaluateTypesForExpressionInContext(node); })?.type; @@ -966,20 +1042,27 @@ export function createTypeEvaluator( } function getTypeResult(node: ExpressionNode): TypeResult | undefined { - return evaluateTypeForSubnode(node, () => { + return evaluateContextualTypeForSubnode(node, () => { evaluateTypesForExpressionInContext(node); }); } function getTypeResultForDecorator(node: DecoratorNode): TypeResult | undefined { - return evaluateTypeForSubnode(node, () => { + return evaluateContextualTypeForSubnode(node, () => { evaluateTypesForExpressionInContext(node.d.expr); }); } // Reads the type of the node from the cache. function getCachedType(node: ExpressionNode | DecoratorNode): Type | undefined { - return readTypeCache(node, EvalFlags.None); + // Prefer the ordinary runtime type when both caches contain an entry for this node. + // Fall back to the contextual cache so TypeForm-only evaluations remain discoverable. + const cacheEntry = readTypeCacheEntry(node) ?? readContextualTypeCacheEntryForNode(node); + if (!cacheEntry || cacheEntry.typeResult.isIncomplete) { + return undefined; + } + + return cacheEntry.typeResult.type; } // Determines the expected type of a specified node based on surrounding @@ -1129,8 +1212,24 @@ export function createTypeEvaluator( flags = EvalFlags.None, inferenceContext?: InferenceContext ): TypeResult { + let useTypeFormCache = (flags & EvalFlags.TypeFormArg) !== 0; + if (inferenceContext) { + inferenceContext.expectedType = transformPossibleRecursiveTypeAlias(inferenceContext.expectedType); + useTypeFormCache ||= expectedTypeWantsTypeForm(inferenceContext.expectedType); + + if (expectedTypeRequiresTypeForm(inferenceContext.expectedType)) { + flags |= EvalFlags.TypeFormArg; + } + } + + if ((flags & EvalFlags.TypeFormArg) !== 0 && (flags & EvalFlags.NoConvertSpecialForm) === 0) { + flags |= EvalFlags.NoParamSpec | EvalFlags.NoTypeVarTuple; + } + // Is this type already cached? - const cacheEntry = readTypeCacheEntry(node); + const cacheEntry = useTypeFormCache + ? readTypeFormTypeCacheEntry(node, inferenceContext?.expectedType) + : readTypeCacheEntry(node); if (cacheEntry) { if (!cacheEntry.typeResult.isIncomplete || cacheEntry.incompleteGenCount === incompleteGenCount) { if (printExpressionTypes) { @@ -1148,7 +1247,9 @@ export function createTypeEvaluator( } // Is it cached in the speculative type cache? - const specCacheEntry = speculativeTypeTracker.getSpeculativeType(node, inferenceContext?.expectedType); + const specCacheEntry = useTypeFormCache + ? undefined + : speculativeTypeTracker.getSpeculativeType(node, inferenceContext?.expectedType); if (specCacheEntry) { if ( !specCacheEntry.typeResult.isIncomplete || @@ -1178,10 +1279,6 @@ export function createTypeEvaluator( // will be thrown at this point. checkForCancellation(); - if (inferenceContext) { - inferenceContext.expectedType = transformPossibleRecursiveTypeAlias(inferenceContext.expectedType); - } - // If we haven't already fetched some core type definitions from the // typeshed stubs, do so here. It would be better to fetch this when it's // needed in assignType, but we don't have access to the parse tree @@ -1661,7 +1758,10 @@ export function createTypeEvaluator( ): TypeResult { let typeResult: TypeResult | undefined; - if ((flags & EvalFlags.StrLiteralAsType) !== 0 && (flags & EvalFlags.TypeFormArg) === 0) { + if ( + (flags & EvalFlags.StrLiteralAsType) !== 0 && + ((flags & EvalFlags.TypeFormArg) === 0 || (flags & EvalFlags.NoConvertSpecialForm) !== 0) + ) { return getTypeOfStringListAsType(node, flags); } @@ -1799,8 +1899,6 @@ export function createTypeEvaluator( updatedFlags |= EvalFlags.NotParsed; } - updatedFlags &= ~EvalFlags.TypeFormArg; - if (node.d.annotation && (flags & EvalFlags.TypeExpression) !== 0) { return getTypeOfExpression(node.d.annotation, updatedFlags); } @@ -5138,6 +5236,11 @@ export function createTypeEvaluator( } } + if (isTypeVarTuple(type) && (flags & EvalFlags.NoTypeVarTuple) !== 0 && !type.priv.isInUnion) { + addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.typeVarTupleContext(), node); + type = UnknownType.create(); + } + // If we're expecting a type expression and got a sentinel literal instance, // treat it as its instantiable counterpart. This is similar to how None // is treated in a type expression context. @@ -5151,10 +5254,13 @@ export function createTypeEvaluator( reportUseOfTypeCheckOnly(type, node); } - if ((flags & EvalFlags.InstantiableType) !== 0) { + if ((flags & (EvalFlags.InstantiableType | EvalFlags.TypeFormArg)) !== 0) { if ((flags & EvalFlags.AllowGeneric) === 0) { if (isInstantiableClass(type) && ClassType.isBuiltIn(type, 'Generic')) { addDiagnostic(DiagnosticRule.reportGeneralTypeIssues, LocMessage.genericNotAllowed(), node); + if ((flags & EvalFlags.TypeFormArg) !== 0) { + type = UnknownType.create(); + } } } } @@ -5162,7 +5268,59 @@ export function createTypeEvaluator( return { type, isIncomplete }; } + const typeFormSpecialFormDiagnosticFactories: [string | string[], () => string][] = [ + ['Final', () => LocMessage.finalContext()], + ['Optional', () => LocMessage.optionalExtraArgs()], + ['Protocol', () => LocMessage.protocolNotAllowed()], + ['TypedDict', () => LocMessage.typedDictNotAllowed()], + ['TypeAlias', () => LocMessage.typeAnnotationVariable()], + ['Literal', () => LocMessage.literalNotAllowed()], + [['TypeGuard', 'TypeIs'], () => LocMessage.typeGuardArgCount()], + ['Union', () => LocMessage.unionTypeArgCount()], + ['Annotated', () => LocMessage.annotatedTypeArgMissing()], + ['ClassVar', () => LocMessage.classVarNotAllowed()], + ['Required', () => LocMessage.requiredArgCount()], + ['NotRequired', () => LocMessage.notRequiredArgCount()], + ['ReadOnly', () => LocMessage.readOnlyArgCount()], + ['Unpack', () => LocMessage.unpackArgCount()], + ['Concatenate', () => LocMessage.concatenateContext()], + ]; + + function rejectBareSpecialFormInTypeForm(type: ClassType, node: ExpressionNode): Type | undefined { + for (const [className, diagnosticFactory] of typeFormSpecialFormDiagnosticFactories) { + if (ClassType.isBuiltIn(type, className)) { + addDiagnostic(DiagnosticRule.reportInvalidTypeForm, diagnosticFactory(), node); + return UnknownType.create(); + } + } + + return undefined; + } + function addTypeFormForSymbol(node: ExpressionNode, type: Type, flags: EvalFlags, includesVarDecl: boolean): Type { + const isIndexBase = node.parent?.nodeType === ParseNodeType.Index && node.parent.d.leftExpr === node; + if ((flags & EvalFlags.TypeFormArg) !== 0 && isTypeVar(type) && TypeVarType.isSelf(type)) { + type = TypeBase.cloneWithTypeForm(type, convertToInstance(type)); + } + + if ((flags & EvalFlags.TypeFormArg) !== 0 && isInstantiableClass(type) && !isIndexBase) { + if (isTypeFormClass(type)) { + return createTypeFormType(type, node, /* typeArgs */ undefined); + } + + const rejectedType = rejectBareSpecialFormInTypeForm(type, node); + if (rejectedType) { + return rejectedType; + } + + if (ClassType.isBuiltIn(type, 'Self')) { + type = createSelfType(type, node, /* typeArgs */ undefined, flags); + if (isTypeVar(type)) { + type = TypeBase.cloneWithTypeForm(type, convertToInstance(type)); + } + } + } + const isValid = isSymbolValidTypeExpression(type, includesVarDecl); // If the type already has type information associated with it, don't replace. @@ -5612,7 +5770,7 @@ export function createTypeEvaluator( // Is this a generic class that needs to be specialized? if (isInstantiableClass(type)) { if ((flags & EvalFlags.InstantiableType) !== 0 && (flags & EvalFlags.AllowMissingTypeArgs) === 0) { - if (!type.props?.typeAliasInfo && requiresTypeArgs(type)) { + if (!type.props?.typeAliasInfo && !isTypeFormClass(type) && requiresTypeArgs(type)) { if (!type.priv.typeArgs || !type.priv.isTypeArgExplicit) { addDiagnostic( DiagnosticRule.reportMissingTypeArgument, @@ -7956,9 +8114,10 @@ export function createTypeEvaluator( if (ClassType.isBuiltIn(concreteSubtype, 'InitVar')) { // Special-case InitVar, used in dataclasses. const typeArgs = getTypeArgs(node, flags); + const isTypeFormArg = (flags & EvalFlags.TypeFormArg) !== 0; - if ((flags & EvalFlags.TypeExpression) !== 0) { - if ((flags & EvalFlags.VarTypeAnnotation) === 0) { + if ((flags & EvalFlags.TypeExpression) !== 0 || isTypeFormArg) { + if (isTypeFormArg || (flags & EvalFlags.VarTypeAnnotation) === 0) { addDiagnostic( DiagnosticRule.reportInvalidTypeForm, LocMessage.initVarNotAllowed(), @@ -7967,6 +8126,10 @@ export function createTypeEvaluator( } } + if (isTypeFormArg) { + return UnknownType.create(); + } + if (typeArgs.length === 1) { return typeArgs[0].type; } else { @@ -8475,7 +8638,9 @@ export function createTypeEvaluator( function getTypeArgs(node: IndexNode, flags: EvalFlags, options?: GetTypeArgsOptions): TypeResultWithNode[] { const typeArgs: TypeResultWithNode[] = []; let adjFlags = flags | EvalFlags.NoConvertSpecialForm; - adjFlags &= ~EvalFlags.TypeFormArg; + if ((adjFlags & EvalFlags.TypeFormArg) !== 0) { + adjFlags |= EvalFlags.TypeExpression; + } const allowFinalClassVar = () => { // If the annotation is a variable within the body of a dataclass, a @@ -8881,8 +9046,9 @@ export function createTypeEvaluator( isInstantiableClass(baseTypeResult.type) && ClassType.isBuiltIn(baseTypeResult.type, 'TypeVar') && AnalyzerNodeInfo.getFileInfo(node).isTypingStubFile; + const isTypeFormCall = isInstantiableClass(baseTypeResult.type) && isTypeFormClass(baseTypeResult.type); - if (!isCyclicalTypeVarCall) { + if (!isCyclicalTypeVarCall && !isTypeFormCall) { argList.forEach((arg) => { if ( arg.valueExpression && @@ -15899,7 +16065,7 @@ export function createTypeEvaluator( // If no type arguments are provided, the resulting type // depends on whether we're evaluating a type annotation or // we're in some other context. - if ((flags & EvalFlags.TypeExpression) !== 0) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) !== 0) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.optionalExtraArgs(), errorNode); return UnknownType.create(); } @@ -15997,7 +16163,10 @@ export function createTypeEvaluator( type = UnknownType.create(); isValidTypeForm = false; } - } else if (itemExpr.nodeType === ParseNodeType.StringList) { + } else if ( + itemExpr.nodeType === ParseNodeType.StringList && + itemExpr.d.strings.every((stringNode) => stringNode.nodeType === ParseNodeType.String) + ) { const isBytes = (itemExpr.d.strings[0].d.token.flags & StringTokenFlags.Bytes) !== 0; const value = itemExpr.d.strings.map((s) => s.d.value).join(''); if (isBytes) { @@ -16111,9 +16280,9 @@ export function createTypeEvaluator( typeArgs: TypeResultWithNode[] | undefined, flags: EvalFlags ): Type { - if (flags & EvalFlags.NoClassVar) { + if (flags & (EvalFlags.NoClassVar | EvalFlags.TypeFormArg)) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.classVarNotAllowed(), errorNode); - return AnyType.create(); + return (flags & EvalFlags.TypeFormArg) !== 0 ? UnknownType.create() : AnyType.create(); } if (!typeArgs) { @@ -16146,8 +16315,22 @@ export function createTypeEvaluator( errorNode: ExpressionNode, typeArgs: TypeResultWithNode[] | undefined ): Type { - if (!typeArgs || typeArgs.length === 0) { - return ClassType.specialize(classType, [UnknownType.create()]); + if (!typeArgs) { + const specializedType = ClassType.specialize(classType, [AnyType.create()]); + return TypeBase.cloneWithTypeForm(specializedType, ClassType.cloneAsInstance(specializedType)); + } + + if (typeArgs.length === 0) { + addDiagnostic( + DiagnosticRule.reportInvalidTypeForm, + LocMessage.typeArgsTooFew().format({ + name: classType.priv.aliasName || classType.shared.name, + expected: 1, + received: 0, + }), + errorNode + ); + return UnknownType.create(); } if (typeArgs.length > 1) { @@ -16184,11 +16367,11 @@ export function createTypeEvaluator( // depends on whether we're evaluating a type annotation or // we're in some other context. if (!typeArgs) { - if ((flags & EvalFlags.TypeExpression) !== 0) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) !== 0) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.typeGuardArgCount(), errorNode); } - return classType; + return (flags & EvalFlags.TypeFormArg) !== 0 ? UnknownType.create() : classType; } else if (typeArgs.length !== 1) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.typeGuardArgCount(), errorNode); return UnknownType.create(); @@ -16233,7 +16416,7 @@ export function createTypeEvaluator( const enclosingClassTypeResult = enclosingClass ? getTypeOfClass(enclosingClass) : undefined; if (!enclosingClassTypeResult) { - if ((flags & (EvalFlags.TypeExpression | EvalFlags.InstantiableType)) !== 0) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.InstantiableType | EvalFlags.TypeFormArg)) !== 0) { addDiagnostic(DiagnosticRule.reportGeneralTypeIssues, LocMessage.selfTypeContext(), errorNode); } @@ -16309,12 +16492,14 @@ export function createTypeEvaluator( // If no type arguments are provided, the resulting type // depends on whether we're evaluating a type annotation or // we're in some other context. - if (!typeArgs && (flags & EvalFlags.TypeExpression) === 0) { + const typeExpressionFlags = EvalFlags.TypeExpression | EvalFlags.TypeFormArg; + + if (!typeArgs && (flags & typeExpressionFlags) === 0) { return { type: classType }; } if (!typeArgs || typeArgs.length !== 1) { - if ((flags & EvalFlags.TypeExpression) !== 0) { + if ((flags & typeExpressionFlags) !== 0) { addDiagnostic( DiagnosticRule.reportInvalidTypeForm, classType.shared.name === 'ReadOnly' @@ -16326,7 +16511,7 @@ export function createTypeEvaluator( ); } - return { type: classType }; + return { type: (flags & EvalFlags.TypeFormArg) !== 0 ? UnknownType.create() : classType }; } const typeArgType = typeArgs[0].type; @@ -16378,7 +16563,7 @@ export function createTypeEvaluator( } if (!isUsageLegal) { - if ((flags & EvalFlags.TypeExpression) !== 0) { + if ((flags & typeExpressionFlags) !== 0) { addDiagnostic( DiagnosticRule.reportInvalidTypeForm, classType.shared.name === 'ReadOnly' @@ -16390,7 +16575,7 @@ export function createTypeEvaluator( ); } - return { type: classType }; + return { type: (flags & EvalFlags.TypeFormArg) !== 0 ? UnknownType.create() : classType }; } return { type: typeArgType, isReadOnly, isRequired, isNotRequired }; @@ -16403,10 +16588,10 @@ export function createTypeEvaluator( flags: EvalFlags ): Type { if (!typeArgs || typeArgs.length !== 1) { - if ((flags & EvalFlags.TypeExpression) !== 0) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) !== 0) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.unpackArgCount(), errorNode); } - return classType; + return (flags & EvalFlags.TypeFormArg) !== 0 ? UnknownType.create() : classType; } const typeArgType = typeArgs[0].type; @@ -16417,7 +16602,7 @@ export function createTypeEvaluator( return unpackedType; } - if ((flags & EvalFlags.TypeExpression) === 0) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) === 0) { return classType; } addDiagnostic(DiagnosticRule.reportGeneralTypeIssues, LocMessage.unpackExpectedTypeVarTuple(), errorNode); @@ -16436,9 +16621,17 @@ export function createTypeEvaluator( return UnknownType.create(); } - if ((flags & EvalFlags.TypeExpression) === 0) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) === 0) { return classType; } + if ((flags & EvalFlags.TypeFormArg) !== 0) { + if (isUnknown(typeArgType)) { + return typeArgType; + } + if (isTypeVar(typeArgType) && !typeArgType.priv.scopeId) { + return UnknownType.create(); + } + } addDiagnostic(DiagnosticRule.reportGeneralTypeIssues, LocMessage.unpackNotAllowed(), errorNode); return UnknownType.create(); } @@ -16450,11 +16643,11 @@ export function createTypeEvaluator( typeArgs: TypeResultWithNode[] | undefined, flags: EvalFlags ): Type { - if (flags & EvalFlags.NoFinal) { - if ((flags & EvalFlags.TypeExpression) !== 0) { + if (flags & (EvalFlags.NoFinal | EvalFlags.TypeFormArg)) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) !== 0) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.finalContext(), errorNode); } - return classType; + return (flags & EvalFlags.TypeFormArg) !== 0 ? UnknownType.create() : classType; } if ((flags & EvalFlags.TypeExpression) === 0 || !typeArgs || typeArgs.length === 0) { @@ -16475,10 +16668,10 @@ export function createTypeEvaluator( flags: EvalFlags ): Type { if ((flags & EvalFlags.AllowConcatenate) === 0) { - if ((flags & EvalFlags.TypeExpression) !== 0) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) !== 0) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.concatenateContext(), errorNode); } - return classType; + return (flags & EvalFlags.TypeFormArg) !== 0 ? UnknownType.create() : classType; } if (!typeArgs || typeArgs.length === 0) { @@ -16742,7 +16935,7 @@ export function createTypeEvaluator( // If no type arguments are provided, the resulting type // depends on whether we're evaluating a type annotation or // we're in some other context. - if ((flags & EvalFlags.TypeExpression) !== 0) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) !== 0) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.unionTypeArgCount(), errorNode); return NeverType.createNever(); } @@ -16788,7 +16981,7 @@ export function createTypeEvaluator( // is allowed if it's an unpacked TypeVarTuple or tuple. None is also allowed // since it is used to define NoReturn in typeshed stubs). if (types.length === 1 && !allowSingleTypeArg && !isNoneInstance(types[0])) { - if ((flags & EvalFlags.TypeExpression) !== 0) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) !== 0) { addDiagnostic(DiagnosticRule.reportInvalidTypeArguments, LocMessage.unionTypeArgCount(), errorNode); } isValidTypeForm = false; @@ -16823,11 +17016,16 @@ export function createTypeEvaluator( // If no type arguments are provided, the resulting type // depends on whether we're evaluating a type annotation or // we're in some other context. - if ((flags & (EvalFlags.TypeExpression | EvalFlags.NoNakedGeneric)) !== 0) { + if ((flags & (EvalFlags.TypeExpression | EvalFlags.NoNakedGeneric | EvalFlags.TypeFormArg)) !== 0) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.genericTypeArgMissing(), errorNode); } - return classType; + return (flags & EvalFlags.TypeFormArg) !== 0 ? UnknownType.create() : classType; + } + + if ((flags & EvalFlags.TypeFormArg) !== 0) { + addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.genericNotAllowed(), errorNode); + return UnknownType.create(); } const uniqueTypeVars: TypeVarType[] = []; @@ -17226,6 +17424,33 @@ export function createTypeEvaluator( return undefined; } + function cachedAssignmentTargetMayHaveDeclaredType(expression: ExpressionNode): boolean { + switch (expression.nodeType) { + case ParseNodeType.Name: { + const symbolWithScope = lookUpSymbolRecursive(expression, expression.d.value, /* honorCodeFlow */ true); + return ( + !!symbolWithScope && + (symbolWithScope.symbol.hasTypedDeclarations() || symbolWithScope.scope.type === ScopeType.Class) + ); + } + + case ParseNodeType.TypeAnnotation: + case ParseNodeType.MemberAccess: + case ParseNodeType.Index: + return true; + + case ParseNodeType.Tuple: + return ( + expression.d.items.length > 0 && + !expression.d.items.some((item) => item.nodeType === ParseNodeType.Unpack) && + expression.d.items.every((item) => cachedAssignmentTargetMayHaveDeclaredType(item)) + ); + + default: + return false; + } + } + function evaluateTypesForAssignmentStatement(node: AssignmentNode): void { const fileInfo = AnalyzerNodeInfo.getFileInfo(node); @@ -17255,6 +17480,19 @@ export function createTypeEvaluator( let rightHandType = readTypeCache(node.d.rightExpr, /* flags */ undefined); let isIncomplete = false; let expectedTypeDiagAddendum: DiagnosticAddendum | undefined; + let declaredType: Type | undefined; + let declaredTypeResolved = false; + + // A runtime-first query may have cached the RHS without its assignment + // context. Re-evaluate it when the annotation expects a TypeForm so the + // ordinary cache cannot suppress contextual validation and conversion. + if (rightHandType && cachedAssignmentTargetMayHaveDeclaredType(node.d.leftExpr)) { + declaredType = getDeclaredTypeForExpression(node.d.leftExpr, { method: 'set' }); + declaredTypeResolved = true; + if (declaredType && expectedTypeWantsTypeForm(declaredType)) { + rightHandType = undefined; + } + } if (!rightHandType) { // Special-case the typing.pyi file, which contains some special @@ -17325,7 +17563,9 @@ export function createTypeEvaluator( } } - let declaredType = getDeclaredTypeForExpression(node.d.leftExpr, { method: 'set' }); + if (!declaredTypeResolved) { + declaredType = getDeclaredTypeForExpression(node.d.leftExpr, { method: 'set' }); + } if (declaredType) { const liveTypeVarScopes = ParseTreeUtils.getTypeVarScopesForNode(node); @@ -21234,9 +21474,21 @@ export function createTypeEvaluator( // within that tree. If the type cannot be determined (because it's part // of a cyclical dependency), the function returns undefined. function evaluateTypeForSubnode(subnode: ParseNode, callback: () => void): TypeResult | undefined { + return evaluateTypeForSubnodeWithCache(subnode, callback, readTypeCacheEntry); + } + + function evaluateContextualTypeForSubnode(subnode: ParseNode, callback: () => void): TypeResult | undefined { + return evaluateTypeForSubnodeWithCache(subnode, callback, readContextualTypeCacheEntryForNode); + } + + function evaluateTypeForSubnodeWithCache( + subnode: ParseNode, + callback: () => void, + readCacheEntry: (node: ParseNode) => TypeCacheEntry | undefined + ): TypeResult | undefined { // If the type cache is already populated with a complete type, // don't bother doing additional work. - let cacheEntry = readTypeCacheEntry(subnode); + let cacheEntry = readCacheEntry(subnode); if (cacheEntry && !cacheEntry.typeResult.isIncomplete) { const typeResult = cacheEntry.typeResult; @@ -21254,7 +21506,7 @@ export function createTypeEvaluator( } callback(); - cacheEntry = readTypeCacheEntry(subnode); + cacheEntry = readCacheEntry(subnode); if (cacheEntry) { return cacheEntry.typeResult; } @@ -21416,7 +21668,11 @@ export function createTypeEvaluator( } case 'Protocol': { - if ((flags & (EvalFlags.NoNonTypeSpecialForms | EvalFlags.TypeExpression)) !== 0) { + if ( + (flags & + (EvalFlags.NoNonTypeSpecialForms | EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) !== + 0 + ) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.protocolNotAllowed(), errorNode); } @@ -21441,7 +21697,11 @@ export function createTypeEvaluator( } case 'TypedDict': { - if ((flags & (EvalFlags.NoNonTypeSpecialForms | EvalFlags.TypeExpression)) !== 0) { + if ( + (flags & + (EvalFlags.NoNonTypeSpecialForms | EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) !== + 0 + ) { const isInlinedTypedDict = AnalyzerNodeInfo.getFileInfo(errorNode).diagnosticRuleSet.enableExperimentalFeatures && !!typeArgs; @@ -21459,13 +21719,29 @@ export function createTypeEvaluator( } case 'Literal': { - if ((flags & (EvalFlags.NoNonTypeSpecialForms | EvalFlags.TypeExpression)) !== 0) { + if ( + (flags & + (EvalFlags.NoNonTypeSpecialForms | EvalFlags.TypeExpression | EvalFlags.TypeFormArg)) !== + 0 + ) { addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.literalNotAllowed(), errorNode); } isValidTypeForm = false; break; } + case 'TypeAlias': { + if ((flags & EvalFlags.TypeFormArg) !== 0) { + addDiagnostic( + DiagnosticRule.reportInvalidTypeForm, + LocMessage.typeAnnotationVariable(), + errorNode + ); + } + isValidTypeForm = false; + break; + } + case 'Tuple': { return { type: createSpecialType( @@ -21624,7 +21900,12 @@ export function createTypeEvaluator( ); } - return { type: typeArgs[0].inlinedTypeDict }; + let inlinedTypeDict: Type = typeArgs[0].inlinedTypeDict; + if ((flags & EvalFlags.TypeFormArg) !== 0) { + inlinedTypeDict = TypeBase.cloneWithTypeForm(inlinedTypeDict, convertToInstance(inlinedTypeDict)); + } + + return { type: inlinedTypeDict }; } else if (typeArgCount > typeParams.length) { if (!ClassType.isPartiallyEvaluated(classType) && !ClassType.isTupleClass(classType)) { if (typeParams.length === 0) { @@ -24000,6 +24281,7 @@ export function createTypeEvaluator( // this function so we can analyze it separately without polluting // the main type cache. const prevTypeCache = returnTypeInferenceTypeCache; + const prevTypeFormTypeCache = returnTypeInferenceTypeFormTypeCache; returnTypeInferenceContextStack.push({ functionNode, codeFlowAnalyzer: codeFlowEngine.createCodeFlowAnalyzer(), @@ -24007,6 +24289,7 @@ export function createTypeEvaluator( try { returnTypeInferenceTypeCache = new Map(); + returnTypeInferenceTypeFormTypeCache = new Map(); let allArgTypesAreUnknown = true; functionNode.d.params.forEach((param, index) => { @@ -24084,6 +24367,7 @@ export function createTypeEvaluator( } finally { returnTypeInferenceContextStack.pop(); returnTypeInferenceTypeCache = prevTypeCache; + returnTypeInferenceTypeFormTypeCache = prevTypeFormTypeCache; } }); @@ -24896,6 +25180,22 @@ export function createTypeEvaluator( } } + // A subscripted runtime built-in such as list[int] is represented as its + // precise class type, but it is also a types.GenericAlias object at runtime. + if ( + isClassInstance(destType) && + ClassType.isBuiltIn(destType, 'GenericAlias') && + isInstantiableClass(srcType) && + !ClassType.isSpecialBuiltIn(srcType) && + !srcType.priv.aliasName && + srcType.shared.moduleName === 'builtins' && + srcType.priv.typeArgs !== undefined && + srcType.props?.typeForm && + classGetItemReturnsGenericAlias(srcType) + ) { + return true; + } + // If the source is a class-like type created by a call to NewType, treat it // as a FunctionClass instance rather than an instantiable class for // purposes of assignability. This reflects its actual runtime type. @@ -25338,7 +25638,7 @@ export function createTypeEvaluator( const destTypeArg = destType.priv.typeArgs && destType.priv.typeArgs.length > 0 ? destType.priv.typeArgs[0] - : UnknownType.create(); + : AnyType.create(); let srcTypeArg: Type | undefined; if (isClassInstance(concreteSrcType) && ClassType.isBuiltIn(concreteSrcType, 'type')) { @@ -25725,13 +26025,52 @@ export function createTypeEvaluator( return isAssignable; } - function expectedTypeWantsTypeForm(expectedType: Type): boolean { - return someSubtypes( - expectedType, - (subtype) => isClassInstance(subtype) && ClassType.isBuiltIn(subtype, 'TypeForm') + function isTypeFormClass(type: ClassType): boolean { + return ( + ClassType.isBuiltIn(type, 'TypeForm') || + (ClassType.isSpecialBuiltIn(type) && + (type.shared.name === 'TypeForm' || type.priv.aliasName === 'TypeForm')) ); } + function isTypeFormType(type: Type): boolean { + return isClassInstance(type) && isTypeFormClass(type); + } + + function classGetItemReturnsGenericAlias(classType: ClassType): boolean { + const member = lookUpClassMember(classType, '__class_getitem__', MemberAccessFlags.SkipInstanceMembers); + if (!member) { + return false; + } + + const memberType = getTypeOfMember(member); + const functionReturnsGenericAlias = (functionType: FunctionType) => { + const returnType = FunctionType.getEffectiveReturnType(functionType); + return !!returnType && isClassInstance(returnType) && ClassType.isBuiltIn(returnType, 'GenericAlias'); + }; + + if (isFunction(memberType)) { + return functionReturnsGenericAlias(memberType); + } + + if (isOverloaded(memberType)) { + return OverloadedType.getOverloads(memberType).every(functionReturnsGenericAlias); + } + + return false; + } + + function expectedTypeRequiresTypeForm(expectedType: Type): boolean { + return ( + someSubtypes(expectedType, isTypeFormType) && + !someSubtypes(expectedType, (subtype) => !isTypeFormType(subtype)) + ); + } + + function expectedTypeWantsTypeForm(expectedType: Type): boolean { + return someSubtypes(expectedType, isTypeFormType); + } + // If the expected type is an explicit TypeForm type, see if the source // type has an implicit TypeForm type that can be assigned to it. If so, // convert to an explicit TypeForm type. @@ -25775,9 +26114,7 @@ export function createTypeEvaluator( } const destTypeFormType = - subtype.priv.typeArgs && subtype.priv.typeArgs.length > 0 - ? subtype.priv.typeArgs[0] - : UnknownType.create(); + subtype.priv.typeArgs && subtype.priv.typeArgs.length > 0 ? subtype.priv.typeArgs[0] : AnyType.create(); if (assignType(destTypeFormType, srcTypeFormType)) { resultType = ClassType.specialize(subtype, [srcTypeFormType]); diff --git a/packages/pyright-internal/src/tests/samples/call5.py b/packages/pyright-internal/src/tests/samples/call5.py index eba6913f1d6d..09ac1010f212 100644 --- a/packages/pyright-internal/src/tests/samples/call5.py +++ b/packages/pyright-internal/src/tests/samples/call5.py @@ -4,6 +4,7 @@ from typing import NamedTuple, List, Tuple +# The field-name strings are ordinary values, not stringified type expressions. X = NamedTuple("X", [("a", int), ("b", str), ("c", str)]) q0: List[Tuple[int, str, str]] = [(1, "", ""), (2, "", "")] @@ -71,6 +72,16 @@ class Z(NamedTuple): reveal_type(b, expected_text="int") +ColumnRecord = NamedTuple("ColumnRecord", [("text", list[str]), ("numbers", list[int])]) +column_record = ColumnRecord(["one"], [1]) + +# This combines functional NamedTuple field-name strings with application code +# that transposes columnar data, the recursion pattern from the TypeForm regression. +for text, number in zip(*column_record): + reveal_type(text, expected_text="str") + reveal_type(number, expected_text="int") + + def func1(a: list[str], c: list[int]): ... diff --git a/packages/pyright-internal/src/tests/samples/typeForm2.py b/packages/pyright-internal/src/tests/samples/typeForm2.py index 078bc149dc0d..c31d5dd44088 100644 --- a/packages/pyright-internal/src/tests/samples/typeForm2.py +++ b/packages/pyright-internal/src/tests/samples/typeForm2.py @@ -162,6 +162,9 @@ def func3[**P, R](x: Callable[P, R]) -> Callable[P, R]: t1 = tf(Callable[Concatenate[int, P], R]) reveal_type(t1, expected_text="TypeForm[(int, **P@func3) -> R@func3]") + t2 = TypeForm(Callable[P, R]) + reveal_type(t2, expected_text="TypeForm[(**P@func3) -> R@func3]") + return x @@ -209,10 +212,8 @@ def func6(x: T) -> T: return x -# These should maybe generage errors, but given -# that the typing spec doesn't say anything about how -# to evaluate the type of a special form when it's used -# in a value expression context, it's not clear. +# These special forms aren't valid type expressions in this context, +# so each should generate an error. def func7(): t1 = tf(Generic) diff --git a/packages/pyright-internal/src/tests/samples/typeForm3.py b/packages/pyright-internal/src/tests/samples/typeForm3.py index b70befa8cda7..119d7a556144 100644 --- a/packages/pyright-internal/src/tests/samples/typeForm3.py +++ b/packages/pyright-internal/src/tests/samples/typeForm3.py @@ -1,6 +1,12 @@ # This sample tests inference behaviors related to TypeForm. -# pyright: strict +# pyright: strict, reportMissingModuleSource=false + +from types import GenericAlias +from enum import member +from typing import Pattern, TypeGuard +from typing_extensions import TypeForm, TypeIs +from warnings import catch_warnings def func1(): @@ -25,3 +31,14 @@ def func2(): v3 = {int | str: str | bytes} reveal_type(v3, expected_text="dict[UnionType, UnionType]") + +v4: GenericAlias = list[int] + +# These should generate errors because typing special forms don't produce +# types.GenericAlias objects at runtime. +v5: GenericAlias = TypeGuard[int] +v6: GenericAlias = TypeIs[int] +v7: GenericAlias = TypeForm[int] +v8: GenericAlias = catch_warnings[None] +v9: GenericAlias = member[int] +v10: GenericAlias = Pattern[str] diff --git a/packages/pyright-internal/src/tests/samples/typeForm4.py b/packages/pyright-internal/src/tests/samples/typeForm4.py index 8a141439d875..566602232dd3 100644 --- a/packages/pyright-internal/src/tests/samples/typeForm4.py +++ b/packages/pyright-internal/src/tests/samples/typeForm4.py @@ -2,10 +2,12 @@ # pyright: reportMissingModuleSource=false +from dataclasses import InitVar from typing import ( Annotated, Any, Callable, + ClassVar, Concatenate, Final, Generic, @@ -16,11 +18,16 @@ NoReturn, NotRequired, Optional, + Protocol, Required, + Self, + assert_type, Type, TypeAlias, TypeGuard, + TypedDict, TypeVar, + TypeVarTuple, Union, Unpack, ) @@ -33,6 +40,9 @@ TA4 = int | str type TA5[T] = int +# TypeAlias is accepted as a nested type argument outside a TypeForm context. +ordinary_type_alias: list[TypeAlias] + def func1(): t1: TypeForm[int | str] = int @@ -115,13 +125,24 @@ def func4(): def func5[**P, R](): t1: TypeForm[LiteralString] = typing.LiteralString t2: TypeForm = TypeForm[int | str] + + # This should generate an error because a ParamSpec isn't a valid + # type expression on its own. t3: TypeForm = "P" + t4: TypeForm = "typing.Callable" t5: TypeForm = "Union[int, str]" t6: TypeForm = NT1 + # These should generate errors because ParamSpecs and TypeVarTuples aren't + # valid type expressions on their own. + t7: TypeForm = P + t8: TypeForm = Ts + t9: TypeForm = "Ts" + T = TypeVar("T") +Ts = TypeVarTuple("Ts") def func6[**P, R](): @@ -239,3 +260,77 @@ def func10[T](x: type[T], y: type[int]): t3: TypeForm = y t4: TypeForm[int] = y t5: TypeForm[float] = y + + +def func11(): + # This should generate an error because Self isn't valid outside a class. + t1: TypeForm = Self + + # This should generate an error because ClassVar isn't valid in a TypeForm. + t2: TypeForm = ClassVar[int] + + # This should generate an error because Final isn't valid in a TypeForm. + t3: TypeForm = Final[int] + + # This should generate an error because Unpack isn't valid in this context. + t4: TypeForm = Unpack[Ts] + + # This should generate an error because Optional requires a type argument. + t5: TypeForm = Optional + + # These should generate errors because these bare special forms aren't + # valid type expressions. + t6: TypeForm = Protocol + t7: TypeForm = TypedDict + t8: TypeForm = TypeAlias + t9: TypeForm = Literal + + # These should generate errors because forbidden qualifiers remain invalid + # when nested within another type expression. + t10: TypeForm = list[Final[int]] + t11: TypeForm = list[Required[int]] + + # These bare special forms aren't valid type expressions. + t12: TypeForm = TypeGuard + t13: TypeForm = TypeIs + t14: TypeForm = Union + t15: TypeForm = Annotated + t16: TypeForm = ClassVar + t17: TypeForm = Required + t18: TypeForm = NotRequired + t19: TypeForm = ReadOnly + t20: TypeForm = Unpack + t21: TypeForm = Concatenate + t22: TypeForm = Generic + t23: TypeForm = Final + + # These should generate errors for an empty TypeForm argument list, + # a nested annotation marker, and a dataclass-only annotation. + t24: TypeForm = TypeForm[()] + t25: TypeForm = list[TypeAlias] + t26: TypeForm = InitVar[int] + + +class ClassWithSelf: + def method(self): + t1 = TypeForm(Self) + assert_type(t1, TypeForm[Self]) + + t2 = TypeForm("Self") + assert_type(t2, TypeForm[Self]) + + t3: TypeForm[Self] = "Self" + + t4 = TypeForm(list[Self]) + assert_type(t4, TypeForm[list[Self]]) + + t5 = TypeForm("list[Self]") + assert_type(t5, TypeForm[list[Self]]) + + t6: TypeForm[list[Self]] = "list[Self]" + + +def func12[*ScopedTs](): + # This should generate an error because a TypeVarTuple isn't a valid + # type expression on its own. + t1: TypeForm = ScopedTs diff --git a/packages/pyright-internal/src/tests/samples/typeForm6.py b/packages/pyright-internal/src/tests/samples/typeForm6.py index bf679f0b9821..b8f447ede5df 100644 --- a/packages/pyright-internal/src/tests/samples/typeForm6.py +++ b/packages/pyright-internal/src/tests/samples/typeForm6.py @@ -1,9 +1,9 @@ # This sample tests the handling of assert_type with TypeForm types. -# pyright: reportMissingModuleSource=false +# pyright: reportMissingModuleSource=false, reportMissingTypeArgument=error from types import UnionType -from typing import assert_type +from typing import Any, assert_type from typing_extensions import TypeForm @@ -60,3 +60,16 @@ def func3[T](x: T) -> T: assert_type(v3_tf, TypeForm[list[str | T] | T]) return x + + +def func4(x: TypeForm): + reveal_type(x, expected_text="TypeForm[Any]") + assert_type(x, TypeForm[Any]) + + +def unwrap[T](x: TypeForm[T]) -> T: + raise NotImplementedError() + + +bare_type_form = unwrap(TypeForm) +reveal_type(bare_type_form, expected_text="TypeForm[Any]") diff --git a/packages/pyright-internal/src/tests/samples/typeForm8.py b/packages/pyright-internal/src/tests/samples/typeForm8.py index 7209d83400fc..ec521a82106b 100644 --- a/packages/pyright-internal/src/tests/samples/typeForm8.py +++ b/packages/pyright-internal/src/tests/samples/typeForm8.py @@ -42,3 +42,4 @@ # A Union element type that includes a TypeForm should still trigger # interpretation of the string as a stringified TypeForm. v11: list[Union[TypeForm, int]] = ["int"] +v12: list[Union[int, TypeForm]] = ["int"] diff --git a/packages/pyright-internal/src/tests/samples/typeForm9.py b/packages/pyright-internal/src/tests/samples/typeForm9.py new file mode 100644 index 000000000000..4659745809d9 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/typeForm9.py @@ -0,0 +1,176 @@ +# This sample tests TypeForm in realistic schema and serializer APIs while +# ensuring ordinary runtime expressions remain outside TypeForm evaluation. + +# pyright: reportMissingModuleSource=false + +from collections.abc import Callable +from types import GenericAlias, UnionType +from typing import ( + Annotated, + Any, + ClassVar, + Concatenate, + Final, + Literal, + NamedTuple, + ParamSpec, + Self, + TypedDict, + TypeVarTuple, + Unpack, + assert_type, +) +from typing_extensions import Sentinel, TypeForm + + +class Codec[T]: + def encode(self, value: T) -> bytes: + raise NotImplementedError + + def decode(self, payload: bytes) -> T: + raise NotImplementedError + + +def codec_for[T](schema: TypeForm[T]) -> Codec[T]: + raise NotImplementedError + + +def serializer[T](schema: TypeForm[T]) -> Callable[[Callable[[T], bytes]], Callable[[T], bytes]]: + def decorate(func: Callable[[T], bytes]) -> Callable[[T], bytes]: + return func + + return decorate + + +class User(TypedDict): + id: int + name: str + + +type UserId = Annotated[int, "user-id"] +type UserBatch = list[User] + +user_codec = codec_for(User) +assert_type(user_codec, Codec[User]) + +forward_user_codec = codec_for("User") +assert_type(forward_user_codec, Codec[User]) + +optional_user_codec = codec_for("User | None") +assert_type(optional_user_codec, Codec[User | None]) + +batch_codec = codec_for(UserBatch) +assert_type(batch_codec, Codec[list[User]]) + +annotated_codec = codec_for(Annotated[UserId, "wire"]) +assert_type(annotated_codec, Codec[int]) + +nested_codec = codec_for("dict[str, list[User]]") +assert_type(nested_codec, Codec[dict[str, list[User]]]) + +schema_registry: dict[str, list[TypeForm[Any]]] = { + "users": [User, "User | None", list[User]], + "ids": [UserId, "list[UserId]"], +} + + +@serializer("User") +def dump_user(value: User) -> bytes: + return str(value).encode() + + +assert_type(dump_user, Callable[[User], bytes]) + + +def runtime_decorator[**P, R](func: Callable[P, R]) -> Callable[P, R]: + return func + + +@runtime_decorator +def user_label(value: User) -> str: + return value["name"] + + +assert_type(user_label({"id": 1, "name": "Ada"}), str) + + +# The same expressions retain their ordinary runtime types outside TypeForm contexts. +runtime_generic_alias: GenericAlias = list[User] +runtime_union_type: UnionType = User | None +typed_generic_alias: TypeForm[list[User]] = list[User] +typed_union_type: TypeForm[User | None] = User | None + + +class Resource: + @classmethod + def codec(cls) -> Codec[Self]: + return codec_for(Self) + + +assert_type(Resource.codec(), Codec[Resource]) + + +def middleware_codec[**P, R]( + handler: Callable[P, R], +) -> Codec[Callable[Concatenate[str, P], R]]: + return codec_for(Callable[Concatenate[str, P], R]) + + +def tuple_codec[*Ts](values: tuple[*Ts]) -> Codec[tuple[*Ts]]: + return codec_for(tuple[Unpack[Ts]]) + + +MISSING = Sentinel("MISSING") +missing_codec = codec_for(MISSING) +assert_type(missing_codec, Codec[MISSING]) + +optional_missing_codec = codec_for(int | MISSING) +assert_type(optional_missing_codec, Codec[int | MISSING]) + +literal_codec = codec_for(Literal["int", "list[str]"]) +assert_type(literal_codec, Codec[Literal["int", "list[str]"]]) + + +# Mixed contexts preserve valid non-TypeForm alternatives. +def accept_string_or_type(value: str | TypeForm[int]) -> None: + pass + + +accept_string_or_type("not a type") +accept_string_or_type(int) +mixed_registry: list[str | TypeForm[int]] = ["not a type", int] + + +# Ordinary strings, Literal values, calls, and NamedTuple field names stay runtime-scoped. +ordinary_names = ["int", "list[str]"] +assert_type(ordinary_names, list[str]) + +ordinary_literal: Literal["int"] = "int" +assert_type(ordinary_literal, Literal["int"]) + +Pair = NamedTuple("Pair", [("name", str), ("value", int)]) +pair = Pair("count", 1) +assert_type(pair.name, str) +assert_type(pair.value, int) + + +P = ParamSpec("P") +Ts = TypeVarTuple("Ts") +literal_value = "int" + + +def make_user() -> User: + return {"id": 1, "name": "Ada"} + + +# These should each generate an error because the argument is not a valid type expression. +bad_string = codec_for("not a type") +bad_expression = codec_for(1 + 2) +bad_call = codec_for(make_user()) +bad_class_var = codec_for(ClassVar[int]) +bad_final = codec_for(Final[int]) +bad_param_spec = codec_for(P) +bad_type_var_tuple = codec_for(Ts) +bad_unpack = codec_for(Unpack[Ts]) +bad_literal_variable = codec_for(Literal[literal_value]) +bad_literal_f_string = codec_for(Literal[f"{literal_value}"]) diff --git a/packages/pyright-internal/src/tests/samples/typedDictInline1.py b/packages/pyright-internal/src/tests/samples/typedDictInline1.py index 77524544ccb5..a1948df9b82e 100644 --- a/packages/pyright-internal/src/tests/samples/typedDictInline1.py +++ b/packages/pyright-internal/src/tests/samples/typedDictInline1.py @@ -1,6 +1,7 @@ # This sample tests support for inlined TypedDict definitions. -from typing import NotRequired, ReadOnly, Required, TypedDict +from typing import NotRequired, ReadOnly, Required, TypedDict, assert_type +from typing_extensions import TypeForm # pyright: ignore[reportMissingModuleSource] td1: TypedDict[{"a": int, "b": str}] = {"a": 0, "b": ""} @@ -48,3 +49,22 @@ class Outer1[T]: def __init__(self, v: T) -> None: self.attr1 = {"a": [v]} + + +tf1 = TypeForm(TypedDict[{"a": int}]) +reveal_type(tf1, expected_text="TypeForm[]") + +tf2 = TypeForm(list[TypedDict[{"a": int}]]) +reveal_type(tf2, expected_text="TypeForm[list[]]") + + +def deserialize[T](schema: TypeForm[T], payload: bytes) -> T: + raise NotImplementedError + + +inline_user = deserialize(TypedDict[{"id": int, "name": str}], b"") +assert_type(inline_user["id"], int) +assert_type(inline_user["name"], str) + +inline_users = deserialize(list[TypedDict[{"id": int}]], b"") +assert_type(inline_users[0]["id"], int) diff --git a/packages/pyright-internal/src/tests/typeCacheUtils.test.ts b/packages/pyright-internal/src/tests/typeCacheUtils.test.ts new file mode 100644 index 000000000000..e683351c8181 --- /dev/null +++ b/packages/pyright-internal/src/tests/typeCacheUtils.test.ts @@ -0,0 +1,63 @@ +/* + * typeCacheUtils.test.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * Author: Microsoft Corporation. + * + * Unit tests for type cache utilities. + */ + +import * as assert from 'assert'; + +import { + addContextualTypeCacheEntry, + ContextualTypeCacheEntry, + contextualTypeCacheEntryMatches, +} from '../analyzer/typeCacheUtils'; +import { Type, TypeVarType } from '../analyzer/types'; + +interface TestCacheEntry extends ContextualTypeCacheEntry { + value: number; +} + +test('ContextualTypeCacheEntryMatching', () => { + const expectedType = TypeVarType.createInstance('T'); + const otherExpectedType = TypeVarType.createInstance('U'); + const entry: TestCacheEntry = { expectedType, value: 1 }; + const noExpectedTypeEntry: TestCacheEntry = { expectedType: undefined, value: 2 }; + + assert.ok(contextualTypeCacheEntryMatches(entry, expectedType)); + assert.ok(!contextualTypeCacheEntryMatches(entry, otherExpectedType)); + assert.ok(!contextualTypeCacheEntryMatches(entry, undefined)); + assert.ok(contextualTypeCacheEntryMatches(noExpectedTypeEntry, undefined)); +}); + +test('ContextualTypeCacheEntryReplacementAndEviction', () => { + const expectedTypes: Type[] = Array.from({ length: 9 }, (_, index) => TypeVarType.createInstance(`T${index}`)); + let entries: TestCacheEntry[] = []; + + expectedTypes.forEach((expectedType, index) => { + entries = addContextualTypeCacheEntry(entries, { expectedType, value: index }); + }); + + assert.deepStrictEqual( + entries.map((entry) => entry.value), + [1, 2, 3, 4, 5, 6, 7, 8] + ); + + entries = addContextualTypeCacheEntry(entries, { expectedType: expectedTypes[4], value: 9 }); + assert.deepStrictEqual( + entries.map((entry) => entry.value), + [1, 2, 3, 5, 6, 7, 8, 9] + ); + + entries = addContextualTypeCacheEntry( + entries, + { expectedType: undefined, value: 10 }, + (entry) => entry.value !== 2 + ); + assert.deepStrictEqual( + entries.map((entry) => entry.value), + [1, 3, 5, 6, 7, 8, 9, 10] + ); +}); diff --git a/packages/pyright-internal/src/tests/typeEvaluator8.test.ts b/packages/pyright-internal/src/tests/typeEvaluator8.test.ts index 2af5098a6d43..db23d1a9fec0 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator8.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator8.test.ts @@ -10,9 +10,13 @@ import * as assert from 'assert'; +import { EvalFlags } from '../analyzer/typeEvaluatorTypes'; +import { ClassType, isClassInstance, isInstantiableClass, UnknownType } from '../analyzer/types'; import { ConfigOptions } from '../common/configOptions'; import { pythonVersion3_10, pythonVersion3_11, pythonVersion3_8, pythonVersion3_12 } from '../common/pythonVersion'; import { Uri } from '../common/uri/uri'; +import { ParseNodeType } from '../parser/parseNodes'; +import { getNodeAtMarker, parseAndGetTestState } from './harness/fourslash/testState'; import * as TestUtils from './testUtils'; test('Import1', () => { @@ -1057,6 +1061,287 @@ test('SpecialForm4', () => { // TypeForm support is enabled by default and no longer requires // enableExperimentalFeatures, so these tests intentionally leave it off. +test('TypeFormCache', () => { + const code = ` +// @filename: test.py +//// from typing_extensions import TypeForm +//// value: TypeForm[int] = /*marker*/int + `; + const state = parseAndGetTestState(code).state; + const node = getNodeAtMarker(state); + assert.ok(node.nodeType === ParseNodeType.Name); + + const type = state.program.evaluator!.getType(node); + assert.ok(type); + assert.ok(isClassInstance(type)); + assert.ok(ClassType.isBuiltIn(type, 'TypeForm')); + + const typeArg = type.priv.typeArgs?.[0]; + assert.ok(typeArg); + assert.ok(isClassInstance(typeArg)); + assert.ok(ClassType.isBuiltIn(typeArg, 'int')); + + const runtimeType = state.program.evaluator!.getTypeOfExpression(node).type; + assert.ok(isInstantiableClass(runtimeType)); + assert.ok(ClassType.isBuiltIn(runtimeType, 'int')); + + const subnodeTypeResult = state.program.evaluator!.evaluateTypeForSubnode(node, () => { + assert.fail('Runtime type is already cached'); + }); + assert.strictEqual(subnodeTypeResult?.type, runtimeType); + + const cachedType = state.program.evaluator!.getCachedType(node); + assert.strictEqual(cachedType, runtimeType); +}); + +test('TypeFormCacheRuntimeFirst', () => { + const code = ` +// @filename: test.py +//// from typing_extensions import TypeForm +//// value: TypeForm[int] = /*marker*/int + `; + const state = parseAndGetTestState(code).state; + const node = getNodeAtMarker(state); + assert.ok(node.nodeType === ParseNodeType.Name); + + const runtimeType = state.program.evaluator!.getTypeOfExpression(node).type; + assert.ok(isInstantiableClass(runtimeType)); + assert.ok(ClassType.isBuiltIn(runtimeType, 'int')); + assert.strictEqual(state.program.evaluator!.getCachedType(node), runtimeType); + + state.program.evaluator!.evaluateTypesForStatement(node); + + const contextualType = state.program.evaluator!.getType(node); + assert.ok(contextualType); + assert.ok(isClassInstance(contextualType)); + assert.ok(ClassType.isBuiltIn(contextualType, 'TypeForm')); + + const typeArg = contextualType.priv.typeArgs?.[0]; + assert.ok(typeArg); + assert.ok(isClassInstance(typeArg)); + assert.ok(ClassType.isBuiltIn(typeArg, 'int')); + + assert.strictEqual(state.program.evaluator!.getCachedType(node), runtimeType); + assert.strictEqual(state.program.evaluator!.getCachedType(node), runtimeType); +}); + +test('TypeFormCacheRuntimeFirstReassignment', () => { + const code = ` +// @filename: test.py +//// from typing_extensions import TypeForm +//// value: TypeForm[int] +//// value = /*marker*/int + `; + const state = parseAndGetTestState(code).state; + const node = getNodeAtMarker(state); + assert.ok(node.nodeType === ParseNodeType.Name); + + const runtimeType = state.program.evaluator!.getTypeOfExpression(node).type; + assert.ok(isInstantiableClass(runtimeType)); + assert.ok(ClassType.isBuiltIn(runtimeType, 'int')); + + state.program.evaluator!.evaluateTypesForStatement(node); + + const contextualType = state.program.evaluator!.getType(node); + assert.ok(contextualType); + assert.ok(isClassInstance(contextualType)); + assert.ok(ClassType.isBuiltIn(contextualType, 'TypeForm')); + + const typeArg = contextualType.priv.typeArgs?.[0]; + assert.ok(typeArg); + assert.ok(isClassInstance(typeArg)); + assert.ok(ClassType.isBuiltIn(typeArg, 'int')); + + assert.strictEqual(state.program.evaluator!.getCachedType(node), runtimeType); +}); + +test('TypeFormCacheRuntimeFirstString', () => { + const code = ` +// @filename: test.py +//// from typing_extensions import TypeForm +//// value: TypeForm[int] = /*marker*/"int" + `; + const state = parseAndGetTestState(code).state; + const markerNode = getNodeAtMarker(state); + const node = markerNode.nodeType === ParseNodeType.String ? markerNode.parent : markerNode; + assert.ok(node?.nodeType === ParseNodeType.StringList); + + const runtimeType = state.program.evaluator!.getTypeOfExpression(node).type; + assert.ok(isClassInstance(runtimeType)); + assert.ok(ClassType.isBuiltIn(runtimeType, 'str')); + assert.strictEqual(runtimeType.props?.typeForm, undefined); + + state.program.evaluator!.evaluateTypesForStatement(node); + + const contextualType = state.program.evaluator!.getType(node); + assert.ok(contextualType); + assert.ok(isClassInstance(contextualType)); + assert.ok(ClassType.isBuiltIn(contextualType, 'TypeForm')); + + const typeArg = contextualType.priv.typeArgs?.[0]; + assert.ok(typeArg); + assert.ok(isClassInstance(typeArg)); + assert.ok(ClassType.isBuiltIn(typeArg, 'int')); + + assert.strictEqual(state.program.evaluator!.getCachedType(node), runtimeType); +}); + +test('TypeFormCacheRuntimeFirstMemberAssignment', () => { + const code = ` +// @filename: test.py +//// from typing_extensions import TypeForm +//// class Holder: +//// value: TypeForm[int] +//// holder = Holder() +//// holder.value = /*marker*/int + `; + const state = parseAndGetTestState(code).state; + const node = getNodeAtMarker(state); + assert.ok(node.nodeType === ParseNodeType.Name); + + const runtimeType = state.program.evaluator!.getTypeOfExpression(node).type; + assert.ok(isInstantiableClass(runtimeType)); + assert.ok(ClassType.isBuiltIn(runtimeType, 'int')); + + state.program.evaluator!.evaluateTypesForStatement(node); + + const contextualType = state.program.evaluator!.getType(node); + assert.ok(contextualType); + assert.ok(isClassInstance(contextualType)); + assert.ok(ClassType.isBuiltIn(contextualType, 'TypeForm')); + const typeArg = contextualType.priv.typeArgs?.[0]; + assert.ok(typeArg); + assert.ok(isClassInstance(typeArg)); + assert.ok(ClassType.isBuiltIn(typeArg, 'int')); + + assert.strictEqual(state.program.evaluator!.getCachedType(node), runtimeType); +}); + +test('TypeFormCacheRuntimeFirstIndexAssignment', () => { + const code = ` +// @filename: test.py +//// from typing import TypedDict, cast +//// from typing_extensions import TypeForm +//// class Holder(TypedDict): +//// value: TypeForm[int] +//// holder = cast(Holder, {}) +//// holder["value"] = /*marker*/int + `; + const state = parseAndGetTestState(code).state; + const node = getNodeAtMarker(state); + assert.ok(node.nodeType === ParseNodeType.Name); + + const runtimeType = state.program.evaluator!.getTypeOfExpression(node).type; + assert.ok(isInstantiableClass(runtimeType)); + assert.ok(ClassType.isBuiltIn(runtimeType, 'int')); + + state.program.evaluator!.evaluateTypesForStatement(node); + + const contextualType = state.program.evaluator!.getType(node); + assert.ok(contextualType); + assert.ok(isClassInstance(contextualType)); + assert.ok(ClassType.isBuiltIn(contextualType, 'TypeForm')); + const typeArg = contextualType.priv.typeArgs?.[0]; + assert.ok(typeArg); + assert.ok(isClassInstance(typeArg)); + assert.ok(ClassType.isBuiltIn(typeArg, 'int')); + + assert.strictEqual(state.program.evaluator!.getCachedType(node), runtimeType); +}); + +test('TypeFormSpeculativeCacheDoesNotInvalidateIncompleteCache', () => { + const code = ` +// @filename: test.py +//// from typing_extensions import TypeForm +//// other = /*incomplete*/1 +//// value: TypeForm[int] = /*typeForm*/int + `; + const state = parseAndGetTestState(code).state; + const node = getNodeAtMarker(state, 'typeForm'); + const incompleteNode = getNodeAtMarker(state, 'incomplete'); + assert.ok(node.nodeType === ParseNodeType.Name); + assert.ok(incompleteNode.nodeType === ParseNodeType.Number); + + const incompleteType = UnknownType.create(); + state.program.evaluator!.setTypeResultForNode(incompleteNode, { + type: incompleteType, + isIncomplete: true, + }); + + state.program.evaluator!.useSpeculativeMode(node, () => { + state.program.evaluator!.setTypeResultForNode(node, { type: UnknownType.create() }, EvalFlags.TypeFormArg); + }); + + assert.strictEqual(state.program.evaluator!.getCachedType(node), undefined); + const incompleteResult = state.program.evaluator!.getTypeOfExpression(incompleteNode); + assert.ok( + incompleteResult.type === incompleteType, + 'Speculative TypeForm write invalidated an unrelated incomplete cache entry' + ); + assert.strictEqual(incompleteResult.isIncomplete, true); +}); + +test('TypeFormCacheDoesNotSkipRuntimeEvaluation', () => { + const code = ` +// @filename: test.py +//// from typing_extensions import TypeForm +//// def consume(value: TypeForm[int]): ... +//// consume(/*marker*/int) + `; + const state = parseAndGetTestState(code).state; + const node = getNodeAtMarker(state); + assert.ok(node.nodeType === ParseNodeType.Name); + + const type = state.program.evaluator!.getType(node); + assert.ok(type); + assert.ok(isClassInstance(type)); + assert.ok(ClassType.isBuiltIn(type, 'TypeForm')); + + assert.ok(node.parent?.nodeType === ParseNodeType.Argument); + const callNode = node.parent.parent; + assert.ok(callNode?.nodeType === ParseNodeType.Call); + state.program.evaluator!.getTypeOfExpression(callNode); + + const cachedType = state.program.evaluator!.getCachedType(node); + assert.ok(cachedType); + assert.ok(isInstantiableClass(cachedType)); + assert.ok(ClassType.isBuiltIn(cachedType, 'int')); +}); + +test('TypeFormExplicitCache', () => { + const code = ` +// @filename: pyrightconfig.json +//// { "enableExperimentalFeatures": true } +// @filename: test.py +//// from typing import TypedDict +//// from typing_extensions import TypeForm +//// value = TypeForm(/*marker*/TypedDict[{"a": int}]) + `; + const state = parseAndGetTestState(code).state; + const node = getNodeAtMarker(state); + assert.ok(node.nodeType === ParseNodeType.Name); + assert.ok(node.parent?.nodeType === ParseNodeType.Index); + + const type = state.program.evaluator!.getType(node.parent); + assert.ok(type); + assert.ok(isClassInstance(type)); + assert.ok(ClassType.isBuiltIn(type, 'TypeForm')); + + const typeArg = type.priv.typeArgs?.[0]; + assert.ok(typeArg); + assert.ok(isClassInstance(typeArg)); + assert.ok(ClassType.isTypedDictClass(typeArg)); + + const runtimeTypeResult = state.program.evaluator!.getTypeOfExpression(node.parent); + assert.ok(isInstantiableClass(runtimeTypeResult.type)); + assert.ok(ClassType.isTypedDictClass(runtimeTypeResult.type)); + + const subnodeTypeResult = state.program.evaluator!.evaluateTypeForSubnode(node.parent, () => { + assert.fail('Runtime type is already cached'); + }); + assert.strictEqual(subnodeTypeResult?.type, runtimeTypeResult.type); +}); + test('TypeForm1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeForm1.py']); @@ -1066,19 +1351,19 @@ test('TypeForm1', () => { test('TypeForm2', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeForm2.py']); - TestUtils.validateResults(analysisResults, 0); + TestUtils.validateResults(analysisResults, 8); }); test('TypeForm3', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeForm3.py']); - TestUtils.validateResults(analysisResults, 0); + TestUtils.validateResults(analysisResults, 6); }); test('TypeForm4', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeForm4.py']); - TestUtils.validateResults(analysisResults, 27); + TestUtils.validateResults(analysisResults, 58); }); test('TypeForm5', () => { @@ -1104,3 +1389,9 @@ test('TypeForm8', () => { TestUtils.validateResults(analysisResults, 2); }); + +test('TypeForm9', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeForm9.py']); + + TestUtils.validateResults(analysisResults, 10); +});