Skip to content

Commit d58a851

Browse files
authored
Implement miscellaneous fixes for partially-defined check (#14175)
These are the issues that I've found using mypy-primer. You should be able to review this PR commit-by-commit. Each commit includes the relevant tests: - Process imports correctly - Support for function names - Skip stub files (this change has no tests) - Handle builtins and implicit module attrs (e.g. `str` and `__doc__`) - Improved support for lambdas.
1 parent db0beb1 commit d58a851

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed

mypy/build.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2349,6 +2349,9 @@ def type_check_second_pass(self) -> bool:
23492349

23502350
def detect_partially_defined_vars(self, type_map: dict[Expression, Type]) -> None:
23512351
assert self.tree is not None, "Internal error: method must be called on parsed file only"
2352+
if self.tree.is_stub:
2353+
# We skip stub files because they aren't actually executed.
2354+
return
23522355
manager = self.manager
23532356
if manager.errors.is_error_code_enabled(
23542357
codes.PARTIALLY_DEFINED

mypy/partially_defined.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,21 @@
1616
FuncItem,
1717
GeneratorExpr,
1818
IfStmt,
19+
Import,
20+
ImportFrom,
21+
LambdaExpr,
1922
ListExpr,
2023
Lvalue,
2124
MatchStmt,
2225
NameExpr,
2326
RaiseStmt,
27+
RefExpr,
2428
ReturnStmt,
29+
StarExpr,
2530
TupleExpr,
2631
WhileStmt,
2732
WithStmt,
33+
implicit_module_attrs,
2834
)
2935
from mypy.patterns import AsPattern, StarredPattern
3036
from mypy.reachability import ALWAYS_TRUE, infer_pattern_value
@@ -213,6 +219,10 @@ def is_undefined(self, name: str) -> bool:
213219
return self._scope().branch_stmts[-1].is_undefined(name)
214220

215221

222+
def refers_to_builtin(o: RefExpr) -> bool:
223+
return o.fullname is not None and o.fullname.startswith("builtins.")
224+
225+
216226
class PartiallyDefinedVariableVisitor(ExtendedTraverserVisitor):
217227
"""Detects the following cases:
218228
- A variable that's defined only part of the time.
@@ -236,6 +246,8 @@ def __init__(self, msg: MessageBuilder, type_map: dict[Expression, Type]) -> Non
236246
self.type_map = type_map
237247
self.loop_depth = 0
238248
self.tracker = DefinedVariableTracker()
249+
for name in implicit_module_attrs:
250+
self.tracker.record_definition(name)
239251

240252
def process_lvalue(self, lvalue: Lvalue | None) -> None:
241253
if isinstance(lvalue, NameExpr):
@@ -244,6 +256,8 @@ def process_lvalue(self, lvalue: Lvalue | None) -> None:
244256
for ref in refs:
245257
self.msg.var_used_before_def(lvalue.name, ref)
246258
self.tracker.record_definition(lvalue.name)
259+
elif isinstance(lvalue, StarExpr):
260+
self.process_lvalue(lvalue.expr)
247261
elif isinstance(lvalue, (ListExpr, TupleExpr)):
248262
for item in lvalue.items:
249263
self.process_lvalue(item)
@@ -291,6 +305,7 @@ def visit_match_stmt(self, o: MatchStmt) -> None:
291305
self.tracker.end_branch_statement()
292306

293307
def visit_func_def(self, o: FuncDef) -> None:
308+
self.tracker.record_definition(o.name)
294309
self.tracker.enter_scope()
295310
super().visit_func_def(o)
296311
self.tracker.exit_scope()
@@ -332,6 +347,11 @@ def visit_return_stmt(self, o: ReturnStmt) -> None:
332347
super().visit_return_stmt(o)
333348
self.tracker.skip_branch()
334349

350+
def visit_lambda_expr(self, o: LambdaExpr) -> None:
351+
self.tracker.enter_scope()
352+
super().visit_lambda_expr(o)
353+
self.tracker.exit_scope()
354+
335355
def visit_assert_stmt(self, o: AssertStmt) -> None:
336356
super().visit_assert_stmt(o)
337357
if checker.is_false_literal(o.expr):
@@ -377,6 +397,8 @@ def visit_starred_pattern(self, o: StarredPattern) -> None:
377397
super().visit_starred_pattern(o)
378398

379399
def visit_name_expr(self, o: NameExpr) -> None:
400+
if refers_to_builtin(o):
401+
return
380402
if self.tracker.is_partially_defined(o.name):
381403
# A variable is only defined in some branches.
382404
if self.msg.errors.is_error_code_enabled(errorcodes.PARTIALLY_DEFINED):
@@ -404,3 +426,24 @@ def visit_with_stmt(self, o: WithStmt) -> None:
404426
expr.accept(self)
405427
self.process_lvalue(idx)
406428
o.body.accept(self)
429+
430+
def visit_import(self, o: Import) -> None:
431+
for mod, alias in o.ids:
432+
if alias is not None:
433+
self.tracker.record_definition(alias)
434+
else:
435+
# When you do `import x.y`, only `x` becomes defined.
436+
names = mod.split(".")
437+
if len(names) > 0:
438+
# `names` should always be nonempty, but we don't want mypy
439+
# to crash on invalid code.
440+
self.tracker.record_definition(names[0])
441+
super().visit_import(o)
442+
443+
def visit_import_from(self, o: ImportFrom) -> None:
444+
for mod, alias in o.names:
445+
name = alias
446+
if name is None:
447+
name = mod
448+
self.tracker.record_definition(name)
449+
super().visit_import_from(o)

test-data/unit/check-partially-defined.test

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ else:
9090
a = y + x # E: Name "x" may be undefined
9191
a = y + z # E: Name "z" may be undefined
9292

93+
[case testIndexExpr]
94+
# flags: --enable-error-code partially-defined
95+
96+
if int():
97+
*x, y = (1, 2)
98+
else:
99+
x = [1, 2]
100+
a = x # No error.
101+
b = y # E: Name "y" may be undefined
102+
93103
[case testRedefined]
94104
# flags: --enable-error-code partially-defined
95105
y = 3
@@ -104,6 +114,32 @@ else:
104114

105115
x = y + 2
106116

117+
[case testFunction]
118+
# flags: --enable-error-code partially-defined
119+
def f0() -> None:
120+
if int():
121+
def some_func() -> None:
122+
pass
123+
124+
some_func() # E: Name "some_func" may be undefined
125+
126+
def f1() -> None:
127+
if int():
128+
def some_func() -> None:
129+
pass
130+
else:
131+
def some_func() -> None:
132+
pass
133+
134+
some_func() # No error.
135+
136+
[case testLambda]
137+
# flags: --enable-error-code partially-defined
138+
def f0(b: bool) -> None:
139+
if b:
140+
fn = lambda: 2
141+
y = fn # E: Name "fn" may be undefined
142+
107143
[case testGenerator]
108144
# flags: --enable-error-code partially-defined
109145
if int():
@@ -460,3 +496,88 @@ def f4() -> None:
460496
y = z # E: Name "z" is used before definition
461497
x = z # E: Name "z" is used before definition
462498
z: int = 2
499+
500+
[case testUseBeforeDefImportsBasic]
501+
# flags: --enable-error-code use-before-def
502+
import foo # type: ignore
503+
import x.y # type: ignore
504+
505+
def f0() -> None:
506+
a = foo # No error.
507+
foo: int = 1
508+
509+
def f1() -> None:
510+
a = y # E: Name "y" is used before definition
511+
y: int = 1
512+
513+
def f2() -> None:
514+
a = x # No error.
515+
x: int = 1
516+
517+
def f3() -> None:
518+
a = x.y # No error.
519+
x: int = 1
520+
521+
[case testUseBeforeDefImportBasicRename]
522+
# flags: --enable-error-code use-before-def
523+
import x.y as z # type: ignore
524+
from typing import Any
525+
526+
def f0() -> None:
527+
a = z # No error.
528+
z: int = 1
529+
530+
def f1() -> None:
531+
a = x # E: Name "x" is used before definition
532+
x: int = 1
533+
534+
def f2() -> None:
535+
a = x.y # E: Name "x" is used before definition
536+
x: Any = 1
537+
538+
def f3() -> None:
539+
a = y # E: Name "y" is used before definition
540+
y: int = 1
541+
542+
[case testUseBeforeDefImportFrom]
543+
# flags: --enable-error-code use-before-def
544+
from foo import x # type: ignore
545+
546+
def f0() -> None:
547+
a = x # No error.
548+
x: int = 1
549+
550+
[case testUseBeforeDefImportFromRename]
551+
# flags: --enable-error-code use-before-def
552+
from foo import x as y # type: ignore
553+
554+
def f0() -> None:
555+
a = y # No error.
556+
y: int = 1
557+
558+
def f1() -> None:
559+
a = x # E: Name "x" is used before definition
560+
x: int = 1
561+
562+
[case testUseBeforeDefFunctionDeclarations]
563+
# flags: --enable-error-code use-before-def
564+
565+
def f0() -> None:
566+
def inner() -> None:
567+
pass
568+
569+
inner() # No error.
570+
inner = lambda: None
571+
572+
[case testUseBeforeDefBuiltins]
573+
# flags: --enable-error-code use-before-def
574+
575+
def f0() -> None:
576+
s = type(123)
577+
type = "abc"
578+
a = type
579+
580+
[case testUseBeforeDefImplicitModuleAttrs]
581+
# flags: --enable-error-code use-before-def
582+
a = __name__ # No error.
583+
__name__ = "abc"

0 commit comments

Comments
 (0)