diff --git a/packages/pyright-internal/src/analyzer/types.ts b/packages/pyright-internal/src/analyzer/types.ts index d3e1bdbea71c..82ee46646311 100644 --- a/packages/pyright-internal/src/analyzer/types.ts +++ b/packages/pyright-internal/src/analyzer/types.ts @@ -3343,6 +3343,8 @@ export function isTypeSame(type1: Type, type2: Type, options: TypeSameOptions = } recursionCount++; + const recursiveOptions = options.ignoreTypeFlags ? { ...options, ignoreTypeFlags: false } : options; + if (options.honorTypeForm) { const typeForm1 = type1.props?.typeForm; const typeForm2 = type2.props?.typeForm; @@ -3375,41 +3377,49 @@ export function isTypeSame(type1: Type, type2: Type, options: TypeSameOptions = if (!options.ignorePseudoGeneric || !ClassType.isPseudoGenericClass(type1)) { // Make sure the type args match. - if (type1.priv.tupleTypeArgs && classType2.priv.tupleTypeArgs) { - const type1TupleTypeArgs = type1.priv.tupleTypeArgs || []; - const type2TupleTypeArgs = classType2.priv.tupleTypeArgs || []; + const type1TupleTypeArgs = type1.priv.tupleTypeArgs; + const type2TupleTypeArgs = classType2.priv.tupleTypeArgs; + + if (type1TupleTypeArgs && type2TupleTypeArgs) { if (type1TupleTypeArgs.length !== type2TupleTypeArgs.length) { return false; } - for (let i = 0; i < type1TupleTypeArgs.length; i++) { - if ( - !isTypeSame( - type1TupleTypeArgs[i].type, - type2TupleTypeArgs[i].type, - { ...options, ignoreTypeFlags: false }, - recursionCount - ) - ) { - return false; - } - - if (type1TupleTypeArgs[i].isUnbounded !== type2TupleTypeArgs[i].isUnbounded) { - return false; + if (type1TupleTypeArgs !== type2TupleTypeArgs) { + for (let i = 0; i < type1TupleTypeArgs.length; i++) { + if ( + !isTypeSame( + type1TupleTypeArgs[i].type, + type2TupleTypeArgs[i].type, + recursiveOptions, + recursionCount + ) + ) { + return false; + } + + if (type1TupleTypeArgs[i].isUnbounded !== type2TupleTypeArgs[i].isUnbounded) { + return false; + } } } } else { - const type1TypeArgs = type1.priv.typeArgs || []; - const type2TypeArgs = classType2.priv.typeArgs || []; - const typeArgCount = Math.max(type1TypeArgs.length, type2TypeArgs.length); - - for (let i = 0; i < typeArgCount; i++) { - // Assume that missing type args are "Unknown". - const typeArg1 = i < type1TypeArgs.length ? type1TypeArgs[i] : UnknownType.create(); - const typeArg2 = i < type2TypeArgs.length ? type2TypeArgs[i] : UnknownType.create(); - - if (!isTypeSame(typeArg1, typeArg2, { ...options, ignoreTypeFlags: false }, recursionCount)) { - return false; + const type1TypeArgs = type1.priv.typeArgs; + const type2TypeArgs = classType2.priv.typeArgs; + + if (type1TypeArgs !== type2TypeArgs) { + const typeArgCount = Math.max(type1TypeArgs?.length ?? 0, type2TypeArgs?.length ?? 0); + + for (let i = 0; i < typeArgCount; i++) { + // Assume that missing type args are "Unknown". + const typeArg1 = + type1TypeArgs && i < type1TypeArgs.length ? type1TypeArgs[i] : UnknownType.create(); + const typeArg2 = + type2TypeArgs && i < type2TypeArgs.length ? type2TypeArgs[i] : UnknownType.create(); + + if (!isTypeSame(typeArg1, typeArg2, recursiveOptions, recursionCount)) { + return false; + } } } } @@ -3455,39 +3465,51 @@ export function isTypeSame(type1: Type, type2: Type, options: TypeSameOptions = return false; } - const positionOnlyIndex1 = params1.findIndex((param) => isPositionOnlySeparator(param)); - const positionOnlyIndex2 = params2.findIndex((param) => isPositionOnlySeparator(param)); + const specializedTypes1 = type1.priv.specializedTypes; + const specializedTypes2 = functionType2.priv.specializedTypes; - // Make sure the parameter details match. - for (let i = 0; i < params1.length; i++) { - const param1 = params1[i]; - const param2 = params2[i]; + if (type1.shared === functionType2.shared && specializedTypes1 === specializedTypes2) { + return true; + } - if (param1.category !== param2.category) { - return false; - } + const haveSameParamSignature = + params1 === params2 && specializedTypes1?.parameterTypes === specializedTypes2?.parameterTypes; - const isName1Relevant = positionOnlyIndex1 !== undefined && i > positionOnlyIndex1; - const isName2Relevant = positionOnlyIndex2 !== undefined && i > positionOnlyIndex2; + if (!haveSameParamSignature) { + const positionOnlyIndex1 = params1.findIndex((param) => isPositionOnlySeparator(param)); + const positionOnlyIndex2 = params2.findIndex((param) => isPositionOnlySeparator(param)); - if (isName1Relevant !== isName2Relevant) { - return false; - } + // Make sure the parameter details match. + for (let i = 0; i < params1.length; i++) { + const param1 = params1[i]; + const param2 = params2[i]; - if (isName1Relevant) { - if (param1.name !== param2.name) { + if (param1.category !== param2.category) { return false; } - } else if (isPositionOnlySeparator(param1) && isPositionOnlySeparator(param2)) { - continue; - } else if (isKeywordOnlySeparator(param1) && isKeywordOnlySeparator(param2)) { - continue; - } - const param1Type = FunctionType.getParamType(type1, i); - const param2Type = FunctionType.getParamType(functionType2, i); - if (!isTypeSame(param1Type, param2Type, { ...options, ignoreTypeFlags: false }, recursionCount)) { - return false; + const isName1Relevant = positionOnlyIndex1 !== undefined && i > positionOnlyIndex1; + const isName2Relevant = positionOnlyIndex2 !== undefined && i > positionOnlyIndex2; + + if (isName1Relevant !== isName2Relevant) { + return false; + } + + if (isName1Relevant) { + if (param1.name !== param2.name) { + return false; + } + } else if (isPositionOnlySeparator(param1) && isPositionOnlySeparator(param2)) { + continue; + } else if (isKeywordOnlySeparator(param1) && isKeywordOnlySeparator(param2)) { + continue; + } + + const param1Type = FunctionType.getParamType(type1, i); + const param2Type = FunctionType.getParamType(functionType2, i); + if (!isTypeSame(param1Type, param2Type, recursiveOptions, recursionCount)) { + return false; + } } } @@ -3512,7 +3534,7 @@ export function isTypeSame(type1: Type, type2: Type, options: TypeSameOptions = if ( !return1Type || !return2Type || - !isTypeSame(return1Type, return2Type, { ...options, ignoreTypeFlags: false }, recursionCount) + !isTypeSame(return1Type, return2Type, recursiveOptions, recursionCount) ) { return false; } @@ -3524,6 +3546,10 @@ export function isTypeSame(type1: Type, type2: Type, options: TypeSameOptions = case TypeCategory.Overloaded: { // Make sure the overload counts match. const functionType2 = type2 as OverloadedType; + if (type1.priv._overloads === functionType2.priv._overloads) { + return true; + } + if (type1.priv._overloads.length !== functionType2.priv._overloads.length) { return false; } @@ -3548,6 +3574,10 @@ export function isTypeSame(type1: Type, type2: Type, options: TypeSameOptions = return false; } + if (subtypes1 === subtypes2) { + return true; + } + // The types do not have a particular order, so we need to // do the comparison in an order-independent manner. const exclusionSet = new Set(); @@ -3582,7 +3612,7 @@ export function isTypeSame(type1: Type, type2: Type, options: TypeSameOptions = const typeArg1 = i < type1TypeArgs.length ? type1TypeArgs[i] : AnyType.create(); const typeArg2 = i < type2TypeArgs.length ? type2TypeArgs[i] : AnyType.create(); - if (!isTypeSame(typeArg1, typeArg2, { ...options, ignoreTypeFlags: false }, recursionCount)) { + if (!isTypeSame(typeArg1, typeArg2, recursiveOptions, recursionCount)) { return false; } } @@ -3617,36 +3647,28 @@ export function isTypeSame(type1: Type, type2: Type, options: TypeSameOptions = const boundType1 = type1.shared.boundType; const boundType2 = type2TypeVar.shared.boundType; - if (boundType1) { - if ( - !boundType2 || - !isTypeSame(boundType1, boundType2, { ...options, ignoreTypeFlags: false }, recursionCount) - ) { + if (boundType1 !== boundType2) { + if (!boundType1 || !boundType2) { return false; } - } else { - if (boundType2) { + + if (!isTypeSame(boundType1, boundType2, recursiveOptions, recursionCount)) { return false; } } const constraints1 = type1.shared.constraints; const constraints2 = type2TypeVar.shared.constraints; - if (constraints1.length !== constraints2.length) { - return false; - } - - for (let i = 0; i < constraints1.length; i++) { - if ( - !isTypeSame( - constraints1[i], - constraints2[i], - { ...options, ignoreTypeFlags: false }, - recursionCount - ) - ) { + if (constraints1 !== constraints2) { + if (constraints1.length !== constraints2.length) { return false; } + + for (let i = 0; i < constraints1.length; i++) { + if (!isTypeSame(constraints1[i], constraints2[i], recursiveOptions, recursionCount)) { + return false; + } + } } return true; diff --git a/packages/pyright-internal/src/tests/typeEquality.test.ts b/packages/pyright-internal/src/tests/typeEquality.test.ts new file mode 100644 index 000000000000..73a72528bba7 --- /dev/null +++ b/packages/pyright-internal/src/tests/typeEquality.test.ts @@ -0,0 +1,100 @@ +/* + * typeEquality.test.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Unit tests for type equality helpers. + */ + +import * as assert from 'assert'; + +import { Uri } from '../common/uri/uri'; +import { ParamCategory } from '../parser/parseNodes'; +import { + AnyType, + ClassType, + ClassTypeFlags, + FunctionParam, + FunctionType, + isTypeSame, + TypeBase, + TypeVarType, + UnionType, + UnknownType, +} from '../analyzer/types'; + +test('IsTypeSameClassClonesShareTypeArgs', () => { + const intType = ClassType.cloneAsInstance(createClassType('int', ClassTypeFlags.BuiltIn)); + const listClass = createClassType('list', ClassTypeFlags.BuiltIn); + const specializedList = ClassType.cloneAsInstance(ClassType.specialize(listClass, [intType])); + const deprecatedList = ClassType.cloneForDeprecatedInstance(specializedList, 'deprecated'); + + assert.strictEqual(specializedList.priv.typeArgs, deprecatedList.priv.typeArgs); + assert.strictEqual(isTypeSame(specializedList, deprecatedList), true); +}); + +test('IsTypeSameFunctionClonesShareParameters', () => { + const functionType = FunctionType.createInstance('f', 'module.f', 'module', 0); + FunctionType.addParam(functionType, FunctionParam.create(ParamCategory.Simple, AnyType.create(), 0, 'value')); + functionType.shared.declaredReturnType = UnknownType.create(); + + const clonedFunctionType = FunctionType.cloneWithDocString(functionType, 'doc'); + + assert.strictEqual(functionType.shared.parameters, clonedFunctionType.shared.parameters); + assert.strictEqual(isTypeSame(functionType, clonedFunctionType), true); +}); + +test('IsTypeSameUnionClonesShareSubtypes', () => { + const unionType = UnionType.create(); + UnionType.addType(unionType, AnyType.create()); + UnionType.addType(unionType, UnknownType.create()); + + const unionTypeClone = TypeBase.cloneType(unionType); + + assert.strictEqual(unionType.priv.subtypes, unionTypeClone.priv.subtypes); + assert.strictEqual(isTypeSame(unionType, unionTypeClone), true); +}); + +test('IsTypeSameTypeVarClonesShareBoundAndConstraints', () => { + const baseTypeVar = TypeVarType.createInstance('_T'); + const boundType = ClassType.cloneAsInstance(createClassType('int', ClassTypeFlags.BuiltIn)); + + baseTypeVar.shared.boundType = boundType; + TypeVarType.addConstraint(baseTypeVar, ClassType.cloneAsInstance(createClassType('str', ClassTypeFlags.BuiltIn))); + + const clone1 = TypeVarType.cloneForScopeId(baseTypeVar, 'scope', 'scope', undefined); + const clone2 = TypeVarType.cloneForScopeId(baseTypeVar, 'scope', 'scope', undefined); + + assert.strictEqual(clone1.shared.boundType, clone2.shared.boundType); + assert.strictEqual(clone1.shared.constraints, clone2.shared.constraints); + assert.strictEqual(isTypeSame(clone1, clone2), true); + + const distinctConstraints = TypeVarType.cloneForScopeId( + TypeVarType.createInstance('_T'), + 'scope', + 'scope', + undefined + ); + distinctConstraints.shared = { + ...distinctConstraints.shared, + boundType, + constraints: [ClassType.cloneAsInstance(createClassType('bytes', ClassTypeFlags.BuiltIn))], + }; + + assert.strictEqual(isTypeSame(clone1, distinctConstraints), false); +}); + +function createClassType(name: string, flags = ClassTypeFlags.None) { + const classType = ClassType.createInstantiable( + name, + name, + '', + Uri.empty(), + flags, + 0, + /* declaredMetaclass*/ undefined, + /* effectiveMetaclass */ undefined + ); + classType.shared.mro.push(classType); + return classType; +}