Skip to content

[ty] hover + goto-declaration for subscript literals#22837

Open
Hugo-Polloli wants to merge 5 commits intoastral-sh:mainfrom
Hugo-Polloli:subscript-literal-hover
Open

[ty] hover + goto-declaration for subscript literals#22837
Hugo-Polloli wants to merge 5 commits intoastral-sh:mainfrom
Hugo-Polloli:subscript-literal-hover

Conversation

@Hugo-Polloli
Copy link
Contributor

@Hugo-Polloli Hugo-Polloli commented Jan 24, 2026

Summary

Fixes astral-sh/ty#1410; fixes astral-sh/ty#2477; fixes astral-sh/ty#2630

Improve hover behavior for subscript expressions when the cursor is on a literal subscript index/key (e.g. values[0], person["name"]). For TypedDict string-literal keys, render a denser hover line that includes the key name (e.g. (key of Person) name: str) and include the field docstring when available. Also support go-to-declaration on TypedDict keys (cmd+click) to jump to the corresponding field declaration.

Test Plan

  • Added hover_subscript_literal_index to cover hovering on literal subscript indices (e.g. values[0]).
  • Updated hover_typed_dict_key_literal to assert (key) name: str (and field docstring) for TypedDict string-literal keys.

Also see attached screenshots from a vscode test run:
image
image
image

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 24, 2026

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 24, 2026

mypy_primer results

