lint on the global keyword if there's no explicit definition in the global scope
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This commit is contained in:
Jack O'Connor 2025-07-14 15:44:02 -07:00
parent a1edb69ea5
commit e73a8ba571
8 changed files with 280 additions and 78 deletions

View file

@ -85,6 +85,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&STATIC_ASSERT_ERROR);
registry.register_lint(&INVALID_ATTRIBUTE_ACCESS);
registry.register_lint(&REDUNDANT_CAST);
registry.register_lint(&UNRESOLVED_GLOBAL);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@ -1560,6 +1561,56 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Detects variables declared as `global` in an inner scope that have no explicit
/// bindings or declarations in the global scope.
///
/// ## Why is this bad?
/// Function bodies with `global` statements can run in any order (or not at all), which makes
/// it hard for static analysis tools to infer the types of globals without
/// explicit definitions or declarations.
///
/// ## Example
/// ```python
/// def f():
/// global x # unresolved global
/// x = 42
///
/// def g():
/// print(x) # unresolved reference
/// ```
///
/// Use instead:
/// ```python
/// x: int
///
/// def f():
/// global x
/// x = 42
///
/// def g():
/// print(x)
/// ```
///
/// Or:
/// ```python
/// x: int | None = None
///
/// def f():
/// global x
/// x = 42
///
/// def g():
/// print(x)
/// ```
pub(crate) static UNRESOLVED_GLOBAL = {
summary: "detects `global` statements with no definition in the global scope",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
/// A collection of type check diagnostics.
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
pub struct TypeCheckDiagnostics {

View file

@ -94,12 +94,13 @@ use crate::types::diagnostic::{
INVALID_DECLARATION, INVALID_GENERIC_CLASS, 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_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, report_invalid_generator_function_return_type,
report_invalid_return_type, report_possibly_unbound_attribute,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, 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,
report_invalid_generator_function_return_type, report_invalid_return_type,
report_possibly_unbound_attribute,
};
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
@ -2255,11 +2256,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::Stmt::Return(ret) => self.infer_return_statement(ret),
ast::Stmt::Delete(delete) => self.infer_delete_statement(delete),
ast::Stmt::Nonlocal(nonlocal) => self.infer_nonlocal_statement(nonlocal),
ast::Stmt::Global(global) => self.infer_global_statement(global),
ast::Stmt::Break(_)
| ast::Stmt::Continue(_)
| ast::Stmt::Pass(_)
| ast::Stmt::IpyEscapeCommand(_)
| ast::Stmt::Global(_) => {
| ast::Stmt::IpyEscapeCommand(_) => {
// No-op
}
}
@ -4653,6 +4654,61 @@ 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(place_id) = global_place_table.place_id_by_name(name) {
let place = global_place_table.place_expr(place_id);
if place.is_bound() || place.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, range) 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 infer_nonlocal_statement(&mut self, nonlocal: &ast::StmtNonlocal) {
let ast::StmtNonlocal {
node_index: _,