Skip to content

[ty] report unused bindings as unnecessary hint diagnostics#23305

Merged
MichaReiser merged 22 commits intoastral-sh:mainfrom
denyszhak:feat/unused-binding-diagnostics
Mar 27, 2026
Merged

[ty] report unused bindings as unnecessary hint diagnostics#23305
MichaReiser merged 22 commits intoastral-sh:mainfrom
denyszhak:feat/unused-binding-diagnostics

Conversation

@denyszhak
Copy link
Copy Markdown
Contributor

@denyszhak denyszhak commented Feb 16, 2026

Fixes astral-sh/ty#2607

Summary

Add unused-binding dimming in ty via diagnostics (Hint + DiagnosticTag::Unnecessary) so VS Code fades unused locals even without Pylance. VS Code default fading for “unused” is diagnostics-driven, so this PR uses diagnostics, not semantic tokens.

The unused-binding detection now lives in ty_python_semantic and is consumed by ty_server, with local-scope to avoid false positives for now. Cros-modile/class reference will be addressed in a follow up PR because it requires a bit more thinking and effort. Unused-binding analysis centralized in semantic (unused_bindings) so other IDE surfaces can reuse it later (for example emit semantic info).

Test Plan

Added unit tests in unused_bindings.rs
Added/updated e2e diagnostics coverage in pull_diagnostics.rs
Manually tested in VSCode

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Feb 16, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 86.59%. The percentage of expected errors that received a diagnostic held steady at 80.96%. The number of fully passing files held steady at 68/132.

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Feb 16, 2026

mypy_primer results

Changes were detected when running on open source projects
pydantic (https://github.com/pydantic/pydantic)
- pydantic/_internal/_core_metadata.py:87:54: error[invalid-assignment] Invalid assignment to key "pydantic_js_extra" with declared type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | ((dict[str, int | float | str | ... omitted 3 union elements], type[Any], /) -> None)` on TypedDict `CoreMetadata`: value of type `dict[object, object]`
+ pydantic/_internal/_core_metadata.py:87:54: error[invalid-assignment] Invalid assignment to key "pydantic_js_extra" with declared type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | ((dict[str, Divergent], type[Any], /) -> None)` on TypedDict `CoreMetadata`: value of type `dict[object, object]`
- pydantic/fields.py:949:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:949:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:989:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:989:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1032:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1032:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1072:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1072:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1115:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1115:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1154:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1154:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1194:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1194:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1573:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`, found `dict[str, int | float | str | ... omitted 3 union elements] | dict[Never, Never] | (((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`
+ pydantic/fields.py:1573:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`, found `dict[str, Divergent] | dict[Never, Never] | (((dict[str, Divergent], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`

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 60 diagnostics
+ Found 59 diagnostics

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Feb 16, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 712.18MB 713.55MB +0.19% (1.37MB)
sphinx 264.36MB 264.90MB +0.20% (548.86kB)
trio 117.42MB 117.73MB +0.26% (312.35kB)
flake8 47.88MB 48.04MB +0.34% (166.92kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
semantic_index 169.68MB 171.05MB +0.80% (1.37MB)

sphinx

Name Old New Diff Outcome
semantic_index 60.44MB 60.97MB +0.89% (548.86kB)

trio

Name Old New Diff Outcome
semantic_index 29.59MB 29.90MB +1.03% (312.35kB)

flake8

Name Old New Diff Outcome
semantic_index 13.51MB 13.68MB +1.21% (166.92kB)

@AlexWaygood AlexWaygood added server Related to the LSP server ty Multi-file analysis & type inference labels Feb 16, 2026
@denyszhak
Copy link
Copy Markdown
Contributor Author

mypy_primer results

Changes were detected when running on open source projects

Don't seem related but maybe I messed up somewhere, will review tomorrow

@carljm
Copy link
Copy Markdown
Contributor

carljm commented Feb 16, 2026

Thanks for working on this! I have no opinion on the LSP side (that'd be @BurntSushi or @dhruvmanila or @MichaReiser), but in terms of how this data is collected: I suspect it could be done with less repeated work and less added code (and thus less potential for independent bugs / ty disagreeing with itself) by doing it in SemanticIndexBuilder / UseDefMapBuilder, where we already build up use->def links. I think it should be fairly easy to add a compact data structure there that tracks a "used" bit for each Binding and sets it whenever we record a use of a binding. Then we'd just need an API to query that bit from UseDefMap or SemanticIndex.

Did you explore the possibility of such an implementation at all?

@carljm
Copy link
Copy Markdown
Contributor

carljm commented Feb 16, 2026

Don't seem related but maybe I messed up somewhere, will review tomorrow

We currently have some non-determinism, so it could be that. I just added ecosystem-analyzer tag, the ecosystem analyzer will tell you which projects have flaky results.

@carljm carljm self-assigned this Feb 16, 2026
@carljm
Copy link
Copy Markdown
Contributor

carljm commented Feb 16, 2026

(Assigning myself here at least on the topic of how we collect the data; maybe someone from LSP team can also assign yourself as reviewer on the LSP-side implementation?)

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Feb 16, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-await 40 0 0
invalid-return-type 1 0 0
Total 41 0 0

Changes in flaky projects detected. Raw diff output excludes flaky projects; see the HTML report for details.

Full report with detailed diff (timing results)

@denyszhak
Copy link
Copy Markdown
Contributor Author

Don't seem related but maybe I messed up somewhere, will review tomorrow

We currently have some non-determinism, so it could be that. I just added ecosystem-analyzer tag, the ecosystem analyzer will tell you which projects have flaky results.

@denyszhak denyszhak closed this Feb 16, 2026
@denyszhak denyszhak reopened this Feb 16, 2026
@denyszhak
Copy link
Copy Markdown
Contributor Author

denyszhak commented Feb 16, 2026

Thanks for working on this! I have no opinion on the LSP side (that'd be @BurntSushi or @dhruvmanila or @MichaReiser), but in terms of how this data is collected: I suspect it could be done with less repeated work and less added code (and thus less potential for independent bugs / ty disagreeing with itself) by doing it in SemanticIndexBuilder / UseDefMapBuilder, where we already build up use->def links. I think it should be fairly easy to add a compact data structure there that tracks a "used" bit for each Binding and sets it whenever we record a use of a binding. Then we'd just need an API to query that bit from UseDefMap or SemanticIndex.

Did you explore the possibility of such an implementation at all?

Repeated/duplicated code in traversal was my main concern before shipping, I though about alternatives but not the exact option you suggesting. I treated the semantic index as something to query rather than looking at what it already computes.
Having said that, I agree with you and I'll look into refactoring to that approach today.

(closed this pr accidentally so I reopened it back)

@sharkdp sharkdp removed their request for review February 16, 2026 10:21
Copy link
Copy Markdown
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Thanks, this is looking pretty nice!

Main semantic issue I see currently is that we wrongly report a closure-captured binding as unused; unfortunately this may not be trivial to properly fix.

Simplest way to fix this (might be fine for now, results only in false negatives) is to consider all bindings "used" if their scope contains any nested scopes at all.

More precise way is to consider a binding used only if a nested scope actually references its Place.

@denyszhak denyszhak force-pushed the feat/unused-binding-diagnostics branch from 93a16a0 to 183cff0 Compare March 26, 2026 15:31
Comment on lines +144 to +146
if !parsed.errors().is_empty() {
return Vec::new();
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think I asked this before. What's the reason for bailing early if there are syntax errors? It results in "flickering" while transitioning between valid and invalid syntax (which is always when typing)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was conservative and assumed there could be false positives but now after reviewing particular test cases it seems fine and event better not to have it, I added two tests related to that as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The test for notebooks has now also slightly different assertion
51a2da1

|| function_is_overload_declaration
|| method_has_stub_body
|| *skip_unused_parameters_for_override.get_or_insert_with(|| {
method_name_exists_in_superclass(db, index, &parsed, file_scope_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This check still allows false positives in cases where a superclass method may have a simpler implementation that doesn't use all parameters, but the parameters are still needed for use by some subclass overrides. If we want to avoid these false positives, then we can only consider parameters unused if the method or class is final.

On the other hand, it seems like pylance doesn't care about either kind of false positive. It will happily mark a parameter as unused even if it is part of the signature of an override method. So if we want to follow pylance's lead, we could also not care about that case, and eliminate a bunch of extra code here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@carljm I addressed your other suggestions. On this point, I'm also fine with removing this suppression and simplifying the code if we want to align with Pylance. Since this overlaps with @MichaReiser's earlier concern as well, I will wait for both of your views before changing it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With the most recent changes here this looks merge-ready to me, but I'm curious for @MichaReiser thoughts on this. Should we follow pylance, keep things simple, and mark parameters unused regardless of override possibilities? Is there a strong rationale for avoiding the false positives only in the "unused in override" case, but not in the "unused in base method" case (as the PR currently does)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

unused in base method" case (as the PR currently does)?

I don't think this is intentional

Given that these are only hints, having false positives does very little harm. But we should probably revisit this if we decide to promote those checks to lint rules where it would become annoying.

So I'm fine removing the extra complexity but I also don't see much harm in having them. @denyszhak I let you make the final call. Let me know when it's ready to merge.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that wasn't intentional. Since you both seemed fine with the simpler behavior, I removed that suppression for consistency. If we revisit this later, I think it should be addressed more holistically, since doing it properly would likely require more sophisticated logic and doesn't seem worth the complexity for a hint level feature.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@MichaReiser I pushed the change if you can give it a final look before the merge

Copy link
Copy Markdown
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

I only re-reviewed the unused_bindings.rs module, but it looks good to me now.

Just one question above for @MichaReiser

@carljm carljm assigned MichaReiser and unassigned carljm Mar 27, 2026
@carljm
Copy link
Copy Markdown
Contributor

carljm commented Mar 27, 2026

Thank you @denyszhak for your persistent and solid work on this PR!

@MichaReiser
Copy link
Copy Markdown
Member

Awesome work. Thank you so much.

@MichaReiser MichaReiser merged commit 4d96314 into astral-sh:main Mar 27, 2026
48 checks passed
carljm added a commit that referenced this pull request Mar 31, 2026
* main: (40 commits)
  [ty] resolve union-likes in emitting union attribute errors (#24263)
  [ty] Improve support for `Callable` type context (#23888)
  [ty] Propagate type context through `await` expressions (#24256)
  [`pyflakes`] Flag annotated variable redeclarations as `F811` in preview mode (#24244)
  [ty] Preserve `Divergent` when materializing recursive aliases (#24245)
  Fix W391 fixes for consecutive empty notebook cells (#24236)
  [flake8-bugbear] Clarify RUF071 fix safety for non-path string comparisons (#24149)
  [ty] Ban type qualifiers in PEP-695 type aliases (#24242)
  [ty] Include keyword-prefixed symbols in completions for attributes (#24232)
  [ty] Add tests for TypedDict method overloads on unions (#24230)
  [ty] report unused bindings as unnecessary hint diagnostics (#23305)
  Remove unused `non_root` variable (#24238)
  Extend F507 to flag %-format strings with zero placeholders (#24215)
  [`flake8-simplify`] Suppress `SIM105` for `except*` before Python 3.12 (#23869)
  Ignore pre-initialization references in SIM113 (#24235)
  Parenthesize expression in RUF050 fix (#24234)
  Publish playgrounds using the `release-playground` environment (#24223)
  [ty] Fix instance-attribute lookup in methods of protocol classes (#24213)
  [ty] Used shared expression cache during generic call inference (#24219)
  [ty] make `Type::BoundMethod` include instances of same-named methods bound to a subclass (#24039)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer 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.

LSP not showing unused variable (in VSCode)

8 participants