mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-20 02:20:25 +00:00
[ty] Reduce the overwhelming complexity of TypeInferenceBuilder::infer_call_expression
(#18943)
Some checks are pending
CI / cargo clippy (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
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 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
Some checks are pending
CI / cargo clippy (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
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 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
## Summary
This function is huge, and hugely indented. This PR breaks most of it
out into two helper functions: `KnownFunction::check_call()` and
`KnownClass::check_call`.
My immediate motivation is that we need to add yet more special cases to
this function in order to properly handle `tuple` instantiations and
instantiations of tuple subclasses. But I really don't relish the
thought of doing that with the function's current structure 😆
## Test Plan
Existing tests all pass. No new ones are added; this is a pure refactor
that should have no functional change.
This commit is contained in:
parent
c77e72ea1a
commit
4a5715b97a
4 changed files with 592 additions and 646 deletions
|
@ -9,14 +9,20 @@ use super::{
|
|||
function::{FunctionDecorators, FunctionType},
|
||||
infer_expression_type, infer_unpack_types,
|
||||
};
|
||||
use crate::semantic_index::DeclarationWithConstraint;
|
||||
use crate::semantic_index::definition::{Definition, DefinitionState};
|
||||
use crate::semantic_index::place::NodeWithScopeKind;
|
||||
use crate::semantic_index::{DeclarationWithConstraint, SemanticIndex};
|
||||
use crate::types::context::InferContext;
|
||||
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
|
||||
use crate::types::function::{DataclassTransformerParams, KnownFunction};
|
||||
use crate::types::generics::{GenericContext, Specialization};
|
||||
use crate::types::infer::nearest_enclosing_class;
|
||||
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
|
||||
use crate::types::tuple::TupleType;
|
||||
use crate::types::{
|
||||
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeRelation, TypeVarInstance,
|
||||
BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams,
|
||||
KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, TypeVarBoundOrConstraints,
|
||||
TypeVarInstance, TypeVarKind, infer_definition_types,
|
||||
};
|
||||
use crate::{
|
||||
Db, FxOrderSet, KnownModule, Program,
|
||||
|
@ -2330,7 +2336,7 @@ pub enum KnownClass {
|
|||
NamedTupleFallback,
|
||||
}
|
||||
|
||||
impl<'db> KnownClass {
|
||||
impl KnownClass {
|
||||
pub(crate) const fn is_bool(self) -> bool {
|
||||
matches!(self, Self::Bool)
|
||||
}
|
||||
|
@ -2571,7 +2577,7 @@ impl<'db> KnownClass {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn name(self, db: &'db dyn Db) -> &'static str {
|
||||
pub(crate) fn name(self, db: &dyn Db) -> &'static str {
|
||||
match self {
|
||||
Self::Any => "Any",
|
||||
Self::Bool => "bool",
|
||||
|
@ -2649,7 +2655,7 @@ impl<'db> KnownClass {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
|
||||
pub(super) fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ {
|
||||
struct KnownClassDisplay<'db> {
|
||||
db: &'db dyn Db,
|
||||
class: KnownClass,
|
||||
|
@ -2677,7 +2683,7 @@ impl<'db> KnownClass {
|
|||
/// representing all possible instances of the class.
|
||||
///
|
||||
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
|
||||
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> {
|
||||
pub(crate) fn to_instance(self, db: &dyn Db) -> Type {
|
||||
self.to_class_literal(db)
|
||||
.to_class_type(db)
|
||||
.map(|class| Type::instance(db, class))
|
||||
|
@ -2689,7 +2695,7 @@ impl<'db> KnownClass {
|
|||
///
|
||||
/// If the class cannot be found in typeshed, or if you provide a specialization with the wrong
|
||||
/// number of types, a debug-level log message will be emitted stating this.
|
||||
pub(crate) fn to_specialized_class_type(
|
||||
pub(crate) fn to_specialized_class_type<'db>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: impl IntoIterator<Item = Type<'db>>,
|
||||
|
@ -2722,7 +2728,7 @@ impl<'db> KnownClass {
|
|||
///
|
||||
/// If the class cannot be found in typeshed, or if you provide a specialization with the wrong
|
||||
/// number of types, a debug-level log message will be emitted stating this.
|
||||
pub(crate) fn to_specialized_instance(
|
||||
pub(crate) fn to_specialized_instance<'db>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: impl IntoIterator<Item = Type<'db>>,
|
||||
|
@ -2738,8 +2744,8 @@ impl<'db> KnownClass {
|
|||
/// or if the symbol is not a class definition, or if the symbol is possibly unbound.
|
||||
fn try_to_class_literal_without_logging(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> Result<ClassLiteral<'db>, KnownClassLookupError<'db>> {
|
||||
db: &dyn Db,
|
||||
) -> Result<ClassLiteral, KnownClassLookupError> {
|
||||
let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place;
|
||||
match symbol {
|
||||
Place::Type(Type::ClassLiteral(class_literal), Boundness::Bound) => Ok(class_literal),
|
||||
|
@ -2756,7 +2762,7 @@ impl<'db> KnownClass {
|
|||
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
|
||||
///
|
||||
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
|
||||
pub(crate) fn try_to_class_literal(self, db: &'db dyn Db) -> Option<ClassLiteral<'db>> {
|
||||
pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option<ClassLiteral> {
|
||||
// a cache of the `KnownClass`es that we have already failed to lookup in typeshed
|
||||
// (and therefore that we've already logged a warning for)
|
||||
static MESSAGES: LazyLock<Mutex<FxHashSet<KnownClass>>> = LazyLock::new(Mutex::default);
|
||||
|
@ -2791,7 +2797,7 @@ impl<'db> KnownClass {
|
|||
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
|
||||
///
|
||||
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
|
||||
pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
|
||||
pub(crate) fn to_class_literal(self, db: &dyn Db) -> Type {
|
||||
self.try_to_class_literal(db)
|
||||
.map(Type::ClassLiteral)
|
||||
.unwrap_or_else(Type::unknown)
|
||||
|
@ -2801,7 +2807,7 @@ impl<'db> KnownClass {
|
|||
/// representing that class and all possible subclasses of the class.
|
||||
///
|
||||
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
|
||||
pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> {
|
||||
pub(crate) fn to_subclass_of(self, db: &dyn Db) -> Type {
|
||||
self.to_class_literal(db)
|
||||
.to_class_type(db)
|
||||
.map(|class| SubclassOfType::from(db, class))
|
||||
|
@ -2810,13 +2816,13 @@ impl<'db> KnownClass {
|
|||
|
||||
/// Return `true` if this symbol can be resolved to a class definition `class` in typeshed,
|
||||
/// *and* `class` is a subclass of `other`.
|
||||
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
pub(super) fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
self.try_to_class_literal_without_logging(db)
|
||||
.is_ok_and(|class| class.is_subclass_of(db, None, other))
|
||||
}
|
||||
|
||||
/// Return the module in which we should look up the definition for this class
|
||||
fn canonical_module(self, db: &'db dyn Db) -> KnownModule {
|
||||
fn canonical_module(self, db: &dyn Db) -> KnownModule {
|
||||
match self {
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
|
@ -3114,7 +3120,7 @@ impl<'db> KnownClass {
|
|||
}
|
||||
|
||||
/// Return `true` if the module of `self` matches `module`
|
||||
fn check_module(self, db: &'db dyn Db, module: KnownModule) -> bool {
|
||||
fn check_module(self, db: &dyn Db, module: KnownModule) -> bool {
|
||||
match self {
|
||||
Self::Any
|
||||
| Self::Bool
|
||||
|
@ -3177,6 +3183,278 @@ impl<'db> KnownClass {
|
|||
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate a call to this known class, and emit any diagnostics that are necessary
|
||||
/// as a result of the call.
|
||||
pub(super) fn check_call<'db>(
|
||||
self,
|
||||
context: &InferContext<'db, '_>,
|
||||
index: &SemanticIndex<'db>,
|
||||
overload_binding: &mut Binding<'db>,
|
||||
call_argument_types: &CallArgumentTypes<'_, 'db>,
|
||||
call_expression: &ast::ExprCall,
|
||||
) {
|
||||
let db = context.db();
|
||||
let scope = context.scope();
|
||||
let module = context.module();
|
||||
|
||||
match self {
|
||||
KnownClass::Super => {
|
||||
// Handle the case where `super()` is called with no arguments.
|
||||
// In this case, we need to infer the two arguments:
|
||||
// 1. The nearest enclosing class
|
||||
// 2. The first parameter of the current function (typically `self` or `cls`)
|
||||
match overload_binding.parameter_types() {
|
||||
[] => {
|
||||
let Some(enclosing_class) =
|
||||
nearest_enclosing_class(db, index, scope, module)
|
||||
else {
|
||||
overload_binding.set_return_type(Type::unknown());
|
||||
BoundSuperError::UnavailableImplicitArguments
|
||||
.report_diagnostic(context, call_expression.into());
|
||||
return;
|
||||
};
|
||||
|
||||
// The type of the first parameter if the given scope is function-like (i.e. function or lambda).
|
||||
// `None` if the scope is not function-like, or has no parameters.
|
||||
let first_param = match scope.node(db) {
|
||||
NodeWithScopeKind::Function(f) => {
|
||||
f.node(module).parameters.iter().next()
|
||||
}
|
||||
NodeWithScopeKind::Lambda(l) => l
|
||||
.node(module)
|
||||
.parameters
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.next(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let Some(first_param) = first_param else {
|
||||
overload_binding.set_return_type(Type::unknown());
|
||||
BoundSuperError::UnavailableImplicitArguments
|
||||
.report_diagnostic(context, call_expression.into());
|
||||
return;
|
||||
};
|
||||
|
||||
let definition = index.expect_single_definition(first_param);
|
||||
let first_param =
|
||||
infer_definition_types(db, definition).binding_type(definition);
|
||||
|
||||
let bound_super = BoundSuperType::build(
|
||||
db,
|
||||
Type::ClassLiteral(enclosing_class),
|
||||
first_param,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
err.report_diagnostic(context, call_expression.into());
|
||||
Type::unknown()
|
||||
});
|
||||
|
||||
overload_binding.set_return_type(bound_super);
|
||||
}
|
||||
[Some(pivot_class_type), Some(owner_type)] => {
|
||||
let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type)
|
||||
.unwrap_or_else(|err| {
|
||||
err.report_diagnostic(context, call_expression.into());
|
||||
Type::unknown()
|
||||
});
|
||||
|
||||
overload_binding.set_return_type(bound_super);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
KnownClass::TypeVar => {
|
||||
let assigned_to = index
|
||||
.try_expression(ast::ExprRef::from(call_expression))
|
||||
.and_then(|expr| expr.assigned_to(db));
|
||||
|
||||
let Some(target) = assigned_to.as_ref().and_then(|assigned_to| {
|
||||
match assigned_to.node(module).targets.as_slice() {
|
||||
[ast::Expr::Name(target)] => Some(target),
|
||||
_ => None,
|
||||
}
|
||||
}) else {
|
||||
if let Some(builder) =
|
||||
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
|
||||
{
|
||||
builder.into_diagnostic(
|
||||
"A legacy `typing.TypeVar` must be immediately assigned to a variable",
|
||||
);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
let [
|
||||
Some(name_param),
|
||||
constraints,
|
||||
bound,
|
||||
default,
|
||||
contravariant,
|
||||
covariant,
|
||||
_infer_variance,
|
||||
] = overload_binding.parameter_types()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let covariant = covariant
|
||||
.map(|ty| ty.bool(db))
|
||||
.unwrap_or(Truthiness::AlwaysFalse);
|
||||
|
||||
let contravariant = contravariant
|
||||
.map(|ty| ty.bool(db))
|
||||
.unwrap_or(Truthiness::AlwaysFalse);
|
||||
|
||||
let variance = match (contravariant, covariant) {
|
||||
(Truthiness::Ambiguous, _) => {
|
||||
let Some(builder) =
|
||||
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
builder.into_diagnostic(
|
||||
"The `contravariant` parameter of a legacy `typing.TypeVar` \
|
||||
cannot have an ambiguous value",
|
||||
);
|
||||
return;
|
||||
}
|
||||
(_, Truthiness::Ambiguous) => {
|
||||
let Some(builder) =
|
||||
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
builder.into_diagnostic(
|
||||
"The `covariant` parameter of a legacy `typing.TypeVar` \
|
||||
cannot have an ambiguous value",
|
||||
);
|
||||
return;
|
||||
}
|
||||
(Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => {
|
||||
let Some(builder) =
|
||||
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
builder.into_diagnostic(
|
||||
"A legacy `typing.TypeVar` cannot be both covariant and contravariant",
|
||||
);
|
||||
return;
|
||||
}
|
||||
(Truthiness::AlwaysTrue, Truthiness::AlwaysFalse) => {
|
||||
TypeVarVariance::Contravariant
|
||||
}
|
||||
(Truthiness::AlwaysFalse, Truthiness::AlwaysTrue) => TypeVarVariance::Covariant,
|
||||
(Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => {
|
||||
TypeVarVariance::Invariant
|
||||
}
|
||||
};
|
||||
|
||||
let name_param = name_param.into_string_literal().map(|name| name.value(db));
|
||||
|
||||
if name_param.is_none_or(|name_param| name_param != target.id) {
|
||||
let Some(builder) =
|
||||
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
builder.into_diagnostic(format_args!(
|
||||
"The name of a legacy `typing.TypeVar`{} must match \
|
||||
the name of the variable it is assigned to (`{}`)",
|
||||
if let Some(name_param) = name_param {
|
||||
format!(" (`{name_param}`)")
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
target.id,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
let bound_or_constraint = match (bound, constraints) {
|
||||
(Some(bound), None) => Some(TypeVarBoundOrConstraints::UpperBound(*bound)),
|
||||
|
||||
(None, Some(_constraints)) => {
|
||||
// We don't use UnionType::from_elements or UnionBuilder here,
|
||||
// because we don't want to simplify the list of constraints like
|
||||
// we do with the elements of an actual union type.
|
||||
// TODO: Consider using a new `OneOfType` connective here instead,
|
||||
// since that more accurately represents the actual semantics of
|
||||
// typevar constraints.
|
||||
let elements = UnionType::new(
|
||||
db,
|
||||
overload_binding
|
||||
.arguments_for_parameter(call_argument_types, 1)
|
||||
.map(|(_, ty)| ty)
|
||||
.collect::<Box<_>>(),
|
||||
);
|
||||
Some(TypeVarBoundOrConstraints::Constraints(elements))
|
||||
}
|
||||
|
||||
// TODO: Emit a diagnostic that TypeVar cannot be both bounded and
|
||||
// constrained
|
||||
(Some(_), Some(_)) => return,
|
||||
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
let containing_assignment = index.expect_single_definition(target);
|
||||
overload_binding.set_return_type(Type::KnownInstance(KnownInstanceType::TypeVar(
|
||||
TypeVarInstance::new(
|
||||
db,
|
||||
target.id.clone(),
|
||||
Some(containing_assignment),
|
||||
bound_or_constraint,
|
||||
variance,
|
||||
*default,
|
||||
TypeVarKind::Legacy,
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
KnownClass::TypeAliasType => {
|
||||
let assigned_to = index
|
||||
.try_expression(ast::ExprRef::from(call_expression))
|
||||
.and_then(|expr| expr.assigned_to(db));
|
||||
|
||||
let containing_assignment = assigned_to.as_ref().and_then(|assigned_to| {
|
||||
match assigned_to.node(module).targets.as_slice() {
|
||||
[ast::Expr::Name(target)] => Some(index.expect_single_definition(target)),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
let [Some(name), Some(value), ..] = overload_binding.parameter_types() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(name) = name.into_string_literal() {
|
||||
overload_binding.set_return_type(Type::KnownInstance(
|
||||
KnownInstanceType::TypeAliasType(TypeAliasType::Bare(
|
||||
BareTypeAliasType::new(
|
||||
db,
|
||||
ast::name::Name::new(name.value(db)),
|
||||
containing_assignment,
|
||||
value,
|
||||
),
|
||||
)),
|
||||
));
|
||||
} else if let Some(builder) =
|
||||
context.report_lint(&INVALID_TYPE_ALIAS_TYPE, call_expression)
|
||||
{
|
||||
builder.into_diagnostic(
|
||||
"The name of a `typing.TypeAlias` must be a string literal",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration of ways in which looking up a [`KnownClass`] in typeshed could fail.
|
||||
|
|
|
@ -68,6 +68,10 @@ impl<'db, 'ast> InferContext<'db, 'ast> {
|
|||
self.module
|
||||
}
|
||||
|
||||
pub(crate) fn scope(&self) -> ScopeId<'db> {
|
||||
self.scope
|
||||
}
|
||||
|
||||
/// Create a span with the range of the given expression
|
||||
/// in the file being currently type checked.
|
||||
///
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use ruff_db::diagnostic::Span;
|
||||
use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
use ruff_python_ast as ast;
|
||||
|
@ -64,11 +64,18 @@ use crate::semantic_index::ast_ids::HasScopedUseId;
|
|||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::place::ScopeId;
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::types::context::InferContext;
|
||||
use crate::types::diagnostic::{
|
||||
REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE,
|
||||
report_bad_argument_to_get_protocol_members,
|
||||
report_runtime_check_against_non_runtime_checkable_protocol,
|
||||
};
|
||||
use crate::types::generics::GenericContext;
|
||||
use crate::types::narrow::ClassInfoConstraintFunction;
|
||||
use crate::types::signatures::{CallableSignature, Signature};
|
||||
use crate::types::{
|
||||
BoundMethodType, CallableType, Type, TypeMapping, TypeRelation, TypeVarInstance,
|
||||
Binding, BoundMethodType, CallableType, DynamicType, Type, TypeMapping, TypeRelation,
|
||||
TypeVarInstance,
|
||||
};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
|
@ -942,6 +949,199 @@ impl KnownFunction {
|
|||
| Self::AllMembers => module.is_ty_extensions(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
self,
|
||||
context: &InferContext,
|
||||
overload_binding: &mut Binding,
|
||||
call_expression: &ast::ExprCall,
|
||||
) {
|
||||
let db = context.db();
|
||||
|
||||
match self {
|
||||
KnownFunction::RevealType => {
|
||||
let [Some(revealed_type)] = overload_binding.parameter_types() else {
|
||||
return;
|
||||
};
|
||||
let Some(builder) =
|
||||
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
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))),
|
||||
);
|
||||
}
|
||||
KnownFunction::AssertType => {
|
||||
let [Some(actual_ty), Some(asserted_ty)] = overload_binding.parameter_types()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if actual_ty.is_equivalent_to(db, *asserted_ty) {
|
||||
return;
|
||||
}
|
||||
let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"Argument does not have asserted type `{}`",
|
||||
asserted_ty.display(db),
|
||||
));
|
||||
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(context.span(&call_expression.arguments.args[0]))
|
||||
.message(format_args!(
|
||||
"Inferred type of argument is `{}`",
|
||||
actual_ty.display(db),
|
||||
)),
|
||||
);
|
||||
|
||||
diagnostic.info(format_args!(
|
||||
"`{asserted_type}` and `{inferred_type}` are not equivalent types",
|
||||
asserted_type = asserted_ty.display(db),
|
||||
inferred_type = actual_ty.display(db),
|
||||
));
|
||||
}
|
||||
KnownFunction::AssertNever => {
|
||||
let [Some(actual_ty)] = overload_binding.parameter_types() else {
|
||||
return;
|
||||
};
|
||||
if actual_ty.is_equivalent_to(db, Type::Never) {
|
||||
return;
|
||||
}
|
||||
let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic =
|
||||
builder.into_diagnostic("Argument does not have asserted type `Never`");
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(context.span(&call_expression.arguments.args[0]))
|
||||
.message(format_args!(
|
||||
"Inferred type of argument is `{}`",
|
||||
actual_ty.display(db)
|
||||
)),
|
||||
);
|
||||
diagnostic.info(format_args!(
|
||||
"`Never` and `{inferred_type}` are not equivalent types",
|
||||
inferred_type = actual_ty.display(db),
|
||||
));
|
||||
}
|
||||
KnownFunction::StaticAssert => {
|
||||
let [Some(parameter_ty), message] = overload_binding.parameter_types() else {
|
||||
return;
|
||||
};
|
||||
let truthiness = match parameter_ty.try_bool(db) {
|
||||
Ok(truthiness) => truthiness,
|
||||
Err(err) => {
|
||||
let condition = call_expression
|
||||
.arguments
|
||||
.find_argument("condition", 0)
|
||||
.map(|argument| match argument {
|
||||
ruff_python_ast::ArgOrKeyword::Arg(expr) => {
|
||||
ast::AnyNodeRef::from(expr)
|
||||
}
|
||||
ruff_python_ast::ArgOrKeyword::Keyword(keyword) => {
|
||||
ast::AnyNodeRef::from(keyword)
|
||||
}
|
||||
})
|
||||
.unwrap_or(ast::AnyNodeRef::from(call_expression));
|
||||
|
||||
err.report_diagnostic(context, condition);
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(builder) = context.report_lint(&STATIC_ASSERT_ERROR, call_expression)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if truthiness.is_always_true() {
|
||||
return;
|
||||
}
|
||||
if let Some(message) = message
|
||||
.and_then(Type::into_string_literal)
|
||||
.map(|s| s.value(db))
|
||||
{
|
||||
builder.into_diagnostic(format_args!("Static assertion error: {message}"));
|
||||
} else if *parameter_ty == Type::BooleanLiteral(false) {
|
||||
builder
|
||||
.into_diagnostic("Static assertion error: argument evaluates to `False`");
|
||||
} else if truthiness.is_always_false() {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Static assertion error: argument of type `{parameter_ty}` \
|
||||
is statically known to be falsy",
|
||||
parameter_ty = parameter_ty.display(db)
|
||||
));
|
||||
} else {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Static assertion error: argument of type `{parameter_ty}` \
|
||||
has an ambiguous static truthiness",
|
||||
parameter_ty = parameter_ty.display(db)
|
||||
));
|
||||
}
|
||||
}
|
||||
KnownFunction::Cast => {
|
||||
let [Some(casted_type), Some(source_type)] = overload_binding.parameter_types()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let contains_unknown_or_todo =
|
||||
|ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
|
||||
if source_type.is_equivalent_to(db, *casted_type)
|
||||
&& !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;
|
||||
};
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Value is already of type `{}`",
|
||||
casted_type.display(db),
|
||||
));
|
||||
}
|
||||
}
|
||||
KnownFunction::GetProtocolMembers => {
|
||||
let [Some(Type::ClassLiteral(class))] = overload_binding.parameter_types() else {
|
||||
return;
|
||||
};
|
||||
if class.is_protocol(db) {
|
||||
return;
|
||||
}
|
||||
report_bad_argument_to_get_protocol_members(context, call_expression, *class);
|
||||
}
|
||||
KnownFunction::IsInstance | KnownFunction::IsSubclass => {
|
||||
let [_, Some(Type::ClassLiteral(class))] = overload_binding.parameter_types()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(protocol_class) = class.into_protocol_class(db) else {
|
||||
return;
|
||||
};
|
||||
if protocol_class.is_runtime_checkable(db) {
|
||||
return;
|
||||
}
|
||||
report_runtime_check_against_non_runtime_checkable_protocol(
|
||||
context,
|
||||
call_expression,
|
||||
protocol_class,
|
||||
self,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
//! be considered a bug.)
|
||||
|
||||
use itertools::{Either, Itertools};
|
||||
use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
use ruff_python_ast::visitor::{Visitor, walk_expr};
|
||||
|
@ -79,16 +78,15 @@ use crate::types::diagnostic::{
|
|||
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
|
||||
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
|
||||
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
|
||||
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE,
|
||||
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_ALIAS_TYPE, 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,
|
||||
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,
|
||||
};
|
||||
use crate::types::function::{
|
||||
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
|
||||
|
@ -99,11 +97,11 @@ use crate::types::signatures::{CallableSignature, Signature};
|
|||
use crate::types::tuple::{TupleSpec, TupleType};
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
BareTypeAliasType, CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams,
|
||||
DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType,
|
||||
LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter,
|
||||
ParameterForm, Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Truthiness,
|
||||
Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeIsType, TypeQualifiers,
|
||||
CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType,
|
||||
IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard,
|
||||
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm,
|
||||
Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Truthiness, Type,
|
||||
TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeIsType, TypeQualifiers,
|
||||
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder,
|
||||
UnionType, binding_type, todo_type,
|
||||
};
|
||||
|
@ -113,25 +111,19 @@ use crate::{Db, FxOrderSet, Program};
|
|||
|
||||
use super::context::{InNoTypeCheck, InferContext};
|
||||
use super::diagnostic::{
|
||||
INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, REDUNDANT_CAST, STATIC_ASSERT_ERROR,
|
||||
SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
|
||||
INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, SUBCLASS_OF_FINAL_CLASS,
|
||||
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
|
||||
report_bad_argument_to_get_protocol_members, report_duplicate_bases,
|
||||
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
|
||||
report_invalid_exception_raised, report_invalid_or_unsupported_base,
|
||||
report_invalid_type_checking_constant, report_non_subscriptable,
|
||||
report_possibly_unresolved_reference,
|
||||
report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero,
|
||||
report_duplicate_bases, report_index_out_of_bounds, report_invalid_exception_caught,
|
||||
report_invalid_exception_cause, report_invalid_exception_raised,
|
||||
report_invalid_or_unsupported_base, report_invalid_type_checking_constant,
|
||||
report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero,
|
||||
};
|
||||
use super::generics::LegacyGenericBase;
|
||||
use super::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
|
||||
};
|
||||
use super::subclass_of::SubclassOfInner;
|
||||
use super::{
|
||||
BoundSuperError, BoundSuperType, ClassBase, NominalInstanceType,
|
||||
add_inferred_python_version_hint_to_diagnostic,
|
||||
};
|
||||
use super::{ClassBase, NominalInstanceType, add_inferred_python_version_hint_to_diagnostic};
|
||||
|
||||
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
|
||||
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
|
||||
|
@ -4682,9 +4674,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
ast::Expr::Named(named) => self.infer_named_expression(named),
|
||||
ast::Expr::If(if_expression) => self.infer_if_expression(if_expression),
|
||||
ast::Expr::Lambda(lambda_expression) => self.infer_lambda_expression(lambda_expression),
|
||||
ast::Expr::Call(call_expression) => {
|
||||
self.infer_call_expression(expression, call_expression)
|
||||
}
|
||||
ast::Expr::Call(call_expression) => self.infer_call_expression(call_expression),
|
||||
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
|
||||
ast::Expr::Yield(yield_expression) => self.infer_yield_expression(yield_expression),
|
||||
ast::Expr::YieldFrom(yield_from) => self.infer_yield_from_expression(yield_from),
|
||||
|
@ -5292,27 +5282,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
CallableType::function_like(self.db(), Signature::new(parameters, Some(Type::unknown())))
|
||||
}
|
||||
|
||||
/// Returns the type of the first parameter if the given scope is function-like (i.e. function or lambda).
|
||||
/// Returns `None` if the scope is not function-like, or has no parameters.
|
||||
fn first_param_type_in_scope(&self, scope: ScopeId) -> Option<Type<'db>> {
|
||||
let first_param = match scope.node(self.db()) {
|
||||
NodeWithScopeKind::Function(f) => f.node(self.module()).parameters.iter().next(),
|
||||
NodeWithScopeKind::Lambda(l) => {
|
||||
l.node(self.module()).parameters.as_ref()?.iter().next()
|
||||
}
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let definition = self.index.expect_single_definition(first_param);
|
||||
|
||||
Some(infer_definition_types(self.db(), definition).binding_type(definition))
|
||||
}
|
||||
|
||||
fn infer_call_expression(
|
||||
&mut self,
|
||||
call_expression_node: &ast::Expr,
|
||||
call_expression: &ast::ExprCall,
|
||||
) -> Type<'db> {
|
||||
fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> {
|
||||
let ast::ExprCall {
|
||||
range: _,
|
||||
node_index: _,
|
||||
|
@ -5412,584 +5382,78 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
let call_argument_types =
|
||||
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);
|
||||
|
||||
match bindings.check_types(self.db(), &call_argument_types) {
|
||||
Ok(mut bindings) => {
|
||||
for binding in &mut bindings {
|
||||
let binding_type = binding.callable_type;
|
||||
for (_, overload) in binding.matching_overloads_mut() {
|
||||
match binding_type {
|
||||
Type::FunctionLiteral(function_literal) => {
|
||||
let Some(known_function) = function_literal.known(self.db()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match known_function {
|
||||
KnownFunction::RevealType => {
|
||||
if let [Some(revealed_type)] = overload.parameter_types() {
|
||||
if let Some(builder) = self.context.report_diagnostic(
|
||||
DiagnosticId::RevealedType,
|
||||
Severity::Info,
|
||||
) {
|
||||
let mut diag =
|
||||
builder.into_diagnostic("Revealed type");
|
||||
let span = self
|
||||
.context
|
||||
.span(&call_expression.arguments.args[0]);
|
||||
diag.annotate(Annotation::primary(span).message(
|
||||
format_args!(
|
||||
"`{}`",
|
||||
revealed_type.display(self.db())
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
KnownFunction::AssertType => {
|
||||
if let [Some(actual_ty), Some(asserted_ty)] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
if !actual_ty.is_equivalent_to(self.db(), *asserted_ty)
|
||||
{
|
||||
if let Some(builder) = self.context.report_lint(
|
||||
&TYPE_ASSERTION_FAILURE,
|
||||
call_expression,
|
||||
) {
|
||||
let mut diagnostic =
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Argument does not have asserted type `{}`",
|
||||
asserted_ty.display(self.db()),
|
||||
));
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(self.context.span(
|
||||
&call_expression.arguments.args[0],
|
||||
))
|
||||
.message(format_args!(
|
||||
"Inferred type of argument is `{}`",
|
||||
actual_ty.display(self.db()),
|
||||
)),
|
||||
);
|
||||
diagnostic.info(
|
||||
format_args!(
|
||||
"`{asserted_type}` and `{inferred_type}` are not equivalent types",
|
||||
asserted_type = asserted_ty.display(self.db()),
|
||||
inferred_type = actual_ty.display(self.db()),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KnownFunction::AssertNever => {
|
||||
if let [Some(actual_ty)] = overload.parameter_types() {
|
||||
if !actual_ty.is_equivalent_to(self.db(), Type::Never) {
|
||||
if let Some(builder) = self.context.report_lint(
|
||||
&TYPE_ASSERTION_FAILURE,
|
||||
call_expression,
|
||||
) {
|
||||
let mut diagnostic = builder.into_diagnostic(
|
||||
"Argument does not have asserted type `Never`",
|
||||
);
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(self.context.span(
|
||||
&call_expression.arguments.args[0],
|
||||
))
|
||||
.message(format_args!(
|
||||
"Inferred type of argument is `{}`",
|
||||
actual_ty.display(self.db())
|
||||
)),
|
||||
);
|
||||
diagnostic.info(
|
||||
format_args!(
|
||||
"`Never` and `{inferred_type}` are not equivalent types",
|
||||
inferred_type = actual_ty.display(self.db()),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KnownFunction::StaticAssert => {
|
||||
if let [Some(parameter_ty), message] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
let truthiness = match parameter_ty.try_bool(self.db())
|
||||
{
|
||||
Ok(truthiness) => truthiness,
|
||||
Err(err) => {
|
||||
let condition = arguments
|
||||
.find_argument("condition", 0)
|
||||
.map(|argument| {
|
||||
match argument {
|
||||
ruff_python_ast::ArgOrKeyword::Arg(
|
||||
expr,
|
||||
) => ast::AnyNodeRef::from(expr),
|
||||
ruff_python_ast::ArgOrKeyword::Keyword(
|
||||
keyword,
|
||||
) => ast::AnyNodeRef::from(keyword),
|
||||
}
|
||||
})
|
||||
.unwrap_or(ast::AnyNodeRef::from(
|
||||
call_expression,
|
||||
));
|
||||
|
||||
err.report_diagnostic(&self.context, condition);
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&STATIC_ASSERT_ERROR, call_expression)
|
||||
{
|
||||
if !truthiness.is_always_true() {
|
||||
if let Some(message) = message
|
||||
.and_then(Type::into_string_literal)
|
||||
.map(|s| s.value(self.db()))
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Static assertion error: {message}"
|
||||
));
|
||||
} else if *parameter_ty
|
||||
== Type::BooleanLiteral(false)
|
||||
{
|
||||
builder.into_diagnostic(
|
||||
"Static assertion error: \
|
||||
argument evaluates to `False`",
|
||||
);
|
||||
} else if truthiness.is_always_false() {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Static assertion error: \
|
||||
argument of type `{parameter_ty}` \
|
||||
is statically known to be falsy",
|
||||
parameter_ty =
|
||||
parameter_ty.display(self.db())
|
||||
));
|
||||
} else {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Static assertion error: \
|
||||
argument of type `{parameter_ty}` \
|
||||
has an ambiguous static truthiness",
|
||||
parameter_ty =
|
||||
parameter_ty.display(self.db())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KnownFunction::Cast => {
|
||||
if let [Some(casted_type), Some(source_type)] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
let db = self.db();
|
||||
let contains_unknown_or_todo = |ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
|
||||
if source_type.is_equivalent_to(db, *casted_type)
|
||||
&& !casted_type.any_over_type(db, &|ty| {
|
||||
contains_unknown_or_todo(ty)
|
||||
})
|
||||
&& !source_type.any_over_type(db, &|ty| {
|
||||
contains_unknown_or_todo(ty)
|
||||
})
|
||||
{
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&REDUNDANT_CAST, call_expression)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Value is already of type `{}`",
|
||||
casted_type.display(db),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KnownFunction::GetProtocolMembers => {
|
||||
if let [Some(Type::ClassLiteral(class))] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
if !class.is_protocol(self.db()) {
|
||||
report_bad_argument_to_get_protocol_members(
|
||||
&self.context,
|
||||
call_expression,
|
||||
*class,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
KnownFunction::IsInstance | KnownFunction::IsSubclass => {
|
||||
if let [_, Some(Type::ClassLiteral(class))] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
if let Some(protocol_class) =
|
||||
class.into_protocol_class(self.db())
|
||||
{
|
||||
if !protocol_class.is_runtime_checkable(self.db()) {
|
||||
report_runtime_check_against_non_runtime_checkable_protocol(
|
||||
&self.context,
|
||||
call_expression,
|
||||
protocol_class,
|
||||
known_function
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Type::ClassLiteral(class) => {
|
||||
let Some(known_class) = class.known(self.db()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match known_class {
|
||||
KnownClass::Super => {
|
||||
// Handle the case where `super()` is called with no arguments.
|
||||
// In this case, we need to infer the two arguments:
|
||||
// 1. The nearest enclosing class
|
||||
// 2. The first parameter of the current function (typically `self` or `cls`)
|
||||
match overload.parameter_types() {
|
||||
[] => {
|
||||
let scope = self.scope();
|
||||
|
||||
let Some(enclosing_class) = nearest_enclosing_class(
|
||||
self.db(),
|
||||
self.index,
|
||||
scope,
|
||||
self.module(),
|
||||
) else {
|
||||
overload.set_return_type(Type::unknown());
|
||||
BoundSuperError::UnavailableImplicitArguments
|
||||
.report_diagnostic(
|
||||
&self.context,
|
||||
call_expression.into(),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(first_param) =
|
||||
self.first_param_type_in_scope(scope)
|
||||
else {
|
||||
overload.set_return_type(Type::unknown());
|
||||
BoundSuperError::UnavailableImplicitArguments
|
||||
.report_diagnostic(
|
||||
&self.context,
|
||||
call_expression.into(),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let bound_super = BoundSuperType::build(
|
||||
self.db(),
|
||||
Type::ClassLiteral(enclosing_class),
|
||||
first_param,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
err.report_diagnostic(
|
||||
&self.context,
|
||||
call_expression.into(),
|
||||
);
|
||||
Type::unknown()
|
||||
});
|
||||
|
||||
overload.set_return_type(bound_super);
|
||||
}
|
||||
[Some(pivot_class_type), Some(owner_type)] => {
|
||||
let bound_super = BoundSuperType::build(
|
||||
self.db(),
|
||||
*pivot_class_type,
|
||||
*owner_type,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
err.report_diagnostic(
|
||||
&self.context,
|
||||
call_expression.into(),
|
||||
);
|
||||
Type::unknown()
|
||||
});
|
||||
|
||||
overload.set_return_type(bound_super);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
KnownClass::TypeVar => {
|
||||
let assigned_to = (self.index)
|
||||
.try_expression(call_expression_node)
|
||||
.and_then(|expr| expr.assigned_to(self.db()));
|
||||
|
||||
let Some(target) =
|
||||
assigned_to.as_ref().and_then(|assigned_to| {
|
||||
match assigned_to
|
||||
.node(self.module())
|
||||
.targets
|
||||
.as_slice()
|
||||
{
|
||||
[ast::Expr::Name(target)] => Some(target),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
else {
|
||||
if let Some(builder) = self.context.report_lint(
|
||||
&INVALID_LEGACY_TYPE_VARIABLE,
|
||||
call_expression,
|
||||
) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"A legacy `typing.TypeVar` must be immediately assigned to a variable",
|
||||
));
|
||||
}
|
||||
continue;
|
||||
};
|
||||
|
||||
let [
|
||||
Some(name_param),
|
||||
constraints,
|
||||
bound,
|
||||
default,
|
||||
contravariant,
|
||||
covariant,
|
||||
_infer_variance,
|
||||
] = overload.parameter_types()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let covariant = match covariant {
|
||||
Some(ty) => ty.bool(self.db()),
|
||||
None => Truthiness::AlwaysFalse,
|
||||
};
|
||||
|
||||
let contravariant = match contravariant {
|
||||
Some(ty) => ty.bool(self.db()),
|
||||
None => Truthiness::AlwaysFalse,
|
||||
};
|
||||
|
||||
let variance = match (contravariant, covariant) {
|
||||
(Truthiness::Ambiguous, _) => {
|
||||
if let Some(builder) = self.context.report_lint(
|
||||
&INVALID_LEGACY_TYPE_VARIABLE,
|
||||
call_expression,
|
||||
) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"The `contravariant` parameter of \
|
||||
a legacy `typing.TypeVar` cannot have \
|
||||
an ambiguous value",
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
(_, Truthiness::Ambiguous) => {
|
||||
if let Some(builder) = self.context.report_lint(
|
||||
&INVALID_LEGACY_TYPE_VARIABLE,
|
||||
call_expression,
|
||||
) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"The `covariant` parameter of \
|
||||
a legacy `typing.TypeVar` cannot have \
|
||||
an ambiguous value",
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
(Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => {
|
||||
if let Some(builder) = self.context.report_lint(
|
||||
&INVALID_LEGACY_TYPE_VARIABLE,
|
||||
call_expression,
|
||||
) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"A legacy `typing.TypeVar` cannot be \
|
||||
both covariant and contravariant",
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
(Truthiness::AlwaysTrue, Truthiness::AlwaysFalse) => {
|
||||
TypeVarVariance::Contravariant
|
||||
}
|
||||
(Truthiness::AlwaysFalse, Truthiness::AlwaysTrue) => {
|
||||
TypeVarVariance::Covariant
|
||||
}
|
||||
(Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => {
|
||||
TypeVarVariance::Invariant
|
||||
}
|
||||
};
|
||||
|
||||
let name_param = name_param
|
||||
.into_string_literal()
|
||||
.map(|name| name.value(self.db()));
|
||||
if name_param
|
||||
.is_none_or(|name_param| name_param != target.id)
|
||||
{
|
||||
if let Some(builder) = self.context.report_lint(
|
||||
&INVALID_LEGACY_TYPE_VARIABLE,
|
||||
call_expression,
|
||||
) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"The name of a legacy `typing.TypeVar`{} must match \
|
||||
the name of the variable it is assigned to (`{}`)",
|
||||
if let Some(name_param) = name_param {
|
||||
format!(" (`{name_param}`)")
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
target.id,
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let bound_or_constraint = match (bound, constraints) {
|
||||
(Some(bound), None) => {
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(*bound))
|
||||
}
|
||||
|
||||
(None, Some(_constraints)) => {
|
||||
// We don't use UnionType::from_elements or UnionBuilder here,
|
||||
// because we don't want to simplify the list of constraints like
|
||||
// we do with the elements of an actual union type.
|
||||
// TODO: Consider using a new `OneOfType` connective here instead,
|
||||
// since that more accurately represents the actual semantics of
|
||||
// typevar constraints.
|
||||
let elements = UnionType::new(
|
||||
self.db(),
|
||||
overload
|
||||
.arguments_for_parameter(
|
||||
&call_argument_types,
|
||||
1,
|
||||
)
|
||||
.map(|(_, ty)| ty)
|
||||
.collect::<Box<_>>(),
|
||||
);
|
||||
Some(TypeVarBoundOrConstraints::Constraints(
|
||||
elements,
|
||||
))
|
||||
}
|
||||
|
||||
// TODO: Emit a diagnostic that TypeVar cannot be both bounded and
|
||||
// constrained
|
||||
(Some(_), Some(_)) => continue,
|
||||
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
let containing_assignment =
|
||||
self.index.expect_single_definition(target);
|
||||
overload.set_return_type(Type::KnownInstance(
|
||||
KnownInstanceType::TypeVar(TypeVarInstance::new(
|
||||
self.db(),
|
||||
target.id.clone(),
|
||||
Some(containing_assignment),
|
||||
bound_or_constraint,
|
||||
variance,
|
||||
*default,
|
||||
TypeVarKind::Legacy,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
KnownClass::TypeAliasType => {
|
||||
let assigned_to = (self.index)
|
||||
.try_expression(call_expression_node)
|
||||
.and_then(|expr| expr.assigned_to(self.db()));
|
||||
|
||||
let containing_assignment =
|
||||
assigned_to.as_ref().and_then(|assigned_to| {
|
||||
match assigned_to
|
||||
.node(self.module())
|
||||
.targets
|
||||
.as_slice()
|
||||
{
|
||||
[ast::Expr::Name(target)] => Some(
|
||||
self.index.expect_single_definition(target),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
let [Some(name), Some(value), ..] =
|
||||
overload.parameter_types()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(name) = name.into_string_literal() {
|
||||
overload.set_return_type(Type::KnownInstance(
|
||||
KnownInstanceType::TypeAliasType(
|
||||
TypeAliasType::Bare(BareTypeAliasType::new(
|
||||
self.db(),
|
||||
ast::name::Name::new(name.value(self.db())),
|
||||
containing_assignment,
|
||||
value,
|
||||
)),
|
||||
),
|
||||
));
|
||||
} else {
|
||||
if let Some(builder) = self.context.report_lint(
|
||||
&INVALID_TYPE_ALIAS_TYPE,
|
||||
call_expression,
|
||||
) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"The name of a `typing.TypeAlias` must be a string literal",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let db = self.db();
|
||||
let scope = self.scope();
|
||||
let return_ty = bindings.return_type(db);
|
||||
|
||||
let find_narrowed_place = || match arguments.args.first() {
|
||||
None => {
|
||||
// This branch looks extraneous, especially in the face of `missing-arguments`.
|
||||
// However, that lint won't be able to catch this:
|
||||
//
|
||||
// ```python
|
||||
// def f(v: object = object()) -> TypeIs[int]: ...
|
||||
//
|
||||
// if f(): ...
|
||||
// ```
|
||||
//
|
||||
// TODO: Will this report things that is actually fine?
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_TYPE_GUARD_CALL, arguments)
|
||||
{
|
||||
builder.into_diagnostic("Type guard call does not have a target");
|
||||
}
|
||||
None
|
||||
}
|
||||
Some(expr) => match PlaceExpr::try_from(expr) {
|
||||
Ok(place_expr) => place_table(db, scope).place_id_by_expr(&place_expr),
|
||||
Err(()) => None,
|
||||
},
|
||||
};
|
||||
|
||||
match return_ty {
|
||||
// TODO: TypeGuard
|
||||
Type::TypeIs(type_is) => match find_narrowed_place() {
|
||||
Some(place) => type_is.bind(db, scope, place),
|
||||
None => return_ty,
|
||||
},
|
||||
_ => return_ty,
|
||||
}
|
||||
}
|
||||
|
||||
let mut bindings = match bindings.check_types(self.db(), &call_argument_types) {
|
||||
Ok(bindings) => bindings,
|
||||
Err(CallError(_, bindings)) => {
|
||||
bindings.report_diagnostics(&self.context, call_expression.into());
|
||||
bindings.return_type(self.db())
|
||||
return bindings.return_type(self.db());
|
||||
}
|
||||
};
|
||||
|
||||
for binding in &mut bindings {
|
||||
let binding_type = binding.callable_type;
|
||||
for (_, overload) in binding.matching_overloads_mut() {
|
||||
match binding_type {
|
||||
Type::FunctionLiteral(function_literal) => {
|
||||
if let Some(known_function) = function_literal.known(self.db()) {
|
||||
known_function.check_call(&self.context, overload, call_expression);
|
||||
}
|
||||
}
|
||||
|
||||
Type::ClassLiteral(class) => {
|
||||
let Some(known_class) = class.known(self.db()) else {
|
||||
continue;
|
||||
};
|
||||
known_class.check_call(
|
||||
&self.context,
|
||||
self.index,
|
||||
overload,
|
||||
&call_argument_types,
|
||||
call_expression,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let db = self.db();
|
||||
let scope = self.scope();
|
||||
let return_ty = bindings.return_type(db);
|
||||
|
||||
let find_narrowed_place = || match arguments.args.first() {
|
||||
None => {
|
||||
// This branch looks extraneous, especially in the face of `missing-arguments`.
|
||||
// However, that lint won't be able to catch this:
|
||||
//
|
||||
// ```python
|
||||
// def f(v: object = object()) -> TypeIs[int]: ...
|
||||
//
|
||||
// if f(): ...
|
||||
// ```
|
||||
//
|
||||
// TODO: Will this report things that is actually fine?
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_TYPE_GUARD_CALL, arguments)
|
||||
{
|
||||
builder.into_diagnostic("Type guard call does not have a target");
|
||||
}
|
||||
None
|
||||
}
|
||||
Some(expr) => match PlaceExpr::try_from(expr) {
|
||||
Ok(place_expr) => place_table(db, scope).place_id_by_expr(&place_expr),
|
||||
Err(()) => None,
|
||||
},
|
||||
};
|
||||
|
||||
match return_ty {
|
||||
// TODO: TypeGuard
|
||||
Type::TypeIs(type_is) => match find_narrowed_place() {
|
||||
Some(place) => type_is.bind(db, scope, place),
|
||||
None => return_ty,
|
||||
},
|
||||
_ => return_ty,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9177,7 +8641,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
|||
}
|
||||
|
||||
ast::Expr::Call(call_expr) => {
|
||||
self.infer_call_expression(expression, call_expr);
|
||||
self.infer_call_expression(call_expr);
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Function calls are not allowed in type expressions"),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue