diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index 3e0d959d5..bbeba61ae 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -297,6 +297,14 @@ "lineCount": 1 } }, + { + "code": "reportUnsolvedTypeVar", + "range": { + "startColumn": 4, + "endColumn": 5, + "lineCount": 7 + } + }, { "code": "reportUnusedCallResult", "range": { @@ -305,6 +313,14 @@ "lineCount": 7 } }, + { + "code": "reportUnsolvedTypeVar", + "range": { + "startColumn": 4, + "endColumn": 5, + "lineCount": 6 + } + }, { "code": "reportUnusedCallResult", "range": { diff --git a/packages/pyright-internal/src/analyzer/constructors.ts b/packages/pyright-internal/src/analyzer/constructors.ts index bb629366a..47796761c 100644 --- a/packages/pyright-internal/src/analyzer/constructors.ts +++ b/packages/pyright-internal/src/analyzer/constructors.ts @@ -15,6 +15,7 @@ import { appendArray } from '../common/collectionUtils'; import { DiagnosticAddendum } from '../common/diagnostic'; import { DiagnosticRule } from '../common/diagnosticRules'; +import { LocMessage } from '../localization/localize'; import { ExpressionNode, ParamCategory } from '../parser/parseNodes'; import { ConstraintSolution } from './constraintSolution'; import { addConstraintsForExpectedType } from './constraintSolver'; @@ -40,7 +41,9 @@ import { isInstantiableClass, isNever, isOverloaded, + isParamSpec, isTypeVar, + isTypeVarTuple, isUnknown, } from './types'; import { @@ -478,6 +481,10 @@ function validateNewMethod( newReturnType = applyExpectedTypeForConstructor(evaluator, type, inferenceContext, constraints); } + if (!argumentErrors && !isTypeIncomplete && !useSpeculativeModeForArgs) { + reportUnsolvedClassTypeParams(evaluator, type, constraints, errorNode); + } + return { argumentErrors, returnType: newReturnType, isTypeIncomplete, overloadsUsedForCall }; } @@ -531,8 +538,11 @@ function validateInitMethod( if (callResult.argumentErrors) { argumentErrors = true; - } else if (callResult.overloadsUsedForCall) { - overloadsUsedForCall.push(...callResult.overloadsUsedForCall); + } else { + if (callResult.overloadsUsedForCall) { + overloadsUsedForCall.push(...callResult.overloadsUsedForCall); + } + reportUnsolvedClassTypeParams(evaluator, adjustedClassType, constraints, errorNode); } return { argumentErrors, returnType, isTypeIncomplete, overloadsUsedForCall }; @@ -640,6 +650,34 @@ function applyExpectedSubtypeForConstructor( return specializedType; } +// Reports errors for class-level type parameters that have no solution in the constraints. +function reportUnsolvedClassTypeParams( + evaluator: TypeEvaluator, + classType: ClassType, + constraints: ConstraintTracker, + errorNode: ExpressionNode +): void { + if (classType.shared.typeParams.length === 0 || classType.priv.typeArgs) { + return; + } + const constraintSet = constraints.getMainConstraintSet(); + for (const typeParam of classType.shared.typeParams) { + if (typeParam.shared.isDefaultExplicit) { + continue; + } + if (isParamSpec(typeParam) || isTypeVarTuple(typeParam)) { + continue; + } + if (!constraintSet.getTypeVar(typeParam)?.lowerBound) { + evaluator.addDiagnostic( + DiagnosticRule.reportUnsolvedTypeVar, + LocMessage.typeVarUnsolved().format({ name: typeParam.shared.name }), + errorNode + ); + } + } +} + // Handles the case where a constructor is a generic type and the type // arguments are not specified but can be provided by the expected type. function applyExpectedTypeForConstructor( diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 3677d74f9..211fdb4f1 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -12617,6 +12617,50 @@ export function createTypeEvaluator( specializedInitSelfType = solveAndApplyConstraints(specializedInitSelfType, constraints); } + // check for unsolved type variables and report errors + if (!argumentErrors && !isTypeIncomplete && type.shared.typeParams.length > 0) { + const solution = solveConstraints(evaluatorInterface, constraints); + + const solutionSet = solution.getMainSolutionSet(); + const scopeIds = getTypeVarScopeIds(type); + const unsolvedExemptTypeVars = getUnknownExemptTypeVarsForReturnType(type, returnType); + + for (const typeParam of type.shared.typeParams) { + // skip type parameters that have explicit default values + if (typeParam.shared.isDefaultExplicit) { + continue; + } + + // skip ParamSpecs and TypeVarTuples for now - they have different semantics + if (isParamSpec(typeParam) || isTypeVarTuple(typeParam)) { + continue; + } + + // only check type parameters that are in scope for this function + if (typeParam.priv.scopeId && scopeIds && !scopeIds.includes(typeParam.priv.scopeId)) { + continue; + } + + // skip type parameters that are exempt from being solved (e.g., they appear + // only in the return type as part of a returned callable) + if (unsolvedExemptTypeVars.some((exemptVar) => isTypeSame(exemptVar, typeParam))) { + continue; + } + + const solvedType = solutionSet.getType(typeParam); + + if (!solvedType) { + addDiagnostic( + DiagnosticRule.reportUnsolvedTypeVar, + LocMessage.typeVarUnsolved().format({ + name: typeParam.shared.name, + }), + errorNode + ); + } + } + } + matchResults.argumentMatchScore = argumentMatchScore; return { @@ -15077,6 +15121,17 @@ export function createTypeEvaluator( } } else { isEmptyContainer = true; + + // Report unsolved type variable for empty containers without expected type + if (!hasExpectedType) { + const diag = new DiagnosticAddendum(); + diag.addMessage(LocAddendum.typeVarUnsolvedCollectionHint().format({ className: builtInClassName })); + addDiagnostic( + DiagnosticRule.reportUnsolvedTypeVar, + LocMessage.typeVarUnsolved().format({ name: 'T' }) + diag.getString(), + node + ); + } } const listOrSetClass = getBuiltInType(node, builtInClassName); diff --git a/packages/pyright-internal/src/common/configOptions.ts b/packages/pyright-internal/src/common/configOptions.ts index 56b36b031..72438daff 100644 --- a/packages/pyright-internal/src/common/configOptions.ts +++ b/packages/pyright-internal/src/common/configOptions.ts @@ -430,6 +430,7 @@ export interface DiagnosticRuleSet { reportIncompatibleUnannotatedOverride: DiagnosticLevel; reportInvalidAbstractMethod: DiagnosticLevel; reportSelfClsDefault: DiagnosticLevel; + reportUnsolvedTypeVar: DiagnosticLevel; allowedUntypedLibraries: string[]; } @@ -564,6 +565,7 @@ export function getDiagLevelDiagnosticRules() { DiagnosticRule.reportIncompatibleUnannotatedOverride, DiagnosticRule.reportInvalidAbstractMethod, DiagnosticRule.reportSelfClsDefault, + DiagnosticRule.reportUnsolvedTypeVar, ]; } @@ -703,6 +705,7 @@ export function getOffDiagnosticRuleSet(): DiagnosticRuleSet { reportIncompatibleUnannotatedOverride: 'none', reportInvalidAbstractMethod: 'none', reportSelfClsDefault: 'none', + reportUnsolvedTypeVar: 'none', allowedUntypedLibraries: [], }; @@ -824,6 +827,7 @@ export function getBasicDiagnosticRuleSet(): DiagnosticRuleSet { reportIncompatibleUnannotatedOverride: 'none', reportInvalidAbstractMethod: 'none', reportSelfClsDefault: 'none', + reportUnsolvedTypeVar: 'none', allowedUntypedLibraries: [], }; @@ -945,6 +949,7 @@ export function getStandardDiagnosticRuleSet(): DiagnosticRuleSet { reportIncompatibleUnannotatedOverride: 'none', reportInvalidAbstractMethod: 'none', reportSelfClsDefault: 'none', + reportUnsolvedTypeVar: 'none', allowedUntypedLibraries: [], }; @@ -1065,6 +1070,7 @@ export const getRecommendedDiagnosticRuleSet = (): DiagnosticRuleSet => ({ reportIncompatibleUnannotatedOverride: 'none', // TODO: change to error when we're confident there's no performance issues with this rule reportInvalidAbstractMethod: 'warning', reportSelfClsDefault: 'warning', + reportUnsolvedTypeVar: 'error', allowedUntypedLibraries: [], }); @@ -1182,6 +1188,7 @@ export const getAllDiagnosticRuleSet = (): DiagnosticRuleSet => ({ reportIncompatibleUnannotatedOverride: 'error', reportInvalidAbstractMethod: 'error', reportSelfClsDefault: 'error', + reportUnsolvedTypeVar: 'error', allowedUntypedLibraries: [], }); @@ -1300,6 +1307,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet { reportIncompatibleUnannotatedOverride: 'none', reportInvalidAbstractMethod: 'none', reportSelfClsDefault: 'none', + reportUnsolvedTypeVar: 'none', allowedUntypedLibraries: [], }; diff --git a/packages/pyright-internal/src/common/diagnosticRules.ts b/packages/pyright-internal/src/common/diagnosticRules.ts index 102536777..3fac345f0 100644 --- a/packages/pyright-internal/src/common/diagnosticRules.ts +++ b/packages/pyright-internal/src/common/diagnosticRules.ts @@ -122,4 +122,5 @@ export enum DiagnosticRule { reportIncompatibleUnannotatedOverride = 'reportIncompatibleUnannotatedOverride', reportInvalidAbstractMethod = 'reportInvalidAbstractMethod', reportSelfClsDefault = 'reportSelfClsDefault', + reportUnsolvedTypeVar = 'reportUnsolvedTypeVar', } diff --git a/packages/pyright-internal/src/localization/localize.ts b/packages/pyright-internal/src/localization/localize.ts index e30e7d691..f2afde33c 100644 --- a/packages/pyright-internal/src/localization/localize.ts +++ b/packages/pyright-internal/src/localization/localize.ts @@ -1113,6 +1113,8 @@ export namespace Localizer { new ParameterizedString<{ type: string; name: string }>( getRawString('Diagnostic.typeVarAssignmentMismatch') ); + export const typeVarUnsolved = () => + new ParameterizedString<{ name: string }>(getRawString('Diagnostic.typeVarUnsolved')); export const typeVarBoundAndConstrained = () => getRawString('Diagnostic.typeVarBoundAndConstrained'); export const typeVarBoundGeneric = () => getRawString('Diagnostic.typeVarBoundGeneric'); export const typeVarConstraintGeneric = () => getRawString('Diagnostic.typeVarConstraintGeneric'); @@ -1670,6 +1672,10 @@ export namespace Localizer { new ParameterizedString<{ exceptionType: string; parentType: string }>( getRawString('DiagnosticAddendum.unreachableExcept') ); + export const typeVarUnsolvedCollectionHint = () => + new ParameterizedString<{ className: string }>( + getRawString('DiagnosticAddendum.typeVarUnsolvedCollectionHint') + ); export const useDictInstead = () => getRawString('DiagnosticAddendum.useDictInstead'); export const useListInstead = () => getRawString('DiagnosticAddendum.useListInstead'); export const useTupleInstead = () => getRawString('DiagnosticAddendum.useTupleInstead'); diff --git a/packages/pyright-internal/src/localization/package.nls.en-us.json b/packages/pyright-internal/src/localization/package.nls.en-us.json index c99266ce5..d3e4523a0 100644 --- a/packages/pyright-internal/src/localization/package.nls.en-us.json +++ b/packages/pyright-internal/src/localization/package.nls.en-us.json @@ -1436,6 +1436,9 @@ "comment": "{Locked='TypeVar'}" }, "typeVarAssignmentMismatch": "Type \"{type}\" cannot be assigned to type variable \"{name}\"", + "typeVarUnsolved": { + "message": "Type variable \"{name}\" has no solution; consider providing it explicitly" + }, "typeVarBoundAndConstrained": { "message": "TypeVar cannot be both bound and constrained", "comment": "{Locked='TypeVar'}" @@ -2185,6 +2188,10 @@ "message": "Use tuple[T1, ..., Tn] to indicate a tuple type or T1 | T2 to indicate a union type", "comment": "{Locked='tuple[T1, ..., Tn]','tuple','T1 | T2','union'}" }, + "typeVarUnsolvedCollectionHint": { + "message": "e.g. \"{className}[]()\"", + "comment": "{Locked='{className}[]()'}" + }, "useTypeInstead": { "message": "Use type[T] instead", "comment": "{Locked='type[T]'}" diff --git a/packages/pyright-internal/src/tests/samples/typeVarUnsolved1.py b/packages/pyright-internal/src/tests/samples/typeVarUnsolved1.py new file mode 100644 index 000000000..7c413db0d --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/typeVarUnsolved1.py @@ -0,0 +1,72 @@ +# this sample tests the detection of unsolved type variables in function calls + +from typing import Any, Callable + +def func1[T]() -> T: ... + + +def func2[T](x: T) -> T: + return x + + +def func3[T, U](x: T) -> U: ... + + +def func4[T = int]() -> T: ... + + +def func5[T, U = str](x: T) -> U: ... + + +# Error: T cannot be inferred +a = func1() + +# OK: T is inferred from argument +b = func2(1) + +# Error: U cannot be inferred (T is fine) +c = func3(1) # pyright: ignore[reportUnsolvedTypeVar] + +# OK: T has a default value +d = func4() + +# OK: T is inferred, U has a default value +e = func5(1) + + +# test with explicit type arguments +f: int = func1() + +class MyClass[T]: ... + +# ok: T is explicit +i = MyClass[int]() + +# error: can't infer class type from constructor +i = MyClass() # pyright: ignore[reportUnsolvedTypeVar] + +class Default[T=Any]: ... + +# ok: T is explicit +i = Default[int]() + +# ok: uses default +i = Default() + +def deco[T]() -> Callable[[Callable[[], T]], Callable[[], T]]: ... + +# ok: T is None +@deco() +def func6(): + pass + +deco()(lambda: None) + +# OK: T is inferred from argument +j: int = func2(func1()) + +# error +k = [] + +# no error +l: list[int] = [] diff --git a/packages/pyright-internal/src/tests/typeEvaluator5.test.ts b/packages/pyright-internal/src/tests/typeEvaluator5.test.ts index bf35f9c0e..8a5634835 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator5.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator5.test.ts @@ -12,6 +12,7 @@ import { ConfigOptions } from '../common/configOptions'; import { pythonVersion3_11, pythonVersion3_12, pythonVersion3_13 } from '../common/pythonVersion'; import { Uri } from '../common/uri/uri'; import * as TestUtils from './testUtils'; +import { DiagnosticRule } from '../common/diagnosticRules'; test('TypeParams1', () => { const configOptions = new ConfigOptions(Uri.empty()); @@ -198,6 +199,31 @@ test('TypeVarDefault1', () => { TestUtils.validateResults(analysisResults, 14); }); +test('TypeVarUnsolved1', () => { + const configOptions = new ConfigOptions(Uri.empty()); + configOptions.diagnosticRuleSet.reportUnnecessaryTypeIgnoreComment = 'error'; + configOptions.diagnosticRuleSet.reportInvalidTypeVarUse = 'none'; + configOptions.diagnosticRuleSet.reportUnusedParameter = 'none'; + configOptions.diagnosticRuleSet.reportUnsolvedTypeVar = 'error'; + + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarUnsolved1.py'], configOptions); + TestUtils.validateResultsButBased(analysisResults, { + errors: [ + // some errors to validate message, the rest use `pyright: ignore` + { + line: 21, + code: DiagnosticRule.reportUnsolvedTypeVar, + message: 'Type variable "T" has no solution; consider providing it explicitly', + }, + { + line: 68, + code: DiagnosticRule.reportUnsolvedTypeVar, + message: 'Type variable "T" has no solution; consider providing it explicitly\n  e.g. "list[]()"', + }, + ], + }); +}); + test('TypeVarDefault2', () => { const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = pythonVersion3_13; diff --git a/packages/vscode-pyright/package.json b/packages/vscode-pyright/package.json index 0700e0673..2733f601f 100644 --- a/packages/vscode-pyright/package.json +++ b/packages/vscode-pyright/package.json @@ -439,6 +439,23 @@ false ] }, + "reportUnsolvedTypeVar": { + "type": [ + "string", + "boolean" + ], + "description": "Diagnostics for a call-site with an unsolved type variable.", + "default": "none", + "enum": [ + "none", + "hint", + "information", + "warning", + "error", + true, + false + ] + }, "reportDuplicateImport": { "type": [ "string", diff --git a/packages/vscode-pyright/schemas/pyrightconfig.schema.json b/packages/vscode-pyright/schemas/pyrightconfig.schema.json index 1207bf8b5..e280ca8af 100644 --- a/packages/vscode-pyright/schemas/pyrightconfig.schema.json +++ b/packages/vscode-pyright/schemas/pyrightconfig.schema.json @@ -170,6 +170,11 @@ "title": "Controls reporting of local variables that are not accessed", "default": "hint" }, + "reportUnsolvedTypeVar": { + "$ref": "#/definitions/diagnostic", + "title": "Controls reporting of function calls with unresolved type variables", + "default": "none" + }, "reportDuplicateImport": { "$ref": "#/definitions/diagnostic", "title": "Controls reporting of symbols or modules that are imported more than once", @@ -740,6 +745,9 @@ "reportUnusedVariable": { "$ref": "#/definitions/reportUnusedVariable" }, + "reportUnsolvedTypeVar": { + "$ref": "#/definitions/reportUnsolvedTypeVar" + }, "reportDuplicateImport": { "$ref": "#/definitions/reportDuplicateImport" }, @@ -1108,6 +1116,9 @@ "reportUnusedVariable": { "$ref": "#/definitions/reportUnusedVariable" }, + "reportUnsolvedTypeVar": { + "$ref": "#/definitions/reportUnsolvedTypeVar" + }, "reportDuplicateImport": { "$ref": "#/definitions/reportDuplicateImport" },