Skip to content

Commit 6ff7dcd

Browse files
committed
fixup! report error for unsolved type variables in function calls
1 parent 2caa8ef commit 6ff7dcd

6 files changed

Lines changed: 69 additions & 7 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: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12567,7 +12567,9 @@ export function createTypeEvaluator(
1256712567
eliminateUnsolvedInUnions = false;
1256812568
}
1256912569

12570-
let specializedReturnType = solveAndApplyConstraints(returnType, constraints, {
12570+
// Solve constraints once and reuse the solution
12571+
const solution = solveConstraints(evaluatorInterface, constraints);
12572+
let specializedReturnType = applySolvedTypeVars(returnType, solution, {
1257112573
replaceUnsolved: {
1257212574
scopeIds: getTypeVarScopeIds(type),
1257312575
unsolvedExemptTypeVars: getUnknownExemptTypeVarsForReturnType(type, returnType),
@@ -12604,13 +12606,14 @@ export function createTypeEvaluator(
1260412606
specializedReturnType = adjustCallableReturnType(errorNode, specializedReturnType, liveTypeVarScopes);
1260512607

1260612608
if (specializedInitSelfType) {
12607-
specializedInitSelfType = solveAndApplyConstraints(specializedInitSelfType, constraints);
12609+
specializedInitSelfType = applySolvedTypeVars(specializedInitSelfType, solution);
1260812610
}
1260912611

1261012612
// check for unsolved type variables and report errors
1261112613
if (!argumentErrors && !isTypeIncomplete && type.shared.typeParams.length > 0) {
12612-
const solution = solveConstraints(evaluatorInterface, constraints);
1261312614
const solutionSet = solution.getMainSolutionSet();
12615+
const scopeIds = getTypeVarScopeIds(type);
12616+
const unsolvedExemptTypeVars = getUnknownExemptTypeVarsForReturnType(type, returnType);
1261412617

1261512618
for (const typeParam of type.shared.typeParams) {
1261612619
// skip type parameters that have explicit default values
@@ -12623,12 +12626,22 @@ export function createTypeEvaluator(
1262312626
continue;
1262412627
}
1262512628

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+
1262612640
const solvedType = solutionSet.getType(typeParam);
1262712641

12628-
// if there's no solution for this type parameter, report an error
1262912642
if (!solvedType) {
1263012643
addDiagnostic(
12631-
DiagnosticRule.reportCallIssue,
12644+
DiagnosticRule.reportUnsolvedTypeVar,
1263212645
LocMessage.typeVarUnsolved().format({
1263312646
name: typeParam.shared.name,
1263412647
}),

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/tests/samples/typeVarUnsolved1.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# this sample tests the detection of unsolved type variables in function calls
22

3+
from typing import Callable
4+
35
def func1[T]() -> T: ...
46

57

@@ -53,3 +55,15 @@ def create_from(x: T) -> "MyClass[T]": ...
5355

5456
# ok: T is explicit
5557
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: 12 additions & 2 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());
@@ -202,12 +203,21 @@ test('TypeVarUnsolved1', () => {
202203
const configOptions = new ConfigOptions(Uri.empty());
203204
configOptions.diagnosticRuleSet.reportInvalidTypeVarUse = 'none';
204205
configOptions.diagnosticRuleSet.reportUnusedParameter = 'none';
206+
configOptions.diagnosticRuleSet.reportUnsolvedTypeVar = 'error';
205207

206208
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarUnsolved1.py'], configOptions);
207209
TestUtils.validateResultsButBased(analysisResults, {
208210
errors: [
209-
{ line: 19, message: 'Type variable "T" has no solution; consider providing it explicitly' },
210-
{ line: 25, message: 'Type variable "U" has no solution; consider providing it explicitly' },
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+
},
211221
],
212222
});
213223
});

0 commit comments

Comments
 (0)