mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:14:52 +00:00
[pyflakes
] Fix false positives for __annotate__
(Py3.14+) and __warningregistry__
(F821
) (#20154)
## Summary Fixes #19970 --------- Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
parent
742f8a4ee6
commit
346842f003
7 changed files with 153 additions and 28 deletions
|
@ -56,7 +56,7 @@ use ruff_python_semantic::{
|
||||||
Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel,
|
Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel,
|
||||||
SemanticModelFlags, StarImport, SubmoduleImport,
|
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_python_trivia::CommentRanges;
|
||||||
use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow};
|
use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow};
|
||||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
|
@ -2550,7 +2550,7 @@ impl<'a> Checker<'a> {
|
||||||
for builtin in standard_builtins {
|
for builtin in standard_builtins {
|
||||||
bind_builtin(builtin);
|
bind_builtin(builtin);
|
||||||
}
|
}
|
||||||
for builtin in MAGIC_GLOBALS {
|
for builtin in python_magic_globals(target_version.minor) {
|
||||||
bind_builtin(builtin);
|
bind_builtin(builtin);
|
||||||
}
|
}
|
||||||
for builtin in &settings.builtins {
|
for builtin in &settings.builtins {
|
||||||
|
|
|
@ -920,6 +920,42 @@ mod tests {
|
||||||
flakes("__annotations__", &[]);
|
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]
|
#[test]
|
||||||
fn magic_globals_file() {
|
fn magic_globals_file() {
|
||||||
// Use of the C{__file__} magic global should not emit an undefined name
|
// Use of the C{__file__} magic global should not emit an undefined name
|
||||||
|
|
|
@ -20,9 +20,15 @@ pub const MAGIC_GLOBALS: &[&str] = &[
|
||||||
"__annotations__",
|
"__annotations__",
|
||||||
"__builtins__",
|
"__builtins__",
|
||||||
"__cached__",
|
"__cached__",
|
||||||
|
"__warningregistry__",
|
||||||
"__file__",
|
"__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] = &[
|
static ALWAYS_AVAILABLE_BUILTINS: &[&str] = &[
|
||||||
"ArithmeticError",
|
"ArithmeticError",
|
||||||
"AssertionError",
|
"AssertionError",
|
||||||
|
@ -216,6 +222,21 @@ pub fn python_builtins(minor_version: u8, is_notebook: bool) -> impl Iterator<It
|
||||||
.copied()
|
.copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the list of magic globals for the given Python minor version.
|
||||||
|
pub fn python_magic_globals(minor_version: u8) -> impl Iterator<Item = &'static str> {
|
||||||
|
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.
|
/// Returns `true` if the given name is that of a Python builtin.
|
||||||
///
|
///
|
||||||
/// Intended to be kept in sync with [`python_builtins`].
|
/// Intended to be kept in sync with [`python_builtins`].
|
||||||
|
|
|
@ -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]
|
||||||
|
```
|
|
@ -16,6 +16,8 @@ reveal_type(__doc__) # revealed: str | None
|
||||||
reveal_type(__spec__) # revealed: ModuleSpec | None
|
reveal_type(__spec__) # revealed: ModuleSpec | None
|
||||||
reveal_type(__path__) # revealed: MutableSequence[str]
|
reveal_type(__path__) # revealed: MutableSequence[str]
|
||||||
reveal_type(__builtins__) # revealed: Any
|
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
|
import sys
|
||||||
|
|
||||||
|
@ -75,6 +77,8 @@ reveal_type(module.__file__) # revealed: Unknown | None
|
||||||
reveal_type(module.__path__) # revealed: list[str]
|
reveal_type(module.__path__) # revealed: list[str]
|
||||||
reveal_type(module.__doc__) # revealed: Unknown
|
reveal_type(module.__doc__) # revealed: Unknown
|
||||||
reveal_type(module.__spec__) # revealed: Unknown | ModuleSpec | None
|
reveal_type(module.__spec__) # revealed: Unknown | ModuleSpec | None
|
||||||
|
# error: [unresolved-attribute]
|
||||||
|
reveal_type(module.__warningregistry__) # revealed: Unknown
|
||||||
|
|
||||||
def nested_scope():
|
def nested_scope():
|
||||||
global __loader__
|
global __loader__
|
||||||
|
|
|
@ -1339,12 +1339,15 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool {
|
||||||
|
|
||||||
mod implicit_globals {
|
mod implicit_globals {
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
|
use ruff_python_ast::name::Name;
|
||||||
|
|
||||||
|
use crate::Program;
|
||||||
use crate::db::Db;
|
use crate::db::Db;
|
||||||
use crate::place::PlaceAndQualifiers;
|
use crate::place::{Boundness, PlaceAndQualifiers};
|
||||||
use crate::semantic_index::symbol::Symbol;
|
use crate::semantic_index::symbol::Symbol;
|
||||||
use crate::semantic_index::{place_table, use_def_map};
|
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};
|
use super::{Place, place_from_declarations};
|
||||||
|
|
||||||
|
@ -1392,28 +1395,58 @@ mod implicit_globals {
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> PlaceAndQualifiers<'db> {
|
) -> PlaceAndQualifiers<'db> {
|
||||||
// We special-case `__file__` here because we know that for an internal implicit global
|
match name {
|
||||||
// lookup in a Python module, it is always a string, even though typeshed says `str |
|
// We special-case `__file__` here because we know that for an internal implicit global
|
||||||
// None`.
|
// lookup in a Python module, it is always a string, even though typeshed says `str |
|
||||||
if name == "__file__" {
|
// None`.
|
||||||
Place::bound(KnownClass::Str.to_instance(db)).into()
|
"__file__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
|
||||||
} else if name == "__builtins__" {
|
|
||||||
Place::bound(Type::any()).into()
|
"__builtins__" => Place::bound(Type::any()).into(),
|
||||||
} else if name == "__debug__" {
|
|
||||||
Place::bound(KnownClass::Bool.to_instance(db)).into()
|
"__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
|
// Created lazily by the warnings machinery; may be absent.
|
||||||
// `.member()` call on the instance type -- we'd just do the `.member`() call on the instance
|
// Model as possibly-unbound to avoid false negatives.
|
||||||
// type, since it has the same end result. The reason to only call `.member()` on `ModuleType`
|
"__warningregistry__" => Place::Type(
|
||||||
// when absolutely necessary is that this function is used in a very hot path (name resolution
|
KnownClass::Dict
|
||||||
// in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation.
|
.to_specialized_instance(db, [Type::any(), KnownClass::Int.to_instance(db)]),
|
||||||
else if module_type_symbols(db)
|
Boundness::PossiblyUnbound,
|
||||||
.iter()
|
)
|
||||||
.any(|module_type_member| &**module_type_member == name)
|
.into(),
|
||||||
{
|
|
||||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
// Marked as possibly-unbound as it is only present in the module namespace
|
||||||
} else {
|
// if at least one global symbol is annotated in the module.
|
||||||
Place::Unbound.into()
|
"__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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ pub(crate) use self::infer::{
|
||||||
infer_expression_types, infer_isolated_expression, infer_scope_types,
|
infer_expression_types, infer_isolated_expression, infer_scope_types,
|
||||||
static_expression_truthiness,
|
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};
|
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
|
||||||
use crate::module_name::ModuleName;
|
use crate::module_name::ModuleName;
|
||||||
use crate::module_resolver::{KnownModule, resolve_module};
|
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::infer::infer_unpack_types;
|
||||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||||
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
|
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;
|
use crate::types::tuple::TupleSpec;
|
||||||
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
|
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
|
||||||
use crate::types::variance::{TypeVarVariance, VarianceInferable};
|
use crate::types::variance::{TypeVarVariance, VarianceInferable};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue