Skip to content

Conversation

@Gankra
Copy link
Contributor

@Gankra Gankra commented Nov 20, 2025

This PR generalizes the signature_help system's SignatureWriter which could get the subspans of function parameters.
We now have TypeDetailsWriter which is threaded between type's display implementations via a new fmt_detailed method that many of the Display types now have.

With this information we can properly add goto-type targets to our inlay hints. This also lays groundwork for any future "I want to render a type but get spans" work.

Also a ton of lifetimes are introduced to avoid things getting conflated with 'db.

This PR is broken up into a series of commits:

  • Generalizing SignatureWriter to TypeDetailsWriter, but not using it anywhere else. This commit was confirmed to be a non-functional change (no test results changed)
  • Introducing fmt_detailed everywhere to thread through TypeDetailsWriter and annotate various spans as "being" a given Type -- this is also where I had to reckon with a ton of erroneous &'db self. This commit was also confirmed to be a non-functional change.
  • Finally, actually using the results for goto-type on inlay hints!
  • Regenerating snapshots, fixups, etc.

@Gankra Gankra added the server Related to the LSP server label Nov 20, 2025
@Gankra Gankra requested a review from carljm as a code owner November 20, 2025 05:07
@Gankra Gankra added the ty Multi-file analysis & type inference label Nov 20, 2025
@Gankra
Copy link
Contributor Author

Gankra commented Nov 20, 2025

Have had this bee in my bonnet all week the IDE feels SO much better with these being clickable!

@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 20, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

Comment on lines +516 to +517
f.with_detail(TypeDetail::Type(Type::ClassLiteral(self.class)))
.write_str(self.class.name(self.db))?;
Copy link
Contributor Author

@Gankra Gankra Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the single-most important type span we introduce, since it's nearly every nominal type's name.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 20, 2025

mypy_primer results

Changes were detected when running on open source projects
scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/build/wheel.py:98:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 44 diagnostics
+ Found 43 diagnostics

No memory usage changes detected ✅

Comment on lines -470 to +673
write!(
f,
"bound method {instance}.{method}{type_parameters}{signature}",
method = function.name(self.db),
instance = self_ty.display_with(self.db, self.settings.singleline()),
type_parameters = type_parameters,
signature = signature
.bind_self(self.db, Some(typing_self_ty))
.display_with(self.db, self.settings.clone())
)
f.write_str("bound method ")?;
self_ty
.display_with(self.db, self.settings.singleline())
.fmt_detailed(f)?;
f.write_char('.')?;
f.with_detail(TypeDetail::Type(self.ty))
.write_str(function.name(self.db))?;
type_parameters.fmt_detailed(f)?;
signature
.bind_self(self.db, Some(typing_self_ty))
.display_with(self.db, self.settings.clone())
.fmt_detailed(f)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of the PR is me breaking up a nice pretty write! into individual parts to invoke fmt_detailed where appropriate.

Comment on lines 655 to +863
impl<'db> TupleSpec<'db> {
pub(crate) fn display_with(
&'db self,
pub(crate) fn display_with<'a>(
&'a self,
db: &'db dyn Db,
settings: DisplaySettings<'db>,
) -> DisplayTuple<'db> {
) -> DisplayTuple<'a, 'db> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deconflating these lifetimes was unfortunately loadbearing. I had to do that a lot.

Comment on lines 1190 to 1452

/// Internal method to write signature with the signature writer
fn write_signature(&self, writer: &mut SignatureWriter) -> fmt::Result {
fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
// Immediately write a marker signaling we're starting a signature
let _ = f.with_detail(TypeDetail::SignatureStart);
// When we exit this function, write a marker signaling we're ending a signature
let mut f = f.with_detail(TypeDetail::SignatureEnd);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An incredible victory for not wanting to have to reverse-engineer what parameters belong to nested signatures.

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks good to me. I've one suggestion that should help to reduce the necessary changes because of Join.

I do find the test very confusing as we now see go to targets in places where we don't show any inlay hints. I think we should change this before landing this PR


impl Drop for TypeDetailGuard<'_, '_, '_, '_> {
fn drop(&mut self) {
// The fallibility here is primarily retrieving `TypeWriter::Details`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we bail early if std::thread::panicking?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's necessary?

let hints = inlay_hints(&self.db, self.file, self.range, settings);

let mut buf = source_text(&self.db, self.file).as_str().to_string();

let mut diagnostics = Vec::new();
let mut tbd_diagnostics = Vec::new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are those to be defined diagnostics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to gather them up transiently while we create the annotated file, and then create the annotated file, and then create them properly.

@Gankra
Copy link
Contributor Author

Gankra commented Nov 20, 2025

Micha you're god damn incredible, the join impl Just Worked.

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool!

| Type::BytesLiteral(_)
| Type::EnumLiteral(_) => {
write!(f, "Literal[{representation}]")
f.write_str("Literal[")?;
Copy link
Member

@AlexWaygood AlexWaygood Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be cool if we could also have goto-definition for the Literal part of e.g. Literal[42]. We don't have any documentation for typing.Literal in our vendored typeshed stubs right now (because we currently only add docstrings for classes, functions and modules), but it would be nice to add that capability to docstring-adder at some point. Then when you goto definition for the symbol Literal you'd see all of this lovely explanation in the stub file to tell you how Literal types work (this is the docstring for the symbol at runtime):

% uvx python -m pydoc typing.Literal   
Help on _TypedCacheSpecialForm in typing:

typing.Literal = typing.Literal
    Special typing form to define literal types (a.k.a. value types).

    This form can be used to indicate to type checkers that the corresponding
    variable or function parameter has a value equivalent to the provided
    literal (or one of several literals)::

        def validate_simple(data: Any) -> Literal[True]:  # always returns True
            ...

        MODE = Literal['r', 'rb', 'w', 'wb']
        def open_helper(file: str, mode: MODE) -> str:
            ...

        open_helper('/some/path', 'r')  # Passes type check
        open_helper('/other/path', 'typo')  # Error in type checker

    Literal[...] cannot be subclassed. At runtime, an arbitrary value
    is allowed as type argument to Literal[...], but type checkers may
    impose restrictions.

The symbol typing.Literal itself has type Type::SpecialForm(SpecialFormType::Literal) in our model, so would you do that like this?

Suggested change
f.write_str("Literal[")?;
f.with_detail(TypeDetail::Type(Type::SpecialForm(SpecialFormType::Literal))).write_str("Literal")?;
f.write_char('[')?;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you literally gave me the implementation but I'd like to keep these things to followups (I'm terrified of this PR getting merge conflicted to hell).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np

match self.ty {
Type::Dynamic(dynamic) => dynamic.fmt(f),
Type::Never => f.write_str("Never"),
Type::Dynamic(dynamic) => write!(f, "{dynamic}"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the type is Type::Dynamic(DynamicType::Any), it would be cool if we could goto the definition for the Any symbol (with has type Type::SpecialForm(SpecialFormType::Any) in our model)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(similarly leaving as a followup)

Copy link
Member

@AlexWaygood AlexWaygood Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add tests for generic-alias types, e.g.

class Foo[T]: ...

a [: <class 'Foo[int]'>] = Foo[int]

and subclass-of types, e.g.

def f(x: list[str]):
    y [: type[list[str]]] = type(x)

For the first one I'd expect goto targets for both Foo and int in <class 'Foo[int]>. For the second one I'd expect goto targets for both list and str in type[list[str]]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool with this also being a followup?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of course!

@AlexWaygood
Copy link
Member

looks like you'll need to add some snapshot filters to get snapshots that include filepaths working on Windows 🙃

@Gankra
Copy link
Contributor Author

Gankra commented Nov 20, 2025

Ughhhh

      299   299 │ info[inlay-hint-location]: Inlay Hint Target
      300       │-  --> stdlib/string/templatelib.pyi:10:7
            300 │+  --> stdlib\string\templatelib.pyi:10:7

This is really annoying because \ can appear in like \x08 and I think I'm just gonna ask everyone to agree to not Notice I'm clobbering those in the inlay hint tests.

@Gankra
Copy link
Contributor Author

Gankra commented Nov 20, 2025

Actually I'm extremely confused why this isn't a problem in any of the goto tests

@Gankra
Copy link
Contributor Author

Gankra commented Nov 20, 2025

(More followups for the followup pile I guess)

@Gankra Gankra merged commit 416e226 into main Nov 20, 2025
40 checks passed
@Gankra Gankra deleted the gankra/type-print branch November 20, 2025 14:40
@MatthewMckee4
Copy link
Contributor

@Gankra, Sick!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Related to the LSP server ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants