From 03fb2e5ac1481e498f84474800b42a966e9843e1 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 10 Dec 2024 12:03:29 -0800 Subject: [PATCH] [red-knot] split call-outcome enums to their own submodule (#14898) ## Summary This is already several hundred lines of code, and it will get more complex with call-signature checking. ## Test Plan This is a pure code move; the moved code wasn't changed, just imports. Existing tests pass. --- crates/red_knot_python_semantic/src/types.rs | 313 +----------------- .../src/types/call.rs | 312 +++++++++++++++++ 2 files changed, 316 insertions(+), 309 deletions(-) create mode 100644 crates/red_knot_python_semantic/src/types/call.rs diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 308ef49aeb..04aa2dad46 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2,7 +2,7 @@ use std::hash::Hash; use indexmap::IndexSet; use itertools::Itertools; -use ruff_db::diagnostic::{DiagnosticId, Severity}; +use ruff_db::diagnostic::Severity; use ruff_db::files::File; use ruff_python_ast as ast; @@ -25,12 +25,14 @@ use crate::stdlib::{ builtins_symbol, core_module_symbol, typing_extensions_symbol, CoreStdlibModule, }; use crate::symbol::{Boundness, Symbol}; -use crate::types::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE}; +use crate::types::call::{CallDunderResult, CallOutcome}; +use crate::types::diagnostic::TypeCheckDiagnosticsBuilder; use crate::types::mro::{ClassBase, Mro, MroError, MroIterator}; use crate::types::narrow::narrowing_constraint; use crate::{Db, FxOrderSet, Module, Program, PythonVersion}; mod builder; +mod call; mod diagnostic; mod display; mod infer; @@ -2214,313 +2216,6 @@ pub enum TypeVarBoundOrConstraints<'db> { Constraints(TupleType<'db>), } -#[derive(Debug, Clone, PartialEq, Eq)] -enum CallOutcome<'db> { - Callable { - return_ty: Type<'db>, - }, - RevealType { - return_ty: Type<'db>, - revealed_ty: Type<'db>, - }, - NotCallable { - not_callable_ty: Type<'db>, - }, - Union { - called_ty: Type<'db>, - outcomes: Box<[CallOutcome<'db>]>, - }, - PossiblyUnboundDunderCall { - called_ty: Type<'db>, - call_outcome: Box>, - }, -} - -impl<'db> CallOutcome<'db> { - /// Create a new `CallOutcome::Callable` with given return type. - fn callable(return_ty: Type<'db>) -> CallOutcome<'db> { - CallOutcome::Callable { return_ty } - } - - /// Create a new `CallOutcome::NotCallable` with given not-callable type. - fn not_callable(not_callable_ty: Type<'db>) -> CallOutcome<'db> { - CallOutcome::NotCallable { not_callable_ty } - } - - /// Create a new `CallOutcome::RevealType` with given revealed and return types. - fn revealed(return_ty: Type<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> { - CallOutcome::RevealType { - return_ty, - revealed_ty, - } - } - - /// Create a new `CallOutcome::Union` with given wrapped outcomes. - fn union( - called_ty: Type<'db>, - outcomes: impl IntoIterator>, - ) -> CallOutcome<'db> { - CallOutcome::Union { - called_ty, - outcomes: outcomes.into_iter().collect(), - } - } - - /// Get the return type of the call, or `None` if not callable. - fn return_ty(&self, db: &'db dyn Db) -> Option> { - match self { - Self::Callable { return_ty } => Some(*return_ty), - Self::RevealType { - return_ty, - revealed_ty: _, - } => Some(*return_ty), - Self::NotCallable { not_callable_ty: _ } => None, - Self::Union { - outcomes, - called_ty: _, - } => outcomes - .iter() - // If all outcomes are NotCallable, we return None; if some outcomes are callable - // and some are not, we return a union including Unknown. - .fold(None, |acc, outcome| { - let ty = outcome.return_ty(db); - match (acc, ty) { - (None, None) => None, - (None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)), - (Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::Unknown))), - } - }) - .map(UnionBuilder::build), - Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db), - } - } - - /// Get the return type of the call, emitting default diagnostics if needed. - fn unwrap_with_diagnostic<'a>( - &self, - db: &'db dyn Db, - node: ast::AnyNodeRef, - diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>, - ) -> Type<'db> { - match self.return_ty_result(db, node, diagnostics) { - Ok(return_ty) => return_ty, - Err(NotCallableError::Type { - not_callable_ty, - return_ty, - }) => { - diagnostics.add_lint( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable", - not_callable_ty.display(db) - ), - ); - return_ty - } - Err(NotCallableError::UnionElement { - not_callable_ty, - called_ty, - return_ty, - }) => { - diagnostics.add_lint( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable (due to union element `{}`)", - called_ty.display(db), - not_callable_ty.display(db), - ), - ); - return_ty - } - Err(NotCallableError::UnionElements { - not_callable_tys, - called_ty, - return_ty, - }) => { - diagnostics.add_lint( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable (due to union elements {})", - called_ty.display(db), - not_callable_tys.display(db), - ), - ); - return_ty - } - Err(NotCallableError::PossiblyUnboundDunderCall { - callable_ty: called_ty, - return_ty, - }) => { - diagnostics.add_lint( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable (possibly unbound `__call__` method)", - called_ty.display(db) - ), - ); - return_ty - } - } - } - - /// Get the return type of the call as a result. - fn return_ty_result<'a>( - &self, - db: &'db dyn Db, - node: ast::AnyNodeRef, - diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>, - ) -> Result, NotCallableError<'db>> { - match self { - Self::Callable { return_ty } => Ok(*return_ty), - Self::RevealType { - return_ty, - revealed_ty, - } => { - diagnostics.add( - node, - DiagnosticId::RevealedType, - Severity::Info, - format_args!("Revealed type is `{}`", revealed_ty.display(db)), - ); - Ok(*return_ty) - } - Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type { - not_callable_ty: *not_callable_ty, - return_ty: Type::Unknown, - }), - Self::PossiblyUnboundDunderCall { - called_ty, - call_outcome, - } => Err(NotCallableError::PossiblyUnboundDunderCall { - callable_ty: *called_ty, - return_ty: call_outcome.return_ty(db).unwrap_or(Type::Unknown), - }), - Self::Union { - outcomes, - called_ty, - } => { - let mut not_callable = vec![]; - let mut union_builder = UnionBuilder::new(db); - let mut revealed = false; - for outcome in outcomes { - let return_ty = match outcome { - Self::NotCallable { not_callable_ty } => { - not_callable.push(*not_callable_ty); - Type::Unknown - } - Self::RevealType { - return_ty, - revealed_ty: _, - } => { - if revealed { - *return_ty - } else { - revealed = true; - outcome.unwrap_with_diagnostic(db, node, diagnostics) - } - } - _ => outcome.unwrap_with_diagnostic(db, node, diagnostics), - }; - union_builder = union_builder.add(return_ty); - } - let return_ty = union_builder.build(); - match not_callable[..] { - [] => Ok(return_ty), - [elem] => Err(NotCallableError::UnionElement { - not_callable_ty: elem, - called_ty: *called_ty, - return_ty, - }), - _ if not_callable.len() == outcomes.len() => Err(NotCallableError::Type { - not_callable_ty: *called_ty, - return_ty, - }), - _ => Err(NotCallableError::UnionElements { - not_callable_tys: not_callable.into_boxed_slice(), - called_ty: *called_ty, - return_ty, - }), - } - } - } - } -} - -enum CallDunderResult<'db> { - CallOutcome(CallOutcome<'db>), - PossiblyUnbound(CallOutcome<'db>), - MethodNotAvailable, -} - -impl<'db> CallDunderResult<'db> { - fn return_ty(&self, db: &'db dyn Db) -> Option> { - match self { - Self::CallOutcome(outcome) => outcome.return_ty(db), - Self::PossiblyUnbound { .. } => None, - Self::MethodNotAvailable => None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum NotCallableError<'db> { - /// The type is not callable. - Type { - not_callable_ty: Type<'db>, - return_ty: Type<'db>, - }, - /// A single union element is not callable. - UnionElement { - not_callable_ty: Type<'db>, - called_ty: Type<'db>, - return_ty: Type<'db>, - }, - /// Multiple (but not all) union elements are not callable. - UnionElements { - not_callable_tys: Box<[Type<'db>]>, - called_ty: Type<'db>, - return_ty: Type<'db>, - }, - PossiblyUnboundDunderCall { - callable_ty: Type<'db>, - return_ty: Type<'db>, - }, -} - -impl<'db> NotCallableError<'db> { - /// The return type that should be used when a call is not callable. - fn return_ty(&self) -> Type<'db> { - match self { - Self::Type { return_ty, .. } => *return_ty, - Self::UnionElement { return_ty, .. } => *return_ty, - Self::UnionElements { return_ty, .. } => *return_ty, - Self::PossiblyUnboundDunderCall { return_ty, .. } => *return_ty, - } - } - - /// The resolved type that was not callable. - /// - /// For unions, returns the union type itself, which may contain a mix of callable and - /// non-callable types. - fn called_ty(&self) -> Type<'db> { - match self { - Self::Type { - not_callable_ty, .. - } => *not_callable_ty, - Self::UnionElement { called_ty, .. } => *called_ty, - Self::UnionElements { called_ty, .. } => *called_ty, - Self::PossiblyUnboundDunderCall { - callable_ty: called_ty, - .. - } => *called_ty, - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum IterationOutcome<'db> { Iterable { diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs new file mode 100644 index 0000000000..b573db1139 --- /dev/null +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -0,0 +1,312 @@ +use super::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE}; +use super::{Severity, Type, TypeArrayDisplay, UnionBuilder}; +use crate::Db; +use ruff_db::diagnostic::DiagnosticId; +use ruff_python_ast as ast; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum CallOutcome<'db> { + Callable { + return_ty: Type<'db>, + }, + RevealType { + return_ty: Type<'db>, + revealed_ty: Type<'db>, + }, + NotCallable { + not_callable_ty: Type<'db>, + }, + Union { + called_ty: Type<'db>, + outcomes: Box<[CallOutcome<'db>]>, + }, + PossiblyUnboundDunderCall { + called_ty: Type<'db>, + call_outcome: Box>, + }, +} + +impl<'db> CallOutcome<'db> { + /// Create a new `CallOutcome::Callable` with given return type. + pub(super) fn callable(return_ty: Type<'db>) -> CallOutcome<'db> { + CallOutcome::Callable { return_ty } + } + + /// Create a new `CallOutcome::NotCallable` with given not-callable type. + pub(super) fn not_callable(not_callable_ty: Type<'db>) -> CallOutcome<'db> { + CallOutcome::NotCallable { not_callable_ty } + } + + /// Create a new `CallOutcome::RevealType` with given revealed and return types. + pub(super) fn revealed(return_ty: Type<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> { + CallOutcome::RevealType { + return_ty, + revealed_ty, + } + } + + /// Create a new `CallOutcome::Union` with given wrapped outcomes. + pub(super) fn union( + called_ty: Type<'db>, + outcomes: impl IntoIterator>, + ) -> CallOutcome<'db> { + CallOutcome::Union { + called_ty, + outcomes: outcomes.into_iter().collect(), + } + } + + /// Get the return type of the call, or `None` if not callable. + pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option> { + match self { + Self::Callable { return_ty } => Some(*return_ty), + Self::RevealType { + return_ty, + revealed_ty: _, + } => Some(*return_ty), + Self::NotCallable { not_callable_ty: _ } => None, + Self::Union { + outcomes, + called_ty: _, + } => outcomes + .iter() + // If all outcomes are NotCallable, we return None; if some outcomes are callable + // and some are not, we return a union including Unknown. + .fold(None, |acc, outcome| { + let ty = outcome.return_ty(db); + match (acc, ty) { + (None, None) => None, + (None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)), + (Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::Unknown))), + } + }) + .map(UnionBuilder::build), + Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db), + } + } + + /// Get the return type of the call, emitting default diagnostics if needed. + pub(super) fn unwrap_with_diagnostic<'a>( + &self, + db: &'db dyn Db, + node: ast::AnyNodeRef, + diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>, + ) -> Type<'db> { + match self.return_ty_result(db, node, diagnostics) { + Ok(return_ty) => return_ty, + Err(NotCallableError::Type { + not_callable_ty, + return_ty, + }) => { + diagnostics.add_lint( + &CALL_NON_CALLABLE, + node, + format_args!( + "Object of type `{}` is not callable", + not_callable_ty.display(db) + ), + ); + return_ty + } + Err(NotCallableError::UnionElement { + not_callable_ty, + called_ty, + return_ty, + }) => { + diagnostics.add_lint( + &CALL_NON_CALLABLE, + node, + format_args!( + "Object of type `{}` is not callable (due to union element `{}`)", + called_ty.display(db), + not_callable_ty.display(db), + ), + ); + return_ty + } + Err(NotCallableError::UnionElements { + not_callable_tys, + called_ty, + return_ty, + }) => { + diagnostics.add_lint( + &CALL_NON_CALLABLE, + node, + format_args!( + "Object of type `{}` is not callable (due to union elements {})", + called_ty.display(db), + not_callable_tys.display(db), + ), + ); + return_ty + } + Err(NotCallableError::PossiblyUnboundDunderCall { + callable_ty: called_ty, + return_ty, + }) => { + diagnostics.add_lint( + &CALL_NON_CALLABLE, + node, + format_args!( + "Object of type `{}` is not callable (possibly unbound `__call__` method)", + called_ty.display(db) + ), + ); + return_ty + } + } + } + + /// Get the return type of the call as a result. + pub(super) fn return_ty_result<'a>( + &self, + db: &'db dyn Db, + node: ast::AnyNodeRef, + diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>, + ) -> Result, NotCallableError<'db>> { + match self { + Self::Callable { return_ty } => Ok(*return_ty), + Self::RevealType { + return_ty, + revealed_ty, + } => { + diagnostics.add( + node, + DiagnosticId::RevealedType, + Severity::Info, + format_args!("Revealed type is `{}`", revealed_ty.display(db)), + ); + Ok(*return_ty) + } + Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type { + not_callable_ty: *not_callable_ty, + return_ty: Type::Unknown, + }), + Self::PossiblyUnboundDunderCall { + called_ty, + call_outcome, + } => Err(NotCallableError::PossiblyUnboundDunderCall { + callable_ty: *called_ty, + return_ty: call_outcome.return_ty(db).unwrap_or(Type::Unknown), + }), + Self::Union { + outcomes, + called_ty, + } => { + let mut not_callable = vec![]; + let mut union_builder = UnionBuilder::new(db); + let mut revealed = false; + for outcome in outcomes { + let return_ty = match outcome { + Self::NotCallable { not_callable_ty } => { + not_callable.push(*not_callable_ty); + Type::Unknown + } + Self::RevealType { + return_ty, + revealed_ty: _, + } => { + if revealed { + *return_ty + } else { + revealed = true; + outcome.unwrap_with_diagnostic(db, node, diagnostics) + } + } + _ => outcome.unwrap_with_diagnostic(db, node, diagnostics), + }; + union_builder = union_builder.add(return_ty); + } + let return_ty = union_builder.build(); + match not_callable[..] { + [] => Ok(return_ty), + [elem] => Err(NotCallableError::UnionElement { + not_callable_ty: elem, + called_ty: *called_ty, + return_ty, + }), + _ if not_callable.len() == outcomes.len() => Err(NotCallableError::Type { + not_callable_ty: *called_ty, + return_ty, + }), + _ => Err(NotCallableError::UnionElements { + not_callable_tys: not_callable.into_boxed_slice(), + called_ty: *called_ty, + return_ty, + }), + } + } + } + } +} + +pub(super) enum CallDunderResult<'db> { + CallOutcome(CallOutcome<'db>), + PossiblyUnbound(CallOutcome<'db>), + MethodNotAvailable, +} + +impl<'db> CallDunderResult<'db> { + pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option> { + match self { + Self::CallOutcome(outcome) => outcome.return_ty(db), + Self::PossiblyUnbound { .. } => None, + Self::MethodNotAvailable => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum NotCallableError<'db> { + /// The type is not callable. + Type { + not_callable_ty: Type<'db>, + return_ty: Type<'db>, + }, + /// A single union element is not callable. + UnionElement { + not_callable_ty: Type<'db>, + called_ty: Type<'db>, + return_ty: Type<'db>, + }, + /// Multiple (but not all) union elements are not callable. + UnionElements { + not_callable_tys: Box<[Type<'db>]>, + called_ty: Type<'db>, + return_ty: Type<'db>, + }, + PossiblyUnboundDunderCall { + callable_ty: Type<'db>, + return_ty: Type<'db>, + }, +} + +impl<'db> NotCallableError<'db> { + /// The return type that should be used when a call is not callable. + pub(super) fn return_ty(&self) -> Type<'db> { + match self { + Self::Type { return_ty, .. } => *return_ty, + Self::UnionElement { return_ty, .. } => *return_ty, + Self::UnionElements { return_ty, .. } => *return_ty, + Self::PossiblyUnboundDunderCall { return_ty, .. } => *return_ty, + } + } + + /// The resolved type that was not callable. + /// + /// For unions, returns the union type itself, which may contain a mix of callable and + /// non-callable types. + pub(super) fn called_ty(&self) -> Type<'db> { + match self { + Self::Type { + not_callable_ty, .. + } => *not_callable_ty, + Self::UnionElement { called_ty, .. } => *called_ty, + Self::UnionElements { called_ty, .. } => *called_ty, + Self::PossiblyUnboundDunderCall { + callable_ty: called_ty, + .. + } => *called_ty, + } + } +}