Fixed several bugs related to recursive type aliases. The hover text was sometimes incorrect, type narrowing for "isinstance" was broken in some cases, and the reportUnnecessaryIsInstance rule was reporting incorrect errors.

This commit is contained in:
Eric Traut 2020-09-25 23:28:33 -07:00
parent 67d69ba0d1
commit 604a4dc70b
7 changed files with 83 additions and 14 deletions

View File

@ -114,6 +114,7 @@ import {
makeTypeVarsConcrete,
partiallySpecializeType,
specializeType,
transformPossibleRecursiveTypeAlias,
transformTypeObjectToClass,
} from './typeUtils';
@ -1428,7 +1429,7 @@ export class Checker extends ParseTreeWalker {
return;
}
arg0Type = doForSubtypes(arg0Type, (subtype) => {
return transformTypeObjectToClass(subtype);
return transformPossibleRecursiveTypeAlias(transformTypeObjectToClass(subtype));
});
if (derivesFromAnyOrUnknown(arg0Type)) {

View File

@ -12382,13 +12382,15 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
isInstanceCheck: boolean,
isPositiveTest: boolean
): Type {
if (isTypeVar(type)) {
// If it's a constrained TypeVar, treat it as a union for the
// purposes of narrowing.
type = getConcreteTypeFromTypeVar(type, /* convertConstraintsToUnion */ true);
}
let effectiveType = doForSubtypes(type, (subtype) => {
subtype = transformPossibleRecursiveTypeAlias(subtype);
if (isTypeVar(subtype)) {
// If it's a constrained TypeVar, treat it as a union for the
// purposes of narrowing.
subtype = getConcreteTypeFromTypeVar(subtype, /* convertConstraintsToUnion */ true);
}
return transformTypeObjectToClass(subtype);
});
@ -12498,6 +12500,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
if (!isObject(containerType) || !ClassType.isBuiltIn(containerType.classType)) {
return referenceType;
}
const classType = containerType.classType;
const builtInName = classType.details.aliasClass
? classType.details.aliasClass.details.name
@ -16383,6 +16386,9 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
// If it's a synthesized type var used to implement recursive type
// aliases, return the type alias name.
if (type.details.recursiveTypeAliasName) {
if (expandTypeAlias && type.details.boundType) {
return printType(type.details.boundType, expandTypeAlias, recursionCount + 1);
}
return type.details.recursiveTypeAliasName;
}

View File

@ -359,7 +359,7 @@ export function transformPossibleRecursiveTypeAlias(type: Type): Type;
export function transformPossibleRecursiveTypeAlias(type: Type | undefined): Type | undefined;
export function transformPossibleRecursiveTypeAlias(type: Type | undefined): Type | undefined {
if (type) {
if (type.category === TypeCategory.TypeVar && type.details.recursiveTypeAliasName && type.details.boundType) {
if (isTypeVar(type) && type.details.recursiveTypeAliasName && type.details.boundType) {
if (TypeBase.isInstance(type)) {
return convertToInstance(type.details.boundType);
}

View File

@ -1404,6 +1404,23 @@ export function isOverloadedFunction(type: Type): type is OverloadedFunctionType
return type.category === TypeCategory.OverloadedFunction;
}
export function getTypeAliasInfo(type: Type) {
if (type.typeAliasInfo) {
return type.typeAliasInfo;
}
if (
isTypeVar(type) &&
type.details.recursiveTypeAliasName &&
type.details.boundType &&
type.details.boundType.typeAliasInfo
) {
return type.details.boundType.typeAliasInfo;
}
return undefined;
}
export function isTypeSame(type1: Type, type2: Type, recursionCount = 0): boolean {
if (type1.category !== type2.category) {
return false;

View File

@ -23,7 +23,7 @@ import {
getOverloadedFunctionDocStrings,
} from '../analyzer/typeDocStringUtils';
import { TypeEvaluator } from '../analyzer/typeEvaluator';
import { isClass, isModule, isObject, Type, TypeCategory, UnknownType } from '../analyzer/types';
import { getTypeAliasInfo, isClass, isModule, isObject, Type, TypeCategory, UnknownType } from '../analyzer/types';
import { ClassMemberLookupFlags, isProperty, lookUpClassMember } from '../analyzer/typeUtils';
import { throwIfCancellationRequested } from '../common/cancellationUtils';
import { convertOffsetToPosition, convertPositionToOffset } from '../common/positionUtils';
@ -150,12 +150,15 @@ export class HoverProvider {
// the type alias when printing the type information.
const type = evaluator.getType(typeNode);
let expandTypeAlias = false;
if (type?.typeAliasInfo && node.value === type.typeAliasInfo.aliasName) {
if (type.typeAliasInfo.aliasName === typeNode.value) {
expandTypeAlias = true;
}
if (type) {
const typeAliasInfo = getTypeAliasInfo(type);
if (typeAliasInfo) {
if (typeAliasInfo.aliasName === typeNode.value) {
expandTypeAlias = true;
}
label = 'type alias';
label = 'type alias';
}
}
this._addResultsPart(

View File

@ -1098,6 +1098,12 @@ test('TypeAlias8', () => {
validateResults(analysisResults, 4);
});
test('TypeAlias9', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeAlias9.py']);
validateResults(analysisResults, 0, 0, 4);
});
test('Dictionary1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dictionary1.py']);

View File

@ -0,0 +1,36 @@
# This sample tests the handling of complex recursive types.
# pyright: strict, reportUnusedVariable=false
from typing import Dict, List, Literal, Union
JSONArray = List["JSONType"]
JSONObject = Dict[str, "JSONType"]
JSONPrimitive = Union[str, float, int, bool, None]
JSONStructured = Union[JSONArray, JSONObject]
JSONType = Union[JSONPrimitive, JSONStructured]
# Using type alias checking for list:
def f2(args: JSONStructured):
if isinstance(args, List):
t1: Literal[
"List[str | float | int | bool | JSONArray | Dict[str, JSONType] | None]"
] = reveal_type(args)
else:
t2: Literal["Dict[str, JSONType]"] = reveal_type(args)
dargs: JSONObject = args
# Using type alias checking for dict:
def f3(args: JSONStructured):
if isinstance(args, Dict):
t1: Literal["Dict[str, JSONType]"] = reveal_type(args)
else:
t2: Literal[
"List[str | float | int | bool | JSONArray | Dict[str, JSONType] | None]"
] = reveal_type(args)
largs: JSONArray = args