mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-30 23:27:27 +00:00
[ty] Create separate FunctionLiteral
and FunctionType
types (#18360)
This updates our representation of functions to more closely match our representation of classes. The new `OverloadLiteral` and `FunctionLiteral` classes represent a function definition in the AST. If a function is generic, this is unspecialized. `FunctionType` has been updated to represent a function type, which is specialized if the function is generic. (These names are chosen to match `ClassLiteral` and `ClassType` on the class side.) This PR does not add a separate `Type` variant for `FunctionLiteral`. Maybe we should? Possibly as a follow-on PR? Part of https://github.com/astral-sh/ty/issues/462 --------- Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
8d98c601d8
commit
2c3b3d3230
12 changed files with 1280 additions and 1021 deletions
|
@ -2,7 +2,6 @@ use infer::nearest_enclosing_class;
|
|||
use itertools::Either;
|
||||
|
||||
use std::slice::Iter;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use call::{CallDunderError, CallError, CallErrorKind};
|
||||
|
@ -14,7 +13,7 @@ use diagnostic::{
|
|||
use ruff_db::diagnostic::{
|
||||
Annotation, Severity, Span, SubDiagnostic, create_semantic_syntax_diagnostic,
|
||||
};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
@ -28,23 +27,23 @@ pub(crate) use self::infer::{
|
|||
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types,
|
||||
infer_scope_types,
|
||||
};
|
||||
pub(crate) use self::narrow::ClassInfoConstraintFunction;
|
||||
pub(crate) use self::signatures::{CallableSignature, Signature};
|
||||
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{KnownModule, file_to_module, resolve_module};
|
||||
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId};
|
||||
use crate::module_resolver::{KnownModule, resolve_module};
|
||||
use crate::semantic_index::ast_ids::HasScopedExpressionId;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::semantic_index::{imported_modules, semantic_index};
|
||||
use crate::suppression::check_suppressions;
|
||||
use crate::symbol::{
|
||||
Boundness, Symbol, SymbolAndQualifiers, imported_symbol, symbol_from_bindings,
|
||||
};
|
||||
use crate::symbol::{Boundness, Symbol, SymbolAndQualifiers, imported_symbol};
|
||||
use crate::types::call::{Binding, Bindings, CallArgumentTypes, CallableBinding};
|
||||
pub(crate) use crate::types::class_base::ClassBase;
|
||||
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
|
||||
use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
|
||||
use crate::types::function::{
|
||||
DataclassTransformerParams, FunctionSpans, FunctionType, KnownFunction,
|
||||
};
|
||||
use crate::types::generics::{GenericContext, PartialSpecialization, Specialization};
|
||||
use crate::types::infer::infer_unpack_types;
|
||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||
|
@ -64,6 +63,7 @@ mod class_base;
|
|||
mod context;
|
||||
mod diagnostic;
|
||||
mod display;
|
||||
mod function;
|
||||
mod generics;
|
||||
mod ide_support;
|
||||
mod infer;
|
||||
|
@ -432,26 +432,6 @@ impl From<DataclassTransformerParams> for DataclassParams {
|
|||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Used for the return type of `dataclass_transform(…)` calls. Keeps track of the
|
||||
/// arguments that were passed in. For the precise meaning of the fields, see [1].
|
||||
///
|
||||
/// [1]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub struct DataclassTransformerParams: u8 {
|
||||
const EQ_DEFAULT = 0b0000_0001;
|
||||
const ORDER_DEFAULT = 0b0000_0010;
|
||||
const KW_ONLY_DEFAULT = 0b0000_0100;
|
||||
const FROZEN_DEFAULT = 0b0000_1000;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DataclassTransformerParams {
|
||||
fn default() -> Self {
|
||||
Self::EQ_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a type: a set of possible values at runtime.
|
||||
///
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
|
@ -7040,710 +7020,6 @@ impl From<bool> for Truthiness {
|
|||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Hash)]
|
||||
pub struct FunctionDecorators: u8 {
|
||||
/// `@classmethod`
|
||||
const CLASSMETHOD = 1 << 0;
|
||||
/// `@typing.no_type_check`
|
||||
const NO_TYPE_CHECK = 1 << 1;
|
||||
/// `@typing.overload`
|
||||
const OVERLOAD = 1 << 2;
|
||||
/// `@abc.abstractmethod`
|
||||
const ABSTRACT_METHOD = 1 << 3;
|
||||
/// `@typing.final`
|
||||
const FINAL = 1 << 4;
|
||||
/// `@typing.override`
|
||||
const OVERRIDE = 1 << 6;
|
||||
}
|
||||
}
|
||||
|
||||
/// A function signature, which optionally includes an implementation signature if the function is
|
||||
/// overloaded.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub(crate) struct FunctionSignature<'db> {
|
||||
pub(crate) overloads: CallableSignature<'db>,
|
||||
pub(crate) implementation: Option<Signature<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> FunctionSignature<'db> {
|
||||
/// Returns the "bottom" signature (subtype of all fully-static signatures.)
|
||||
pub(crate) fn bottom(db: &'db dyn Db) -> Self {
|
||||
FunctionSignature {
|
||||
overloads: CallableSignature::single(Signature::bottom(db)),
|
||||
implementation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An overloaded function.
|
||||
///
|
||||
/// This is created by the [`to_overloaded`] method on [`FunctionType`].
|
||||
///
|
||||
/// [`to_overloaded`]: FunctionType::to_overloaded
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update)]
|
||||
struct OverloadedFunction<'db> {
|
||||
/// The overloads of this function.
|
||||
overloads: Vec<FunctionType<'db>>,
|
||||
/// The implementation of this overloaded function, if any.
|
||||
implementation: Option<FunctionType<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> OverloadedFunction<'db> {
|
||||
/// Returns an iterator over all overloads and the implementation, in that order.
|
||||
fn all(&self) -> impl Iterator<Item = FunctionType<'db>> + '_ {
|
||||
self.overloads.iter().copied().chain(self.implementation)
|
||||
}
|
||||
}
|
||||
|
||||
/// # Ordering
|
||||
/// Ordering is based on the function type's salsa-assigned id and not on its values.
|
||||
/// The id may change between runs, or when the function type was garbage collected and recreated.
|
||||
#[salsa::interned(debug)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct FunctionType<'db> {
|
||||
/// Name of the function at definition.
|
||||
#[returns(ref)]
|
||||
pub name: ast::name::Name,
|
||||
|
||||
/// Is this a function that we special-case somehow? If so, which one?
|
||||
known: Option<KnownFunction>,
|
||||
|
||||
/// The scope that's created by the function, in which the function body is evaluated.
|
||||
body_scope: ScopeId<'db>,
|
||||
|
||||
/// A set of special decorators that were applied to this function
|
||||
decorators: FunctionDecorators,
|
||||
|
||||
/// The arguments to `dataclass_transformer`, if this function was annotated
|
||||
/// with `@dataclass_transformer(...)`.
|
||||
dataclass_transformer_params: Option<DataclassTransformerParams>,
|
||||
|
||||
/// The inherited generic context, if this function is a class method being used to infer the
|
||||
/// specialization of its generic class. If the method is itself generic, this is in addition
|
||||
/// to its own generic context.
|
||||
inherited_generic_context: Option<GenericContext<'db>>,
|
||||
|
||||
/// Type mappings that should be applied to the function's parameter and return types.
|
||||
type_mappings: Box<[TypeMapping<'db, 'db>]>,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> FunctionType<'db> {
|
||||
/// Returns the [`File`] in which this function is defined.
|
||||
pub(crate) fn file(self, db: &'db dyn Db) -> File {
|
||||
// NOTE: Do not use `self.definition(db).file(db)` here, as that could create a
|
||||
// cross-module dependency on the full AST.
|
||||
self.body_scope(db).file(db)
|
||||
}
|
||||
|
||||
pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool {
|
||||
self.decorators(db).contains(decorator)
|
||||
}
|
||||
|
||||
/// Convert the `FunctionType` into a [`Type::Callable`].
|
||||
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
Type::Callable(CallableType::new(
|
||||
db,
|
||||
self.signature(db).overloads.clone(),
|
||||
false,
|
||||
))
|
||||
}
|
||||
|
||||
/// Convert the `FunctionType` into a [`Type::BoundMethod`].
|
||||
pub(crate) fn into_bound_method_type(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
self_instance: Type<'db>,
|
||||
) -> Type<'db> {
|
||||
Type::BoundMethod(BoundMethodType::new(db, self, self_instance))
|
||||
}
|
||||
|
||||
/// Returns the AST node for this function.
|
||||
pub(crate) fn node(self, db: &'db dyn Db, file: File) -> &'db ast::StmtFunctionDef {
|
||||
debug_assert_eq!(
|
||||
file,
|
||||
self.file(db),
|
||||
"FunctionType::node() must be called with the same file as the one where \
|
||||
the function is defined."
|
||||
);
|
||||
|
||||
self.body_scope(db).node(db).expect_function()
|
||||
}
|
||||
|
||||
/// Returns the [`FileRange`] of the function's name.
|
||||
pub fn focus_range(self, db: &dyn Db) -> FileRange {
|
||||
FileRange::new(
|
||||
self.file(db),
|
||||
self.body_scope(db).node(db).expect_function().name.range,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn full_range(self, db: &dyn Db) -> FileRange {
|
||||
FileRange::new(
|
||||
self.file(db),
|
||||
self.body_scope(db).node(db).expect_function().range,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the [`Definition`] of this function.
|
||||
///
|
||||
/// ## Warning
|
||||
///
|
||||
/// This uses the semantic index to find the definition of the function. This means that if the
|
||||
/// calling query is not in the same file as this function is defined in, then this will create
|
||||
/// a cross-module dependency directly on the full AST which will lead to cache
|
||||
/// over-invalidation.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
let body_scope = self.body_scope(db);
|
||||
let index = semantic_index(db, body_scope.file(db));
|
||||
index.expect_single_definition(body_scope.node(db).expect_function())
|
||||
}
|
||||
|
||||
/// Typed externally-visible signature for this function.
|
||||
///
|
||||
/// This is the signature as seen by external callers, possibly modified by decorators and/or
|
||||
/// overloaded.
|
||||
///
|
||||
/// ## Why is this a salsa query?
|
||||
///
|
||||
/// This is a salsa query to short-circuit the invalidation
|
||||
/// when the function's AST node changes.
|
||||
///
|
||||
/// Were this not a salsa query, then the calling query
|
||||
/// would depend on the function's AST and rerun for every change in that file.
|
||||
#[salsa::tracked(returns(ref), cycle_fn=signature_cycle_recover, cycle_initial=signature_cycle_initial)]
|
||||
pub(crate) fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> {
|
||||
let inherited_generic_context = self.inherited_generic_context(db);
|
||||
let type_mappings = self.type_mappings(db);
|
||||
if let Some(overloaded) = self.to_overloaded(db) {
|
||||
FunctionSignature {
|
||||
overloads: CallableSignature::from_overloads(
|
||||
overloaded.overloads.iter().copied().map(|overload| {
|
||||
type_mappings.iter().fold(
|
||||
overload.internal_signature(db, inherited_generic_context),
|
||||
|ty, mapping| ty.apply_type_mapping(db, mapping),
|
||||
)
|
||||
}),
|
||||
),
|
||||
implementation: overloaded.implementation.map(|implementation| {
|
||||
type_mappings.iter().fold(
|
||||
implementation.internal_signature(db, inherited_generic_context),
|
||||
|ty, mapping| ty.apply_type_mapping(db, mapping),
|
||||
)
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
FunctionSignature {
|
||||
overloads: CallableSignature::single(type_mappings.iter().fold(
|
||||
self.internal_signature(db, inherited_generic_context),
|
||||
|ty, mapping| ty.apply_type_mapping(db, mapping),
|
||||
)),
|
||||
implementation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed internally-visible signature for this function.
|
||||
///
|
||||
/// This represents the annotations on the function itself, unmodified by decorators and
|
||||
/// overloads.
|
||||
///
|
||||
/// These are the parameter and return types that should be used for type checking the body of
|
||||
/// the function.
|
||||
///
|
||||
/// Don't call this when checking any other file; only when type-checking the function body
|
||||
/// scope.
|
||||
fn internal_signature(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
inherited_generic_context: Option<GenericContext<'db>>,
|
||||
) -> Signature<'db> {
|
||||
let scope = self.body_scope(db);
|
||||
let function_stmt_node = scope.node(db).expect_function();
|
||||
let definition = self.definition(db);
|
||||
let generic_context = function_stmt_node.type_params.as_ref().map(|type_params| {
|
||||
let index = semantic_index(db, scope.file(db));
|
||||
GenericContext::from_type_params(db, index, type_params)
|
||||
});
|
||||
Signature::from_function(
|
||||
db,
|
||||
generic_context,
|
||||
inherited_generic_context,
|
||||
definition,
|
||||
function_stmt_node,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool {
|
||||
self.known(db) == Some(known_function)
|
||||
}
|
||||
|
||||
fn with_dataclass_transformer_params(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
params: DataclassTransformerParams,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
db,
|
||||
self.name(db).clone(),
|
||||
self.known(db),
|
||||
self.body_scope(db),
|
||||
self.decorators(db),
|
||||
Some(params),
|
||||
self.inherited_generic_context(db),
|
||||
self.type_mappings(db),
|
||||
)
|
||||
}
|
||||
|
||||
fn with_inherited_generic_context(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
inherited_generic_context: GenericContext<'db>,
|
||||
) -> Self {
|
||||
// A function cannot inherit more than one generic context from its containing class.
|
||||
debug_assert!(self.inherited_generic_context(db).is_none());
|
||||
Self::new(
|
||||
db,
|
||||
self.name(db).clone(),
|
||||
self.known(db),
|
||||
self.body_scope(db),
|
||||
self.decorators(db),
|
||||
self.dataclass_transformer_params(db),
|
||||
Some(inherited_generic_context),
|
||||
self.type_mappings(db),
|
||||
)
|
||||
}
|
||||
|
||||
fn with_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
|
||||
let type_mappings: Box<[_]> = self
|
||||
.type_mappings(db)
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(type_mapping.to_owned()))
|
||||
.collect();
|
||||
Self::new(
|
||||
db,
|
||||
self.name(db).clone(),
|
||||
self.known(db),
|
||||
self.body_scope(db),
|
||||
self.decorators(db),
|
||||
self.dataclass_transformer_params(db),
|
||||
self.inherited_generic_context(db),
|
||||
type_mappings,
|
||||
)
|
||||
}
|
||||
|
||||
fn find_legacy_typevars(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
|
||||
) {
|
||||
let signatures = self.signature(db);
|
||||
for signature in &signatures.overloads {
|
||||
signature.find_legacy_typevars(db, typevars);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `self` as [`OverloadedFunction`] if it is overloaded, [`None`] otherwise.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// The way this method works only allows us to "see" the overloads that are defined before
|
||||
/// this function definition. This is because the semantic model records a use for each
|
||||
/// function on the name node which is used to get the previous function definition with the
|
||||
/// same name. This means that [`OverloadedFunction`] would only include the functions that
|
||||
/// comes before this function definition. Consider the following example:
|
||||
///
|
||||
/// ```py
|
||||
/// from typing import overload
|
||||
///
|
||||
/// @overload
|
||||
/// def foo() -> None: ...
|
||||
/// @overload
|
||||
/// def foo(x: int) -> int: ...
|
||||
/// def foo(x: int | None) -> int | None:
|
||||
/// return x
|
||||
/// ```
|
||||
///
|
||||
/// Here, when the `to_overloaded` method is invoked on the
|
||||
/// 1. first `foo` definition, it would only contain a single overload which is itself and no
|
||||
/// implementation
|
||||
/// 2. second `foo` definition, it would contain both overloads and still no implementation
|
||||
/// 3. third `foo` definition, it would contain both overloads and the implementation which is
|
||||
/// itself
|
||||
#[salsa::tracked(returns(as_ref))]
|
||||
fn to_overloaded(self, db: &'db dyn Db) -> Option<OverloadedFunction<'db>> {
|
||||
let mut current = self;
|
||||
let mut overloads = vec![];
|
||||
|
||||
loop {
|
||||
// The semantic model records a use for each function on the name node. This is used
|
||||
// here to get the previous function definition with the same name.
|
||||
let scope = current.definition(db).scope(db);
|
||||
let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db));
|
||||
let use_id = current
|
||||
.body_scope(db)
|
||||
.node(db)
|
||||
.expect_function()
|
||||
.name
|
||||
.scoped_use_id(db, scope);
|
||||
|
||||
let Symbol::Type(Type::FunctionLiteral(previous), Boundness::Bound) =
|
||||
symbol_from_bindings(db, use_def.bindings_at_use(use_id))
|
||||
else {
|
||||
break;
|
||||
};
|
||||
|
||||
if previous.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
|
||||
overloads.push(previous);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
current = previous;
|
||||
}
|
||||
|
||||
// Overloads are inserted in reverse order, from bottom to top.
|
||||
overloads.reverse();
|
||||
|
||||
let implementation = if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
|
||||
overloads.push(self);
|
||||
None
|
||||
} else {
|
||||
Some(self)
|
||||
};
|
||||
|
||||
if overloads.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(OverloadedFunction {
|
||||
overloads,
|
||||
implementation,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
// A function literal is the subtype of itself, and not of any other function literal.
|
||||
// However, our representation of a function literal includes any specialization that
|
||||
// should be applied to the signature. Different specializations of the same function
|
||||
// literal are only subtypes of each other if they result in subtype signatures.
|
||||
self.normalized(db) == other.normalized(db)
|
||||
|| (self.body_scope(db) == other.body_scope(db)
|
||||
&& self
|
||||
.into_callable_type(db)
|
||||
.is_subtype_of(db, other.into_callable_type(db)))
|
||||
}
|
||||
|
||||
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
// A function literal is assignable to itself, and not to any other function literal.
|
||||
// However, our representation of a function literal includes any specialization that
|
||||
// should be applied to the signature. Different specializations of the same function
|
||||
// literal are only assignable to each other if they result in assignable signatures.
|
||||
self.body_scope(db) == other.body_scope(db)
|
||||
&& self
|
||||
.into_callable_type(db)
|
||||
.is_assignable_to(db, other.into_callable_type(db))
|
||||
}
|
||||
|
||||
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.normalized(db) == other.normalized(db)
|
||||
|| (self.body_scope(db) == other.body_scope(db)
|
||||
&& self
|
||||
.into_callable_type(db)
|
||||
.is_equivalent_to(db, other.into_callable_type(db)))
|
||||
}
|
||||
|
||||
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.body_scope(db) == other.body_scope(db)
|
||||
&& self
|
||||
.into_callable_type(db)
|
||||
.is_gradual_equivalent_to(db, other.into_callable_type(db))
|
||||
}
|
||||
|
||||
fn normalized(self, db: &'db dyn Db) -> Self {
|
||||
let context = self
|
||||
.inherited_generic_context(db)
|
||||
.map(|ctx| ctx.normalized(db));
|
||||
|
||||
let mappings: Box<_> = self
|
||||
.type_mappings(db)
|
||||
.iter()
|
||||
.map(|mapping| mapping.normalized(db))
|
||||
.collect();
|
||||
|
||||
Self::new(
|
||||
db,
|
||||
self.name(db),
|
||||
self.known(db),
|
||||
self.body_scope(db),
|
||||
self.decorators(db),
|
||||
self.dataclass_transformer_params(db),
|
||||
context,
|
||||
mappings,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a tuple of two spans. The first is
|
||||
/// the span for the identifier of the function
|
||||
/// definition for `self`. The second is
|
||||
/// the span for the parameter in the function
|
||||
/// definition for `self`.
|
||||
///
|
||||
/// If there are no meaningful spans, then this
|
||||
/// returns `None`. For example, when this type
|
||||
/// isn't callable.
|
||||
///
|
||||
/// When `parameter_index` is `None`, then the
|
||||
/// second span returned covers the entire parameter
|
||||
/// list.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// Note that this may introduce cross-module
|
||||
/// dependencies. This can have an impact on
|
||||
/// the effectiveness of incremental caching
|
||||
/// and should therefore be used judiciously.
|
||||
///
|
||||
/// An example of a good use case is to improve
|
||||
/// a diagnostic.
|
||||
fn parameter_span(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
parameter_index: Option<usize>,
|
||||
) -> Option<(Span, Span)> {
|
||||
let function_scope = self.body_scope(db);
|
||||
let span = Span::from(function_scope.file(db));
|
||||
let node = function_scope.node(db);
|
||||
let func_def = node.as_function()?;
|
||||
let range = parameter_index
|
||||
.and_then(|parameter_index| {
|
||||
func_def
|
||||
.parameters
|
||||
.iter()
|
||||
.nth(parameter_index)
|
||||
.map(|param| param.range())
|
||||
})
|
||||
.unwrap_or(func_def.parameters.range);
|
||||
let name_span = span.clone().with_range(func_def.name.range);
|
||||
let parameter_span = span.with_range(range);
|
||||
Some((name_span, parameter_span))
|
||||
}
|
||||
|
||||
/// Returns a collection of useful spans for a
|
||||
/// function signature. These are useful for
|
||||
/// creating annotations on diagnostics.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// Note that this may introduce cross-module
|
||||
/// dependencies. This can have an impact on
|
||||
/// the effectiveness of incremental caching
|
||||
/// and should therefore be used judiciously.
|
||||
///
|
||||
/// An example of a good use case is to improve
|
||||
/// a diagnostic.
|
||||
fn spans(self, db: &'db dyn Db) -> Option<FunctionSpans> {
|
||||
let function_scope = self.body_scope(db);
|
||||
let span = Span::from(function_scope.file(db));
|
||||
let node = function_scope.node(db);
|
||||
let func_def = node.as_function()?;
|
||||
let return_type_range = func_def.returns.as_ref().map(|returns| returns.range());
|
||||
let mut signature = func_def.name.range.cover(func_def.parameters.range);
|
||||
if let Some(return_type_range) = return_type_range {
|
||||
signature = signature.cover(return_type_range);
|
||||
}
|
||||
Some(FunctionSpans {
|
||||
signature: span.clone().with_range(signature),
|
||||
name: span.clone().with_range(func_def.name.range),
|
||||
parameters: span.clone().with_range(func_def.parameters.range),
|
||||
return_type: return_type_range.map(|range| span.clone().with_range(range)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of useful spans for annotating functions.
|
||||
///
|
||||
/// This can be retrieved via `FunctionType::spans` or
|
||||
/// `Type::function_spans`.
|
||||
struct FunctionSpans {
|
||||
/// The span of the entire function "signature." This includes
|
||||
/// the name, parameter list and return type (if present).
|
||||
signature: Span,
|
||||
/// The span of the function name. i.e., `foo` in `def foo(): ...`.
|
||||
name: Span,
|
||||
/// The span of the parameter list, including the opening and
|
||||
/// closing parentheses.
|
||||
#[expect(dead_code)]
|
||||
parameters: Span,
|
||||
/// The span of the annotated return type, if present.
|
||||
return_type: Option<Span>,
|
||||
}
|
||||
|
||||
fn signature_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &FunctionSignature<'db>,
|
||||
_count: u32,
|
||||
_function: FunctionType<'db>,
|
||||
) -> salsa::CycleRecoveryAction<FunctionSignature<'db>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn signature_cycle_initial<'db>(
|
||||
db: &'db dyn Db,
|
||||
_function: FunctionType<'db>,
|
||||
) -> FunctionSignature<'db> {
|
||||
FunctionSignature::bottom(db)
|
||||
}
|
||||
|
||||
/// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might
|
||||
/// have special behavior.
|
||||
#[derive(
|
||||
Debug, Copy, Clone, PartialEq, Eq, Hash, strum_macros::EnumString, strum_macros::IntoStaticStr,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[cfg_attr(test, derive(strum_macros::EnumIter))]
|
||||
pub enum KnownFunction {
|
||||
/// `builtins.isinstance`
|
||||
#[strum(serialize = "isinstance")]
|
||||
IsInstance,
|
||||
/// `builtins.issubclass`
|
||||
#[strum(serialize = "issubclass")]
|
||||
IsSubclass,
|
||||
/// `builtins.hasattr`
|
||||
#[strum(serialize = "hasattr")]
|
||||
HasAttr,
|
||||
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
|
||||
RevealType,
|
||||
/// `builtins.len`
|
||||
Len,
|
||||
/// `builtins.repr`
|
||||
Repr,
|
||||
/// `typing(_extensions).final`
|
||||
Final,
|
||||
|
||||
/// [`typing(_extensions).no_type_check`](https://typing.python.org/en/latest/spec/directives.html#no-type-check)
|
||||
NoTypeCheck,
|
||||
|
||||
/// `typing(_extensions).assert_type`
|
||||
AssertType,
|
||||
/// `typing(_extensions).assert_never`
|
||||
AssertNever,
|
||||
/// `typing(_extensions).cast`
|
||||
Cast,
|
||||
/// `typing(_extensions).overload`
|
||||
Overload,
|
||||
/// `typing(_extensions).override`
|
||||
Override,
|
||||
/// `typing(_extensions).is_protocol`
|
||||
IsProtocol,
|
||||
/// `typing(_extensions).get_protocol_members`
|
||||
GetProtocolMembers,
|
||||
/// `typing(_extensions).runtime_checkable`
|
||||
RuntimeCheckable,
|
||||
/// `typing(_extensions).dataclass_transform`
|
||||
DataclassTransform,
|
||||
|
||||
/// `abc.abstractmethod`
|
||||
#[strum(serialize = "abstractmethod")]
|
||||
AbstractMethod,
|
||||
|
||||
/// `dataclasses.dataclass`
|
||||
Dataclass,
|
||||
|
||||
/// `inspect.getattr_static`
|
||||
GetattrStatic,
|
||||
|
||||
/// `ty_extensions.static_assert`
|
||||
StaticAssert,
|
||||
/// `ty_extensions.is_equivalent_to`
|
||||
IsEquivalentTo,
|
||||
/// `ty_extensions.is_subtype_of`
|
||||
IsSubtypeOf,
|
||||
/// `ty_extensions.is_assignable_to`
|
||||
IsAssignableTo,
|
||||
/// `ty_extensions.is_disjoint_from`
|
||||
IsDisjointFrom,
|
||||
/// `ty_extensions.is_gradual_equivalent_to`
|
||||
IsGradualEquivalentTo,
|
||||
/// `ty_extensions.is_fully_static`
|
||||
IsFullyStatic,
|
||||
/// `ty_extensions.is_singleton`
|
||||
IsSingleton,
|
||||
/// `ty_extensions.is_single_valued`
|
||||
IsSingleValued,
|
||||
/// `ty_extensions.generic_context`
|
||||
GenericContext,
|
||||
/// `ty_extensions.dunder_all_names`
|
||||
DunderAllNames,
|
||||
/// `ty_extensions.all_members`
|
||||
AllMembers,
|
||||
}
|
||||
|
||||
impl KnownFunction {
|
||||
pub fn into_classinfo_constraint_function(self) -> Option<ClassInfoConstraintFunction> {
|
||||
match self {
|
||||
Self::IsInstance => Some(ClassInfoConstraintFunction::IsInstance),
|
||||
Self::IsSubclass => Some(ClassInfoConstraintFunction::IsSubclass),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_from_definition_and_name<'db>(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
name: &str,
|
||||
) -> Option<Self> {
|
||||
let candidate = Self::from_str(name).ok()?;
|
||||
candidate
|
||||
.check_module(file_to_module(db, definition.file(db))?.known()?)
|
||||
.then_some(candidate)
|
||||
}
|
||||
|
||||
/// 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::AssertType
|
||||
| Self::AssertNever
|
||||
| Self::Cast
|
||||
| Self::Overload
|
||||
| Self::Override
|
||||
| Self::RevealType
|
||||
| Self::Final
|
||||
| Self::IsProtocol
|
||||
| Self::GetProtocolMembers
|
||||
| Self::RuntimeCheckable
|
||||
| Self::DataclassTransform
|
||||
| Self::NoTypeCheck => {
|
||||
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
|
||||
}
|
||||
Self::AbstractMethod => {
|
||||
matches!(module, KnownModule::Abc)
|
||||
}
|
||||
Self::Dataclass => {
|
||||
matches!(module, KnownModule::Dataclasses)
|
||||
}
|
||||
Self::GetattrStatic => module.is_inspect(),
|
||||
Self::IsAssignableTo
|
||||
| Self::IsDisjointFrom
|
||||
| Self::IsEquivalentTo
|
||||
| Self::IsGradualEquivalentTo
|
||||
| Self::IsFullyStatic
|
||||
| Self::IsSingleValued
|
||||
| Self::IsSingleton
|
||||
| Self::IsSubtypeOf
|
||||
| Self::GenericContext
|
||||
| Self::DunderAllNames
|
||||
| Self::StaticAssert
|
||||
| Self::AllMembers => module.is_ty_extensions(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This type represents bound method objects that are created when a method is accessed
|
||||
/// on an instance of a class. For example, the expression `Path("a.txt").touch` creates
|
||||
/// a bound method object that represents the `Path.touch` method which is bound to the
|
||||
|
@ -9213,15 +8489,12 @@ static_assertions::assert_eq_size!(Type, [u8; 16]);
|
|||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::{TestDbBuilder, setup_db};
|
||||
use crate::symbol::{
|
||||
global_symbol, known_module_symbol, typing_extensions_symbol, typing_symbol,
|
||||
};
|
||||
use crate::symbol::{global_symbol, typing_extensions_symbol, typing_symbol};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::DbWithWritableSystem as _;
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use strum::IntoEnumIterator;
|
||||
use test_case::test_case;
|
||||
|
||||
/// Explicitly test for Python version <3.13 and >=3.13, to ensure that
|
||||
|
@ -9364,69 +8637,4 @@ pub(crate) mod tests {
|
|||
.is_todo()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_function_roundtrip_from_str() {
|
||||
let db = setup_db();
|
||||
|
||||
for function in KnownFunction::iter() {
|
||||
let function_name: &'static str = function.into();
|
||||
|
||||
let module = match function {
|
||||
KnownFunction::Len
|
||||
| KnownFunction::Repr
|
||||
| KnownFunction::IsInstance
|
||||
| KnownFunction::HasAttr
|
||||
| KnownFunction::IsSubclass => KnownModule::Builtins,
|
||||
|
||||
KnownFunction::AbstractMethod => KnownModule::Abc,
|
||||
|
||||
KnownFunction::Dataclass => KnownModule::Dataclasses,
|
||||
|
||||
KnownFunction::GetattrStatic => KnownModule::Inspect,
|
||||
|
||||
KnownFunction::Cast
|
||||
| KnownFunction::Final
|
||||
| KnownFunction::Overload
|
||||
| KnownFunction::Override
|
||||
| KnownFunction::RevealType
|
||||
| KnownFunction::AssertType
|
||||
| KnownFunction::AssertNever
|
||||
| KnownFunction::IsProtocol
|
||||
| KnownFunction::GetProtocolMembers
|
||||
| KnownFunction::RuntimeCheckable
|
||||
| KnownFunction::DataclassTransform
|
||||
| KnownFunction::NoTypeCheck => KnownModule::TypingExtensions,
|
||||
|
||||
KnownFunction::IsSingleton
|
||||
| KnownFunction::IsSubtypeOf
|
||||
| KnownFunction::GenericContext
|
||||
| KnownFunction::DunderAllNames
|
||||
| KnownFunction::StaticAssert
|
||||
| KnownFunction::IsFullyStatic
|
||||
| KnownFunction::IsDisjointFrom
|
||||
| KnownFunction::IsSingleValued
|
||||
| KnownFunction::IsAssignableTo
|
||||
| KnownFunction::IsEquivalentTo
|
||||
| KnownFunction::IsGradualEquivalentTo
|
||||
| KnownFunction::AllMembers => KnownModule::TyExtensions,
|
||||
};
|
||||
|
||||
let function_definition = known_module_symbol(&db, module, function_name)
|
||||
.symbol
|
||||
.expect_type()
|
||||
.expect_function_literal()
|
||||
.definition(&db);
|
||||
|
||||
assert_eq!(
|
||||
KnownFunction::try_from_definition_and_name(
|
||||
&db,
|
||||
function_definition,
|
||||
function_name
|
||||
),
|
||||
Some(function),
|
||||
"The strum `EnumString` implementation appears to be incorrect for `{function_name}`"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,13 +18,13 @@ use crate::types::diagnostic::{
|
|||
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS,
|
||||
UNKNOWN_ARGUMENT,
|
||||
};
|
||||
use crate::types::function::{DataclassTransformerParams, FunctionDecorators, KnownFunction};
|
||||
use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError};
|
||||
use crate::types::signatures::{Parameter, ParameterForm};
|
||||
use crate::types::{
|
||||
BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, FunctionType,
|
||||
KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType,
|
||||
SpecialFormType, TupleType, TypeMapping, UnionType, WrapperDescriptorKind, ide_support,
|
||||
todo_type,
|
||||
BoundMethodType, DataclassParams, KnownClass, KnownInstanceType, MethodWrapperKind,
|
||||
PropertyInstanceType, SpecialFormType, TupleType, TypeMapping, UnionType,
|
||||
WrapperDescriptorKind, ide_support, todo_type,
|
||||
};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};
|
||||
use ruff_python_ast as ast;
|
||||
|
@ -871,47 +871,47 @@ impl<'db> Bindings<'db> {
|
|||
}
|
||||
|
||||
_ => {
|
||||
let mut handle_dataclass_transformer_params =
|
||||
|function_type: &FunctionType| {
|
||||
if let Some(params) =
|
||||
function_type.dataclass_transformer_params(db)
|
||||
{
|
||||
// This is a call to a custom function that was decorated with `@dataclass_transformer`.
|
||||
// If this function was called with a keyword argument like `order=False`, we extract
|
||||
// the argument type and overwrite the corresponding flag in `dataclass_params` after
|
||||
// constructing them from the `dataclass_transformer`-parameter defaults.
|
||||
|
||||
let mut dataclass_params = DataclassParams::from(params);
|
||||
|
||||
if let Some(Some(Type::BooleanLiteral(order))) = overload
|
||||
.signature
|
||||
.parameters()
|
||||
.keyword_by_name("order")
|
||||
.map(|(idx, _)| idx)
|
||||
.and_then(|idx| overload.parameter_types().get(idx))
|
||||
{
|
||||
dataclass_params.set(DataclassParams::ORDER, *order);
|
||||
}
|
||||
|
||||
overload.set_return_type(Type::DataclassDecorator(
|
||||
dataclass_params,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Ideally, either the implementation, or exactly one of the overloads
|
||||
// of the function can have the dataclass_transform decorator applied.
|
||||
// However, we do not yet enforce this, and in the case of multiple
|
||||
// applications of the decorator, we will only consider the last one
|
||||
// for the return value, since the prior ones will be over-written.
|
||||
if let Some(overloaded) = function_type.to_overloaded(db) {
|
||||
overloaded
|
||||
.overloads
|
||||
.iter()
|
||||
.for_each(&mut handle_dataclass_transformer_params);
|
||||
}
|
||||
let return_type = function_type
|
||||
.iter_overloads_and_implementation(db)
|
||||
.filter_map(|function_overload| {
|
||||
function_overload.dataclass_transformer_params(db).map(
|
||||
|params| {
|
||||
// This is a call to a custom function that was decorated with `@dataclass_transformer`.
|
||||
// If this function was called with a keyword argument like `order=False`, we extract
|
||||
// the argument type and overwrite the corresponding flag in `dataclass_params` after
|
||||
// constructing them from the `dataclass_transformer`-parameter defaults.
|
||||
|
||||
handle_dataclass_transformer_params(&function_type);
|
||||
let mut dataclass_params =
|
||||
DataclassParams::from(params);
|
||||
|
||||
if let Some(Some(Type::BooleanLiteral(order))) =
|
||||
overload
|
||||
.signature
|
||||
.parameters()
|
||||
.keyword_by_name("order")
|
||||
.map(|(idx, _)| idx)
|
||||
.and_then(|idx| {
|
||||
overload.parameter_types().get(idx)
|
||||
})
|
||||
{
|
||||
dataclass_params
|
||||
.set(DataclassParams::ORDER, *order);
|
||||
}
|
||||
|
||||
Type::DataclassDecorator(dataclass_params)
|
||||
},
|
||||
)
|
||||
})
|
||||
.last();
|
||||
|
||||
if let Some(return_type) = return_type {
|
||||
overload.set_return_type(return_type);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1261,47 +1261,49 @@ impl<'db> CallableBinding<'db> {
|
|||
_ => None,
|
||||
};
|
||||
if let Some((kind, function)) = function_type_and_kind {
|
||||
if let Some(overloaded_function) = function.to_overloaded(context.db()) {
|
||||
if let Some(spans) = overloaded_function
|
||||
.overloads
|
||||
.first()
|
||||
.and_then(|overload| overload.spans(context.db()))
|
||||
{
|
||||
let mut sub =
|
||||
SubDiagnostic::new(Severity::Info, "First overload defined here");
|
||||
sub.annotate(Annotation::primary(spans.signature));
|
||||
diag.sub(sub);
|
||||
}
|
||||
let (overloads, implementation) =
|
||||
function.overloads_and_implementation(context.db());
|
||||
|
||||
if let Some(spans) = overloads
|
||||
.first()
|
||||
.and_then(|overload| overload.spans(context.db()))
|
||||
{
|
||||
let mut sub =
|
||||
SubDiagnostic::new(Severity::Info, "First overload defined here");
|
||||
sub.annotate(Annotation::primary(spans.signature));
|
||||
diag.sub(sub);
|
||||
}
|
||||
|
||||
diag.info(format_args!(
|
||||
"Possible overloads for {kind} `{}`:",
|
||||
function.name(context.db())
|
||||
));
|
||||
|
||||
for overload in overloads.iter().take(MAXIMUM_OVERLOADS) {
|
||||
diag.info(format_args!(
|
||||
"Possible overloads for {kind} `{}`:",
|
||||
function.name(context.db())
|
||||
" {}",
|
||||
overload.signature(context.db(), None).display(context.db())
|
||||
));
|
||||
}
|
||||
if overloads.len() > MAXIMUM_OVERLOADS {
|
||||
diag.info(format_args!(
|
||||
"... omitted {remaining} overloads",
|
||||
remaining = overloads.len() - MAXIMUM_OVERLOADS
|
||||
));
|
||||
}
|
||||
|
||||
let overloads = &function.signature(context.db()).overloads.overloads;
|
||||
for overload in overloads.iter().take(MAXIMUM_OVERLOADS) {
|
||||
diag.info(format_args!(" {}", overload.display(context.db())));
|
||||
}
|
||||
if overloads.len() > MAXIMUM_OVERLOADS {
|
||||
diag.info(format_args!(
|
||||
"... omitted {remaining} overloads",
|
||||
remaining = overloads.len() - MAXIMUM_OVERLOADS
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(spans) = overloaded_function
|
||||
.implementation
|
||||
.and_then(|function| function.spans(context.db()))
|
||||
{
|
||||
let mut sub = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Overload implementation defined here",
|
||||
);
|
||||
sub.annotate(Annotation::primary(spans.signature));
|
||||
diag.sub(sub);
|
||||
}
|
||||
if let Some(spans) =
|
||||
implementation.and_then(|function| function.spans(context.db()))
|
||||
{
|
||||
let mut sub = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Overload implementation defined here",
|
||||
);
|
||||
sub.annotate(Annotation::primary(spans.signature));
|
||||
diag.sub(sub);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(union_diag) = union_diag {
|
||||
union_diag.add_union_context(context.db(), &mut diag);
|
||||
}
|
||||
|
|
|
@ -2,17 +2,17 @@ use std::hash::BuildHasherDefault;
|
|||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use super::{
|
||||
IntersectionBuilder, KnownFunction, MemberLookupPolicy, Mro, MroError, MroIterator,
|
||||
SpecialFormType, SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase,
|
||||
infer_expression_type, infer_unpack_types,
|
||||
IntersectionBuilder, MemberLookupPolicy, Mro, MroError, MroIterator, SpecialFormType,
|
||||
SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase, infer_expression_type,
|
||||
infer_unpack_types,
|
||||
};
|
||||
use crate::semantic_index::DeclarationWithConstraint;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::types::function::{DataclassTransformerParams, KnownFunction};
|
||||
use crate::types::generics::{GenericContext, Specialization};
|
||||
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
|
||||
use crate::types::{
|
||||
CallableType, DataclassParams, DataclassTransformerParams, KnownInstanceType, TypeMapping,
|
||||
TypeVarInstance,
|
||||
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeVarInstance,
|
||||
};
|
||||
use crate::{
|
||||
Db, FxOrderSet, KnownModule, Program,
|
||||
|
|
|
@ -11,13 +11,14 @@ use ruff_text_size::{Ranged, TextRange};
|
|||
use super::{Type, TypeCheckDiagnostics, binding_type};
|
||||
|
||||
use crate::lint::LintSource;
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::types::function::FunctionDecorators;
|
||||
use crate::{
|
||||
Db,
|
||||
lint::{LintId, LintMetadata},
|
||||
suppression::suppressions,
|
||||
};
|
||||
use crate::{semantic_index::semantic_index, types::FunctionDecorators};
|
||||
|
||||
/// Context for inferring the types of a single file.
|
||||
///
|
||||
|
|
|
@ -8,12 +8,13 @@ use super::{
|
|||
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
|
||||
use crate::suppression::FileSuppressionId;
|
||||
use crate::types::LintDiagnosticGuard;
|
||||
use crate::types::function::KnownFunction;
|
||||
use crate::types::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
RAW_STRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use crate::types::{KnownFunction, SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
|
||||
use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
|
||||
use crate::{Db, Module, ModuleName, Program, declare_lint};
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};
|
||||
|
|
|
@ -7,6 +7,7 @@ use ruff_python_ast::str::{Quote, TripleQuotes};
|
|||
use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::class::{ClassLiteral, ClassType, GenericAlias};
|
||||
use crate::types::function::{FunctionType, OverloadLiteral};
|
||||
use crate::types::generics::{GenericContext, Specialization};
|
||||
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
|
||||
use crate::types::{
|
||||
|
@ -112,34 +113,7 @@ impl Display for DisplayRepresentation<'_> {
|
|||
},
|
||||
Type::SpecialForm(special_form) => special_form.fmt(f),
|
||||
Type::KnownInstance(known_instance) => known_instance.repr(self.db).fmt(f),
|
||||
Type::FunctionLiteral(function) => {
|
||||
let signature = function.signature(self.db);
|
||||
|
||||
// TODO: when generic function types are supported, we should add
|
||||
// the generic type parameters to the signature, i.e.
|
||||
// show `def foo[T](x: T) -> T`.
|
||||
|
||||
match signature.overloads.as_slice() {
|
||||
[signature] => {
|
||||
write!(
|
||||
f,
|
||||
// "def {name}{specialization}{signature}",
|
||||
"def {name}{signature}",
|
||||
name = function.name(self.db),
|
||||
signature = signature.display(self.db)
|
||||
)
|
||||
}
|
||||
signatures => {
|
||||
// TODO: How to display overloads?
|
||||
f.write_str("Overload[")?;
|
||||
let mut join = f.join(", ");
|
||||
for signature in signatures {
|
||||
join.entry(&signature.display(self.db));
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
}
|
||||
}
|
||||
Type::FunctionLiteral(function) => function.display(self.db).fmt(f),
|
||||
Type::Callable(callable) => callable.display(self.db).fmt(f),
|
||||
Type::BoundMethod(bound_method) => {
|
||||
let function = bound_method.function(self.db);
|
||||
|
@ -241,6 +215,71 @@ impl Display for DisplayRepresentation<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'db> OverloadLiteral<'db> {
|
||||
// Not currently used, but useful for debugging.
|
||||
#[expect(dead_code)]
|
||||
pub(crate) fn display(self, db: &'db dyn Db) -> DisplayOverloadLiteral<'db> {
|
||||
DisplayOverloadLiteral { literal: self, db }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DisplayOverloadLiteral<'db> {
|
||||
literal: OverloadLiteral<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayOverloadLiteral<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let signature = self.literal.signature(self.db, None);
|
||||
write!(
|
||||
f,
|
||||
"def {name}{signature}",
|
||||
name = self.literal.name(self.db),
|
||||
signature = signature.display(self.db)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> FunctionType<'db> {
|
||||
pub(crate) fn display(self, db: &'db dyn Db) -> DisplayFunctionType<'db> {
|
||||
DisplayFunctionType { ty: self, db }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DisplayFunctionType<'db> {
|
||||
ty: FunctionType<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayFunctionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let signature = self.ty.signature(self.db);
|
||||
|
||||
// TODO: We should consider adding the type parameters to the signature of a generic
|
||||
// function, i.e. `def foo[T](x: T) -> T`.
|
||||
|
||||
match signature.overloads.as_slice() {
|
||||
[signature] => {
|
||||
write!(
|
||||
f,
|
||||
"def {name}{signature}",
|
||||
name = self.ty.name(self.db),
|
||||
signature = signature.display(self.db)
|
||||
)
|
||||
}
|
||||
signatures => {
|
||||
// TODO: How to display overloads?
|
||||
f.write_str("Overload[")?;
|
||||
let mut join = f.join(", ");
|
||||
for signature in signatures {
|
||||
join.entry(&signature.display(self.db));
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> GenericAlias<'db> {
|
||||
pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayGenericAlias<'db> {
|
||||
DisplayGenericAlias {
|
||||
|
|
996
crates/ty_python_semantic/src/types/function.rs
Normal file
996
crates/ty_python_semantic/src/types/function.rs
Normal file
|
@ -0,0 +1,996 @@
|
|||
//! Contains representations of function literals. There are several complicating factors:
|
||||
//!
|
||||
//! - Functions can be generic, and can have specializations applied to them. These are not the
|
||||
//! same thing! For instance, a method of a generic class might not itself be generic, but it can
|
||||
//! still have the class's specialization applied to it.
|
||||
//!
|
||||
//! - Functions can be overloaded, and each overload can be independently generic or not, with
|
||||
//! different sets of typevars for different generic overloads. In some cases we need to consider
|
||||
//! each overload separately; in others we need to consider all of the overloads (and any
|
||||
//! implementation) as a single collective entity.
|
||||
//!
|
||||
//! - Certain “known” functions need special treatment — for instance, inferring a special return
|
||||
//! type, or raising custom diagnostics.
|
||||
//!
|
||||
//! - TODO: Some functions don't correspond to a function definition in the AST, and are instead
|
||||
//! synthesized as we mimic the behavior of the Python interpreter. Even though they are
|
||||
//! synthesized, and are “implemented” as Rust code, they are still functions from the POV of the
|
||||
//! rest of the type system.
|
||||
//!
|
||||
//! Given these constraints, we have the following representation: a function is a list of one or
|
||||
//! more overloads, with zero or more specializations (more specifically, “type mappings”) applied
|
||||
//! to it. [`FunctionType`] is the outermost type, which is what [`Type::FunctionLiteral`] wraps.
|
||||
//! It contains the list of type mappings to apply. It wraps a [`FunctionLiteral`], which collects
|
||||
//! together all of the overloads (and implementation) of an overloaded function. An
|
||||
//! [`OverloadLiteral`] represents an individual function definition in the AST — that is, each
|
||||
//! overload (and implementation) of an overloaded function, or the single definition of a
|
||||
//! non-overloaded function.
|
||||
//!
|
||||
//! Technically, each `FunctionLiteral` wraps a particular overload and all _previous_ overloads.
|
||||
//! So it's only true that it wraps _all_ overloads if you are looking at the last definition. For
|
||||
//! instance, in
|
||||
//!
|
||||
//! ```py
|
||||
//! @overload
|
||||
//! def f(x: int) -> None: ...
|
||||
//! # <-- 1
|
||||
//!
|
||||
//! @overload
|
||||
//! def f(x: str) -> None: ...
|
||||
//! # <-- 2
|
||||
//!
|
||||
//! def f(x): pass
|
||||
//! # <-- 3
|
||||
//! ```
|
||||
//!
|
||||
//! resolving `f` at each of the three numbered positions will give you a `FunctionType`, which
|
||||
//! wraps a `FunctionLiteral`, which contain `OverloadLiteral`s only for the definitions that
|
||||
//! appear before that position. We rely on the fact that later definitions shadow earlier ones, so
|
||||
//! the public type of `f` is resolved at position 3, correctly giving you all of the overloads
|
||||
//! (and the implementation).
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use ruff_db::diagnostic::Span;
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::module_resolver::{KnownModule, file_to_module};
|
||||
use crate::semantic_index::ast_ids::HasScopedUseId;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::symbol::{Boundness, Symbol, symbol_from_bindings};
|
||||
use crate::types::generics::GenericContext;
|
||||
use crate::types::narrow::ClassInfoConstraintFunction;
|
||||
use crate::types::signatures::{CallableSignature, Signature};
|
||||
use crate::types::{BoundMethodType, CallableType, Type, TypeMapping, TypeVarInstance};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
/// A collection of useful spans for annotating functions.
|
||||
///
|
||||
/// This can be retrieved via `FunctionType::spans` or
|
||||
/// `Type::function_spans`.
|
||||
pub(crate) struct FunctionSpans {
|
||||
/// The span of the entire function "signature." This includes
|
||||
/// the name, parameter list and return type (if present).
|
||||
pub(crate) signature: Span,
|
||||
/// The span of the function name. i.e., `foo` in `def foo(): ...`.
|
||||
pub(crate) name: Span,
|
||||
/// The span of the parameter list, including the opening and
|
||||
/// closing parentheses.
|
||||
#[expect(dead_code)]
|
||||
pub(crate) parameters: Span,
|
||||
/// The span of the annotated return type, if present.
|
||||
pub(crate) return_type: Option<Span>,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Hash)]
|
||||
pub struct FunctionDecorators: u8 {
|
||||
/// `@classmethod`
|
||||
const CLASSMETHOD = 1 << 0;
|
||||
/// `@typing.no_type_check`
|
||||
const NO_TYPE_CHECK = 1 << 1;
|
||||
/// `@typing.overload`
|
||||
const OVERLOAD = 1 << 2;
|
||||
/// `@abc.abstractmethod`
|
||||
const ABSTRACT_METHOD = 1 << 3;
|
||||
/// `@typing.final`
|
||||
const FINAL = 1 << 4;
|
||||
/// `@typing.override`
|
||||
const OVERRIDE = 1 << 6;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Used for the return type of `dataclass_transform(…)` calls. Keeps track of the
|
||||
/// arguments that were passed in. For the precise meaning of the fields, see [1].
|
||||
///
|
||||
/// [1]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub struct DataclassTransformerParams: u8 {
|
||||
const EQ_DEFAULT = 1 << 0;
|
||||
const ORDER_DEFAULT = 1 << 1;
|
||||
const KW_ONLY_DEFAULT = 1 << 2;
|
||||
const FROZEN_DEFAULT = 1 << 3;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DataclassTransformerParams {
|
||||
fn default() -> Self {
|
||||
Self::EQ_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a function definition in the AST: either a non-generic function, or a generic
|
||||
/// function that has not been specialized.
|
||||
///
|
||||
/// If a function has multiple overloads, each overload is represented by a separate function
|
||||
/// definition in the AST, and is therefore a separate `OverloadLiteral` instance.
|
||||
///
|
||||
/// # Ordering
|
||||
/// Ordering is based on the function's id assigned by salsa and not on the function literal's
|
||||
/// values. The id may change between runs, or when the function literal was garbage collected and
|
||||
/// recreated.
|
||||
#[salsa::interned(debug)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct OverloadLiteral<'db> {
|
||||
/// Name of the function at definition.
|
||||
#[returns(ref)]
|
||||
pub name: ast::name::Name,
|
||||
|
||||
/// Is this a function that we special-case somehow? If so, which one?
|
||||
pub(crate) known: Option<KnownFunction>,
|
||||
|
||||
/// The scope that's created by the function, in which the function body is evaluated.
|
||||
pub(crate) body_scope: ScopeId<'db>,
|
||||
|
||||
/// A set of special decorators that were applied to this function
|
||||
pub(crate) decorators: FunctionDecorators,
|
||||
|
||||
/// The arguments to `dataclass_transformer`, if this function was annotated
|
||||
/// with `@dataclass_transformer(...)`.
|
||||
pub(crate) dataclass_transformer_params: Option<DataclassTransformerParams>,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> OverloadLiteral<'db> {
|
||||
fn with_dataclass_transformer_params(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
params: DataclassTransformerParams,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
db,
|
||||
self.name(db).clone(),
|
||||
self.known(db),
|
||||
self.body_scope(db),
|
||||
self.decorators(db),
|
||||
Some(params),
|
||||
)
|
||||
}
|
||||
|
||||
fn file(self, db: &'db dyn Db) -> File {
|
||||
// NOTE: Do not use `self.definition(db).file(db)` here, as that could create a
|
||||
// cross-module dependency on the full AST.
|
||||
self.body_scope(db).file(db)
|
||||
}
|
||||
|
||||
pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool {
|
||||
self.decorators(db).contains(decorator)
|
||||
}
|
||||
|
||||
pub(crate) fn is_overload(self, db: &dyn Db) -> bool {
|
||||
self.has_known_decorator(db, FunctionDecorators::OVERLOAD)
|
||||
}
|
||||
|
||||
fn node(self, db: &'db dyn Db, file: File) -> &'db ast::StmtFunctionDef {
|
||||
debug_assert_eq!(
|
||||
file,
|
||||
self.file(db),
|
||||
"OverloadLiteral::node() must be called with the same file as the one where \
|
||||
the function is defined."
|
||||
);
|
||||
|
||||
self.body_scope(db).node(db).expect_function()
|
||||
}
|
||||
|
||||
/// Returns the [`FileRange`] of the function's name.
|
||||
pub(crate) fn focus_range(self, db: &dyn Db) -> FileRange {
|
||||
FileRange::new(
|
||||
self.file(db),
|
||||
self.body_scope(db).node(db).expect_function().name.range,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the [`Definition`] of this function.
|
||||
///
|
||||
/// ## Warning
|
||||
///
|
||||
/// This uses the semantic index to find the definition of the function. This means that if the
|
||||
/// calling query is not in the same file as this function is defined in, then this will create
|
||||
/// a cross-module dependency directly on the full AST which will lead to cache
|
||||
/// over-invalidation.
|
||||
fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
let body_scope = self.body_scope(db);
|
||||
let index = semantic_index(db, body_scope.file(db));
|
||||
index.expect_single_definition(body_scope.node(db).expect_function())
|
||||
}
|
||||
|
||||
/// Returns the overload immediately before this one in the AST. Returns `None` if there is no
|
||||
/// previous overload.
|
||||
fn previous_overload(self, db: &'db dyn Db) -> Option<FunctionLiteral<'db>> {
|
||||
// The semantic model records a use for each function on the name node. This is used
|
||||
// here to get the previous function definition with the same name.
|
||||
let scope = self.definition(db).scope(db);
|
||||
let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db));
|
||||
let use_id = self
|
||||
.body_scope(db)
|
||||
.node(db)
|
||||
.expect_function()
|
||||
.name
|
||||
.scoped_use_id(db, scope);
|
||||
|
||||
let Symbol::Type(Type::FunctionLiteral(previous_type), Boundness::Bound) =
|
||||
symbol_from_bindings(db, use_def.bindings_at_use(use_id))
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let previous_literal = previous_type.literal(db);
|
||||
let previous_overload = previous_literal.last_definition(db);
|
||||
if !previous_overload.is_overload(db) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(previous_literal)
|
||||
}
|
||||
|
||||
/// Typed internally-visible signature for this function.
|
||||
///
|
||||
/// This represents the annotations on the function itself, unmodified by decorators and
|
||||
/// overloads.
|
||||
///
|
||||
/// ## Warning
|
||||
///
|
||||
/// This uses the semantic index to find the definition of the function. This means that if the
|
||||
/// calling query is not in the same file as this function is defined in, then this will create
|
||||
/// a cross-module dependency directly on the full AST which will lead to cache
|
||||
/// over-invalidation.
|
||||
pub(crate) fn signature(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
inherited_generic_context: Option<GenericContext<'db>>,
|
||||
) -> Signature<'db> {
|
||||
let scope = self.body_scope(db);
|
||||
let function_stmt_node = scope.node(db).expect_function();
|
||||
let definition = self.definition(db);
|
||||
let generic_context = function_stmt_node.type_params.as_ref().map(|type_params| {
|
||||
let index = semantic_index(db, scope.file(db));
|
||||
GenericContext::from_type_params(db, index, type_params)
|
||||
});
|
||||
Signature::from_function(
|
||||
db,
|
||||
generic_context,
|
||||
inherited_generic_context,
|
||||
definition,
|
||||
function_stmt_node,
|
||||
)
|
||||
}
|
||||
|
||||
fn parameter_span(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
parameter_index: Option<usize>,
|
||||
) -> Option<(Span, Span)> {
|
||||
let function_scope = self.body_scope(db);
|
||||
let span = Span::from(function_scope.file(db));
|
||||
let node = function_scope.node(db);
|
||||
let func_def = node.as_function()?;
|
||||
let range = parameter_index
|
||||
.and_then(|parameter_index| {
|
||||
func_def
|
||||
.parameters
|
||||
.iter()
|
||||
.nth(parameter_index)
|
||||
.map(|param| param.range())
|
||||
})
|
||||
.unwrap_or(func_def.parameters.range);
|
||||
let name_span = span.clone().with_range(func_def.name.range);
|
||||
let parameter_span = span.with_range(range);
|
||||
Some((name_span, parameter_span))
|
||||
}
|
||||
|
||||
pub(crate) fn spans(self, db: &'db dyn Db) -> Option<FunctionSpans> {
|
||||
let function_scope = self.body_scope(db);
|
||||
let span = Span::from(function_scope.file(db));
|
||||
let node = function_scope.node(db);
|
||||
let func_def = node.as_function()?;
|
||||
let return_type_range = func_def.returns.as_ref().map(|returns| returns.range());
|
||||
let mut signature = func_def.name.range.cover(func_def.parameters.range);
|
||||
if let Some(return_type_range) = return_type_range {
|
||||
signature = signature.cover(return_type_range);
|
||||
}
|
||||
Some(FunctionSpans {
|
||||
signature: span.clone().with_range(signature),
|
||||
name: span.clone().with_range(func_def.name.range),
|
||||
parameters: span.clone().with_range(func_def.parameters.range),
|
||||
return_type: return_type_range.map(|range| span.clone().with_range(range)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a function definition in the AST, along with any previous overloads of the
|
||||
/// function. Each overload can be separately generic or not, and each generic overload uses
|
||||
/// distinct typevars.
|
||||
///
|
||||
/// # Ordering
|
||||
/// Ordering is based on the function's id assigned by salsa and not on the function literal's
|
||||
/// values. The id may change between runs, or when the function literal was garbage collected and
|
||||
/// recreated.
|
||||
#[salsa::interned(debug)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct FunctionLiteral<'db> {
|
||||
pub(crate) last_definition: OverloadLiteral<'db>,
|
||||
|
||||
/// The inherited generic context, if this function is a constructor method (`__new__` or
|
||||
/// `__init__`) being used to infer the specialization of its generic class. If any of the
|
||||
/// method's overloads are themselves generic, this is in addition to those per-overload
|
||||
/// generic contexts (which are created lazily in [`OverloadLiteral::signature`]).
|
||||
///
|
||||
/// If the function is not a constructor method, this field will always be `None`.
|
||||
///
|
||||
/// If the function is a constructor method, we will end up creating two `FunctionLiteral`
|
||||
/// instances for it. The first is created in [`TypeInferenceBuilder`][infer] when we encounter
|
||||
/// the function definition during type inference. At this point, we don't yet know if the
|
||||
/// function is a constructor method, so we create a `FunctionLiteral` with `None` for this
|
||||
/// field.
|
||||
///
|
||||
/// If at some point we encounter a call expression, which invokes the containing class's
|
||||
/// constructor, as will create a _new_ `FunctionLiteral` instance for the function, with this
|
||||
/// field [updated][] to contain the containing class's generic context.
|
||||
///
|
||||
/// [infer]: crate::types::infer::TypeInferenceBuilder::infer_function_definition
|
||||
/// [updated]: crate::types::class::ClassLiteral::own_class_member
|
||||
inherited_generic_context: Option<GenericContext<'db>>,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> FunctionLiteral<'db> {
|
||||
fn with_inherited_generic_context(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
inherited_generic_context: GenericContext<'db>,
|
||||
) -> Self {
|
||||
// A function cannot inherit more than one generic context from its containing class.
|
||||
debug_assert!(self.inherited_generic_context(db).is_none());
|
||||
Self::new(
|
||||
db,
|
||||
self.last_definition(db),
|
||||
Some(inherited_generic_context),
|
||||
)
|
||||
}
|
||||
|
||||
fn name(self, db: &'db dyn Db) -> &'db ast::name::Name {
|
||||
// All of the overloads of a function literal should have the same name.
|
||||
self.last_definition(db).name(db)
|
||||
}
|
||||
|
||||
fn known(self, db: &'db dyn Db) -> Option<KnownFunction> {
|
||||
// Whether a function is known is based on its name (and its containing module's name), so
|
||||
// all overloads should be known (or not) equivalently.
|
||||
self.last_definition(db).known(db)
|
||||
}
|
||||
|
||||
fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool {
|
||||
self.iter_overloads_and_implementation(db)
|
||||
.any(|overload| overload.decorators(db).contains(decorator))
|
||||
}
|
||||
|
||||
fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
self.last_definition(db).definition(db)
|
||||
}
|
||||
|
||||
fn parameter_span(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
parameter_index: Option<usize>,
|
||||
) -> Option<(Span, Span)> {
|
||||
self.last_definition(db).parameter_span(db, parameter_index)
|
||||
}
|
||||
|
||||
fn spans(self, db: &'db dyn Db) -> Option<FunctionSpans> {
|
||||
self.last_definition(db).spans(db)
|
||||
}
|
||||
|
||||
#[salsa::tracked(returns(ref))]
|
||||
fn overloads_and_implementation(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> (Box<[OverloadLiteral<'db>]>, Option<OverloadLiteral<'db>>) {
|
||||
let self_overload = self.last_definition(db);
|
||||
let mut current = self_overload;
|
||||
let mut overloads = vec![];
|
||||
|
||||
while let Some(previous) = current.previous_overload(db) {
|
||||
let overload = previous.last_definition(db);
|
||||
overloads.push(overload);
|
||||
current = overload;
|
||||
}
|
||||
|
||||
// Overloads are inserted in reverse order, from bottom to top.
|
||||
overloads.reverse();
|
||||
|
||||
let implementation = if self_overload.is_overload(db) {
|
||||
overloads.push(self_overload);
|
||||
None
|
||||
} else {
|
||||
Some(self_overload)
|
||||
};
|
||||
|
||||
(overloads.into_boxed_slice(), implementation)
|
||||
}
|
||||
|
||||
fn iter_overloads_and_implementation(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> impl Iterator<Item = OverloadLiteral<'db>> + 'db {
|
||||
let (implementation, overloads) = self.overloads_and_implementation(db);
|
||||
overloads.iter().chain(implementation).copied()
|
||||
}
|
||||
|
||||
/// Typed externally-visible signature for this function.
|
||||
///
|
||||
/// This is the signature as seen by external callers, possibly modified by decorators and/or
|
||||
/// overloaded.
|
||||
///
|
||||
/// ## Warning
|
||||
///
|
||||
/// This uses the semantic index to find the definition of the function. This means that if the
|
||||
/// calling query is not in the same file as this function is defined in, then this will create
|
||||
/// a cross-module dependency directly on the full AST which will lead to cache
|
||||
/// over-invalidation.
|
||||
fn signature<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
type_mappings: &'a [TypeMapping<'a, 'db>],
|
||||
) -> CallableSignature<'db>
|
||||
where
|
||||
'db: 'a,
|
||||
{
|
||||
// We only include an implementation (i.e. a definition not decorated with `@overload`) if
|
||||
// it's the only definition.
|
||||
let inherited_generic_context = self.inherited_generic_context(db);
|
||||
let (overloads, implementation) = self.overloads_and_implementation(db);
|
||||
if let Some(implementation) = implementation {
|
||||
if overloads.is_empty() {
|
||||
return CallableSignature::single(type_mappings.iter().fold(
|
||||
implementation.signature(db, inherited_generic_context),
|
||||
|ty, mapping| ty.apply_type_mapping(db, mapping),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
CallableSignature::from_overloads(overloads.iter().map(|overload| {
|
||||
type_mappings.iter().fold(
|
||||
overload.signature(db, inherited_generic_context),
|
||||
|ty, mapping| ty.apply_type_mapping(db, mapping),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn normalized(self, db: &'db dyn Db) -> Self {
|
||||
let context = self
|
||||
.inherited_generic_context(db)
|
||||
.map(|ctx| ctx.normalized(db));
|
||||
Self::new(db, self.last_definition(db), context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a function type, which might be a non-generic function, or a specialization of a
|
||||
/// generic function.
|
||||
#[salsa::interned(debug)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct FunctionType<'db> {
|
||||
pub(crate) literal: FunctionLiteral<'db>,
|
||||
|
||||
/// Type mappings that should be applied to the function's parameter and return types. This
|
||||
/// might include specializations of enclosing generic contexts (e.g. for non-generic methods
|
||||
/// of a specialized generic class).
|
||||
#[returns(deref)]
|
||||
type_mappings: Box<[TypeMapping<'db, 'db>]>,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> FunctionType<'db> {
|
||||
pub(crate) fn with_inherited_generic_context(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
inherited_generic_context: GenericContext<'db>,
|
||||
) -> Self {
|
||||
let literal = self
|
||||
.literal(db)
|
||||
.with_inherited_generic_context(db, inherited_generic_context);
|
||||
Self::new(db, literal, self.type_mappings(db))
|
||||
}
|
||||
|
||||
pub(crate) fn with_type_mapping<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
type_mapping: &TypeMapping<'a, 'db>,
|
||||
) -> Self {
|
||||
let type_mappings: Box<[_]> = self
|
||||
.type_mappings(db)
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(type_mapping.to_owned()))
|
||||
.collect();
|
||||
Self::new(db, self.literal(db), type_mappings)
|
||||
}
|
||||
|
||||
pub(crate) fn with_dataclass_transformer_params(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
params: DataclassTransformerParams,
|
||||
) -> Self {
|
||||
// A decorator only applies to the specific overload that it is attached to, not to all
|
||||
// previous overloads.
|
||||
let literal = self.literal(db);
|
||||
let last_definition = literal
|
||||
.last_definition(db)
|
||||
.with_dataclass_transformer_params(db, params);
|
||||
let literal =
|
||||
FunctionLiteral::new(db, last_definition, literal.inherited_generic_context(db));
|
||||
Self::new(db, literal, self.type_mappings(db))
|
||||
}
|
||||
|
||||
/// Returns the [`File`] in which this function is defined.
|
||||
pub(crate) fn file(self, db: &'db dyn Db) -> File {
|
||||
self.literal(db).last_definition(db).file(db)
|
||||
}
|
||||
|
||||
/// Returns the AST node for this function.
|
||||
pub(crate) fn node(self, db: &'db dyn Db, file: File) -> &'db ast::StmtFunctionDef {
|
||||
self.literal(db).last_definition(db).node(db, file)
|
||||
}
|
||||
|
||||
pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name {
|
||||
self.literal(db).name(db)
|
||||
}
|
||||
|
||||
pub(crate) fn known(self, db: &'db dyn Db) -> Option<KnownFunction> {
|
||||
self.literal(db).known(db)
|
||||
}
|
||||
|
||||
pub(crate) fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool {
|
||||
self.known(db) == Some(known_function)
|
||||
}
|
||||
|
||||
/// Returns if any of the overloads of this function have a particular decorator.
|
||||
///
|
||||
/// Some decorators are expected to appear on every overload; others are expected to appear
|
||||
/// only the implementation or first overload. This method does not check either of those
|
||||
/// conditions.
|
||||
pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool {
|
||||
self.literal(db).has_known_decorator(db, decorator)
|
||||
}
|
||||
|
||||
/// Returns the [`Definition`] of the implementation or first overload of this function.
|
||||
///
|
||||
/// ## Warning
|
||||
///
|
||||
/// This uses the semantic index to find the definition of the function. This means that if the
|
||||
/// calling query is not in the same file as this function is defined in, then this will create
|
||||
/// a cross-module dependency directly on the full AST which will lead to cache
|
||||
/// over-invalidation.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
self.literal(db).definition(db)
|
||||
}
|
||||
|
||||
/// Returns a tuple of two spans. The first is
|
||||
/// the span for the identifier of the function
|
||||
/// definition for `self`. The second is
|
||||
/// the span for the parameter in the function
|
||||
/// definition for `self`.
|
||||
///
|
||||
/// If there are no meaningful spans, then this
|
||||
/// returns `None`. For example, when this type
|
||||
/// isn't callable.
|
||||
///
|
||||
/// When `parameter_index` is `None`, then the
|
||||
/// second span returned covers the entire parameter
|
||||
/// list.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// Note that this may introduce cross-module
|
||||
/// dependencies. This can have an impact on
|
||||
/// the effectiveness of incremental caching
|
||||
/// and should therefore be used judiciously.
|
||||
///
|
||||
/// An example of a good use case is to improve
|
||||
/// a diagnostic.
|
||||
pub(crate) fn parameter_span(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
parameter_index: Option<usize>,
|
||||
) -> Option<(Span, Span)> {
|
||||
self.literal(db).parameter_span(db, parameter_index)
|
||||
}
|
||||
|
||||
/// Returns a collection of useful spans for a
|
||||
/// function signature. These are useful for
|
||||
/// creating annotations on diagnostics.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// Note that this may introduce cross-module
|
||||
/// dependencies. This can have an impact on
|
||||
/// the effectiveness of incremental caching
|
||||
/// and should therefore be used judiciously.
|
||||
///
|
||||
/// An example of a good use case is to improve
|
||||
/// a diagnostic.
|
||||
pub(crate) fn spans(self, db: &'db dyn Db) -> Option<FunctionSpans> {
|
||||
self.literal(db).spans(db)
|
||||
}
|
||||
|
||||
/// Returns all of the overload signatures and the implementation definition, if any, of this
|
||||
/// function. The overload signatures will be in source order.
|
||||
pub(crate) fn overloads_and_implementation(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> &'db (Box<[OverloadLiteral<'db>]>, Option<OverloadLiteral<'db>>) {
|
||||
self.literal(db).overloads_and_implementation(db)
|
||||
}
|
||||
|
||||
/// Returns an iterator of all of the definitions of this function, including both overload
|
||||
/// signatures and any implementation, all in source order.
|
||||
pub(crate) fn iter_overloads_and_implementation(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> impl Iterator<Item = OverloadLiteral<'db>> + 'db {
|
||||
self.literal(db).iter_overloads_and_implementation(db)
|
||||
}
|
||||
|
||||
/// Typed externally-visible signature for this function.
|
||||
///
|
||||
/// This is the signature as seen by external callers, possibly modified by decorators and/or
|
||||
/// overloaded.
|
||||
///
|
||||
/// ## Why is this a salsa query?
|
||||
///
|
||||
/// This is a salsa query to short-circuit the invalidation
|
||||
/// when the function's AST node changes.
|
||||
///
|
||||
/// Were this not a salsa query, then the calling query
|
||||
/// would depend on the function's AST and rerun for every change in that file.
|
||||
#[salsa::tracked(returns(ref), cycle_fn=signature_cycle_recover, cycle_initial=signature_cycle_initial)]
|
||||
pub(crate) fn signature(self, db: &'db dyn Db) -> CallableSignature<'db> {
|
||||
self.literal(db).signature(db, self.type_mappings(db))
|
||||
}
|
||||
|
||||
/// Convert the `FunctionType` into a [`Type::Callable`].
|
||||
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
Type::Callable(CallableType::new(db, self.signature(db), false))
|
||||
}
|
||||
|
||||
/// Convert the `FunctionType` into a [`Type::BoundMethod`].
|
||||
pub(crate) fn into_bound_method_type(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
self_instance: Type<'db>,
|
||||
) -> Type<'db> {
|
||||
Type::BoundMethod(BoundMethodType::new(db, self, self_instance))
|
||||
}
|
||||
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
// A function type is the subtype of itself, and not of any other function type. However,
|
||||
// our representation of a function type includes any specialization that should be applied
|
||||
// to the signature. Different specializations of the same function type are only subtypes
|
||||
// of each other if they result in subtype signatures.
|
||||
if self.normalized(db) == other.normalized(db) {
|
||||
return true;
|
||||
}
|
||||
if self.literal(db) != other.literal(db) {
|
||||
return false;
|
||||
}
|
||||
let self_signature = self.signature(db);
|
||||
let other_signature = other.signature(db);
|
||||
if !self_signature.is_fully_static(db) || !other_signature.is_fully_static(db) {
|
||||
return false;
|
||||
}
|
||||
self_signature.is_subtype_of(db, other_signature)
|
||||
}
|
||||
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
// A function type is assignable to itself, and not to any other function type. However,
|
||||
// our representation of a function type includes any specialization that should be applied
|
||||
// to the signature. Different specializations of the same function type are only
|
||||
// assignable to each other if they result in assignable signatures.
|
||||
self.literal(db) == other.literal(db)
|
||||
&& self.signature(db).is_assignable_to(db, other.signature(db))
|
||||
}
|
||||
|
||||
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
if self.normalized(db) == other.normalized(db) {
|
||||
return true;
|
||||
}
|
||||
if self.literal(db) != other.literal(db) {
|
||||
return false;
|
||||
}
|
||||
let self_signature = self.signature(db);
|
||||
let other_signature = other.signature(db);
|
||||
if !self_signature.is_fully_static(db) || !other_signature.is_fully_static(db) {
|
||||
return false;
|
||||
}
|
||||
self_signature.is_equivalent_to(db, other_signature)
|
||||
}
|
||||
|
||||
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.literal(db) == other.literal(db)
|
||||
&& self
|
||||
.signature(db)
|
||||
.is_gradual_equivalent_to(db, other.signature(db))
|
||||
}
|
||||
|
||||
pub(crate) fn find_legacy_typevars(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
|
||||
) {
|
||||
let signatures = self.signature(db);
|
||||
for signature in &signatures.overloads {
|
||||
signature.find_legacy_typevars(db, typevars);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
|
||||
let mappings: Box<_> = self
|
||||
.type_mappings(db)
|
||||
.iter()
|
||||
.map(|mapping| mapping.normalized(db))
|
||||
.collect();
|
||||
Self::new(db, self.literal(db).normalized(db), mappings)
|
||||
}
|
||||
}
|
||||
|
||||
fn signature_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &CallableSignature<'db>,
|
||||
_count: u32,
|
||||
_function: FunctionType<'db>,
|
||||
) -> salsa::CycleRecoveryAction<CallableSignature<'db>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn signature_cycle_initial<'db>(
|
||||
db: &'db dyn Db,
|
||||
_function: FunctionType<'db>,
|
||||
) -> CallableSignature<'db> {
|
||||
CallableSignature::single(Signature::bottom(db))
|
||||
}
|
||||
|
||||
/// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might
|
||||
/// have special behavior.
|
||||
#[derive(
|
||||
Debug, Copy, Clone, PartialEq, Eq, Hash, strum_macros::EnumString, strum_macros::IntoStaticStr,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[cfg_attr(test, derive(strum_macros::EnumIter))]
|
||||
pub enum KnownFunction {
|
||||
/// `builtins.isinstance`
|
||||
#[strum(serialize = "isinstance")]
|
||||
IsInstance,
|
||||
/// `builtins.issubclass`
|
||||
#[strum(serialize = "issubclass")]
|
||||
IsSubclass,
|
||||
/// `builtins.hasattr`
|
||||
#[strum(serialize = "hasattr")]
|
||||
HasAttr,
|
||||
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
|
||||
RevealType,
|
||||
/// `builtins.len`
|
||||
Len,
|
||||
/// `builtins.repr`
|
||||
Repr,
|
||||
/// `typing(_extensions).final`
|
||||
Final,
|
||||
|
||||
/// [`typing(_extensions).no_type_check`](https://typing.python.org/en/latest/spec/directives.html#no-type-check)
|
||||
NoTypeCheck,
|
||||
|
||||
/// `typing(_extensions).assert_type`
|
||||
AssertType,
|
||||
/// `typing(_extensions).assert_never`
|
||||
AssertNever,
|
||||
/// `typing(_extensions).cast`
|
||||
Cast,
|
||||
/// `typing(_extensions).overload`
|
||||
Overload,
|
||||
/// `typing(_extensions).override`
|
||||
Override,
|
||||
/// `typing(_extensions).is_protocol`
|
||||
IsProtocol,
|
||||
/// `typing(_extensions).get_protocol_members`
|
||||
GetProtocolMembers,
|
||||
/// `typing(_extensions).runtime_checkable`
|
||||
RuntimeCheckable,
|
||||
/// `typing(_extensions).dataclass_transform`
|
||||
DataclassTransform,
|
||||
|
||||
/// `abc.abstractmethod`
|
||||
#[strum(serialize = "abstractmethod")]
|
||||
AbstractMethod,
|
||||
|
||||
/// `dataclasses.dataclass`
|
||||
Dataclass,
|
||||
|
||||
/// `inspect.getattr_static`
|
||||
GetattrStatic,
|
||||
|
||||
/// `ty_extensions.static_assert`
|
||||
StaticAssert,
|
||||
/// `ty_extensions.is_equivalent_to`
|
||||
IsEquivalentTo,
|
||||
/// `ty_extensions.is_subtype_of`
|
||||
IsSubtypeOf,
|
||||
/// `ty_extensions.is_assignable_to`
|
||||
IsAssignableTo,
|
||||
/// `ty_extensions.is_disjoint_from`
|
||||
IsDisjointFrom,
|
||||
/// `ty_extensions.is_gradual_equivalent_to`
|
||||
IsGradualEquivalentTo,
|
||||
/// `ty_extensions.is_fully_static`
|
||||
IsFullyStatic,
|
||||
/// `ty_extensions.is_singleton`
|
||||
IsSingleton,
|
||||
/// `ty_extensions.is_single_valued`
|
||||
IsSingleValued,
|
||||
/// `ty_extensions.generic_context`
|
||||
GenericContext,
|
||||
/// `ty_extensions.dunder_all_names`
|
||||
DunderAllNames,
|
||||
/// `ty_extensions.all_members`
|
||||
AllMembers,
|
||||
}
|
||||
|
||||
impl KnownFunction {
|
||||
pub fn into_classinfo_constraint_function(self) -> Option<ClassInfoConstraintFunction> {
|
||||
match self {
|
||||
Self::IsInstance => Some(ClassInfoConstraintFunction::IsInstance),
|
||||
Self::IsSubclass => Some(ClassInfoConstraintFunction::IsSubclass),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_definition_and_name<'db>(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
name: &str,
|
||||
) -> Option<Self> {
|
||||
let candidate = Self::from_str(name).ok()?;
|
||||
candidate
|
||||
.check_module(file_to_module(db, definition.file(db))?.known()?)
|
||||
.then_some(candidate)
|
||||
}
|
||||
|
||||
/// 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::AssertType
|
||||
| Self::AssertNever
|
||||
| Self::Cast
|
||||
| Self::Overload
|
||||
| Self::Override
|
||||
| Self::RevealType
|
||||
| Self::Final
|
||||
| Self::IsProtocol
|
||||
| Self::GetProtocolMembers
|
||||
| Self::RuntimeCheckable
|
||||
| Self::DataclassTransform
|
||||
| Self::NoTypeCheck => {
|
||||
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
|
||||
}
|
||||
Self::AbstractMethod => {
|
||||
matches!(module, KnownModule::Abc)
|
||||
}
|
||||
Self::Dataclass => {
|
||||
matches!(module, KnownModule::Dataclasses)
|
||||
}
|
||||
Self::GetattrStatic => module.is_inspect(),
|
||||
Self::IsAssignableTo
|
||||
| Self::IsDisjointFrom
|
||||
| Self::IsEquivalentTo
|
||||
| Self::IsGradualEquivalentTo
|
||||
| Self::IsFullyStatic
|
||||
| Self::IsSingleValued
|
||||
| Self::IsSingleton
|
||||
| Self::IsSubtypeOf
|
||||
| Self::GenericContext
|
||||
| Self::DunderAllNames
|
||||
| Self::StaticAssert
|
||||
| Self::AllMembers => module.is_ty_extensions(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use super::*;
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::symbol::known_module_symbol;
|
||||
|
||||
#[test]
|
||||
fn known_function_roundtrip_from_str() {
|
||||
let db = setup_db();
|
||||
|
||||
for function in KnownFunction::iter() {
|
||||
let function_name: &'static str = function.into();
|
||||
|
||||
let module = match function {
|
||||
KnownFunction::Len
|
||||
| KnownFunction::Repr
|
||||
| KnownFunction::IsInstance
|
||||
| KnownFunction::HasAttr
|
||||
| KnownFunction::IsSubclass => KnownModule::Builtins,
|
||||
|
||||
KnownFunction::AbstractMethod => KnownModule::Abc,
|
||||
|
||||
KnownFunction::Dataclass => KnownModule::Dataclasses,
|
||||
|
||||
KnownFunction::GetattrStatic => KnownModule::Inspect,
|
||||
|
||||
KnownFunction::Cast
|
||||
| KnownFunction::Final
|
||||
| KnownFunction::Overload
|
||||
| KnownFunction::Override
|
||||
| KnownFunction::RevealType
|
||||
| KnownFunction::AssertType
|
||||
| KnownFunction::AssertNever
|
||||
| KnownFunction::IsProtocol
|
||||
| KnownFunction::GetProtocolMembers
|
||||
| KnownFunction::RuntimeCheckable
|
||||
| KnownFunction::DataclassTransform
|
||||
| KnownFunction::NoTypeCheck => KnownModule::TypingExtensions,
|
||||
|
||||
KnownFunction::IsSingleton
|
||||
| KnownFunction::IsSubtypeOf
|
||||
| KnownFunction::GenericContext
|
||||
| KnownFunction::DunderAllNames
|
||||
| KnownFunction::StaticAssert
|
||||
| KnownFunction::IsFullyStatic
|
||||
| KnownFunction::IsDisjointFrom
|
||||
| KnownFunction::IsSingleValued
|
||||
| KnownFunction::IsAssignableTo
|
||||
| KnownFunction::IsEquivalentTo
|
||||
| KnownFunction::IsGradualEquivalentTo
|
||||
| KnownFunction::AllMembers => KnownModule::TyExtensions,
|
||||
};
|
||||
|
||||
let function_definition = known_module_symbol(&db, module, function_name)
|
||||
.symbol
|
||||
.expect_type()
|
||||
.expect_function_literal()
|
||||
.definition(&db);
|
||||
|
||||
assert_eq!(
|
||||
KnownFunction::try_from_definition_and_name(
|
||||
&db,
|
||||
function_definition,
|
||||
function_name
|
||||
),
|
||||
Some(function),
|
||||
"The strum `EnumString` implementation appears to be incorrect for `{function_name}`"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -81,19 +81,21 @@ use crate::types::diagnostic::{
|
|||
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,
|
||||
};
|
||||
use crate::types::generics::GenericContext;
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::signatures::{CallableSignature, Signature};
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
BareTypeAliasType, CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams,
|
||||
DynamicType, FunctionDecorators, FunctionType, GenericAlias, IntersectionBuilder,
|
||||
IntersectionType, KnownClass, KnownFunction, KnownInstanceType, MemberLookupPolicy,
|
||||
MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType,
|
||||
StringLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type,
|
||||
TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints,
|
||||
TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type,
|
||||
todo_type,
|
||||
DynamicType, GenericAlias, IntersectionBuilder, IntersectionType, KnownClass,
|
||||
KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter,
|
||||
ParameterForm, Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Symbol,
|
||||
SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
|
||||
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
|
||||
TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
|
||||
};
|
||||
use crate::unpack::{Unpack, UnpackPosition};
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
|
@ -1131,12 +1133,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
}
|
||||
|
||||
for function in self.called_functions.union(&public_functions) {
|
||||
let Some(overloaded) = function.to_overloaded(self.db()) else {
|
||||
let (overloads, implementation) = function.overloads_and_implementation(self.db());
|
||||
if overloads.is_empty() {
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
// Check that the overloaded function has at least two overloads
|
||||
if let [single_overload] = overloaded.overloads.as_slice() {
|
||||
if let [single_overload] = overloads.as_ref() {
|
||||
let function_node = function.node(self.db(), self.file());
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
|
@ -1157,7 +1160,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
// Check that the overloaded function has an implementation. Overload definitions
|
||||
// within stub files, protocols, and on abstract methods within abstract base classes
|
||||
// are exempt from this check.
|
||||
if overloaded.implementation.is_none() && !self.in_stub() {
|
||||
if implementation.is_none() && !self.in_stub() {
|
||||
let mut implementation_required = true;
|
||||
|
||||
if let NodeWithScopeKind::Class(class_node_ref) = scope {
|
||||
|
@ -1169,7 +1172,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
|
||||
if class.is_protocol(self.db())
|
||||
|| (class.is_abstract(self.db())
|
||||
&& overloaded.overloads.iter().all(|overload| {
|
||||
&& overloads.iter().all(|overload| {
|
||||
overload.has_known_decorator(
|
||||
self.db(),
|
||||
FunctionDecorators::ABSTRACT_METHOD,
|
||||
|
@ -1199,7 +1202,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
let mut decorator_present = false;
|
||||
let mut decorator_missing = vec![];
|
||||
|
||||
for function in overloaded.all() {
|
||||
for function in overloads.iter().chain(implementation.as_ref()) {
|
||||
if function.has_known_decorator(self.db(), decorator) {
|
||||
decorator_present = true;
|
||||
} else {
|
||||
|
@ -1240,8 +1243,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
(FunctionDecorators::FINAL, "final"),
|
||||
(FunctionDecorators::OVERRIDE, "override"),
|
||||
] {
|
||||
if let Some(implementation) = overloaded.implementation.as_ref() {
|
||||
for overload in &overloaded.overloads {
|
||||
if let Some(implementation) = implementation {
|
||||
for overload in overloads.as_ref() {
|
||||
if !overload.has_known_decorator(self.db(), decorator) {
|
||||
continue;
|
||||
}
|
||||
|
@ -1263,7 +1266,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
let mut overloads = overloaded.overloads.iter();
|
||||
let mut overloads = overloads.iter();
|
||||
let Some(first_overload) = overloads.next() else {
|
||||
continue;
|
||||
};
|
||||
|
@ -2027,7 +2030,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
let function_kind =
|
||||
let known_function =
|
||||
KnownFunction::try_from_definition_and_name(self.db(), definition, name);
|
||||
|
||||
let body_scope = self
|
||||
|
@ -2035,17 +2038,23 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
.node_scope(NodeWithScopeRef::Function(function))
|
||||
.to_scope_id(self.db(), self.file());
|
||||
|
||||
let inherited_generic_context = None;
|
||||
let type_mappings = Box::from([]);
|
||||
|
||||
let mut inferred_ty = Type::FunctionLiteral(FunctionType::new(
|
||||
let overload_literal = OverloadLiteral::new(
|
||||
self.db(),
|
||||
&name.id,
|
||||
function_kind,
|
||||
known_function,
|
||||
body_scope,
|
||||
function_decorators,
|
||||
dataclass_transformer_params,
|
||||
inherited_generic_context,
|
||||
);
|
||||
|
||||
let inherited_generic_context = None;
|
||||
let function_literal =
|
||||
FunctionLiteral::new(self.db(), overload_literal, inherited_generic_context);
|
||||
|
||||
let type_mappings = Box::from([]);
|
||||
let mut inferred_ty = Type::FunctionLiteral(FunctionType::new(
|
||||
self.db(),
|
||||
function_literal,
|
||||
type_mappings,
|
||||
));
|
||||
|
||||
|
@ -2314,14 +2323,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
// overload, or an overload and the implementation both. Nevertheless, this is not
|
||||
// allowed. We do not try to treat the offenders intelligently -- just use the
|
||||
// params of the last seen usage of `@dataclass_transform`
|
||||
if let Some(overloaded) = f.to_overloaded(self.db()) {
|
||||
overloaded.overloads.iter().for_each(|overload| {
|
||||
if let Some(params) = overload.dataclass_transformer_params(self.db()) {
|
||||
dataclass_params = Some(params.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(params) = f.dataclass_transformer_params(self.db()) {
|
||||
let params = f
|
||||
.iter_overloads_and_implementation(self.db())
|
||||
.find_map(|overload| overload.dataclass_transformer_params(self.db()));
|
||||
if let Some(params) = params {
|
||||
dataclass_params = Some(params.into());
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::semantic_index::predicate::{
|
|||
};
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
|
||||
use crate::semantic_index::symbol_table;
|
||||
use crate::types::function::KnownFunction;
|
||||
use crate::types::infer::infer_same_file_expression_type;
|
||||
use crate::types::{
|
||||
IntersectionBuilder, KnownClass, SubclassOfType, Truthiness, Type, UnionBuilder,
|
||||
|
@ -20,7 +21,7 @@ use ruff_python_ast::{BoolOp, ExprBoolOp};
|
|||
use rustc_hash::FxHashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use super::{KnownFunction, UnionType};
|
||||
use super::UnionType;
|
||||
|
||||
/// Return the type constraint that `test` (if true) would place on `symbol`, if any.
|
||||
///
|
||||
|
|
|
@ -7,9 +7,8 @@ use ruff_python_ast::name::Name;
|
|||
use crate::{
|
||||
semantic_index::{symbol_table, use_def_map},
|
||||
symbol::{symbol_from_bindings, symbol_from_declarations},
|
||||
types::{
|
||||
ClassBase, ClassLiteral, KnownFunction, Type, TypeMapping, TypeQualifiers, TypeVarInstance,
|
||||
},
|
||||
types::function::KnownFunction,
|
||||
types::{ClassBase, ClassLiteral, Type, TypeMapping, TypeQualifiers, TypeVarInstance},
|
||||
{Db, FxOrderSet},
|
||||
};
|
||||
|
||||
|
|
|
@ -53,10 +53,6 @@ impl<'db> CallableSignature<'db> {
|
|||
self.overloads.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn as_slice(&self) -> &[Signature<'db>] {
|
||||
self.overloads.as_slice()
|
||||
}
|
||||
|
||||
pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self {
|
||||
Self::from_overloads(
|
||||
self.overloads
|
||||
|
@ -1538,7 +1534,8 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::db::tests::{TestDb, setup_db};
|
||||
use crate::symbol::global_symbol;
|
||||
use crate::types::{FunctionSignature, FunctionType, KnownClass};
|
||||
use crate::types::KnownClass;
|
||||
use crate::types::function::FunctionType;
|
||||
use ruff_db::system::DbWithWritableSystem as _;
|
||||
|
||||
#[track_caller]
|
||||
|
@ -1559,9 +1556,11 @@ mod tests {
|
|||
fn empty() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented("/src/a.py", "def f(): ...").unwrap();
|
||||
let func = get_function_f(&db, "/src/a.py");
|
||||
let func = get_function_f(&db, "/src/a.py")
|
||||
.literal(&db)
|
||||
.last_definition(&db);
|
||||
|
||||
let sig = func.internal_signature(&db, None);
|
||||
let sig = func.signature(&db, None);
|
||||
|
||||
assert!(sig.return_ty.is_none());
|
||||
assert_params(&sig, &[]);
|
||||
|
@ -1582,9 +1581,11 @@ mod tests {
|
|||
",
|
||||
)
|
||||
.unwrap();
|
||||
let func = get_function_f(&db, "/src/a.py");
|
||||
let func = get_function_f(&db, "/src/a.py")
|
||||
.literal(&db)
|
||||
.last_definition(&db);
|
||||
|
||||
let sig = func.internal_signature(&db, None);
|
||||
let sig = func.signature(&db, None);
|
||||
|
||||
assert_eq!(sig.return_ty.unwrap().display(&db).to_string(), "bytes");
|
||||
assert_params(
|
||||
|
@ -1633,9 +1634,11 @@ mod tests {
|
|||
",
|
||||
)
|
||||
.unwrap();
|
||||
let func = get_function_f(&db, "/src/a.py");
|
||||
let func = get_function_f(&db, "/src/a.py")
|
||||
.literal(&db)
|
||||
.last_definition(&db);
|
||||
|
||||
let sig = func.internal_signature(&db, None);
|
||||
let sig = func.signature(&db, None);
|
||||
|
||||
let [
|
||||
Parameter {
|
||||
|
@ -1669,9 +1672,11 @@ mod tests {
|
|||
",
|
||||
)
|
||||
.unwrap();
|
||||
let func = get_function_f(&db, "/src/a.pyi");
|
||||
let func = get_function_f(&db, "/src/a.pyi")
|
||||
.literal(&db)
|
||||
.last_definition(&db);
|
||||
|
||||
let sig = func.internal_signature(&db, None);
|
||||
let sig = func.signature(&db, None);
|
||||
|
||||
let [
|
||||
Parameter {
|
||||
|
@ -1705,9 +1710,11 @@ mod tests {
|
|||
",
|
||||
)
|
||||
.unwrap();
|
||||
let func = get_function_f(&db, "/src/a.py");
|
||||
let func = get_function_f(&db, "/src/a.py")
|
||||
.literal(&db)
|
||||
.last_definition(&db);
|
||||
|
||||
let sig = func.internal_signature(&db, None);
|
||||
let sig = func.signature(&db, None);
|
||||
|
||||
let [
|
||||
Parameter {
|
||||
|
@ -1751,9 +1758,11 @@ mod tests {
|
|||
",
|
||||
)
|
||||
.unwrap();
|
||||
let func = get_function_f(&db, "/src/a.pyi");
|
||||
let func = get_function_f(&db, "/src/a.pyi")
|
||||
.literal(&db)
|
||||
.last_definition(&db);
|
||||
|
||||
let sig = func.internal_signature(&db, None);
|
||||
let sig = func.signature(&db, None);
|
||||
|
||||
let [
|
||||
Parameter {
|
||||
|
@ -1789,15 +1798,13 @@ mod tests {
|
|||
.unwrap();
|
||||
let func = get_function_f(&db, "/src/a.py");
|
||||
|
||||
let expected_sig = func.internal_signature(&db, None);
|
||||
let overload = func.literal(&db).last_definition(&db);
|
||||
let expected_sig = overload.signature(&db, None);
|
||||
|
||||
// With no decorators, internal and external signature are the same
|
||||
assert_eq!(
|
||||
func.signature(&db),
|
||||
&FunctionSignature {
|
||||
overloads: CallableSignature::single(expected_sig),
|
||||
implementation: None
|
||||
},
|
||||
&CallableSignature::single(expected_sig)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue