Skip to content

[ty] Inlay hint auto import#22111

Merged
BurntSushi merged 25 commits intoastral-sh:mainfrom
MatthewMckee4:inlay-hint-auto-import
Feb 12, 2026
Merged

[ty] Inlay hint auto import#22111
BurntSushi merged 25 commits intoastral-sh:mainfrom
MatthewMckee4:inlay-hint-auto-import

Conversation

@MatthewMckee4
Copy link
Contributor

@MatthewMckee4 MatthewMckee4 commented Dec 20, 2025

Summary

Part of astral-sh/ty#1625

This PR implements the auto importing for unimported symbols introduced when "applying" an inlay hint.

Screencast_20251222_120138.webm

There are several things to consider here.

We have all of the information about the symbols we need to import,
from the type display details.
So we need to get the definition of the type and attempt to import that.
Also from the type display details, we get the name of the symbol that is
displayed in the inlay hint, this is useful for the following reason.
If we have an inlay hint that contains the same symbol name twice, but
from different symbols, bar.A and baz.A. the label we see in the inlay
hint will be exactly bar.A and baz.A. From this we know that we should
not try to add imports statement from bar import A and from baz import A,
because these will conflict. I believe this is all of the information we can
get that can tell us if we should "qualify" a symbol (bar.A or A).

So, we check if the label (like "bar.A") contains ".", this could possibly
be improved. If it does we force a "import " statement via:

ImportRequest::import(module_name, definition_name).force()

Otherwise, we try to add a "from import " statement via:

ImportRequest::import_from(module_name, definition_name)

We don't force here since it is okay if we still end up adding a
"import " statement and we qualify the name via
"import_action.symbol_text()".

let qualified_name = |import_action: &ImportAction| {
    if import_action.import().is_some() {
        return None;
    }

    let symbol_text = import_action.symbol_text();

    Some(symbol_text.to_string())
};

We also do not consider the symbol being a module. I think this is fine.

Because we store these new imports, and the importer cant work with dynamiclly imported members, we can get two from imports from the same module with two different import statements, like the following.

from ty_extensions import Top
from ty_extensions import Unknown

def f(xyxy: object):
    if isinstance(xyxy, list):
        x: Top[list[Unknown]] = xyxy

Current Issues

