diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 2434ae19cb..ed8ab2b585 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -56,7 +56,7 @@ use ruff_python_semantic::{ Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport, }; -use ruff_python_stdlib::builtins::{MAGIC_GLOBALS, python_builtins}; +use ruff_python_stdlib::builtins::{python_builtins, python_magic_globals}; use ruff_python_trivia::CommentRanges; use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow}; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -2550,7 +2550,7 @@ impl<'a> Checker<'a> { for builtin in standard_builtins { bind_builtin(builtin); } - for builtin in MAGIC_GLOBALS { + for builtin in python_magic_globals(target_version.minor) { bind_builtin(builtin); } for builtin in &settings.builtins { diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 7590c31a39..b645198b49 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -920,6 +920,42 @@ mod tests { flakes("__annotations__", &[]); } + #[test] + fn module_warningregistry() { + // Using __warningregistry__ should not be considered undefined. + flakes("__warningregistry__", &[]); + } + + #[test] + fn module_annotate_py314_available() { + // __annotate__ is available starting in Python 3.14. + let diagnostics = crate::test::test_snippet( + "__annotate__", + &crate::settings::LinterSettings { + unresolved_target_version: ruff_python_ast::PythonVersion::PY314.into(), + ..crate::settings::LinterSettings::for_rules(vec![ + crate::codes::Rule::UndefinedName, + ]) + }, + ); + assert!(diagnostics.is_empty()); + } + + #[test] + fn module_annotate_pre_py314_undefined() { + // __annotate__ is not available before Python 3.14. + let diagnostics = crate::test::test_snippet( + "__annotate__", + &crate::settings::LinterSettings { + unresolved_target_version: ruff_python_ast::PythonVersion::PY313.into(), + ..crate::settings::LinterSettings::for_rules(vec![ + crate::codes::Rule::UndefinedName, + ]) + }, + ); + assert_eq!(diagnostics.len(), 1); + } + #[test] fn magic_globals_file() { // Use of the C{__file__} magic global should not emit an undefined name diff --git a/crates/ruff_python_stdlib/src/builtins.rs b/crates/ruff_python_stdlib/src/builtins.rs index 02830f9360..5176de6a22 100644 --- a/crates/ruff_python_stdlib/src/builtins.rs +++ b/crates/ruff_python_stdlib/src/builtins.rs @@ -20,9 +20,15 @@ pub const MAGIC_GLOBALS: &[&str] = &[ "__annotations__", "__builtins__", "__cached__", + "__warningregistry__", "__file__", ]; +/// Magic globals that are only available starting in specific Python versions. +/// +/// `__annotate__` was introduced in Python 3.14. +static PY314_PLUS_MAGIC_GLOBALS: &[&str] = &["__annotate__"]; + static ALWAYS_AVAILABLE_BUILTINS: &[&str] = &[ "ArithmeticError", "AssertionError", @@ -216,6 +222,21 @@ pub fn python_builtins(minor_version: u8, is_notebook: bool) -> impl Iterator impl Iterator { + let py314_magic_globals = if minor_version >= 14 { + Some(PY314_PLUS_MAGIC_GLOBALS) + } else { + None + }; + + py314_magic_globals + .into_iter() + .flatten() + .chain(MAGIC_GLOBALS) + .copied() +} + /// Returns `true` if the given name is that of a Python builtin. /// /// Intended to be kept in sync with [`python_builtins`]. diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/annotate_global.md b/crates/ty_python_semantic/resources/mdtest/scopes/annotate_global.md new file mode 100644 index 0000000000..221b92d777 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/scopes/annotate_global.md @@ -0,0 +1,31 @@ +# `__annotate__` as an implicit global is version-gated (Py3.14+) + +## Absent before 3.14 + +`__annotate__` is never present in the global namespace on Python \<3.14. + +```toml +[environment] +python-version = "3.13" +``` + +```py +# error: [unresolved-reference] +reveal_type(__annotate__) # revealed: Unknown +``` + +## Present in 3.14+ + +The `__annotate__` global may be present in Python 3.14, but only if at least one global symbol in +the module is annotated (e.g. `x: int` or `x: int = 42`). Currently we model `__annotate__` as +always being possibly unbound on Python 3.14+. + +```toml +[environment] +python-version = "3.14" +``` + +```py +# error: [possibly-unresolved-reference] +reveal_type(__annotate__) # revealed: (format: int, /) -> dict[str, Any] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md index ddc5527bf8..7c6c3e6d9e 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -16,6 +16,8 @@ reveal_type(__doc__) # revealed: str | None reveal_type(__spec__) # revealed: ModuleSpec | None reveal_type(__path__) # revealed: MutableSequence[str] reveal_type(__builtins__) # revealed: Any +# error: [possibly-unresolved-reference] "Name `__warningregistry__` used when possibly not defined" +reveal_type(__warningregistry__) # revealed: dict[Any, int] import sys @@ -75,6 +77,8 @@ reveal_type(module.__file__) # revealed: Unknown | None reveal_type(module.__path__) # revealed: list[str] reveal_type(module.__doc__) # revealed: Unknown reveal_type(module.__spec__) # revealed: Unknown | ModuleSpec | None +# error: [unresolved-attribute] +reveal_type(module.__warningregistry__) # revealed: Unknown def nested_scope(): global __loader__ diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index d140882877..993075cd5d 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1339,12 +1339,15 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { mod implicit_globals { use ruff_python_ast as ast; + use ruff_python_ast::name::Name; + use crate::Program; use crate::db::Db; - use crate::place::PlaceAndQualifiers; + use crate::place::{Boundness, PlaceAndQualifiers}; use crate::semantic_index::symbol::Symbol; use crate::semantic_index::{place_table, use_def_map}; - use crate::types::{KnownClass, Type}; + use crate::types::{CallableType, KnownClass, Parameter, Parameters, Signature, Type}; + use ruff_python_ast::PythonVersion; use super::{Place, place_from_declarations}; @@ -1392,28 +1395,58 @@ mod implicit_globals { db: &'db dyn Db, name: &str, ) -> PlaceAndQualifiers<'db> { - // We special-case `__file__` here because we know that for an internal implicit global - // lookup in a Python module, it is always a string, even though typeshed says `str | - // None`. - if name == "__file__" { - Place::bound(KnownClass::Str.to_instance(db)).into() - } else if name == "__builtins__" { - Place::bound(Type::any()).into() - } else if name == "__debug__" { - Place::bound(KnownClass::Bool.to_instance(db)).into() - } - // In general we wouldn't check to see whether a symbol exists on a class before doing the - // `.member()` call on the instance type -- we'd just do the `.member`() call on the instance - // type, since it has the same end result. The reason to only call `.member()` on `ModuleType` - // when absolutely necessary is that this function is used in a very hot path (name resolution - // in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation. - else if module_type_symbols(db) - .iter() - .any(|module_type_member| &**module_type_member == name) - { - KnownClass::ModuleType.to_instance(db).member(db, name) - } else { - Place::Unbound.into() + match name { + // We special-case `__file__` here because we know that for an internal implicit global + // lookup in a Python module, it is always a string, even though typeshed says `str | + // None`. + "__file__" => Place::bound(KnownClass::Str.to_instance(db)).into(), + + "__builtins__" => Place::bound(Type::any()).into(), + + "__debug__" => Place::bound(KnownClass::Bool.to_instance(db)).into(), + + // Created lazily by the warnings machinery; may be absent. + // Model as possibly-unbound to avoid false negatives. + "__warningregistry__" => Place::Type( + KnownClass::Dict + .to_specialized_instance(db, [Type::any(), KnownClass::Int.to_instance(db)]), + Boundness::PossiblyUnbound, + ) + .into(), + + // Marked as possibly-unbound as it is only present in the module namespace + // if at least one global symbol is annotated in the module. + "__annotate__" if Program::get(db).python_version(db) >= PythonVersion::PY314 => { + let signature = Signature::new( + Parameters::new( + [Parameter::positional_only(Some(Name::new_static("format"))) + .with_annotated_type(KnownClass::Int.to_instance(db))], + ), + Some(KnownClass::Dict.to_specialized_instance( + db, + [KnownClass::Str.to_instance(db), Type::any()], + )), + ); + Place::Type( + CallableType::function_like(db, signature), + Boundness::PossiblyUnbound, + ) + .into() + } + + // In general we wouldn't check to see whether a symbol exists on a class before doing the + // `.member()` call on the instance type -- we'd just do the `.member`() call on the instance + // type, since it has the same end result. The reason to only call `.member()` on `ModuleType` + // when absolutely necessary is that this function is used in a very hot path (name resolution + // in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation. + _ if module_type_symbols(db) + .iter() + .any(|module_type_member| &**module_type_member == name) => + { + KnownClass::ModuleType.to_instance(db).member(db, name) + } + + _ => Place::Unbound.into(), } } diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 4fb4385d1e..8096174af5 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -28,7 +28,7 @@ pub(crate) use self::infer::{ infer_expression_types, infer_isolated_expression, infer_scope_types, static_expression_truthiness, }; -pub(crate) use self::signatures::{CallableSignature, Signature}; +pub(crate) use self::signatures::{CallableSignature, Parameter, Parameters, Signature}; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; use crate::module_name::ModuleName; use crate::module_resolver::{KnownModule, resolve_module}; @@ -64,7 +64,7 @@ pub use crate::types::ide_support::{ use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; -use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature}; +use crate::types::signatures::{ParameterForm, walk_signature}; use crate::types::tuple::TupleSpec; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; use crate::types::variance::{TypeVarVariance, VarianceInferable};