Skip to content

[ty] print diagnostics with fully qualified name to disambiguate some cases#19850

Merged
carljm merged 17 commits intoastral-sh:mainfrom
leandrobbraga:issue-848
Aug 27, 2025
Merged

[ty] print diagnostics with fully qualified name to disambiguate some cases#19850
carljm merged 17 commits intoastral-sh:mainfrom
leandrobbraga:issue-848

Conversation

@leandrobbraga
Copy link
Contributor

@leandrobbraga leandrobbraga commented Aug 10, 2025

There are some situations that we have a confusing diagnostics due to identical class names.

Class with same name from different modules

import pandas
import polars

df: pandas.DataFrame = polars.DataFrame()

This yields the following error:

Actual:
error: [invalid-assignment] "Object of type DataFrame is not assignable to DataFrame"
Expected:
error: [invalid-assignment] "Object of type polars.DataFrame is not assignable to pandas.DataFrame"

Nested classes

from enum import Enum

class A:
    class B(Enum):
        ACTIVE = "active"
        INACTIVE = "inactive"

class C:
    class B(Enum):
        ACTIVE = "active"
        INACTIVE = "inactive"

Actual:
error: [invalid-assignment] "Object of type Literal[B.ACTIVE] is not assignable to B"
Expected:
error: [invalid-assignment] "Object of type Literal[my_module.C.B.ACTIVE] is not assignable to my_module.A.B"

Solution

In this MR we added an heuristics to detect when to use a fully qualified name:

  • There is an invalid assignment and;
  • They are two different classes and;
  • They have the same name

The fully qualified name always includes:

  • module name
  • nested classes name
  • actual class name

There was no QualifiedDisplay so I had to implement it from scratch. I'm very new to the codebase, so I might have done things inefficiently, so I appreciate feedback.

Should we pre-compute the fully qualified name or do it on demand?

Not implemented

Function-local classes

Should we approach this in a different PR?

Example:

# t.py
from __future__ import annotations


def function() -> A:
    class A:
        pass

    return A()


class A:
    pass


a: A = function()

mypy

t.py:8: error: Incompatible return value type (got "t.A@5", expected "t.A")  [return-value]

From my testing the 5 in A@5 comes from the like number.

ty

error[invalid-return-type]: Return type does not match returned value
 --> t.py:4:19
  |
4 | def function() -> A:
  |                   - Expected `A` because of return type
5 |     class A:
6 |         pass
7 |
8 |     return A()
  |            ^^^ expected `A`, found `A`
  |
info: rule `invalid-return-type` is enabled by default

Fixes astral-sh/ty#848

@github-actions
Copy link
Contributor

github-actions bot commented Aug 10, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@github-actions
Copy link
Contributor

github-actions bot commented Aug 10, 2025

mypy_primer results

No ecosystem changes detected ✅
No memory usage changes detected ✅

@leandrobbraga
Copy link
Contributor Author

leandrobbraga commented Aug 10, 2025

It seems there are some false positives, I'll work on those later.

EDIT: it was an important check that I accidentally deleted while refactoring

@AlexWaygood AlexWaygood added ty Multi-file analysis & type inference diagnostics Related to reporting of diagnostics. labels Aug 10, 2025
@leandrobbraga leandrobbraga force-pushed the issue-848 branch 2 times, most recently from ead13fa to 95f8709 Compare August 11, 2025 00:02
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.

Thank you! A few quick things I noticed:


