-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[used before def] improve handling of global definitions in local scopes #14517
Changes from 7 commits
3a14986
5415d4e
66a7802
342c05d
ddc4418
8e69cd4
286840f
33561c2
cff35c6
43ddea4
b5a02a4
78b0f55
9adbe70
39c22d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,7 +79,9 @@ def copy(self) -> BranchState: | |
|
||
|
||
class BranchStatement: | ||
def __init__(self, initial_state: BranchState) -> None: | ||
def __init__(self, initial_state: BranchState | None = None) -> None: | ||
if initial_state is None: | ||
initial_state = BranchState() | ||
self.initial_state = initial_state | ||
self.branches: list[BranchState] = [ | ||
BranchState( | ||
|
@@ -171,7 +173,7 @@ class ScopeType(Enum): | |
Global = 1 | ||
Class = 2 | ||
Func = 3 | ||
Generator = 3 | ||
Generator = 4 | ||
|
||
|
||
class Scope: | ||
|
@@ -199,7 +201,7 @@ class DefinedVariableTracker: | |
|
||
def __init__(self) -> None: | ||
# There's always at least one scope. Within each scope, there's at least one "global" BranchingStatement. | ||
self.scopes: list[Scope] = [Scope([BranchStatement(BranchState())], ScopeType.Global)] | ||
self.scopes: list[Scope] = [Scope([BranchStatement()], ScopeType.Global)] | ||
# disable_branch_skip is used to disable skipping a branch due to a return/raise/etc. This is useful | ||
# in things like try/except/finally statements. | ||
self.disable_branch_skip = False | ||
|
@@ -216,9 +218,11 @@ def _scope(self) -> Scope: | |
|
||
def enter_scope(self, scope_type: ScopeType) -> None: | ||
assert len(self._scope().branch_stmts) > 0 | ||
self.scopes.append( | ||
Scope([BranchStatement(self._scope().branch_stmts[-1].branches[-1])], scope_type) | ||
) | ||
initial_state = None | ||
if scope_type == ScopeType.Generator: | ||
# Generators are special because they inherit the outer scope. | ||
initial_state = self._scope().branch_stmts[-1].branches[-1] | ||
self.scopes.append(Scope([BranchStatement(initial_state)], scope_type)) | ||
|
||
def exit_scope(self) -> None: | ||
self.scopes.pop() | ||
|
@@ -328,6 +332,11 @@ def __init__( | |
self.loops: list[Loop] = [] | ||
self.try_depth = 0 | ||
self.tracker = DefinedVariableTracker() | ||
builtins_mod = names.get("__builtins__", None) | ||
if builtins_mod: | ||
assert isinstance(builtins_mod.node, MypyFile) | ||
for name in builtins_mod.node.names: | ||
self.tracker.record_definition(name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this might degrade performance, since builtins have over 200 definitions, and it looks like we are iterating over all of them for every module. Can you measure the time spent in this loop when performing a self check, for example? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have found a way to deal with this that doesn't involve calling |
||
for name in implicit_module_attrs: | ||
self.tracker.record_definition(name) | ||
|
||
|
@@ -342,13 +351,15 @@ def variable_may_be_undefined(self, name: str, context: Context) -> None: | |
def process_definition(self, name: str) -> None: | ||
# Was this name previously used? If yes, it's a used-before-definition error. | ||
if not self.tracker.in_scope(ScopeType.Class): | ||
# Errors in class scopes are caught by the semantic analyzer. | ||
refs = self.tracker.pop_undefined_ref(name) | ||
for ref in refs: | ||
if self.loops: | ||
self.variable_may_be_undefined(name, ref) | ||
else: | ||
self.var_used_before_def(name, ref) | ||
else: | ||
# Errors in class scopes are caught by the semantic analyzer. | ||
pass | ||
self.tracker.record_definition(name) | ||
|
||
def visit_global_decl(self, o: GlobalDecl) -> None: | ||
|
@@ -603,8 +614,6 @@ def visit_starred_pattern(self, o: StarredPattern) -> None: | |
super().visit_starred_pattern(o) | ||
|
||
def visit_name_expr(self, o: NameExpr) -> None: | ||
if o.name in self.builtins: | ||
return | ||
if self.tracker.is_possibly_undefined(o.name): | ||
# A variable is only defined in some branches. | ||
self.variable_may_be_undefined(o.name, o) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -210,7 +210,6 @@ def f0() -> None: | |
y = x | ||
x = 1 # No error. | ||
|
||
|
||
[case testGlobalDeclarationAfterUsage] | ||
# flags: --enable-error-code possibly-undefined --enable-error-code used-before-def | ||
def f0() -> None: | ||
|
@@ -219,6 +218,7 @@ def f0() -> None: | |
x = 1 # No error. | ||
|
||
x = 2 | ||
|
||
[case testVarDefinedInOuterScope] | ||
# flags: --enable-error-code possibly-undefined --enable-error-code used-before-def | ||
def f0() -> None: | ||
|
@@ -227,13 +227,57 @@ def f0() -> None: | |
|
||
f0() | ||
x = 1 | ||
|
||
[case testDefinedInOuterScopeNoError] | ||
# flags: --enable-error-code possibly-undefined --enable-error-code used-before-def | ||
def foo() -> None: | ||
bar() | ||
|
||
def bar() -> None: | ||
foo() | ||
|
||
[case testClassFromOuterScopeRedefined] | ||
# flags: --enable-error-code possibly-undefined --enable-error-code used-before-def | ||
class c: pass | ||
|
||
def f0() -> None: | ||
s = c() # E: Name "c" is used before definition | ||
class c: pass | ||
|
||
|
||
def f1() -> None: | ||
s = c() # No error. | ||
|
||
|
||
def f2() -> None: | ||
s = c() # E: Name "c" is used before definition | ||
if int(): | ||
class c: pass | ||
|
||
[case testVarFromOuterScopeRedefined] | ||
# flags: --enable-error-code possibly-undefined --enable-error-code used-before-def | ||
x = 0 | ||
|
||
def f0() -> None: | ||
y = x # E: Name "x" is used before definition | ||
x = 0 | ||
|
||
def f1() -> None: | ||
y = x # No error. | ||
|
||
def f2() -> None: | ||
y = x # E: Name "x" is used before definition | ||
global x | ||
|
||
def f3() -> None: | ||
global x | ||
y = x # No error. | ||
|
||
def f4() -> None: | ||
if int(): | ||
x = 0 | ||
y = x # E: Name "x" may be undefined | ||
|
||
[case testFuncParams] | ||
# flags: --enable-error-code possibly-undefined | ||
def foo(a: int) -> None: | ||
|
@@ -829,67 +873,56 @@ def f4() -> None: | |
x = z # E: Name "z" is used before definition | ||
z: int = 2 | ||
|
||
[case testUsedBeforeDefImportsBasic] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when I wrote these import tests, I misunderstood how they would actually behave at runtime. |
||
# flags: --enable-error-code used-before-def | ||
[case testUsedBeforeDefImportsBasicImportNoError] | ||
# flags: --enable-error-code used-before-def --enable-error-code possibly-undefined --disable-error-code no-redef | ||
import foo # type: ignore | ||
import x.y # type: ignore | ||
|
||
def f0() -> None: | ||
a = foo # No error. | ||
foo: int = 1 | ||
a = foo # No error. | ||
foo: int = 1 | ||
|
||
def f1() -> None: | ||
a = y # E: Name "y" is used before definition | ||
y: int = 1 | ||
[case testUsedBeforeDefImportsDotImport] | ||
# flags: --enable-error-code used-before-def --enable-error-code possibly-undefined --disable-error-code no-redef | ||
import x.y # type: ignore | ||
|
||
def f2() -> None: | ||
a = x # No error. | ||
x: int = 1 | ||
a = y # E: Name "y" is used before definition | ||
y: int = 1 | ||
|
||
def f3() -> None: | ||
a = x.y # No error. | ||
x: int = 1 | ||
b = x # No error. | ||
x: int = 1 | ||
|
||
c = x.y # No error. | ||
x: int = 1 | ||
|
||
[case testUsedBeforeDefImportBasicRename] | ||
# flags: --enable-error-code used-before-def | ||
# flags: --enable-error-code used-before-def --disable-error-code=no-redef | ||
import x.y as z # type: ignore | ||
from typing import Any | ||
|
||
def f0() -> None: | ||
a = z # No error. | ||
z: int = 1 | ||
|
||
def f1() -> None: | ||
a = x # E: Name "x" is used before definition | ||
x: int = 1 | ||
a = z # No error. | ||
z: int = 1 | ||
|
||
def f2() -> None: | ||
a = x.y # E: Name "x" is used before definition | ||
x: Any = 1 | ||
a = x # E: Name "x" is used before definition | ||
x: int = 1 | ||
|
||
def f3() -> None: | ||
a = y # E: Name "y" is used before definition | ||
y: int = 1 | ||
a = y # E: Name "y" is used before definition | ||
y: int = 1 | ||
|
||
[case testUsedBeforeDefImportFrom] | ||
# flags: --enable-error-code used-before-def | ||
# flags: --enable-error-code used-before-def --disable-error-code no-redef | ||
from foo import x # type: ignore | ||
|
||
def f0() -> None: | ||
a = x # No error. | ||
x: int = 1 | ||
a = x # No error. | ||
x: int = 1 | ||
|
||
[case testUsedBeforeDefImportFromRename] | ||
# flags: --enable-error-code used-before-def | ||
# flags: --enable-error-code used-before-def --disable-error-code no-redef | ||
from foo import x as y # type: ignore | ||
|
||
def f0() -> None: | ||
a = y # No error. | ||
y: int = 1 | ||
a = y # No error. | ||
y: int = 1 | ||
|
||
def f1() -> None: | ||
a = x # E: Name "x" is used before definition | ||
x: int = 1 | ||
a = x # E: Name "x" is used before definition | ||
x: int = 1 | ||
|
||
[case testUsedBeforeDefFunctionDeclarations] | ||
# flags: --enable-error-code used-before-def | ||
|
@@ -905,10 +938,13 @@ def f0() -> None: | |
# flags: --enable-error-code used-before-def | ||
|
||
def f0() -> None: | ||
s = type(123) | ||
s = type(123) # E: Name "type" is used before definition | ||
type = "abc" | ||
a = type | ||
|
||
def f1() -> None: | ||
s = type(123) | ||
|
||
[case testUsedBeforeDefBuiltinsMultipass] | ||
# flags: --enable-error-code used-before-def | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This didn't matter until we needed to handle generators differently. Generators actually do inherit the scope!