[ty] Add special-cased inference for __import__(name) and importlib.import_module(name) (#19008)
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 / mkdocs (push) Waiting to run
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 / 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:
InSync 2025-06-29 17:49:23 +07:00 committed by GitHub
parent de1f8177be
commit e7aadfc28b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 185 additions and 51 deletions

View file

@ -154,6 +154,8 @@ pub enum KnownModule {
#[strum(serialize = "_typeshed._type_checker_internals")]
TypeCheckerInternals,
TyExtensions,
#[strum(serialize = "importlib")]
ImportLib,
}
impl KnownModule {
@ -172,6 +174,7 @@ impl KnownModule {
Self::Inspect => "inspect",
Self::TypeCheckerInternals => "_typeshed._type_checker_internals",
Self::TyExtensions => "ty_extensions",
Self::ImportLib => "importlib",
}
}
@ -210,6 +213,10 @@ impl KnownModule {
pub const fn is_enum(self) -> bool {
matches!(self, Self::Enum)
}
pub const fn is_importlib(self) -> bool {
matches!(self, Self::ImportLib)
}
}
impl std::fmt::Display for KnownModule {

View file

@ -740,7 +740,7 @@ impl<'db> Bindings<'db> {
Some(KnownFunction::Override) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `typing.overload` is an identity function.
// typeshed definition for `typing.override` is an identity function.
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(*ty);
}
@ -756,7 +756,7 @@ impl<'db> Bindings<'db> {
Some(KnownFunction::Final) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `abc.abstractmethod` is an identity function.
// typeshed definition for `typing.final` is an identity function.
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(*ty);
}

View file

@ -77,7 +77,7 @@ use crate::types::{
BoundMethodType, CallableType, DynamicType, KnownClass, Type, TypeMapping, TypeRelation,
TypeVarInstance,
};
use crate::{Db, FxOrderSet};
use crate::{Db, FxOrderSet, ModuleName, resolve_module};
/// A collection of useful spans for annotating functions.
///
@ -867,6 +867,12 @@ pub enum KnownFunction {
Len,
/// `builtins.repr`
Repr,
/// `builtins.__import__`, which returns the top-level module.
#[strum(serialize = "__import__")]
DunderImport,
/// `importlib.import_module`, which returns the submodule.
ImportModule,
/// `typing(_extensions).final`
Final,
@ -951,9 +957,12 @@ impl KnownFunction {
/// Return `true` if `self` is defined in `module` at runtime.
const fn check_module(self, module: KnownModule) -> bool {
match self {
Self::IsInstance | Self::IsSubclass | Self::HasAttr | Self::Len | Self::Repr => {
module.is_builtins()
}
Self::IsInstance
| Self::IsSubclass
| Self::HasAttr
| Self::Len
| Self::Repr
| Self::DunderImport => module.is_builtins(),
Self::AssertType
| Self::AssertNever
| Self::Cast
@ -987,48 +996,45 @@ impl KnownFunction {
| Self::DunderAllNames
| Self::StaticAssert
| Self::AllMembers => module.is_ty_extensions(),
Self::ImportModule => module.is_importlib(),
}
}
/// Evaluate a call to this known function, and emit any diagnostics that are necessary
/// as a result of the call.
pub(super) fn check_call(
pub(super) fn check_call<'db>(
self,
context: &InferContext,
parameter_types: &[Option<Type<'_>>],
context: &InferContext<'db, '_>,
parameter_types: &[Option<Type<'db>>],
call_expression: &ast::ExprCall,
) {
file: File,
) -> Option<Type<'db>> {
let db = context.db();
match self {
KnownFunction::RevealType => {
let [Some(revealed_type)] = parameter_types else {
return;
};
let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
else {
return;
return None;
};
let builder =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)?;
let mut diag = builder.into_diagnostic("Revealed type");
let span = context.span(&call_expression.arguments.args[0]);
diag.annotate(
Annotation::primary(span)
.message(format_args!("`{}`", revealed_type.display(db))),
);
None
}
KnownFunction::AssertType => {
let [Some(actual_ty), Some(asserted_ty)] = parameter_types else {
return;
return None;
};
if actual_ty.is_equivalent_to(db, *asserted_ty) {
return;
return None;
}
let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)
else {
return;
};
let builder = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)?;
let mut diagnostic = builder.into_diagnostic(format_args!(
"Argument does not have asserted type `{}`",
@ -1048,18 +1054,17 @@ impl KnownFunction {
asserted_type = asserted_ty.display(db),
inferred_type = actual_ty.display(db),
));
None
}
KnownFunction::AssertNever => {
let [Some(actual_ty)] = parameter_types else {
return;
return None;
};
if actual_ty.is_equivalent_to(db, Type::Never) {
return;
return None;
}
let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)
else {
return;
};
let builder = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)?;
let mut diagnostic =
builder.into_diagnostic("Argument does not have asserted type `Never`");
@ -1074,10 +1079,12 @@ impl KnownFunction {
"`Never` and `{inferred_type}` are not equivalent types",
inferred_type = actual_ty.display(db),
));
None
}
KnownFunction::StaticAssert => {
let [Some(parameter_ty), message] = parameter_types else {
return;
return None;
};
let truthiness = match parameter_ty.try_bool(db) {
Ok(truthiness) => truthiness,
@ -1097,16 +1104,13 @@ impl KnownFunction {
err.report_diagnostic(context, condition);
return;
return None;
}
};
let Some(builder) = context.report_lint(&STATIC_ASSERT_ERROR, call_expression)
else {
return;
};
let builder = context.report_lint(&STATIC_ASSERT_ERROR, call_expression)?;
if truthiness.is_always_true() {
return;
return None;
}
if let Some(message) = message
.and_then(Type::into_string_literal)
@ -1129,10 +1133,12 @@ impl KnownFunction {
parameter_ty = parameter_ty.display(db)
));
}
None
}
KnownFunction::Cast => {
let [Some(casted_type), Some(source_type)] = parameter_types else {
return;
return None;
};
let contains_unknown_or_todo =
|ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
@ -1140,34 +1146,31 @@ impl KnownFunction {
&& !casted_type.any_over_type(db, &|ty| contains_unknown_or_todo(ty))
&& !source_type.any_over_type(db, &|ty| contains_unknown_or_todo(ty))
{
let Some(builder) = context.report_lint(&REDUNDANT_CAST, call_expression)
else {
return;
};
let builder = context.report_lint(&REDUNDANT_CAST, call_expression)?;
builder.into_diagnostic(format_args!(
"Value is already of type `{}`",
casted_type.display(db),
));
}
None
}
KnownFunction::GetProtocolMembers => {
let [Some(Type::ClassLiteral(class))] = parameter_types else {
return;
return None;
};
if class.is_protocol(db) {
return;
return None;
}
report_bad_argument_to_get_protocol_members(context, call_expression, *class);
None
}
KnownFunction::IsInstance | KnownFunction::IsSubclass => {
let [_, Some(Type::ClassLiteral(class))] = parameter_types else {
return;
};
let Some(protocol_class) = class.into_protocol_class(db) else {
return;
return None;
};
let protocol_class = class.into_protocol_class(db)?;
if protocol_class.is_runtime_checkable(db) {
return;
return None;
}
report_runtime_check_against_non_runtime_checkable_protocol(
context,
@ -1175,8 +1178,35 @@ impl KnownFunction {
protocol_class,
self,
);
None
}
_ => {}
known @ (KnownFunction::DunderImport | KnownFunction::ImportModule) => {
let [Some(Type::StringLiteral(full_module_name)), rest @ ..] = parameter_types
else {
return None;
};
if rest.iter().any(Option::is_some) {
return None;
}
let module_name = full_module_name.value(db);
if known == KnownFunction::DunderImport && module_name.contains('.') {
// `__import__("collections.abc")` returns the `collections` module.
// `importlib.import_module("collections.abc")` returns the `collections.abc` module.
// ty doesn't have a way to represent the return type of the former yet.
// https://github.com/astral-sh/ruff/pull/19008#discussion_r2173481311
return None;
}
let module_name = ModuleName::new(module_name)?;
let module = resolve_module(db, &module_name)?;
Some(Type::module_literal(db, file, &module))
}
_ => None,
}
}
}
@ -1201,7 +1231,8 @@ pub(crate) mod tests {
| KnownFunction::Repr
| KnownFunction::IsInstance
| KnownFunction::HasAttr
| KnownFunction::IsSubclass => KnownModule::Builtins,
| KnownFunction::IsSubclass
| KnownFunction::DunderImport => KnownModule::Builtins,
KnownFunction::AbstractMethod => KnownModule::Abc,
@ -1234,6 +1265,8 @@ pub(crate) mod tests {
| KnownFunction::TopMaterialization
| KnownFunction::BottomMaterialization
| KnownFunction::AllMembers => KnownModule::TyExtensions,
KnownFunction::ImportModule => KnownModule::ImportLib,
};
let function_definition = known_module_symbol(&db, module, function_name)

View file

@ -5388,11 +5388,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
match binding_type {
Type::FunctionLiteral(function_literal) => {
if let Some(known_function) = function_literal.known(self.db()) {
known_function.check_call(
if let Some(return_type) = known_function.check_call(
&self.context,
overload.parameter_types(),
call_expression,
);
self.file(),
) {
overload.set_return_type(return_type);
}
}
}