Changes were detected when running on open source projects
prefect (https://github.com/PrefectHQ/prefect)
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `dict[str, Any] | int | dict[Any, Any] | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `dict[Any, Any] | int | dict[str, Any] | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `dict[str, Any] | int | dict[Any, Any] | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `dict[Any, Any] | int | dict[str, Any] | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/deployments/runner.py:997:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | ((...) -> Any)`
+ src/prefect/deployments/runner.py:997:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | (((...) -> Any) & ((*args: object, **kwargs: object) -> object))`
- src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Argument type `dict[str, Any] | int | dict[Any, Any] | ... omitted 4 union elements` does not satisfy constraints (`str`, `int`, `int | float`, `bool`, `dict[Any, Any]`, `list[Any]`, `None`) of type variable `T`
+ src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Argument type `dict[Any, Any] | int | dict[str, Any] | ... omitted 4 union elements` does not satisfy constraints (`str`, `int`, `int | float`, `bool`, `dict[Any, Any]`, `list[Any]`, `None`) of type variable `T`
- src/prefect/flow_engine.py:989:32: error[invalid-await] `Unknown | R@FlowRunEngine | Coroutine[Any, Any, R@FlowRunEngine]` is not awaitable
- src/prefect/flow_engine.py:1580:24: error[invalid-await] `Unknown | R@AsyncFlowRunEngine | Coroutine[Any, Any, R@AsyncFlowRunEngine]` is not awaitable
- src/prefect/flow_engine.py:1661: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:1669: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:1703: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:1710:25: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_async`
- src/prefect/flows.py:285:34: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
+ src/prefect/flows.py:285:34: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
- src/prefect/flows.py:403:68: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
+ src/prefect/flows.py:403:68: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
- src/prefect/flows.py:1937:21: error[no-matching-overload] No overload of function `run_coro_as_sync` matches arguments
+ src/prefect/flows.py:1877:53: warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `dict[str, Any] | int | Unknown | ... omitted 4 union elements` on object of type `dict[str, Any]`
+ src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `Unknown | int | dict[str, Any] | ... omitted 4 union elements` on object of type `dict[str, Any]`
- src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[Unknown | dict[str, Any] | int | ... omitted 4 union elements]`
+ src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[Unknown | int | dict[str, Any] | ... omitted 4 union elements]`
- src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Argument type `dict[str, Any] | int | str | ... omitted 3 union elements` does not satisfy constraints (`str`, `int`, `int | float`, `bool`, `dict[Any, Any]`, `list[Any]`, `None`) of type variable `T`
+ src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Argument type `str | int | dict[str, Any] | ... omitted 3 union elements` does not satisfy constraints (`str`, `int`, `int | float`, `bool`, `dict[Any, Any]`, `list[Any]`, `None`) of type variable `T`
- Found 5374 diagnostics
+ Found 5368 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/build/wheel.py:99:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 47 diagnostics
+ Found 46 diagnostics

No memory usage changes detected ✅

HoverContent::TypedDictKey { key, ty } => {
let (ty_string, syntax) = self.ty_string_and_syntax(ty);
self.kind
.fenced_code_block(format!("(key) {key}: {ty_string}"), syntax)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Based on astral-sh/ty#2477, I fully copied the format of pyright, but tell me if you had something else in mind!

Copy link
Member

Choose a reason for hiding this comment

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

Currently for this snippet:

from typing import TypedDict

class F(TypedDict):
    x: int
    """docs for x"""

def bar(f: F):
    f["x"]

the on-hover is:

Screenshot image

What if it was "(key of F) x: int" instead of "(key) x: int"? I think that would be quite nice

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 tried to fiddle with markdown to highlight it the same way you suggested: "(key of F) x: int" but did not manage to do it, though I'm not familiar with how VSCode handles the display, I only used the existing plumbing.

This looks like this for now:
image

Copy link
Member

Choose a reason for hiding this comment

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

it looks good to me -- thanks!

@Hugo-Polloli Hugo-Polloli force-pushed the subscript-literal-hover branch from ba6edb9 to fc7f112 Compare January 24, 2026 17:06
@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Jan 24, 2026
@Hugo-Polloli Hugo-Polloli marked this pull request as ready for review January 24, 2026 17:19
@AlexWaygood AlexWaygood added the server Related to the LSP server label Jan 24, 2026
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.

This is very cool!!

HoverContent::TypedDictKey { key, ty } => {
let (ty_string, syntax) = self.ty_string_and_syntax(ty);
self.kind
.fenced_code_block(format!("(key) {key}: {ty_string}"), syntax)
Copy link
Member

Choose a reason for hiding this comment

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

Currently for this snippet:

from typing import TypedDict

class F(TypedDict):
    x: int
    """docs for x"""

def bar(f: F):
    f["x"]

the on-hover is:

Screenshot image

What if it was "(key of F) x: int" instead of "(key) x: int"? I think that would be quite nice

@Hugo-Polloli Hugo-Polloli force-pushed the subscript-literal-hover branch from fc7f112 to 6c3a53f Compare January 24, 2026 23:25
@AlexWaygood
Copy link
Member

Manually testing on my https://github.com/astral-sh/docstring-adder repo, this still doesn't do a great job for slices, e.g. stack[:-1] or string[1:-1]. Other than that, it looks great, though. And I'd personally be okay with slices being a followup, if you'd prefer not to do that as part of this PR!

@Hugo-Polloli
Copy link
Contributor Author

Manually testing on my https://github.com/astral-sh/docstring-adder repo, this still doesn't do a great job for slices, e.g. stack[:-1] or string[1:-1]. Other than that, it looks great, though. And I'd personally be okay with slices being a followup, if you'd prefer not to do that as part of this PR!

Oh sorry, to be fully honest I hadn't even thought about handling slices and was fully focused on "pure" literals. I think we could keep this PR minimal as is, BUT I think it would be worth it for me to take a crack at it and see how much more work it requires, then I can make a decision depending on the answer!

@Hugo-Polloli Hugo-Polloli force-pushed the subscript-literal-hover branch from 6c3a53f to 698bce3 Compare January 25, 2026 19:04
@Hugo-Polloli
Copy link
Contributor Author

Slices had a few edge cases to cover (esp. around :), I added coverage for those. Everything is inside the add support for slices commit 🚀

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.

This is great! I'll wait to give somebody more familiar with our on-hover functionality a chance to take a look, but I can't find anything amiss with the behaviour now and the code LGTM!

Copy link
Contributor

@Gankra Gankra left a comment

Choose a reason for hiding this comment

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

I have a lot of nits but I'm not sure whether they're worth blocking on.

if expr.is_slice_expr()
&& let Some(subscript) = enclosing_subscript_with_slice_range(expr.range())
{
return Some(GotoTarget::Expression(subscript.into()));
Copy link
Contributor

Choose a reason for hiding this comment

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

It kinda feels like this wants to be its own GotoTarget, in the same way GotoTarget::Call is?

Copy link
Contributor

Choose a reason for hiding this comment

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

In particular I expect these will want a different goto-declaration implementation, as e.g. cmd+clicking on a typed dict key should jump to the declaration of the field? (You don't need to implement that in this PR, but, making a distinct goto-target makes it easy to specialize that).

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 created astral-sh/ty#2630 and added a TODO that references it, is that ok?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hey! since i've had the time to do it while this PR was still open, and since I depend on the previous commits, I've taken the liberty of pushing a new commit for the go to declaration feature to this PR and, i've updated the PR body and title to reflect that

Comment on lines 681 to 694
let signed_literal_expr_range = |expr: ast::ExprRef<'_>| -> Option<TextRange> {
if expr.is_literal_expr() {
return Some(expr.range());
}

let unary_op = expr.unary_op_expr()?;
if matches!(unary_op.op, ast::UnaryOp::USub | ast::UnaryOp::UAdd)
&& unary_op.operand.is_literal_expr()
{
Some(unary_op.range())
} else {
None
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

This closure is only called once, it's unclear to me why it exists. A Build-your-own-try-block?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops at some point in my work i must've been using it twice and factored it out, but then forgot to inline it after changing things up, should be fixed now

Comment on lines 2953 to 2955
hover.lines().next(),
Some("int"),
"case {index} expected `int` hover, got:\n{hover}"
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a huge fan of these loop-tests for maintenance reasons... we prefer snapshots precisely because this is "ui" and we want to see the full "render" and if it changes. (and if it does, right now you can cargo insta accept all changes and this breaks that)

But in this case I also don't think it's particularly robust. You should make it a list of str or something so that we distinguish whether we're returning the elements from the indexes.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am however sympathetic to there being So Many Corner Cases that it's really noisy to have a copy for each, so I don't want to block on the first detail.

Copy link
Contributor

Choose a reason for hiding this comment

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

I do think these tests are missing cases where the subscript stuff shouldn't fire -- in particular slice[-x] or whatnot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok so I kept the spirit of looping over the case, but I generate a full snapshot output now, is that a better way? Also added a case with a non-literal subscript


// Special-case subscripts: if the cursor lands on the index/key, slice bound, or slice
// inside `value[...]`, retarget the covering node to the parent `ExprSubscript`.
// This avoids "no hover" on literals and lets hover/goto show the subscript result type.
Copy link
Contributor

Choose a reason for hiding this comment

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

Might also be worth elucidating that this specifically still prefers value[a<CURSOR>b] being treated as a hover of ab, and only hovers of literals-in-subscripts are "promoted" to a hover of the subscript.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done :)

@Hugo-Polloli
Copy link
Contributor Author

I have a lot of nits but I'm not sure whether they're worth blocking on.

Not really nits! Those were all rly valid, every changes is contained within commit expand coverage using snapshots

@AlexWaygood AlexWaygood requested a review from Gankra January 28, 2026 13:41
@Hugo-Polloli Hugo-Polloli force-pushed the subscript-literal-hover branch from 1475a30 to 0f90db9 Compare January 29, 2026 09:15
@Hugo-Polloli Hugo-Polloli changed the title [ty] improve hover for subscript literals [ty] hover + goto-declaration for subscript literals Jan 29, 2026
@sharkdp sharkdp removed their request for review February 3, 2026 13:51
@carljm
Copy link
Contributor

carljm commented Feb 3, 2026

@Gankra do you have time to take another look at this and see if you're happy with the way your comments were addressed, and land it if so?

@carljm carljm removed their request for review February 3, 2026 19:45
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.

Enable go‑to‑declaration for TypedDict keys Support hover info for TypedDict key access Hover for subscript when the argument is a Literal

4 participants