I have seen issues with Literal (we don't find the right symbol to import) and None (we import NoneType)

Test Plan

Update several ty_ide tests and added some more.

I think i will need to add even more.

Performance Test

import foo

a = foo.C().foo() # 75x
class D[T, U, V]: ...
from typing import Literal
from bar import D
import bar

import pydantic
import numpy as np
import sqlmodel


class A[T]: ...

class B[T]: ...

class C:
    def foo(self) -> B[A[bar.D[np.typing.NDArray, list[sqlmodel.SQLModel | A[B[list[int]]]], pydantic.BaseModel]]]:
        raise NotImplementedError

In main.py, we get the following results.

Before

Inlay hint request returned 75 hints in 23.881372ms
Inlay hint request returned 75 hints in 22.479724ms
Inlay hint request returned 75 hints in 25.541434ms
Inlay hint request returned 75 hints in 23.30347ms
Inlay hint request returned 75 hints in 23.291729ms

After

Inlay hint request returned 75 hints in 38.310759ms
Inlay hint request returned 75 hints in 47.835834ms
Inlay hint request returned 75 hints in 36.666961ms
Inlay hint request returned 75 hints in 36.459227ms
Inlay hint request returned 75 hints in 56.092519ms

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 20, 2025

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 20, 2025

mypy_primer results

Changes were detected when running on open source projects
Expression (https://github.com/cognitedata/Expression)
+ tests/test_compose.py:21:16: error[invalid-assignment] Object of type `(Never, /) -> Never` is not assignable to `(int, /) -> int`
- Found 204 diagnostics
+ Found 205 diagnostics

prefect (https://github.com/PrefectHQ/prefect)
- src/prefect/deployments/runner.py:1017:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | ((...) -> Any)`
+ src/prefect/deployments/runner.py:1017:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | (((...) -> Any) & ((*args: object, **kwargs: object) -> object))`
- src/prefect/flow_engine.py:1004:32: error[invalid-await] `Unknown | R@FlowRunEngine | Coroutine[Any, Any, R@FlowRunEngine]` is not awaitable
- src/prefect/flow_engine.py:1610:24: error[invalid-await] `Unknown | R@AsyncFlowRunEngine | Coroutine[Any, Any, R@AsyncFlowRunEngine]` is not awaitable
- src/prefect/flow_engine.py:1691:43: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Unknown | R@run_generator_flow_sync`
- src/prefect/flow_engine.py:1699:21: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_sync`
- src/prefect/flow_engine.py:1733:44: warning[possibly-missing-attribute] Attribute `__anext__` may be missing on object of type `Unknown | R@run_generator_flow_async`
- src/prefect/flow_engine.py:1740:25: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_async`
- src/prefect/flows.py:286:34: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
+ src/prefect/flows.py:286:34: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
- src/prefect/flows.py:404:68: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
+ src/prefect/flows.py:404:68: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
- src/prefect/flows.py:1946:21: error[no-matching-overload] No overload of function `run_coro_as_sync` matches arguments
+ src/prefect/flows.py:1886:53: warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- Found 5480 diagnostics
+ Found 5474 diagnostics

setuptools (https://github.com/pypa/setuptools)
+ setuptools/_distutils/command/install.py:719:42: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `Iterable[Never]`, found `map[str]`
- Found 1118 diagnostics
+ Found 1119 diagnostics

sympy (https://github.com/sympy/sympy)
+ sympy/algebras/tests/test_quaternion.py:422:33: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/codegen/tests/test_matrix_nodes.py:27:21: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/eigen.py:328:22: error[invalid-argument-type] Argument to bound method `_as_type` is incorrect: Expected `MatrixBase`, found `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Unknown`
+ sympy/matrices/eigen.py:1202:37: error[unresolved-attribute] Object of type `T2'return@call_highest_priority | T1'return@call_highest_priority` has no attribute `pow`
+ sympy/matrices/expressions/tests/test_blockmatrix.py:235:13: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/expressions/tests/test_blockmatrix.py:235:33: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/expressions/tests/test_blockmatrix.py:235:53: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/expressions/tests/test_blockmatrix.py:459:12: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/expressions/tests/test_matadd.py:36:12: error[unsupported-operator] Operator `+` is not supported between objects of type `MatrixBase` and `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Expr`
+ sympy/matrices/expressions/tests/test_matpow.py:124:47: error[unsupported-operator] Operator `+` is not supported between two objects of type `ImmutableDenseMatrix`
+ sympy/matrices/matrixbase.py:979:18: error[unsupported-operator] Operator `+` is not supported between two objects of type `Self@_eval_wilkinson`
+ sympy/matrices/matrixbase.py:2957:16: error[invalid-return-type] Return type does not match returned value: expected `Self@_eval_pow_by_cayley`, found `Self@_eval_pow_by_cayley | T2'return@call_highest_priority | T1'return@call_highest_priority`
+ sympy/matrices/matrixbase.py:3256:16: error[invalid-return-type] Return type does not match returned value: expected `MatrixBase`, found `T2'return@call_highest_priority | T1'return@call_highest_priority`
+ sympy/matrices/matrixbase.py:3256:29: error[invalid-argument-type] Argument is incorrect: Expected `T2'return@call_highest_priority | T1'return@call_highest_priority`, found `MatrixBase`
+ sympy/matrices/matrixbase.py:3310:16: error[unsupported-operator] Operator `+` is not supported between two objects of type `MatrixBase`
+ sympy/matrices/matrixbase.py:3314:16: error[invalid-return-type] Return type does not match returned value: expected `Tmat@__sub__`, found `MatrixBase`
+ sympy/matrices/matrixbase.py:4386:16: error[unsupported-operator] Operator `+` is not supported between two objects of type `Self@add`
+ sympy/matrices/matrixbase.py:4923:16: error[invalid-return-type] Return type does not match returned value: expected `Self@analytic_func`, found `Self@analytic_func | T2'return@call_highest_priority | T1'return@call_highest_priority`
+ sympy/matrices/repmatrix.py:321:17: error[unsupported-operator] Operator `-` is not supported between two objects of type `Self@_eval_is_symmetric`
+ sympy/matrices/solvers.py:637:27: error[invalid-argument-type] Argument to bound method `vstack` is incorrect: Argument type `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Unknown` does not satisfy upper bound `MatrixBase` of type variable `Self`
+ sympy/matrices/solvers.py:637:27: error[invalid-argument-type] Argument to bound method `vstack` is incorrect: Expected `Tmat@_gauss_jordan_solve`, found `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Unknown`
+ sympy/matrices/solvers.py:741:12: error[invalid-return-type] Return type does not match returned value: expected `Tmat@_pinv_solve`, found `T2'return@call_highest_priority | T1'return@call_highest_priority`
+ sympy/matrices/tests/test_commonmatrix.py:1249:31: error[unsupported-operator] Operator `+` is not supported between objects of type `MutableDenseMatrix` and `ImmutableDenseNDimArray`
+ sympy/matrices/tests/test_immutable.py:105:23: error[unsupported-operator] Operator `+` is not supported between two objects of type `ImmutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:121:12: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:123:32: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:141:12: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:2179:22: error[unsupported-operator] Operator `+` is not supported between two objects of type `Unknown | MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:2207:26: error[unsupported-operator] Operator `+` is not supported between two objects of type `Unknown | MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:2908:21: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:2921:13: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:2936:13: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:2937:13: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:2948:14: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:3471:21: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrices.py:3477:12: warning[possibly-missing-attribute] Attribute `rank` may be missing on object of type `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Unknown`
+ sympy/matrices/tests/test_matrices.py:3478:13: error[unsupported-operator] Operator `**` is not supported between objects of type `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Unknown` and `Literal[2]`
+ sympy/matrices/tests/test_matrices.py:3479:13: error[unsupported-operator] Operator `**` is not supported between objects of type `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Unknown` and `Literal[3]`
+ sympy/matrices/tests/test_matrixbase.py:491:12: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:493:32: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:544:12: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:823:31: error[unsupported-operator] Operator `+` is not supported between objects of type `MutableDenseMatrix` and `ImmutableDenseNDimArray`
+ sympy/matrices/tests/test_matrixbase.py:876:12: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:878:32: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:900:12: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:2931:22: error[unsupported-operator] Operator `+` is not supported between two objects of type `Unknown | MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:2959:26: error[unsupported-operator] Operator `+` is not supported between two objects of type `Unknown | MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:3610:21: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:3624:13: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:3639:13: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:3640:13: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_matrixbase.py:3652:14: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_reductions.py:377:21: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_reductions.py:383:12: warning[possibly-missing-attribute] Attribute `rank` may be missing on object of type `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Unknown`
+ sympy/matrices/tests/test_reductions.py:384:13: error[unsupported-operator] Operator `**` is not supported between objects of type `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Unknown` and `Literal[2]`
+ sympy/matrices/tests/test_reductions.py:385:13: error[unsupported-operator] Operator `**` is not supported between objects of type `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Unknown` and `Literal[3]`
+ sympy/matrices/tests/test_solvers.py:68:13: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/matrices/tests/test_sparse.py:573:12: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableSparseMatrix`
+ sympy/matrices/tests/test_sparse.py:577:52: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableSparseMatrix`
+ sympy/matrices/tests/test_sparse.py:593:17: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableSparseMatrix`
+ sympy/parsing/autolev/test-examples/ruletest5.py:13:6: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/parsing/autolev/test-examples/ruletest5.py:16:38: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/parsing/autolev/test-examples/ruletest5.py:16:84: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/parsing/autolev/test-examples/ruletest5.py:16:131: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/parsing/autolev/test-examples/ruletest5.py:21:44: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/parsing/autolev/test-examples/ruletest5.py:21:90: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/parsing/autolev/test-examples/ruletest5.py:21:137: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/parsing/autolev/test-examples/ruletest5.py:25:37: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/parsing/autolev/test-examples/ruletest5.py:25:83: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/parsing/autolev/test-examples/ruletest5.py:25:130: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/physics/control/tests/test_lti.py:3733:31: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/physics/control/tests/test_lti.py:3733:38: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/physics/mechanics/lagrange.py:222:23: error[unsupported-operator] Operator `-` is not supported between two objects of type `Unknown | MutableDenseMatrix`
+ sympy/physics/mechanics/lagrange.py:346:17: error[unsupported-operator] Operator `+` is not supported between two objects of type `Unknown | MutableDenseMatrix`
+ sympy/physics/mechanics/linearize.py:217:28: error[unsupported-operator] Operator `+` is not supported between two objects of type `Unknown | MutableDenseMatrix`
+ sympy/physics/mechanics/linearize.py:229:29: error[unsupported-operator] Operator `+` is not supported between two objects of type `Unknown | MutableDenseMatrix`
+ sympy/physics/mechanics/linearize.py:241:29: error[unsupported-operator] Operator `+` is not supported between two objects of type `Unknown | MutableDenseMatrix`
+ sympy/physics/mechanics/tests/test_jointsmethod.py:247:17: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/physics/mechanics/tests/test_jointsmethod.py:249:17: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/physics/vector/functions.py:380:21: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `Iterable[Unknown]`, found `T2'return@call_highest_priority | T1'return@call_highest_priority | MatrixBase | Unknown`
+ sympy/solvers/tests/test_numeric.py:137:11: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/stats/tests/test_symbolic_multivariate.py:84:36: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/stats/tests/test_symbolic_multivariate.py:84:84: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/stats/tests/test_symbolic_multivariate.py:85:36: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/tensor/tests/test_tensor.py:1693:19: error[unsupported-operator] Operator `+` is not supported between two objects of type `MutableDenseMatrix`
+ sympy/tensor/tests/test_tensor.py:1694:23: error[unsupported-operator] Operator `-` is not supported between two objects of type `MutableDenseMatrix`
- Found 15994 diagnostics
+ Found 16080 diagnostics

@AlexWaygood AlexWaygood added server Related to the LSP server ty Multi-file analysis & type inference labels Dec 20, 2025
@MatthewMckee4 MatthewMckee4 changed the title Inlay hint auto import [ty] Inlay hint auto import Dec 20, 2025
Comment on lines 7615 to 7618
from typing import Any

def foo(x: Any):
a: Any | Literal["some"] = getattr(x, 'foo', "some")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Having an issue here. We are not importing Literal here.

With some printlns we get see that <special-form 'typing.Literal'> is resolving to a definition but we can't find a name for it.

Not to mention us trying import str, for the actual literal type.

details.label = "Any | Literal[\"some\"]"
details.targets = [0..3, 6..13, 14..20]

Type: <special-form 'typing.Literal'>
qualified_label_part
type_definition = SpecialForm(Definition { [salsa id]: Id(1486) })
definition = Definition { [salsa id]: Id(1486) }
definition_name = None

Type: Literal["some"]
qualified_label_part
type_definition = Class(Definition { [salsa id]: Id(6135) })
definition = Definition { [salsa id]: Id(6135) }
definition_name = Some("str")
importing "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.

Not really sure where to go from here, other than marking literals as invalid syntax, which seems wrong.


import foo

a: foo.B[foo.A[D[int, list[str | foo.A[foo.B[int]]]]]] = foo.C().foo()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should this annotation that we insert be the one we show in the inlay hint?

It currently isn't, but interested to what what people think

Copy link
Member

Choose a reason for hiding this comment

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

I think it would be more expensive to show the inlay hint with correct qualifications right? Especially since we have to do that up-front. So I think I'd prefer to start with the less expensive option.

@MatthewMckee4 MatthewMckee4 marked this pull request as ready for review December 20, 2025 14:51
@MatthewMckee4
Copy link
Contributor Author

Would appreciate some initial feedback. thanks

@MatthewMckee4
Copy link
Contributor Author

Already I have seen issues with Literal (we don't find the right symbol to import) and None (we import NoneType)

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.

Thanks for working on this.

I started reviewing this PR but I don't feel like I've enough context myself to do it successfully.

Would you mind adding some more details to your summary explaining your approach (and what you looked at)? This would help me avoid some duplicate work

Comment on lines 29 to 35
db: &dyn Db,
expr: &Expr,
rhs: &Expr,
ty: Type,
db: &dyn Db,
file: File,
parsed: &ParsedModuleRef,
allow_edits: bool,
Copy link
Member

Choose a reason for hiding this comment

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

The arguments here get a bit out of hand and we might also benefit from caching Stylist::from_tokens. It could make sense to have a InlayHintBuilter or similar that's stateful and holds on to db, file (SemanticModel?), ParsedMOduleRef, allow_auto_import` and possibly more

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 added a context as the first argument

struct InlayHintImporterContext<'a, 'db> {
    db: &'db dyn Db,
    file: File,
    dynamic_importer: &'a mut DynamicImporter<'a, 'db>,
}

}

// Get the possible qualified label part
let qualified_label_part = |dynamic_importer: &mut DynamicImporter| {
Copy link
Member

Choose a reason for hiding this comment

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

Have you looked at what we do in completions to import missing symbols?

let source = source_text(db, file);
let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str());
let importer = Importer::new(db, &stylist, file, source.as_str(), parsed);
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
for symbol in all_symbols(db, file, &completions.query) {
if symbol.file() == file || symbol.module().is_known(db, KnownModule::Builtins) {
continue;
}
let module_name = symbol.module().name(db);
let (name, qualified, request) = symbol
.name_in_file()
.map(|name| {
let qualified = format!("{module_name}.{name}");
(name, qualified, create_import_request(module_name, name))
})
.unwrap_or_else(|| {
let name = module_name.as_str();
let qualified = name.to_string();
(name, qualified, ImportRequest::module(name))
});
// FIXME: `all_symbols` doesn't account for wildcard imports.
// Since we're looking at every module, this is probably
// "fine," but it might mean that we import a symbol from the
// "wrong" module.
let import_action = importer.import(request, &members);
// N.B. We use `add` here because `all_symbols` already
// takes our query into account.
completions.add_skip_query(Completion {
name: ast::name::Name::new(name),
qualified: Some(ast::name::Name::new(qualified)),
insert: Some(import_action.symbol_text().into()),
ty: None,
kind: symbol.kind().to_completion_kind(),
module_name: Some(module_name),
import: import_action.import().cloned(),
builtin: false,
// TODO: `is_type_check_only` requires inferring the type of the symbol
is_type_check_only: false,
is_definitively_raisable: false,
documentation: None,
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I have.

I took some things from it, but some things are different in that we have less certainty about whether we should qualify the name (i think).

And we want to be sure about whether to "import" of "import from"

let mut dynamic_importer = should_auto_import.then(|| {
let importer = Importer::new(db, &stylist, file, source.as_str(), parsed);
let members = importer.members_in_scope_at(expr.into(), expr.range().start());
DynamicImporter::new(importer, members)
Copy link
Member

Choose a reason for hiding this comment

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

How much slower do our inlay hints become due to imports? I wonder if it's time to switch to lazily computing inlay hints.

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 not tested this at all yet.

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 small ish test added to the PR description

Copy link
Member

@MichaReiser MichaReiser Jan 9, 2026

Choose a reason for hiding this comment

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

The performance result look concerning to me, considering that the file is very small. It might be worth testing with a larger file, to see how that influences the response times.

I still think that, ideally, we do auto imports in a separte resolve request (where we just maintain enough information in the inlay so that we can resolve the auto import later)

@MatthewMckee4
Copy link
Contributor Author

@MichaReiser thanks!

I have added more content to the PR description.

@carljm carljm removed their request for review December 23, 2025 01:54
@MichaReiser MichaReiser requested a review from Gankra January 6, 2026 10:18
@MichaReiser
Copy link
Member

MichaReiser commented Jan 9, 2026

@Gankra you've probably the most context on this PR

@AlexWaygood AlexWaygood removed their request for review January 13, 2026 13:47
@charliermarsh charliermarsh self-assigned this Feb 11, 2026
Copy link
Member

@BurntSushi BurntSushi left a comment

Choose a reason for hiding this comment

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

With the lazy importing here, how does the performance test fair?

I think even if it makes the status quo a little slower, that seems okay and we can improve on it later. But it might be worth checking inlay hints on a big-but-real Python file and seeing how it fairs.

I think this otherwise LGTM, although I'm not that familiar with the inlay hint implementation. The import handling looks good to me though.


import foo

a: foo.B[foo.A[D[int, list[str | foo.A[foo.B[int]]]]]] = foo.C().foo()
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be more expensive to show the inlay hint with correct qualifications right? Especially since we have to do that up-front. So I think I'd prefer to start with the less expensive option.

@charliermarsh
Copy link
Member

I will leave to @BurntSushi to finalize tomorrow.

@MatthewMckee4
Copy link
Contributor Author

Thanks for the rebase

@BurntSushi
Copy link
Member

I did the test in the OP, and I think this does still make perf a bit worse, but it seems better with the lazy/deduplicated importing implemented. This is on current main:

2026-02-12 08:27:57.185034777 DEBUG request{id=5 method="textDocument/inlayHint"}: Inlay hint request returned 75 hints in 1.70306ms
2026-02-12 08:28:03.535298600 DEBUG request{id=6 method="textDocument/inlayHint"}: Inlay hint request returned 75 hints in 1.86542ms
2026-02-12 08:41:10.258157312 DEBUG request{id=8 method="textDocument/inlayHint"}: Inlay hint request returned 75 hints in 1.646143ms
2026-02-12 08:41:12.240306587 DEBUG request{id=9 method="textDocument/inlayHint"}: Inlay hint request returned 75 hints in 1.668892ms
2026-02-12 08:41:14.567589974 DEBUG request{id=10 method="textDocument/inlayHint"}: Inlay hint request returned 75 hints in 1.68854ms

And with this PR:

2026-02-12 08:28:11.446658139 DEBUG request{id=7 method="textDocument/inlayHint"}: Inlay hint request returned 75 hints in 2.1279ms
2026-02-12 08:40:41.186369286 DEBUG request{id=8 method="textDocument/inlayHint"}: Inlay hint request returned 75 hints in 2.127426ms
2026-02-12 08:40:56.837420705 DEBUG request{id=9 method="textDocument/inlayHint"}: Inlay hint request returned 75 hints in 2.107947ms
2026-02-12 08:40:59.318372822 DEBUG request{id=10 method="textDocument/inlayHint"}: Inlay hint request returned 75 hints in 2.160273ms
2026-02-12 08:41:02.067947997 DEBUG request{id=11 method="textDocument/inlayHint"}: Inlay hint request returned 75 hints in 2.476081ms

I did this by opening neovim in a bare bones configuration and toggling inlay hints with :lua vim.lsp.inlay_hint.enable(vim.lsp.inlay_hint.is_enabled()). I used a build from our profiling Cargo profile.

I also opened some files in Home Assistant where getting inlay hint requests takes a bit longer (sometimes 50-60ms for one file on my machine). But between main and this PR, request times stay in the same ballpark.

I think Micha's suggestion

I still think that, ideally, we do auto imports in a separte resolve request (where we just maintain enough information in the inlay so that we can resolve the auto import later)

Would be good future work. (Possibly also for completions too.) But I'm comfortable bringing this in now given that it's a nice quality of life improvement.

@BurntSushi BurntSushi merged commit 647f660 into astral-sh:main Feb 12, 2026
50 checks passed
@MatthewMckee4 MatthewMckee4 deleted the inlay-hint-auto-import branch February 13, 2026 22:09
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