From 5ffed301efbd4543c36232341caf7bc1ad6aef44 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Thu, 7 Aug 2025 18:45:50 -0700 Subject: [PATCH] [ty] now that inference sees nested bindings, allow unresolved globals --- .../resources/mdtest/import/star.md | 3 - .../resources/mdtest/scopes/global.md | 26 +++++--- crates/ty_python_semantic/src/types/infer.rs | 63 ++----------------- 3 files changed, 20 insertions(+), 72 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/import/star.md b/crates/ty_python_semantic/resources/mdtest/import/star.md index 3b4a2385b8..15766758c2 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/star.md +++ b/crates/ty_python_semantic/resources/mdtest/import/star.md @@ -1299,15 +1299,12 @@ reveal_type(Nope) # revealed: Unknown ## `global` statements in non-global scopes Python allows `global` statements in function bodies to add new variables to the global scope, but -we require a matching global binding or declaration. We lint on unresolved `global` statements, and we don't include the symbols they might define in `*` imports: `a.py`: ```py def f(): - # error: [unresolved-global] "Invalid global declaration of `g`: `g` has no declarations or bindings in the global scope" - # error: [unresolved-global] "Invalid global declaration of `h`: `h` has no declarations or bindings in the global scope" global g, h g = True diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/global.md b/crates/ty_python_semantic/resources/mdtest/scopes/global.md index 87f2c8dafa..1012d73ddc 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/global.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/global.md @@ -241,26 +241,32 @@ def f(): # TODO: reveal_type(x) # revealed: Unknown | Literal["1"] ``` -## Global variables need an explicit definition in the global scope +## Global variables don't need an explicit definition in the global scope You're allowed to use the `global` keyword to define new global variables that don't have any -explicit definition in the global scope, but we consider that fishy and prefer to lint on it: +explicit definition in the global scope: ```py -x = 1 -y: int -# z is neither bound nor declared in the global scope - def f(): - global x, y, z # error: [unresolved-global] "Invalid global declaration of `z`: `z` has no declarations or bindings in the global scope" + global x + x = 42 + +def g(): + print(x) # allowed, resolves to the global `x` defined by `f` + +def h(): + print(y) # error: [unresolved-reference] ``` -You don't need a definition for implicit globals, but you do for built-ins: +However, this only affects the "public" type of the global. It's still considered unbound when +module-scope code refers to it locally. ```py def f(): - global __file__ # allowed, implicit global - global int # error: [unresolved-global] "Invalid global declaration of `int`: `int` has no declarations or bindings in the global scope" + global x + x = 42 + +print(x) # error: [unresolved-reference] ``` ## References to variables before they are defined within a class scope are considered global diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 8859ffa45d..783bf921b8 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -97,8 +97,8 @@ use crate::types::diagnostic::{ INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, - TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, - UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type, + TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, + UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type, report_instance_layout_conflict, report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable, report_invalid_assignment, report_invalid_attribute_assignment, @@ -2547,8 +2547,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::Stmt::Raise(raise) => self.infer_raise_statement(raise), ast::Stmt::Return(ret) => self.infer_return_statement(ret), ast::Stmt::Delete(delete) => self.infer_delete_statement(delete), - ast::Stmt::Global(global) => self.infer_global_statement(global), - ast::Stmt::Nonlocal(_) + ast::Stmt::Global(_) + | ast::Stmt::Nonlocal(_) | ast::Stmt::Break(_) | ast::Stmt::Continue(_) | ast::Stmt::Pass(_) @@ -5299,61 +5299,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - fn infer_global_statement(&mut self, global: &ast::StmtGlobal) { - // CPython allows examples like this, where a global variable is never explicitly defined - // in the global scope: - // - // ```py - // def f(): - // global x - // x = 1 - // def g(): - // print(x) - // ``` - // - // However, allowing this pattern would make it hard for us to guarantee - // accurate analysis about the types and boundness of global-scope symbols, - // so we require the variable to be explicitly defined (either bound or declared) - // in the global scope. - let ast::StmtGlobal { - node_index: _, - range: _, - names, - } = global; - let global_place_table = self.index.place_table(FileScopeId::global()); - for name in names { - if let Some(symbol_id) = global_place_table.symbol_id(name) { - let symbol = global_place_table.symbol(symbol_id); - if symbol.is_bound() || symbol.is_declared() { - // This name is explicitly defined in the global scope (not just in function - // bodies that mark it `global`). - continue; - } - } - if !module_type_implicit_global_symbol(self.db(), name) - .place - .is_unbound() - { - // This name is an implicit global like `__file__` (but not a built-in like `int`). - continue; - } - // This variable isn't explicitly defined in the global scope, nor is it an - // implicit global from `types.ModuleType`, so we consider this `global` statement invalid. - let Some(builder) = self.context.report_lint(&UNRESOLVED_GLOBAL, name) else { - return; - }; - let mut diag = - builder.into_diagnostic(format_args!("Invalid global declaration of `{name}`")); - diag.set_primary_message(format_args!( - "`{name}` has no declarations or bindings in the global scope" - )); - diag.info("This limits ty's ability to make accurate inferences about the boundness and types of global-scope symbols"); - diag.info(format_args!( - "Consider adding a declaration to the global scope, e.g. `{name}: int`" - )); - } - } - fn module_type_from_name(&self, module_name: &ModuleName) -> Option> { resolve_module(self.db(), module_name) .map(|module| Type::module_literal(self.db(), self.file(), module))