impl Display for QualifiedDisplayType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.ty {
Copy link
Member

Choose a reason for hiding this comment

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

I'd also add branches for GenericAlias, ProtocolInstance, SubclassOf and TypedDictType here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was this that you had in mind?

leandrobbraga and others added 5 commits August 11, 2025 22:18
… cases

Example:

import pandas
import polars

df: pandas.DataFrame = polars.DataFrame()  # error: [invalid-assignment]
"Object of type `polars.DataFrame` is not assignable to
`pandas.DataFrame`"
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
@leandrobbraga
Copy link
Contributor Author

leandrobbraga commented Aug 12, 2025

I had to rebase to solve conflicts, but I think I addressed all the suggestions.

I tried doing some testing to the <locals of function 'f'> but I couldn't come up with a working test for this situation.

Copy link
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.

Thank you for working on this!

Sorry for coming in on the late side with a new suggestion, but I think we should take a slightly different approach in the display.rs part of the implementation. Rather than introducing a totally separate QualifiedDisplayType struct, I think we should have a qualified: bool attribute on the existing DisplayType and `DisplayRepresentation structs, which we can respect whenever we are emitting the name of a class or function. (The current PR doesn't seem to handle functions, and it doesn't need to be done in this PR, but at some point I think we'll want to do this for function names, too.) I think this will make it much easier to share parts of the implementation that are not related to whether or not names are qualified, without code duplication.

The qualified flag could be set using a builder pattern, like ty.display(db).qualified() vs just ty.display(db) for the un-qualified display of a type. Though we may also want some form that allows easily passing a boolean value; this will be convenient for when we want to pass along the "qualified" value to display of a nested inner type.

The recently-merged implementation of a "multiline" display option (which probably needs conflict resolution with this PR anyway) is a good model to follow here.

@leandrobbraga leandrobbraga requested a review from carljm August 26, 2025 00:56
@sharkdp sharkdp removed their request for review August 26, 2025 07:05
carljm added 4 commits August 27, 2025 12:32
* main:
  [`ruff`] Preserve relative whitespace in multi-line expressions (`RUF033`) (astral-sh#19647)
  [ty] Optimize TDD atom ordering (astral-sh#20098)
  [`airflow`] Extend `AIR311` and `AIR312` rules (astral-sh#20082)
  [ty] Preserve qualifiers when accessing attributes on unions/intersections (astral-sh#20114)
  [ty] Fix the inferred interface of specialized generic protocols (astral-sh#19866)
  [ty] Infer slightly more precise types for comprehensions (astral-sh#20111)
  [ty] Add more tests for protocols (astral-sh#20095)
  [ty] don't eagerly unpack aliases in user-authored unions (astral-sh#20055)
  [`flake8-use-pathlib`] Update links to the table showing the correspondence between `os` and `pathlib` (astral-sh#20103)
  [`flake8-use-pathlib`] Make `PTH100` fix unsafe because it can change behavior (astral-sh#20100)
  [`flake8-use-pathlib`] Delete unused `Rule::OsSymlink` enabled check (astral-sh#20099)
  [ty] Add search paths info to unresolved import diagnostics (astral-sh#20040)
  [`flake8-logging-format`] Add auto-fix for f-string logging calls (`G004`) (astral-sh#19303)
  Add a `ScopeKind` for the `__class__` cell (astral-sh#20048)
  Fix incorrect D413 links in docstrings convention FAQ (astral-sh#20089)
  [ty] Refactor inlay hints structure to use separate parts (astral-sh#20052)
Copy link
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.

Looking good! I think we should increase our test coverage a bit, and try to reduce code boilerplate.

I think ultimately in order to support nested types we will probably need to change the settings from just having a qualified boolean, to instead having a vector of types whose names should be qualified, so that we can identify the right types to qualify even in nested display. But this can be a separate PR.

Copy link
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 went ahead and made those updates, this looks good to go!

@carljm carljm enabled auto-merge (squash) August 27, 2025 20:42
@carljm carljm merged commit d75ef38 into astral-sh:main Aug 27, 2025
37 checks passed
dcreager added a commit that referenced this pull request Aug 28, 2025
* main:
  Fix mdtest ignore python code blocks (#20139)
  [ty] add support for cyclic legacy generic protocols (#20125)
  [ty] add cycle detection for find_legacy_typevars (#20124)
  Use new diff rendering format in tests (#20101)
  [ty] Fix 'too many cycle iterations' for unions of literals (#20137)
  [ty] No boundness analysis for implicit instance attributes (#20128)
  Bump 0.12.11 (#20136)
  [ty] Benchmarks for problematic implicit instance attributes cases (#20133)
  [`pyflakes`] Fix `allowed-unused-imports` matching for top-level modules (`F401`) (#20115)
  Move GitLab output rendering to `ruff_db` (#20117)
  [ty] Evaluate reachability of non-definitely-bound to Ambiguous (#19579)
  [ty] Introduce a representation for the top/bottom materialization of an invariant generic (#20076)
  [`flake8-async`] Implement `blocking-http-call-httpx` (`ASYNC212`) (#20091)
  [ty] print diagnostics with fully qualified name to disambiguate some cases (#19850)
  [`ruff`] Preserve relative whitespace in multi-line expressions (`RUF033`) (#19647)
second-ed pushed a commit to second-ed/ruff that referenced this pull request Sep 9, 2025
… cases (astral-sh#19850)

There are some situations that we have a confusing diagnostics due to
identical class names.

## Class with same name from different modules

```python
import pandas
import polars

df: pandas.DataFrame = polars.DataFrame()
```

This yields the following error:

**Actual:**
error: [invalid-assignment] "Object of type `DataFrame` is not
assignable to `DataFrame`"
**Expected**:
error: [invalid-assignment] "Object of type `polars.DataFrame` is not
assignable to `pandas.DataFrame`"

## Nested classes

```python
from enum import Enum

class A:
    class B(Enum):
        ACTIVE = "active"
        INACTIVE = "inactive"

class C:
    class B(Enum):
        ACTIVE = "active"
        INACTIVE = "inactive"
```

**Actual**:
error: [invalid-assignment] "Object of type `Literal[B.ACTIVE]` is not
assignable to `B`"
**Expected**:
error: [invalid-assignment] "Object of type
`Literal[my_module.C.B.ACTIVE]` is not assignable to `my_module.A.B`"

## Solution

In this MR we added an heuristics to detect when to use a fully
qualified name:
- There is an invalid assignment and;
- They are two different classes and;
- They have the same name

The fully qualified name always includes:
- module name
- nested classes name
- actual class name

There was no `QualifiedDisplay` so I had to implement it from scratch.
I'm very new to the codebase, so I might have done things inefficiently,
so I appreciate feedback.

Should we pre-compute the fully qualified name or do it on demand? 

## Not implemented

### Function-local classes

Should we approach this in a different PR?

**Example**:
```python 
# t.py
from __future__ import annotations


def function() -> A:
    class A:
        pass

    return A()


class A:
    pass


a: A = function()
```

#### mypy

```console
t.py:8: error: Incompatible return value type (got "t.A@5", expected "t.A")  [return-value]
```

From my testing the 5 in `A@5` comes from the like number. 

#### ty

```console
error[invalid-return-type]: Return type does not match returned value
 --> t.py:4:19
  |
4 | def function() -> A:
  |                   - Expected `A` because of return type
5 |     class A:
6 |         pass
7 |
8 |     return A()
  |            ^^^ expected `A`, found `A`
  |
info: rule `invalid-return-type` is enabled by default
```

Fixes astral-sh/ty#848

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

diagnostics Related to reporting of diagnostics. ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Differentiate between identically named (but distinct) types in diagnostics

3 participants