Skip to content

Commit e73a8ba

Browse files
committed
lint on the global keyword if there's no explicit definition in the global scope
1 parent a1edb69 commit e73a8ba

File tree

8 files changed

+280
-78
lines changed

8 files changed

+280
-78
lines changed

crates/ty/docs/rules.md

Lines changed: 111 additions & 57 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,8 @@ def _():
343343
## Load before `global` declaration
344344

345345
```py
346+
x: int
347+
346348
def f():
347349
x = 1
348350
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"

crates/ty_python_semantic/resources/mdtest/import/star.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,13 +1295,16 @@ reveal_type(Nope) # revealed: Unknown
12951295

12961296
## `global` statements in non-global scopes
12971297

1298-
A `global` statement in a nested function scope, combined with a definition in the same function
1299-
scope of the name that was declared `global`, can add a symbol to the global namespace.
1298+
Python allows `global` statements in function bodies to add new variables to the global scope, but
1299+
we require a matching global binding or declaration. We lint on unresolved `global` statements, and
1300+
we don't include the symbols they might define in `*` imports:
13001301

13011302
`a.py`:
13021303

13031304
```py
13041305
def f():
1306+
# error: [unresolved-global] "Invalid global declaration of `g`: `g` has no declarations or bindings in the global scope"
1307+
# error: [unresolved-global] "Invalid global declaration of `h`: `h` has no declarations or bindings in the global scope"
13051308
global g, h
13061309

13071310
g = True
@@ -1316,16 +1319,12 @@ from a import *
13161319

13171320
reveal_type(f) # revealed: def f() -> Unknown
13181321

1319-
# TODO: we're undecided about whether we should consider this a false positive or not.
1320-
# Mutating the global scope to add a symbol from an inner scope will not *necessarily* result
1321-
# in the symbol being bound from the perspective of other modules (the function that creates
1322-
# the inner scope, and adds the symbol to the global scope, might never be called!)
1323-
# See discussion in https://github.com/astral-sh/ruff/pull/16959
1324-
#
1322+
# This could be considered a false positive, since this use of `g` isn't a runtime error, but we're
1323+
# being conservative.
13251324
# error: [unresolved-reference]
13261325
reveal_type(g) # revealed: Unknown
13271326

1328-
# this diagnostic is accurate, though!
1327+
# However, this is a true positive: `h` is unbound at runtime.
13291328
# error: [unresolved-reference]
13301329
reveal_type(h) # revealed: Unknown
13311330
```

crates/ty_python_semantic/resources/mdtest/scopes/global.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ x = 2
103103
Using a name prior to its `global` declaration in the same scope is a syntax error.
104104

105105
```py
106+
x = 1
107+
y = 2
108+
106109
def f():
107110
print(x)
108111
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
@@ -224,3 +227,25 @@ def f():
224227

225228
# TODO: reveal_type(x) # revealed: Unknown | Literal["1"]
226229
```
230+
231+
## Global variables need an explicit definition in the global scope
232+
233+
You're allowed to use the `global` keyword to define new global variables that don't have any
234+
explicit definition in the global scope, but we consider that fishy and prefer to lint on it:
235+
236+
```py
237+
x = 1
238+
y: int
239+
# z is neither bound nor declared in the global scope
240+
241+
def f():
242+
global x, y, z # error: [unresolved-global] "Invalid global declaration of `z`: `z` has no declarations or bindings in the global scope"
243+
```
244+
245+
You don't need a definition for implicit globals, but you do for built-ins:
246+
247+
```py
248+
def f():
249+
global __file__ # allowed, implicit global
250+
global int # error: [unresolved-global] "Invalid global declaration of `int`: `int` has no declarations or bindings in the global scope"
251+
```

crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,10 @@ def f1():
219219
But a `global` statement breaks the chain:
220220

221221
```py
222+
x = 1
223+
222224
def f():
223-
x = 1
225+
x = 2
224226
def g():
225227
global x
226228
def h():
@@ -240,8 +242,13 @@ def f():
240242
## A complicated mixture of `nonlocal` chaining, empty scopes, class scopes, and the `global` keyword
241243

242244
```py
245+
# Global definitions of `x`, `y`, and `z`.
246+
x: bool = True
247+
y: bool = True
248+
z: bool = True
249+
243250
def f1():
244-
# The original bindings of `x`, `y`, and `z` with type declarations.
251+
# Local definitions of `x`, `y`, and `z`.
245252
x: int = 1
246253
y: int = 2
247254
z: int = 3
@@ -263,7 +270,6 @@ def f1():
263270
x = 4
264271
y = 5
265272
global z
266-
z = 6
267273

268274
def f4():
269275
# This scope sees `x` from `f1` and `y` from `f3`. It *can't* declare `z`
@@ -272,7 +278,6 @@ def f1():
272278
nonlocal x, y, z # error: [invalid-syntax] "no binding for nonlocal `z` found"
273279
x = "string" # error: [invalid-assignment]
274280
y = "string" # allowed, because `f3`'s `y` is untyped
275-
reveal_type(z) # revealed: Unknown | Literal[6]
276281
```
277282

278283
## TODO: `nonlocal` affects the inferred type in the outer scope

crates/ty_python_semantic/src/types/diagnostic.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
8585
registry.register_lint(&STATIC_ASSERT_ERROR);
8686
registry.register_lint(&INVALID_ATTRIBUTE_ACCESS);
8787
registry.register_lint(&REDUNDANT_CAST);
88+
registry.register_lint(&UNRESOLVED_GLOBAL);
8889

8990
// String annotations
9091
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -1560,6 +1561,56 @@ declare_lint! {
15601561
}
15611562
}
15621563

1564+
declare_lint! {
1565+
/// ## What it does
1566+
/// Detects variables declared as `global` in an inner scope that have no explicit
1567+
/// bindings or declarations in the global scope.
1568+
///
1569+
/// ## Why is this bad?
1570+
/// Function bodies with `global` statements can run in any order (or not at all), which makes
1571+
/// it hard for static analysis tools to infer the types of globals without
1572+
/// explicit definitions or declarations.
1573+
///
1574+
/// ## Example
1575+
/// ```python
1576+
/// def f():
1577+
/// global x # unresolved global
1578+
/// x = 42
1579+
///
1580+
/// def g():
1581+
/// print(x) # unresolved reference
1582+
/// ```
1583+
///
1584+
/// Use instead:
1585+
/// ```python
1586+
/// x: int
1587+
///
1588+
/// def f():
1589+
/// global x
1590+
/// x = 42
1591+
///
1592+
/// def g():
1593+
/// print(x)
1594+
/// ```
1595+
///
1596+
/// Or:
1597+
/// ```python
1598+
/// x: int | None = None
1599+
///
1600+
/// def f():
1601+
/// global x
1602+
/// x = 42
1603+
///
1604+
/// def g():
1605+
/// print(x)
1606+
/// ```
1607+
pub(crate) static UNRESOLVED_GLOBAL = {
1608+
summary: "detects `global` statements with no definition in the global scope",
1609+
status: LintStatus::preview("1.0.0"),
1610+
default_level: Level::Warn,
1611+
}
1612+
}
1613+
15631614
/// A collection of type check diagnostics.
15641615
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
15651616
pub struct TypeCheckDiagnostics {

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,13 @@ use crate::types::diagnostic::{
9494
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
9595
INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
9696
POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics,
97-
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
98-
UNSUPPORTED_OPERATOR, report_implicit_return_type, report_instance_layout_conflict,
99-
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
100-
report_invalid_arguments_to_callable, report_invalid_assignment,
101-
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
102-
report_invalid_return_type, report_possibly_unbound_attribute,
97+
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
98+
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
99+
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
100+
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
101+
report_invalid_assignment, report_invalid_attribute_assignment,
102+
report_invalid_generator_function_return_type, report_invalid_return_type,
103+
report_possibly_unbound_attribute,
103104
};
104105
use crate::types::function::{
105106
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
@@ -2255,11 +2256,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
22552256
ast::Stmt::Return(ret) => self.infer_return_statement(ret),
22562257
ast::Stmt::Delete(delete) => self.infer_delete_statement(delete),
22572258
ast::Stmt::Nonlocal(nonlocal) => self.infer_nonlocal_statement(nonlocal),
2259+
ast::Stmt::Global(global) => self.infer_global_statement(global),
22582260
ast::Stmt::Break(_)
22592261
| ast::Stmt::Continue(_)
22602262
| ast::Stmt::Pass(_)
2261-
| ast::Stmt::IpyEscapeCommand(_)
2262-
| ast::Stmt::Global(_) => {
2263+
| ast::Stmt::IpyEscapeCommand(_) => {
22632264
// No-op
22642265
}
22652266
}
@@ -4653,6 +4654,61 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
46534654
}
46544655
}
46554656

4657+
fn infer_global_statement(&mut self, global: &ast::StmtGlobal) {
4658+
// CPython allows examples like this, where a global variable is never explicitly defined
4659+
// in the global scope:
4660+
//
4661+
// ```py
4662+
// def f():
4663+
// global x
4664+
// x = 1
4665+
// def g():
4666+
// print(x)
4667+
// ```
4668+
//
4669+
// However, allowing this pattern would make it hard for us to guarantee
4670+
// accurate analysis about the types and boundness of global-scope symbols,
4671+
// so we require the variable to be explicitly defined (either bound or declared)
4672+
// in the global scope.
4673+
let ast::StmtGlobal {
4674+
node_index: _,
4675+
range,
4676+
names,
4677+
} = global;
4678+
let global_place_table = self.index.place_table(FileScopeId::global());
4679+
for name in names {
4680+
if let Some(place_id) = global_place_table.place_id_by_name(name) {
4681+
let place = global_place_table.place_expr(place_id);
4682+
if place.is_bound() || place.is_declared() {
4683+
// This name is explicitly defined in the global scope (not just in function
4684+
// bodies that mark it `global`).
4685+
continue;
4686+
}
4687+
}
4688+
if !module_type_implicit_global_symbol(self.db(), name)
4689+
.place
4690+
.is_unbound()
4691+
{
4692+
// This name is an implicit global like `__file__` (but not a built-in like `int`).
4693+
continue;
4694+
}
4695+
// This variable isn't explicitly defined in the global scope, nor is it an
4696+
// implicit global from `types.ModuleType`, so we consider this `global` statement invalid.
4697+
let Some(builder) = self.context.report_lint(&UNRESOLVED_GLOBAL, range) else {
4698+
return;
4699+
};
4700+
let mut diag =
4701+
builder.into_diagnostic(format_args!("Invalid global declaration of `{name}`"));
4702+
diag.set_primary_message(format_args!(
4703+
"`{name}` has no declarations or bindings in the global scope"
4704+
));
4705+
diag.info("This limits ty's ability to make accurate inferences about the boundness and types of global-scope symbols");
4706+
diag.info(format_args!(
4707+
"Consider adding a declaration to the global scope, e.g. `{name}: int`"
4708+
));
4709+
}
4710+
}
4711+
46564712
fn infer_nonlocal_statement(&mut self, nonlocal: &ast::StmtNonlocal) {
46574713
let ast::StmtNonlocal {
46584714
node_index: _,

ty.schema.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)