mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +00:00
[ty] Check assignments to implicit global symbols are assignable to the types declared on types.ModuleType
(#18077)
This commit is contained in:
parent
301d9985d8
commit
65e48cb439
3 changed files with 137 additions and 19 deletions
|
@ -40,6 +40,39 @@ reveal_type(__dict__)
|
|||
reveal_type(__init__)
|
||||
```
|
||||
|
||||
## `ModuleType` globals combined with explicit assignments and declarations
|
||||
|
||||
A `ModuleType` attribute can be overridden in the global scope with a different type, but it must be
|
||||
a type assignable to the declaration on `ModuleType` unless it is accompanied by an explicit
|
||||
redeclaration:
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
__file__ = None
|
||||
__path__: list[str] = []
|
||||
__doc__: int # error: [invalid-declaration] "Cannot declare type `int` for inferred type `str | None`"
|
||||
# error: [invalid-declaration] "Cannot shadow implicit global attribute `__package__` with declaration of type `int`"
|
||||
__package__: int = 42
|
||||
__spec__ = 42 # error: [invalid-assignment] "Object of type `Literal[42]` is not assignable to `ModuleSpec | None`"
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import module
|
||||
|
||||
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
|
||||
|
||||
def nested_scope():
|
||||
global __loader__
|
||||
reveal_type(__loader__) # revealed: LoaderProtocol | None
|
||||
__loader__ = 56 # error: [invalid-assignment] "Object of type `Literal[56]` is not assignable to `LoaderProtocol | None`"
|
||||
```
|
||||
|
||||
## Accessed as attributes
|
||||
|
||||
`ModuleType` attributes can also be accessed as attributes on module-literal types. The special
|
||||
|
@ -105,16 +138,16 @@ defined as a global, however, a name lookup should union the `ModuleType` type w
|
|||
conditionally defined type:
|
||||
|
||||
```py
|
||||
__file__ = 42
|
||||
__file__ = "foo"
|
||||
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
__name__ = 1
|
||||
__name__ = 1 # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `str`"
|
||||
|
||||
reveal_type(__file__) # revealed: Literal[42]
|
||||
reveal_type(__name__) # revealed: Literal[1] | str
|
||||
reveal_type(__file__) # revealed: Literal["foo"]
|
||||
reveal_type(__name__) # revealed: str
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute, with annotation
|
||||
|
@ -122,12 +155,14 @@ reveal_type(__name__) # revealed: Literal[1] | str
|
|||
The same is true if the name is annotated:
|
||||
|
||||
```py
|
||||
# error: [invalid-declaration] "Cannot shadow implicit global attribute `__file__` with declaration of type `int`"
|
||||
__file__: int = 42
|
||||
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
# error: [invalid-declaration] "Cannot shadow implicit global attribute `__name__` with declaration of type `int`"
|
||||
__name__: int = 1
|
||||
|
||||
reveal_type(__file__) # revealed: Literal[42]
|
||||
|
|
|
@ -14,7 +14,9 @@ use crate::types::{
|
|||
};
|
||||
use crate::{resolve_module, Db, KnownModule, Program};
|
||||
|
||||
pub(crate) use implicit_globals::module_type_implicit_global_symbol;
|
||||
pub(crate) use implicit_globals::{
|
||||
module_type_implicit_global_declaration, module_type_implicit_global_symbol,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub(crate) enum Boundness {
|
||||
|
@ -275,7 +277,6 @@ pub(crate) fn explicit_global_symbol<'db>(
|
|||
/// rather than being looked up as symbols explicitly defined/declared in the global scope.
|
||||
///
|
||||
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn global_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
|
@ -958,11 +959,36 @@ mod implicit_globals {
|
|||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::semantic_index::{self, symbol_table};
|
||||
use crate::semantic_index::{self, symbol_table, use_def_map};
|
||||
use crate::symbol::SymbolAndQualifiers;
|
||||
use crate::types::KnownClass;
|
||||
use crate::types::{KnownClass, Type};
|
||||
|
||||
use super::Symbol;
|
||||
use super::{symbol_from_declarations, Symbol, SymbolFromDeclarationsResult};
|
||||
|
||||
pub(crate) fn module_type_implicit_global_declaration<'db>(
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
) -> SymbolFromDeclarationsResult<'db> {
|
||||
if !module_type_symbols(db)
|
||||
.iter()
|
||||
.any(|module_type_member| &**module_type_member == name)
|
||||
{
|
||||
return Ok(Symbol::Unbound.into());
|
||||
}
|
||||
let Type::ClassLiteral(module_type_class) = KnownClass::ModuleType.to_class_literal(db)
|
||||
else {
|
||||
return Ok(Symbol::Unbound.into());
|
||||
};
|
||||
let module_type_scope = module_type_class.body_scope(db);
|
||||
let symbol_table = symbol_table(db, module_type_scope);
|
||||
let Some(symbol_id) = symbol_table.symbol_id_by_name(name) else {
|
||||
return Ok(Symbol::Unbound.into());
|
||||
};
|
||||
symbol_from_declarations(
|
||||
db,
|
||||
use_def_map(db, module_type_scope).public_declarations(symbol_id),
|
||||
)
|
||||
}
|
||||
|
||||
/// Looks up the type of an "implicit global symbol". Returns [`Symbol::Unbound`] if
|
||||
/// `name` is not present as an implicit symbol in module-global namespaces.
|
||||
|
|
|
@ -60,9 +60,10 @@ use crate::semantic_index::symbol::{
|
|||
};
|
||||
use crate::semantic_index::{semantic_index, EagerSnapshotResult, SemanticIndex};
|
||||
use crate::symbol::{
|
||||
builtins_module_scope, builtins_symbol, explicit_global_symbol,
|
||||
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||
typing_extensions_symbol, Boundness, LookupError,
|
||||
builtins_module_scope, builtins_symbol, explicit_global_symbol, global_symbol,
|
||||
module_type_implicit_global_declaration, module_type_implicit_global_symbol, symbol,
|
||||
symbol_from_bindings, symbol_from_declarations, typing_extensions_symbol, Boundness,
|
||||
LookupError,
|
||||
};
|
||||
use crate::types::call::{Argument, Bindings, CallArgumentTypes, CallArguments, CallError};
|
||||
use crate::types::class::{MetaclassErrorKind, SliceLiteral};
|
||||
|
@ -1420,8 +1421,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
let symbol_id = binding.symbol(self.db());
|
||||
|
||||
let global_use_def_map = self.index.use_def_map(FileScopeId::global());
|
||||
let declarations = if self.skip_non_global_scopes(file_scope_id, symbol_id) {
|
||||
let symbol_name = symbol_table.symbol(symbol_id).name();
|
||||
let symbol_name = symbol_table.symbol(symbol_id).name();
|
||||
let skip_non_global_scopes = self.skip_non_global_scopes(file_scope_id, symbol_id);
|
||||
let declarations = if skip_non_global_scopes {
|
||||
match self
|
||||
.index
|
||||
.symbol_table(FileScopeId::global())
|
||||
|
@ -1436,6 +1438,20 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
};
|
||||
|
||||
let declared_ty = symbol_from_declarations(self.db(), declarations)
|
||||
.and_then(|symbol| {
|
||||
let symbol = if matches!(symbol.symbol, Symbol::Type(_, Boundness::Bound)) {
|
||||
symbol
|
||||
} else if skip_non_global_scopes
|
||||
|| self.scope().file_scope_id(self.db()).is_global()
|
||||
{
|
||||
let module_type_declarations =
|
||||
module_type_implicit_global_declaration(self.db(), symbol_name)?;
|
||||
symbol.or_fall_back_to(self.db(), || module_type_declarations)
|
||||
} else {
|
||||
symbol
|
||||
};
|
||||
Ok(symbol)
|
||||
})
|
||||
.map(|SymbolAndQualifiers { symbol, .. }| {
|
||||
symbol.ignore_possibly_unbound().unwrap_or(Type::unknown())
|
||||
})
|
||||
|
@ -1487,6 +1503,23 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
let prior_bindings = use_def.bindings_at_declaration(declaration);
|
||||
// unbound_ty is Never because for this check we don't care about unbound
|
||||
let inferred_ty = symbol_from_bindings(self.db(), prior_bindings)
|
||||
.with_qualifiers(TypeQualifiers::empty())
|
||||
.or_fall_back_to(self.db(), || {
|
||||
// Fallback to bindings declared on `types.ModuleType` if it's a global symbol
|
||||
let scope = self.scope().file_scope_id(self.db());
|
||||
if scope.is_global() {
|
||||
module_type_implicit_global_symbol(
|
||||
self.db(),
|
||||
self.index
|
||||
.symbol_table(scope)
|
||||
.symbol(declaration.symbol(self.db()))
|
||||
.name(),
|
||||
)
|
||||
} else {
|
||||
Symbol::Unbound.into()
|
||||
}
|
||||
})
|
||||
.symbol
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::Never);
|
||||
let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) {
|
||||
|
@ -1525,6 +1558,34 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
declared_ty,
|
||||
inferred_ty,
|
||||
} => {
|
||||
let file_scope_id = self.scope().file_scope_id(self.db());
|
||||
if file_scope_id.is_global() {
|
||||
let symbol_table = self.index.symbol_table(file_scope_id);
|
||||
let symbol_name = symbol_table.symbol(definition.symbol(self.db())).name();
|
||||
if let Some(module_type_implicit_declaration) =
|
||||
module_type_implicit_global_declaration(self.db(), symbol_name)
|
||||
.ok()
|
||||
.and_then(|sym| sym.symbol.ignore_possibly_unbound())
|
||||
{
|
||||
let declared_type = declared_ty.inner_type();
|
||||
if !declared_type
|
||||
.is_assignable_to(self.db(), module_type_implicit_declaration)
|
||||
{
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_DECLARATION, node)
|
||||
{
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"Cannot shadow implicit global attribute `{symbol_name}` with declaration of type `{}`",
|
||||
declared_type.display(self.db())
|
||||
));
|
||||
diagnostic.info(format_args!("The global symbol `{}` must always have a type assignable to `{}`",
|
||||
symbol_name,
|
||||
module_type_implicit_declaration.display(self.db())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if inferred_ty.is_assignable_to(self.db(), declared_ty.inner_type()) {
|
||||
(declared_ty, inferred_ty)
|
||||
} else {
|
||||
|
@ -5386,11 +5447,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
.is_some_and(|symbol_id| self.skip_non_global_scopes(file_scope_id, symbol_id));
|
||||
|
||||
if skip_non_global_scopes {
|
||||
return symbol(
|
||||
db,
|
||||
FileScopeId::global().to_scope_id(db, current_file),
|
||||
symbol_name,
|
||||
);
|
||||
return global_symbol(self.db(), self.file(), symbol_name);
|
||||
}
|
||||
|
||||
// If it's a function-like scope and there is one or more binding in this scope (but
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue