diff --git a/crates/ty_ide/src/docstring.rs b/crates/ty_ide/src/docstring.rs index b749b1feb30bbc..be5adc1052588d 100644 --- a/crates/ty_ide/src/docstring.rs +++ b/crates/ty_ide/src/docstring.rs @@ -65,11 +65,10 @@ impl Docstring { /// Render the docstring for markdown display pub fn render_markdown(&self) -> String { let trimmed = documentation_trim(&self.0); - // TODO: now actually parse it and "render" it to markdown. - // - // For now we just wrap the content in a plaintext codeblock - // to avoid the contents erroneously being interpreted as markdown. - format!("```text\n{trimmed}\n```") + + // Try to parse and render the contents as markdown, + // and if we fail, wrap it in a codeblock and display it raw. + try_render_markdown(&trimmed).unwrap_or_else(|| format!("```text\n{trimmed}\n```")) } /// Extract parameter documentation from popular docstring formats. @@ -153,6 +152,26 @@ fn documentation_trim(docs: &str) -> String { output } +fn try_render_markdown(docstring: &str) -> Option { + let mut output = String::new(); + let mut first_line = true; + for line in docstring.lines() { + // We can assume leading whitespace has been normalized + let trimmed_line = line.trim_start_matches(' '); + let num_leading_spaces = line.len() - trimmed_line.len(); + + if !first_line { + output.push_str(" \n"); + } + for _ in 0..num_leading_spaces { + output.push_str(" "); + } + output.push_str(trimmed_line); + first_line = false; + } + Some(output) +} + /// Extract parameter documentation from Google-style docstrings. fn extract_google_style_params(docstring: &str) -> HashMap { let mut param_docs = HashMap::new(); diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 094d2008d2447f..23ff8e0f53f45f 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -15,7 +15,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_python_semantic::ResolvedDefinition; use ty_python_semantic::types::Type; use ty_python_semantic::types::ide_support::{ - call_signature_details, definitions_for_keyword_argument, + call_signature_details, call_type_simplified_by_overloads, definitions_for_keyword_argument, }; use ty_python_semantic::{ HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol, @@ -326,6 +326,18 @@ impl GotoTarget<'_> { Some(ty) } + /// Try to get a simplified display of this callable type by resolving overloads + pub(crate) fn call_type_simplified_by_overloads( + &self, + model: &SemanticModel, + ) -> Option { + if let GotoTarget::Call { call, .. } = self { + call_type_simplified_by_overloads(model.db(), model, call) + } else { + None + } + } + /// Gets the definitions for this goto target. /// /// The `alias_resolution` parameter controls whether import aliases diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index c1278637a16994..d4463fdd34da07 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -20,7 +20,6 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option Option typevar @@ -62,7 +62,7 @@ pub struct Hover<'db> { impl<'db> Hover<'db> { /// Renders the hover to a string using the specified markup kind. - pub const fn display<'a>(&'a self, db: &'a dyn Db, kind: MarkupKind) -> DisplayHover<'a> { + pub const fn display<'a>(&'a self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHover<'db, 'a> { DisplayHover { db, hover: self, @@ -93,13 +93,13 @@ impl<'a, 'db> IntoIterator for &'a Hover<'db> { } } -pub struct DisplayHover<'a> { - db: &'a dyn Db, - hover: &'a Hover<'a>, +pub struct DisplayHover<'db, 'a> { + db: &'db dyn Db, + hover: &'a Hover<'db>, kind: MarkupKind, } -impl fmt::Display for DisplayHover<'_> { +impl fmt::Display for DisplayHover<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let mut first = true; for content in &self.hover.contents { @@ -115,8 +115,9 @@ impl fmt::Display for DisplayHover<'_> { } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone)] pub enum HoverContent<'db> { + Signature(String), Type(Type<'db>, Option), Docstring(Docstring), } @@ -140,6 +141,9 @@ pub(crate) struct DisplayHoverContent<'a, 'db> { impl fmt::Display for DisplayHoverContent<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.content { + HoverContent::Signature(signature) => { + self.kind.fenced_code_block(&signature, "python").fmt(f) + } HoverContent::Type(ty, variance) => { let variance = match variance { Some(TypeVarVariance::Covariant) => " (covariant)", @@ -961,14 +965,12 @@ def ab(a: str): ... assert_snapshot!(test.hover(), @r" (a: int) -> Unknown - (a: str) -> Unknown --------------------------------------------- the int overload --------------------------------------------- ```python (a: int) -> Unknown - (a: str) -> Unknown ``` --- ```text @@ -1025,14 +1027,12 @@ def ab(a: str): .build(); assert_snapshot!(test.hover(), @r#" - (a: int) -> Unknown (a: str) -> Unknown --------------------------------------------- the int overload --------------------------------------------- ```python - (a: int) -> Unknown (a: str) -> Unknown ``` --- @@ -1090,21 +1090,19 @@ def ab(a: int): .build(); assert_snapshot!(test.hover(), @r" - ( + def ab( a: int, b: int ) -> Unknown - (a: int) -> Unknown --------------------------------------------- the two arg overload --------------------------------------------- ```python - ( + def ab( a: int, b: int ) -> Unknown - (a: int) -> Unknown ``` --- ```text @@ -1161,20 +1159,12 @@ def ab(a: int): .build(); assert_snapshot!(test.hover(), @r" - ( - a: int, - b: int - ) -> Unknown (a: int) -> Unknown --------------------------------------------- the two arg overload --------------------------------------------- ```python - ( - a: int, - b: int - ) -> Unknown (a: int) -> Unknown ``` --- @@ -1236,33 +1226,21 @@ def ab(a: int, *, c: int): .build(); assert_snapshot!(test.hover(), @r" - (a: int) -> Unknown - ( + def ab( a: int, *, b: int ) -> Unknown - ( - a: int, - *, - c: int - ) -> Unknown --------------------------------------------- keywordless overload --------------------------------------------- ```python - (a: int) -> Unknown - ( + def ab( a: int, *, b: int ) -> Unknown - ( - a: int, - *, - c: int - ) -> Unknown ``` --- ```text @@ -1323,13 +1301,7 @@ def ab(a: int, *, c: int): .build(); assert_snapshot!(test.hover(), @r" - (a: int) -> Unknown - ( - a: int, - *, - b: int - ) -> Unknown - ( + def ab( a: int, *, c: int @@ -1339,13 +1311,7 @@ def ab(a: int, *, c: int): --------------------------------------------- ```python - (a: int) -> Unknown - ( - a: int, - *, - b: int - ) -> Unknown - ( + def ab( a: int, *, c: int @@ -1397,11 +1363,11 @@ def ab(a: int, *, c: int): ); assert_snapshot!(test.hover(), @r#" - ( + def foo( a: int, b ) -> Unknown - ( + def foo( a: str, b ) -> Unknown @@ -1410,11 +1376,11 @@ def ab(a: int, *, c: int): --------------------------------------------- ```python - ( + def foo( a: int, b ) -> Unknown - ( + def foo( a: str, b ) -> Unknown diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index fc8bf871e5e707..6da85184eae832 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -66,6 +66,39 @@ impl<'a, 'db> CallArguments<'a, 'db> { .collect() } + /// Like [`Self::from_arguments`] but fills as much typing info in as possible. + /// + /// This currently only exists for the LSP usecase, and shouldn't be used in normal + /// typechecking. + pub(crate) fn from_arguments_typed( + arguments: &'a ast::Arguments, + mut infer_argument_type: impl FnMut(Option<&ast::Expr>, &ast::Expr) -> Type<'db>, + ) -> Self { + arguments + .arguments_source_order() + .map(|arg_or_keyword| match arg_or_keyword { + ast::ArgOrKeyword::Arg(arg) => match arg { + ast::Expr::Starred(ast::ExprStarred { value, .. }) => { + let ty = infer_argument_type(Some(arg), value); + (Argument::Variadic, Some(ty)) + } + _ => { + let ty = infer_argument_type(None, arg); + (Argument::Positional, Some(ty)) + } + }, + ast::ArgOrKeyword::Keyword(ast::Keyword { arg, value, .. }) => { + let ty = infer_argument_type(None, value); + if let Some(arg) = arg { + (Argument::Keyword(&arg.id), Some(ty)) + } else { + (Argument::Keywords, Some(ty)) + } + } + }) + .collect() + } + /// Create a [`CallArguments`] with no arguments. pub(crate) fn none() -> Self { Self::default() diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index b8a8a05ac4ca02..e1d9cfe303e40d 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -16,6 +16,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::Db; use crate::module_resolver::file_to_module; +use crate::semantic_index::definition::Definition; use crate::semantic_index::{scope::ScopeKind, semantic_index}; use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; use crate::types::function::{FunctionType, OverloadLiteral}; @@ -40,6 +41,9 @@ pub struct DisplaySettings<'db> { pub qualified: Rc>, /// Whether long unions and literals are displayed in full pub preserve_full_unions: bool, + /// Disallow Signature printing to introduce a name + /// (presumably because we rendered one already) + pub disallow_signature_name: bool, } impl<'db> DisplaySettings<'db> { @@ -59,6 +63,14 @@ impl<'db> DisplaySettings<'db> { } } + #[must_use] + pub fn disallow_signature_name(&self) -> Self { + Self { + disallow_signature_name: true, + ..self.clone() + } + } + #[must_use] pub fn truncate_long_unions(self) -> Self { Self { @@ -473,7 +485,7 @@ impl Display for DisplayRepresentation<'_> { type_parameters = type_parameters, signature = signature .bind_self(self.db, Some(typing_self_ty)) - .display_with(self.db, self.settings.clone()) + .display_with(self.db, self.settings.disallow_signature_name()) ) } signatures => { @@ -768,7 +780,7 @@ impl Display for DisplayOverloadLiteral<'_> { "def {name}{type_parameters}{signature}", name = self.literal.name(self.db), type_parameters = type_parameters, - signature = signature.display_with(self.db, self.settings.clone()) + signature = signature.display_with(self.db, self.settings.disallow_signature_name()) ) } } @@ -810,7 +822,8 @@ impl Display for DisplayFunctionType<'_> { "def {name}{type_parameters}{signature}", name = self.ty.name(self.db), type_parameters = type_parameters, - signature = signature.display_with(self.db, self.settings.clone()) + signature = + signature.display_with(self.db, self.settings.disallow_signature_name()) ) } signatures => { @@ -1081,6 +1094,7 @@ impl<'db> Signature<'db> { settings: DisplaySettings<'db>, ) -> DisplaySignature<'db> { DisplaySignature { + definition: self.definition(), parameters: self.parameters(), return_ty: self.return_ty, db, @@ -1090,6 +1104,7 @@ impl<'db> Signature<'db> { } pub(crate) struct DisplaySignature<'db> { + definition: Option>, parameters: &'db Parameters<'db>, return_ty: Option>, db: &'db dyn Db, @@ -1111,6 +1126,18 @@ impl DisplaySignature<'_> { /// Internal method to write signature with the signature writer fn write_signature(&self, writer: &mut SignatureWriter) -> fmt::Result { let multiline = self.settings.multiline && self.parameters.len() > 1; + // If we're multiline printing and a name hasn't been emitted, try to + // make one up to make things more pretty + if multiline && !self.settings.disallow_signature_name { + writer.write_str("def ")?; + if let Some(definition) = self.definition + && let Some(name) = definition.name(self.db) + { + writer.write_str(&name)?; + } else { + writer.write_str("_")?; + } + } // Opening parenthesis writer.write_char('(')?; if multiline { @@ -1979,7 +2006,7 @@ mod tests { Some(Type::none(&db)) ), @r" - ( + def _( x=int, y: str = str ) -> None @@ -1997,7 +2024,7 @@ mod tests { Some(Type::none(&db)) ), @r" - ( + def _( x, y, / @@ -2016,7 +2043,7 @@ mod tests { Some(Type::none(&db)) ), @r" - ( + def _( x, /, y @@ -2035,7 +2062,7 @@ mod tests { Some(Type::none(&db)) ), @r" - ( + def _( *, x, y @@ -2054,7 +2081,7 @@ mod tests { Some(Type::none(&db)) ), @r" - ( + def _( x, *, y @@ -2093,7 +2120,7 @@ mod tests { Some(KnownClass::Bytes.to_instance(&db)) ), @r" - ( + def _( a, b: int, c=Literal[1], diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 475c3017c7b6fc..8b7b44f7e39d05 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -17,7 +17,7 @@ use crate::types::{ ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type, TypeContext, TypeVarBoundOrConstraints, class::CodeGeneratorKind, }; -use crate::{Db, HasType, NameKind, SemanticModel}; +use crate::{Db, DisplaySettings, HasType, NameKind, SemanticModel}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; @@ -973,6 +973,65 @@ pub fn call_signature_details<'db>( } } +/// Given a call expression that has overloads, and whose overload is resolved to a +/// single option by its arguments, return the type of the Signature. +/// +/// This is only used for simplifying complex call types, so if we ever detect that +/// the given callable type *is* simple, or that our answer *won't* be simple, we +/// bail at out and return None, so that the original type can be used. +/// +/// We do this because `Type::Signature` intentionally loses a lot of context, and +/// so it has a "worse" display than say `Type::FunctionLiteral` or `Type::BoundMethod`, +/// which this analysis would naturally wipe away. The contexts this function +/// succeeds in are those where we would print a complicated/ugly type anyway. +pub fn call_type_simplified_by_overloads<'db>( + db: &'db dyn Db, + model: &SemanticModel<'db>, + call_expr: &ast::ExprCall, +) -> Option { + let func_type = call_expr.func.inferred_type(model); + + // Use into_callable to handle all the complex type conversions + let callable_type = func_type.try_upcast_to_callable(db)?; + let bindings = callable_type.bindings(db); + + // If the callable is trivial this analysis is useless, bail out + if let Some(binding) = bindings.single_element() + && binding.overloads().len() < 2 + { + return None; + } + + // Hand the overload resolution system as much type info as we have + let args = CallArguments::from_arguments_typed(&call_expr.arguments, |_, splatted_value| { + splatted_value.inferred_type(model) + }); + + // Try to resolve overloads with the arguments/types we have + let mut resolved = bindings + .match_parameters(db, &args) + .check_types(db, &args, TypeContext::default(), &[]) + // Only use the Ok + .iter() + .flatten() + .flat_map(|binding| { + binding.matching_overloads().map(|(_, overload)| { + overload + .signature + .display_with(db, DisplaySettings::default().multiline()) + .to_string() + }) + }) + .collect::>(); + + // If at the end of this we still got multiple signatures (or no signatures), give up + if resolved.len() != 1 { + return None; + } + + resolved.pop() +} + /// Returns the definitions of the binary operation along with its callable type. pub fn definitions_for_bin_op<'db>( db: &'db dyn Db,