Skip to content

Commit 09ecbcc

Browse files
committed
report error for unsolved type variables in function calls
1 parent 264b247 commit 09ecbcc

10 files changed

Lines changed: 195 additions & 0 deletions

File tree

.basedpyright/baseline.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,14 @@
297297
"lineCount": 1
298298
}
299299
},
300+
{
301+
"code": "reportUnsolvedTypeVar",
302+
"range": {
303+
"startColumn": 4,
304+
"endColumn": 5,
305+
"lineCount": 7
306+
}
307+
},
300308
{
301309
"code": "reportUnusedCallResult",
302310
"range": {
@@ -305,6 +313,14 @@
305313
"lineCount": 7
306314
}
307315
},
316+
{
317+
"code": "reportUnsolvedTypeVar",
318+
"range": {
319+
"startColumn": 4,
320+
"endColumn": 5,
321+
"lineCount": 6
322+
}
323+
},
308324
{
309325
"code": "reportUnusedCallResult",
310326
"range": {

packages/pyright-internal/src/analyzer/typeEvaluator.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12607,6 +12607,50 @@ export function createTypeEvaluator(
1260712607
specializedInitSelfType = solveAndApplyConstraints(specializedInitSelfType, constraints);
1260812608
}
1260912609

12610+
// check for unsolved type variables and report errors
12611+
if (!argumentErrors && !isTypeIncomplete && type.shared.typeParams.length > 0) {
12612+
const solution = solveConstraints(evaluatorInterface, constraints);
12613+
12614+
const solutionSet = solution.getMainSolutionSet();
12615+
const scopeIds = getTypeVarScopeIds(type);
12616+
const unsolvedExemptTypeVars = getUnknownExemptTypeVarsForReturnType(type, returnType);
12617+
12618+
for (const typeParam of type.shared.typeParams) {
12619+
// skip type parameters that have explicit default values
12620+
if (typeParam.shared.isDefaultExplicit) {
12621+
continue;
12622+
}
12623+
12624+
// skip ParamSpecs and TypeVarTuples for now - they have different semantics
12625+
if (isParamSpec(typeParam) || isTypeVarTuple(typeParam)) {
12626+
continue;
12627+
}
12628+
12629+
// only check type parameters that are in scope for this function
12630+
if (typeParam.priv.scopeId && scopeIds && !scopeIds.includes(typeParam.priv.scopeId)) {
12631+
continue;
12632+
}
12633+
12634+
// skip type parameters that are exempt from being solved (e.g., they appear
12635+
// only in the return type as part of a returned callable)
12636+
if (unsolvedExemptTypeVars.some((exemptVar) => isTypeSame(exemptVar, typeParam))) {
12637+
continue;
12638+
}
12639+
12640+
const solvedType = solutionSet.getType(typeParam);
12641+
12642+
if (!solvedType) {
12643+
addDiagnostic(
12644+
DiagnosticRule.reportUnsolvedTypeVar,
12645+
LocMessage.typeVarUnsolved().format({
12646+
name: typeParam.shared.name,
12647+
}),
12648+
errorNode
12649+
);
12650+
}
12651+
}
12652+
}
12653+
1261012654
matchResults.argumentMatchScore = argumentMatchScore;
1261112655

1261212656
return {

packages/pyright-internal/src/common/configOptions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ export interface DiagnosticRuleSet {
427427
reportIncompatibleUnannotatedOverride: DiagnosticLevel;
428428
reportInvalidAbstractMethod: DiagnosticLevel;
429429
reportSelfClsDefault: DiagnosticLevel;
430+
reportUnsolvedTypeVar: DiagnosticLevel;
430431
allowedUntypedLibraries: string[];
431432
}
432433

@@ -560,6 +561,7 @@ export function getDiagLevelDiagnosticRules() {
560561
DiagnosticRule.reportIncompatibleUnannotatedOverride,
561562
DiagnosticRule.reportInvalidAbstractMethod,
562563
DiagnosticRule.reportSelfClsDefault,
564+
DiagnosticRule.reportUnsolvedTypeVar,
563565
];
564566
}
565567

@@ -698,6 +700,7 @@ export function getOffDiagnosticRuleSet(): DiagnosticRuleSet {
698700
reportIncompatibleUnannotatedOverride: 'none',
699701
reportInvalidAbstractMethod: 'none',
700702
reportSelfClsDefault: 'none',
703+
reportUnsolvedTypeVar: 'none',
701704
allowedUntypedLibraries: [],
702705
};
703706

@@ -818,6 +821,7 @@ export function getBasicDiagnosticRuleSet(): DiagnosticRuleSet {
818821
reportIncompatibleUnannotatedOverride: 'none',
819822
reportInvalidAbstractMethod: 'none',
820823
reportSelfClsDefault: 'none',
824+
reportUnsolvedTypeVar: 'none',
821825
allowedUntypedLibraries: [],
822826
};
823827

@@ -938,6 +942,7 @@ export function getStandardDiagnosticRuleSet(): DiagnosticRuleSet {
938942
reportIncompatibleUnannotatedOverride: 'none',
939943
reportInvalidAbstractMethod: 'none',
940944
reportSelfClsDefault: 'none',
945+
reportUnsolvedTypeVar: 'none',
941946
allowedUntypedLibraries: [],
942947
};
943948

@@ -1057,6 +1062,7 @@ export const getRecommendedDiagnosticRuleSet = (): DiagnosticRuleSet => ({
10571062
reportIncompatibleUnannotatedOverride: 'none', // TODO: change to error when we're confident there's no performance issues with this rule
10581063
reportInvalidAbstractMethod: 'warning',
10591064
reportSelfClsDefault: 'warning',
1065+
reportUnsolvedTypeVar: 'error',
10601066
allowedUntypedLibraries: [],
10611067
});
10621068

@@ -1173,6 +1179,7 @@ export const getAllDiagnosticRuleSet = (): DiagnosticRuleSet => ({
11731179
reportIncompatibleUnannotatedOverride: 'error',
11741180
reportInvalidAbstractMethod: 'error',
11751181
reportSelfClsDefault: 'error',
1182+
reportUnsolvedTypeVar: 'error',
11761183
allowedUntypedLibraries: [],
11771184
});
11781185

@@ -1290,6 +1297,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet {
12901297
reportIncompatibleUnannotatedOverride: 'none',
12911298
reportInvalidAbstractMethod: 'none',
12921299
reportSelfClsDefault: 'none',
1300+
reportUnsolvedTypeVar: 'none',
12931301
allowedUntypedLibraries: [],
12941302
};
12951303

packages/pyright-internal/src/common/diagnosticRules.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,5 @@ export enum DiagnosticRule {
121121
reportIncompatibleUnannotatedOverride = 'reportIncompatibleUnannotatedOverride',
122122
reportInvalidAbstractMethod = 'reportInvalidAbstractMethod',
123123
reportSelfClsDefault = 'reportSelfClsDefault',
124+
reportUnsolvedTypeVar = 'reportUnsolvedTypeVar',
124125
}

packages/pyright-internal/src/localization/localize.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,8 @@ export namespace Localizer {
11131113
new ParameterizedString<{ type: string; name: string }>(
11141114
getRawString('Diagnostic.typeVarAssignmentMismatch')
11151115
);
1116+
export const typeVarUnsolved = () =>
1117+
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.typeVarUnsolved'));
11161118
export const typeVarBoundAndConstrained = () => getRawString('Diagnostic.typeVarBoundAndConstrained');
11171119
export const typeVarBoundGeneric = () => getRawString('Diagnostic.typeVarBoundGeneric');
11181120
export const typeVarConstraintGeneric = () => getRawString('Diagnostic.typeVarConstraintGeneric');

packages/pyright-internal/src/localization/package.nls.en-us.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,9 @@
14361436
"comment": "{Locked='TypeVar'}"
14371437
},
14381438
"typeVarAssignmentMismatch": "Type \"{type}\" cannot be assigned to type variable \"{name}\"",
1439+
"typeVarUnsolved": {
1440+
"message": "Type variable \"{name}\" has no solution; consider providing it explicitly"
1441+
},
14391442
"typeVarBoundAndConstrained": {
14401443
"message": "TypeVar cannot be both bound and constrained",
14411444
"comment": "{Locked='TypeVar'}"
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# this sample tests the detection of unsolved type variables in function calls
2+
3+
from typing import Callable
4+
5+
def func1[T]() -> T: ...
6+
7+
8+
def func2[T](x: T) -> T:
9+
return x
10+
11+
12+
def func3[T, U](x: T) -> U: ...
13+
14+
15+
def func4[T = int]() -> T: ...
16+
17+
18+
def func5[T, U = str](x: T) -> U: ...
19+
20+
21+
# Error: T cannot be inferred
22+
a = func1()
23+
24+
# OK: T is inferred from argument
25+
b = func2(1)
26+
27+
# Error: U cannot be inferred (T is fine)
28+
c = func3(1)
29+
30+
# OK: T has a default value
31+
d = func4()
32+
33+
# OK: T is inferred, U has a default value
34+
e = func5(1)
35+
36+
37+
# test with explicit type arguments
38+
f: int = func1()
39+
40+
41+
# test class with unsolved type var
42+
class MyClass[T]:
43+
@staticmethod
44+
def create() -> "MyClass[T]": ...
45+
46+
@staticmethod
47+
def create_from(x: T) -> "MyClass[T]": ...
48+
49+
50+
# Error: T cannot be inferred
51+
g = MyClass.create()
52+
53+
# OK: T is inferred from argument
54+
h = MyClass.create_from(42)
55+
56+
# ok: T is explicit
57+
i = MyClass[int]()
58+
59+
def deco[T]() -> Callable[[Callable[[], T]], Callable[[], T]]: ...
60+
61+
# ok: T is None
62+
@deco()
63+
def func6():
64+
pass
65+
66+
deco()(lambda: None)
67+
68+
# OK: T is inferred from argument
69+
j: int = func2(func1())

packages/pyright-internal/src/tests/typeEvaluator5.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ConfigOptions } from '../common/configOptions';
1212
import { pythonVersion3_11, pythonVersion3_12, pythonVersion3_13 } from '../common/pythonVersion';
1313
import { Uri } from '../common/uri/uri';
1414
import * as TestUtils from './testUtils';
15+
import { DiagnosticRule } from '../common/diagnosticRules';
1516

1617
test('TypeParams1', () => {
1718
const configOptions = new ConfigOptions(Uri.empty());
@@ -198,6 +199,29 @@ test('TypeVarDefault1', () => {
198199
TestUtils.validateResults(analysisResults, 14);
199200
});
200201

202+
test('TypeVarUnsolved1', () => {
203+
const configOptions = new ConfigOptions(Uri.empty());
204+
configOptions.diagnosticRuleSet.reportInvalidTypeVarUse = 'none';
205+
configOptions.diagnosticRuleSet.reportUnusedParameter = 'none';
206+
configOptions.diagnosticRuleSet.reportUnsolvedTypeVar = 'error';
207+
208+
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarUnsolved1.py'], configOptions);
209+
TestUtils.validateResultsButBased(analysisResults, {
210+
errors: [
211+
{
212+
line: 21,
213+
code: DiagnosticRule.reportUnsolvedTypeVar,
214+
message: 'Type variable "T" has no solution; consider providing it explicitly',
215+
},
216+
{
217+
line: 27,
218+
code: DiagnosticRule.reportUnsolvedTypeVar,
219+
message: 'Type variable "U" has no solution; consider providing it explicitly',
220+
},
221+
],
222+
});
223+
});
224+
201225
test('TypeVarDefault2', () => {
202226
const configOptions = new ConfigOptions(Uri.empty());
203227
configOptions.defaultPythonVersion = pythonVersion3_13;

packages/vscode-pyright/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,23 @@
439439
false
440440
]
441441
},
442+
"reportUnsolvedTypeVar": {
443+
"type": [
444+
"string",
445+
"boolean"
446+
],
447+
"description": "Diagnostics for a call-site with an unsolved type variable.",
448+
"default": "none",
449+
"enum": [
450+
"none",
451+
"hint",
452+
"information",
453+
"warning",
454+
"error",
455+
true,
456+
false
457+
]
458+
},
442459
"reportDuplicateImport": {
443460
"type": [
444461
"string",

packages/vscode-pyright/schemas/pyrightconfig.schema.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@
170170
"title": "Controls reporting of local variables that are not accessed",
171171
"default": "hint"
172172
},
173+
"reportUnsolvedTypeVar": {
174+
"$ref": "#/definitions/diagnostic",
175+
"title": "Controls reporting of function calls with unresolved type variables",
176+
"default": "none"
177+
},
173178
"reportDuplicateImport": {
174179
"$ref": "#/definitions/diagnostic",
175180
"title": "Controls reporting of symbols or modules that are imported more than once",
@@ -735,6 +740,9 @@
735740
"reportUnusedVariable": {
736741
"$ref": "#/definitions/reportUnusedVariable"
737742
},
743+
"reportUnsolvedTypeVar": {
744+
"$ref": "#/definitions/reportUnsolvedTypeVar"
745+
},
738746
"reportDuplicateImport": {
739747
"$ref": "#/definitions/reportDuplicateImport"
740748
},
@@ -1100,6 +1108,9 @@
11001108
"reportUnusedVariable": {
11011109
"$ref": "#/definitions/reportUnusedVariable"
11021110
},
1111+
"reportUnsolvedTypeVar": {
1112+
"$ref": "#/definitions/reportUnsolvedTypeVar"
1113+
},
11031114
"reportDuplicateImport": {
11041115
"$ref": "#/definitions/reportDuplicateImport"
11051116
},

0 commit comments

Comments
 (0)