Compare commits

...

47 Commits

Author SHA1 Message Date
Aria Desires
14f503695d teach file_to_module the meaning of desperation as well 2025-12-01 21:05:25 -05:00
Aria Desires
40ed26aa3f update test results 2025-12-01 20:19:21 -05:00
Aria Desires
5883fc1554 introduce desperate module resolution 2025-12-01 20:14:09 -05:00
Aria Desires
b52cc05102 fixup 2025-12-01 20:13:30 -05:00
Aria Desires
9c6dd1f313 add tests for workspaces 2025-12-01 16:05:03 -05:00
Dylan
ecab623fb2 Bump 0.14.7 (#21684) 2025-11-28 14:34:27 -06:00
David Peter
42f152108a [ty] Generic types aliases (implicit and PEP 613) (#21553)
## Summary

Add support for generic PEP 613 type aliases and generic implicit type
aliases:
```py
from typing import TypeVar

T = TypeVar("T")
ListOrSet = list[T] | set[T]

def _(xs: ListOrSet[int]):
    reveal_type(xs)  # list[int] | set[int]
```

closes https://github.com/astral-sh/ty/issues/1643
closes https://github.com/astral-sh/ty/issues/1629
closes https://github.com/astral-sh/ty/issues/1596
closes https://github.com/astral-sh/ty/issues/573
closes https://github.com/astral-sh/ty/issues/221

## Typing conformance

```diff
-aliases_explicit.py:52:5: error[type-assertion-failure] Type `list[int]` does not match asserted type `@Todo(specialized generic alias in type expression)`
-aliases_explicit.py:53:5: error[type-assertion-failure] Type `tuple[str, ...] | list[str]` does not match asserted type `@Todo(Generic specialization of types.UnionType)`
-aliases_explicit.py:54:5: error[type-assertion-failure] Type `tuple[int, int, int, str]` does not match asserted type `@Todo(specialized generic alias in type expression)`
-aliases_explicit.py:56:5: error[type-assertion-failure] Type `(int, str, /) -> str` does not match asserted type `@Todo(Generic specialization of typing.Callable)`
-aliases_explicit.py:59:5: error[type-assertion-failure] Type `int | str | None | list[list[int]]` does not match asserted type `int | str | None | list[@Todo(specialized generic alias in type expression)]`
```

New true negatives ✔️ 

```diff
+aliases_explicit.py:41:36: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
-aliases_explicit.py:57:5: error[type-assertion-failure] Type `(int, str, str, /) -> None` does not match asserted type `@Todo(Generic specialization of typing.Callable)`
+aliases_explicit.py:57:5: error[type-assertion-failure] Type `(int, str, str, /) -> None` does not match asserted type `(...) -> Unknown`
```

These require `ParamSpec`

```diff
+aliases_explicit.py:67:24: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+aliases_explicit.py:68:24: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+aliases_explicit.py:69:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_explicit.py:70:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_explicit.py:71:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_explicit.py:102:20: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
```

New true positives ✔️ 

```diff
-aliases_implicit.py:63:5: error[type-assertion-failure] Type `list[int]` does not match asserted type `@Todo(specialized generic alias in type expression)`
-aliases_implicit.py:64:5: error[type-assertion-failure] Type `tuple[str, ...] | list[str]` does not match asserted type `@Todo(Generic specialization of types.UnionType)`
-aliases_implicit.py:65:5: error[type-assertion-failure] Type `tuple[int, int, int, str]` does not match asserted type `@Todo(specialized generic alias in type expression)`
-aliases_implicit.py:67:5: error[type-assertion-failure] Type `(int, str, /) -> str` does not match asserted type `@Todo(Generic specialization of typing.Callable)`
-aliases_implicit.py:70:5: error[type-assertion-failure] Type `int | str | None | list[list[int]]` does not match asserted type `int | str | None | list[@Todo(specialized generic alias in type expression)]`
-aliases_implicit.py:71:5: error[type-assertion-failure] Type `list[bool]` does not match asserted type `@Todo(specialized generic alias in type expression)`
```

New true negatives ✔️ 

```diff
+aliases_implicit.py:54:36: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
-aliases_implicit.py:68:5: error[type-assertion-failure] Type `(int, str, str, /) -> None` does not match asserted type `@Todo(Generic specialization of typing.Callable)`
+aliases_implicit.py:68:5: error[type-assertion-failure] Type `(int, str, str, /) -> None` does not match asserted type `(...) -> Unknown`
```

These require `ParamSpec`

```diff
+aliases_implicit.py:76:24: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+aliases_implicit.py:77:24: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+aliases_implicit.py:78:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_implicit.py:79:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_implicit.py:80:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_implicit.py:81:25: error[invalid-type-arguments] Type `str` is not assignable to upper bound `int | float` of type variable `TFloat@GoodTypeAlias12`
+aliases_implicit.py:135:20: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
```

New true positives ✔️ 

```diff
+callables_annotation.py:172:19: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+callables_annotation.py:175:19: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+callables_annotation.py:188:25: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+callables_annotation.py:189:25: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
```

These require `ParamSpec` and `Concatenate`.

```diff
-generics_defaults_specialization.py:26:5: error[type-assertion-failure] Type `SomethingWithNoDefaults[int, str]` does not match asserted type `SomethingWithNoDefaults[int, typing.TypeVar]`
+generics_defaults_specialization.py:26:5: error[type-assertion-failure] Type `SomethingWithNoDefaults[int, str]` does not match asserted type `SomethingWithNoDefaults[int, DefaultStrT]`
```

Favorable diagnostic change ✔️ 

```diff
-generics_defaults_specialization.py:27:5: error[type-assertion-failure] Type `SomethingWithNoDefaults[int, bool]` does not match asserted type `@Todo(specialized generic alias in type expression)`
```

New true negative ✔️ 

```diff
-generics_defaults_specialization.py:30:1: error[non-subscriptable] Cannot subscript object of type `<class 'SomethingWithNoDefaults[int, typing.TypeVar]'>` with no `__class_getitem__` method
+generics_defaults_specialization.py:30:15: error[invalid-type-arguments] Too many type arguments: expected between 0 and 1, got 2
```

Correct new diagnostic ✔️ 


```diff
-generics_variance.py:175:25: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:175:35: error[non-subscriptable] Cannot subscript object of type `<class 'Co[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:179:29: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:179:39: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:183:21: error[non-subscriptable] Cannot subscript object of type `<class 'Co[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:183:27: error[non-subscriptable] Cannot subscript object of type `<class 'Co[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:187:25: error[non-subscriptable] Cannot subscript object of type `<class 'Co[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:187:31: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:191:33: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:191:43: error[non-subscriptable] Cannot subscript object of type `<class 'Co[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:191:49: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:196:5: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:196:15: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:196:25: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
```

One of these should apparently be an error, but not of this kind, so
this is good ✔️

```diff
-specialtypes_type.py:152:16: error[invalid-type-form] `typing.TypeVar` is not a generic class
-specialtypes_type.py:156:16: error[invalid-type-form] `typing.TypeVar` is not a generic class
```

Good, those were false positives. ✔️ 

I skipped the analysis for everything involving `TypeVarTuple`.

## Ecosystem impact

**[Full report with detailed
diff](https://david-generic-implicit-alias.ecosystem-663.pages.dev/diff)**

Previous iterations of this PR showed all kinds of problems. In it's
current state, I do not see any large systematic problems, but it is
hard to tell with 5k diagnostic changes.

## Performance

* There is a huge 4x regression in `colour-science/colour`, related to
[this large
file](https://github.com/colour-science/colour/blob/develop/colour/io/luts/tests/test_lut.py)
with [many assignments of hard-coded arrays (lists of lists) to
`np.NDArray`
types](83e754c8b6/colour/io/luts/tests/test_lut.py (L701-L781))
that we now understand. We now take ~2 seconds to check this file, so
definitely not great, but maybe acceptable for now.

## Test Plan

Updated and new Markdown tests
2025-11-28 20:38:24 +01:00
Alex Waygood
594b7b04d3 [ty] Preserve quoting style when autofixing TypedDict keys (#21682) 2025-11-28 18:40:34 +00:00
Matthew Mckee
b5b4917d7f [ty] Fix override of final method summary (#21681) 2025-11-28 16:18:22 +00:00
David Peter
0084e94f78 [ty] Fix subtyping of type[Any] / type[T] and protocols (#21678)
## Summary

This is a bugfix for subtyping of `type[Any]` / `type[T]` and protocols.

## Test Plan

Regression test that will only be really meaningful once
https://github.com/astral-sh/ruff/pull/21553 lands.
2025-11-28 16:56:22 +01:00
Micha Reiser
566c959add [ty] Rename ReferenceRequestHandler file (#21680) 2025-11-28 16:23:29 +01:00
Alex Waygood
8bcfc198b8 [ty] Implement typing.final for methods (#21646)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-11-28 15:18:02 +00:00
Aria Desires
c534bfaf01 [ty] Implement patterns and typevars in the LSP (#21671)
## Summary

**This is the final goto-targets with missing
goto-definition/declaration implementations!
You can now theoretically click on all the user-defined names in all the
syntax. 🎉**

This adds:

* goto definition/declaration on patterns/typevars
* find-references/rename on patterns/typevars
* fixes syntax highlighting of `*rest` patterns

This notably *does not* add:

* goto-type for patterns/typevars 
* hover for patterns/typevars (because that's just goto-type for names)

Also I realized we were at the precipice of one of the great GotoTarget
sins being resolved, and so I made import aliases also resolve to a
ResolvedDefinition. This removes a ton of cruft and prevents further
backsliding.

Note however that import aliases are, in general, completely jacked up
when it comes to find-references/renames (both before and after this
PR). Previously you could try to rename an import alias and it just
wouldn't do anything. With this change we instead refuse to even let you
try to rename it.

Sorting out why import aliases are jacked up is an ongoing thing I hope
to handle in a followup.

## Test Plan

You'll surely not regret checking in 86 snapshot tests
2025-11-28 13:41:21 +00:00
Aria Desires
5e1b2eef57 [ty] implement rendering of .. code:: lang in docstrings (#21665)
## Summary

* Fixes https://github.com/astral-sh/ty/issues/1650
* Part of https://github.com/astral-sh/ty/issues/1610

We now handle:

* `.. warning::` (and friends) by bolding the line and rendering the
block as normal (non-code) text
* `.. code::` (and friends) by treating it the same as `::` (fully
deleted if seen, introduce a code block)
* `.. code:: lang` (and friends) by letting it set the language on the
codefence
* `.. versionchanged:: 1.2.3` (and friends) by rendering it like
`warning` but with the version included and italicized
* `.. dsfsdf-unknown:: (lang)` by assuming it's the same as `.. code::
(lang)`

## Test Plan

Snapshots added/updated. I also deleted a bunch of useless checks on
plaintext rendering. It's important for some edge-case tests but not for
the vast majority of tests.
2025-11-28 13:27:52 +00:00
Dhruv Manilawala
98681b9356 [ty] Add db parameter to Parameters::new method (#21674)
## Summary

This PR adds a new `db` parameter to `Parameters::new` for
https://github.com/astral-sh/ruff/pull/21445. This change creates a
large diff so thought to split it out as it's just a mechanical change.

The `Parameters::new` method not only creates the `Parameters` but also
analyses the parameters to check what kind it is. For `ParamSpec`
support, it's going to require the `db` to check whether the annotated
type is `ParamSpec` or not. For the current set of parameters that isn't
required because it's only checking whether it's dynamic or not which
doesn't require `db`.
2025-11-28 12:29:58 +00:00
Ibraheem Ahmed
3ed537e9f1 [ty] Support type[T] with type variables (#21650)
## Summary

Adds support for `type[T]`, where `T` is a type variable.

- Resolves https://github.com/astral-sh/ty/issues/501
- Resolves https://github.com/astral-sh/ty/issues/783
- Resolves https://github.com/astral-sh/ty/issues/662
2025-11-28 09:20:24 +01:00
Alex Waygood
53efc82989 [ty] Include all members on type in autocompletion suggestions for type[] types (#21670) 2025-11-27 19:29:25 +00:00
Shahar Naveh
666f488f1b Add python 3.14 as a supported version for PyPi (#21669) 2025-11-27 18:57:39 +00:00
Alex Waygood
aef2fad0c5 [ty] Add IDE autofixes for two "Did you mean...?" suggestions (#21667) 2025-11-27 18:20:02 +00:00
Tsvika Shapira
df66946b89 Show partial fixability indicator in statistics output (#21513)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-11-27 18:03:36 +01:00
David Peter
efb23b01af [ty] Ecosystem analyzer: diff report updates (#21662)
## Summary

Pulls in an ecosystem-analyzer change with a few updates to the diff
report:

* Breakdown of added/removed/changed diagnostics by project
* Option to filter diagnostics by project
* Small button to copy a file path to the clipboard
* `(-R +A ~C)` indicators in the filter dropdowns (removed, added,
changed)
* More concise layout, less scrolling

## Test Plan

Tested on https://github.com/astral-sh/ruff/pull/21553 =>
https://david-generic-implicit-alias.ecosystem-663.pages.dev/diff
2025-11-27 16:47:01 +01:00
Aria Desires
e5818d89fd [ty] Add "import ..." code-action for unresolved references (#21629)
## Summary

Originally I planned to feed this in as a `fix` but I realized that we
probably don't want to be trying to resolve import suggestions while
we're doing type inference. Thus I implemented this as a fallback when
there's no fixes on a diagnostic, which can use the full lsp machinery.

Fixes https://github.com/astral-sh/ty/issues/1552

## Test Plan

Works in the IDE, added some e2e tests.
2025-11-27 10:06:38 -05:00
Alex Waygood
a7d48ffd40 [ty] Add subdiagnostic hint if a variable with type Never is used in a type expression (#21660) 2025-11-27 12:48:18 +00:00
Carl Meyer
77f8fa6906 [ty] more precise inference for a failed specialization (#21651)
## Summary

Previously if an explicit specialization failed (e.g. wrong number of
type arguments or violates an upper bound) we just inferred `Unknown`
for the entire type. This actually caused us to panic on an a case of a
recursive upper bound with invalid specialization; the upper bound would
oscillate indefinitely in fixpoint iteration between `Unknown` and the
given specialization. This could be fixed with a cycle recovery
function, but in this case there's a simpler fix: if we infer
`C[Unknown]` instead of `Unknown` for an invalid attempt to specialize
`C`, that allows fixpoint iteration to quickly converge, as well as
giving a more precise type inference.

Other type checkers actually just go with the attempted specialization
even if it's invalid. So if `C` has a type parameter with upper bound
`int`, and you say `C[str]`, they'll emit a diagnostic but just go with
`C[str]`. Even weirder, if `C` has a single type parameter and you say
`C[str, bytes]`, they'll just go with `C[str]` as the type. I'm not
convinced by this approach; it seems odd to have specializations
floating around that explicitly violate the declared upper bound, or in
the latter case aren't even the specialization the annotation requested.
I prefer `C[Unknown]` for this case.

Fixing this revealed an issue with `collections.namedtuple`, which
returns `type[tuple[Any, ...]]`. Due to
https://github.com/astral-sh/ty/issues/1649 we consider that to be an
invalid specialization. So previously we returned `Unknown`; after this
PR it would be `type[tuple[Unknown]]`, leading to more false positives
from our lack of functional namedtuple support. To avoid that I added an
explicit Todo type for functional namedtuples for now.

## Test Plan

Added and updated mdtests.

The conformance suite changes have to do with `ParamSpec`, so no
meaningful signal there.

The ecosystem changes appear to be the expected effects of having more
precise type information (including occurrences of known issues such as
https://github.com/astral-sh/ty/issues/1495 ). Most effects are just
changes to types in diagnostics.
2025-11-27 13:44:28 +01:00
Micha Reiser
7c7f8d1a17 [ty] Make inlay hint clickable in playground (#21656) 2025-11-27 13:29:11 +01:00
Micha Reiser
761031f729 [ty] Add code action support to playground (#21655) 2025-11-27 10:59:57 +01:00
Alex Waygood
792ec3e96e Improve docs on how to stop Ruff and ty disagreeing with each other (#21644)
## Summary

Lots of Ruff rules encourage you to make changes that might then cause
ty to start complaining about Liskov violations. Most of these Ruff
rules already refrain from complaining about a method if they see that
the method is decorated with `@override`, but this usually isn't
documented. This PR updates the docs of many Ruff rules to note that
they refrain from complaining about `@override`-decorated methods, and
also adds a similar note to the ty `invalid-method-override`
documentation.

Helps with
https://github.com/astral-sh/ty/issues/1644#issuecomment-3581663859

## Test Plan

- `uvx prek run -a` locally
- CI on this PR
2025-11-27 08:18:21 +00:00
Dhruv Manilawala
c7107a5a90 [ty] Use zip to perform explicit specialization (#21635)
## Summary

This PR updates the explicit specialization logic to avoid using the
call machinery.

Previously, the logic would use the call machinery by converting the
list of type variables into a `Binding` with a single `Signature` where
all the type variables are positional-only parameters with bounds and
constraints as the annotated type and the default type as the default
parameter value. This has the advantage that it doesn't need to
implement any specific logic but the disadvantages are subpar diagnostic
messages as it would use the ones specific to a function call. But, an
important disadvantage is that the kind of type variable is lost in this
translation which becomes important in #21445 where a `ParamSpec` can
specialize into a list of types which is provided using list literal.
For example,

```py
class Foo[T, **P]: ...

Foo[int, [int, str]]
```

This PR converts the logic to use a simple loop using `zip_longest` as
all type variables and their corresponding type argument maps on a 1-1
basis. They cannot be specified using keyword argument either e.g.,
`dict[_VT=str, _KT=int]` is invalid.

This PR also makes an initial attempt to improve the diagnostic message
to specifically target the specialization part by using words like "type
argument" instead of just "argument" and including information like the
type variable, bounds, and constraints. Further improvements can be made
by highlighting the type variable definition or the bounds / constraints
as a sub-diagnostic but I'm going to leave that as a follow-up.

## Test Plan

Update messages in existing test cases.
2025-11-27 03:52:22 +00:00
Carl Meyer
e0f3a064b9 [ty] don't iterate over a hashset (#21649)
## Summary

This caused "deterministic but chaotic" ordering of some intersection
types in diagnostics. When calling a union, we infer the argument type
once per matching parameter type, intersecting the inferred types for
the argument expression, and we did that in an unpredictable order.

We do need a hashset here for de-duplication. Sometimes we call large
unions where the type for a given parameter is the same across the
union, we should infer the argument once per parameter type, not once
per union element. So use an `FxIndexSet` instead of an `FxHashSet`.

## Test Plan

With this change, switching between `main` and
https://github.com/astral-sh/ruff/pull/21646 no longer changes the
ordering of the intersection type in the test in
cca3a8045d
2025-11-26 16:39:49 -08:00
JumboBear
e2e21508dc docs: update reference to the pre-commit hook (#21645)
## Summary

The reference to the pre-commit hook inside the tutorial was to the
legacy alias `ruff` instead of the current `ruff-check`.

Ref: https://github.com/astral-sh/ruff-pre-commit/pull/124

## Test Plan

Not applicable.
2025-11-26 16:57:18 -05:00
Shunsuke Shibayama
2c0c5ff4e7 [ty] handle recursive type inference properly (#20566)
## Summary

Derived from #17371

Fixes astral-sh/ty#256
Fixes https://github.com/astral-sh/ty/issues/1415
Fixes https://github.com/astral-sh/ty/issues/1433
Fixes https://github.com/astral-sh/ty/issues/1524

Properly handles any kind of recursive inference and prevents panics.

---

Let me explain techniques for converging fixed-point iterations during
recursive type inference.
There are two types of type inference that naively don't converge
(causing salsa to panic): divergent type inference and oscillating type
inference.

### Divergent type inference

Divergent type inference occurs when eagerly expanding a recursive type.
A typical example is this:

```python
class C:
    def f(self, other: "C"):
        self.x = (other.x, 1)

reveal_type(C().x) # revealed: Unknown | tuple[Unknown | tuple[Unknown | tuple[..., Literal[1]], Literal[1]], Literal[1]]
```

To solve this problem, we have already introduced `Divergent` types
(https://github.com/astral-sh/ruff/pull/20312). `Divergent` types are
treated as a kind of dynamic type [^1].

```python
Unknown | tuple[Unknown | tuple[Unknown | tuple[..., Literal[1]], Literal[1]], Literal[1]]
=> Unknown | tuple[Divergent, Literal[1]]
```

When a query function that returns a type enters a cycle, it sets
`Divergent` as the cycle initial value (instead of `Never`). Then, in
the cycle recovery function, it reduces the nesting of types containing
`Divergent` to converge.

```python
0th: Divergent
1st: Unknown | tuple[Divergent, Literal[1]]
2nd: Unknown | tuple[Unknown | tuple[Divergent, Literal[1]], Literal[1]]
=> Unknown | tuple[Divergent, Literal[1]]
```

Each cycle recovery function for each query should operate only on the
`Divergent` type originating from that query.
For this reason, while `Divergent` appears the same as `Any` to the
user, it internally carries some information: the location where the
cycle occurred. Previously, we roughly identified this by having the
scope where the cycle occurred, but with the update to salsa, functions
that create cycle initial values ​​can now receive a `salsa::Id`
(https://github.com/salsa-rs/salsa/pull/1012). This is an opaque ID that
uniquely identifies the cycle head (the query that is the starting point
for the fixed-point iteration). `Divergent` now has this `salsa::Id`.

### Oscillating type inference

Now, another thing to consider is oscillating type inference.
Oscillating type inference arises from the fact that monotonicity is
broken. Monotonicity here means that for a query function, if it enters
a cycle, the calculation must start from a "bottom value" and progress
towards the final result with each cycle. Monotonicity breaks down in
type systems that have features like overloading and overriding.

```python
class Base:
    def flip(self) -> "Sub":
        return Sub()

class Sub(Base):
    def flip(self) -> "Base":
        return Base()

class C:
    def __init__(self, x: Sub):
        self.x = x

    def replace_with(self, other: "C"):
        self.x = other.x.flip()

reveal_type(C(Sub()).x)
```

Naive fixed-point iteration results in `Divergent -> Sub -> Base -> Sub
-> ...`, which oscillates forever without diverging or converging. To
address this, the salsa API has been modified so that the cycle recovery
function receives the value of the previous cycle
(https://github.com/salsa-rs/salsa/pull/1012).
The cycle recovery function returns the union type of the current cycle
and the previous cycle. In the above example, the result type for each
cycle is `Divergent -> Sub -> Base (= Sub | Base) -> Base`, which
converges.

The final result of oscillating type inference does not contain
`Divergent` because `Divergent` that appears in a union type can be
removed, as is clear from the expansion. This simplification is
performed at the same time as nesting reduction.

```
T | Divergent = T | (T | (T | ...)) = T
```

[^1]: In theory, it may be possible to strictly treat types containing
`Divergent` types as recursive types, but we probably shouldn't go that
deep yet. (AFAIK, there are no PEPs that specify how to handle
implicitly recursive types that aren't named by type aliases)

## Performance analysis

A happy side effect of this PR is that we've observed widespread
performance improvements!
This is likely due to the removal of the `ITERATIONS_BEFORE_FALLBACK`
and max-specialization depth trick
(https://github.com/astral-sh/ty/issues/1433,
https://github.com/astral-sh/ty/issues/1415), which means we reach a
fixed point much sooner.

## Ecosystem analysis

The changes look good overall.
You may notice changes in the converged values ​​for recursive types,
this is because the way recursive types are normalized has been changed.
Previously, types containing `Divergent` types were normalized by
replacing them with the `Divergent` type itself, but in this PR, types
with a nesting level of 2 or more that contain `Divergent` types are
normalized by replacing them with a type with a nesting level of 1. This
means that information about the non-divergent parts of recursive types
is no longer lost.

```python
# previous
tuple[tuple[Divergent, int], int] => Divergent
# now
tuple[tuple[Divergent, int], int] => tuple[Divergent, int]
```

The false positive error introduced in this PR occurs in class
definitions with self-referential base classes, such as the one below.

```python
from typing_extensions import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")

class Base2(Generic[T, U]): ...

# TODO: no error
# error: [unsupported-base] "Unsupported class base with type `<class 'Base2[Sub2, U@Sub2]'> | <class 'Base2[Sub2[Unknown], U@Sub2]'>`"
class Sub2(Base2["Sub2", U]): ...
```

This is due to the lack of support for unions of MROs, or because cyclic
legacy generic types are not inferred as generic types early in the
query cycle.

## Test Plan

All samples listed in astral-sh/ty#256 are tested and passed without any
panic!

## Acknowledgments

Thanks to @MichaReiser for working on bug fixes and improvements to
salsa for this PR. @carljm also contributed early on to the discussion
of the query convergence mechanism proposed in this PR.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-11-26 08:50:26 -08:00
Dan Parizher
adf4f1e3f4 [flake8-bandit] Handle string literal bindings in suspicious-url-open-usage (S310) (#21469)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-11-26 09:21:50 +00:00
Micha Reiser
3dbbb76654 Use diagnostic_diff testing for flake8-bandit preview tests (#21637) 2025-11-26 09:13:45 +00:00
Luca Chiodini
b72120f7ef [ty] Semantic tokens: mark comprehension targets as definitions (#21636) 2025-11-26 09:33:14 +01:00
Shahar Naveh
33713a7e2a Add rule to detect unnecessary class properties (#21535)
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Amethyst Reese <amethyst@n7.gg>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-11-26 09:31:22 +01:00
Aria Desires
5364256190 [ty] hotfix panic in semantic tokens (#21632)
Fixes https://github.com/astral-sh/ty/issues/1637
2025-11-25 17:09:46 -05:00
Alex Waygood
81c97e9e94 [ty] Implement typing.override (#21627)
## Summary

Part of https://github.com/astral-sh/ty/issues/155. This implements the
basic check (`@override`-decorated methods should override things!), but
not the strict check specified in
https://typing.python.org/en/latest/spec/class-compat.html#strict-enforcement-per-project,
which should be a separate error code.

## Test Plan

mdtests and snapshots

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-11-25 10:42:40 -08:00
Ibraheem Ahmed
294f863523 [ty] Avoid expression reinference for diagnostics (#21267)
## Summary

We now use the type context for a lot of things, so re-inferring without
type context actually makes diagnostics more confusing (in most cases).
2025-11-25 09:24:00 -08:00
Rasmus Nygren
4628180fac [ty] Improve autocomplete suppressions of keywords in variable bindings
Autocomplete suggestions were not suppressed correctly during
some variable bindings if the parameter name was currently
matching a keyword. E.g. `def f(foo<CURSOR>` was handled
correctly but not `def f(in<CURSOR>`.
2025-11-25 09:09:38 -05:00
Rasmus Nygren
de32247f30 [ty] Only suggest completions based on text before the cursor
Previously we extracted the entire token as the query
independently of the cursor position. By not doing that
you avoid having to do special range handling
to figure out the start position of the current token.

It's likely also more intuitive from a user perspective
to only consider characters left of the cursor when
suggesting autocompletions.
2025-11-25 09:09:38 -05:00
Aria Desires
209ea06592 Implement goto-definition and find-references for global/nonlocal statements (#21616)
## Summary

The implementation here is to just record the idents of these statements
in `scopes_by_expression` (which already supported idents but only ones
that happened to appear in expressions), so that `definitions_for_name`
Just Works.

goto-type (and therefore hover) notably does not work on these
statements because the typechecker does not record info for them. I am
tempted to just introduce `type_for_name` which runs
`definitions_for_name` to find other expressions and queries the
inferred type... but that's a bit whack because it won't be the computed
type at the right point in the code. It probably wouldn't be
particularly expensive to just compute/record the type at those nodes,
as if they were a load, because global/nonlocal is so scarce?

## Test Plan

Snapshot tests added/re-enabled.
2025-11-25 08:56:57 -05:00
Matthew Mckee
88bfc32dfc [ty] Inlay Hint edit follow up (#21621)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

Don't allow edits of some more invalid syntax types.

## Test Plan

Add a test for `x = Literal['a']` (similar) to show we don't allow
edits.
2025-11-25 08:56:14 -05:00
Aria Desires
66d233134f [ty] Implement lsp support for string annotations (#21577)
Fixes https://github.com/astral-sh/ty/issues/1009

## Summary

This adds support for:

* semantic-tokens (syntax highlighting)
* goto-type **(partially implemented, but want to land as-is)**
* goto-declaration
* goto-definition (falls out of goto-declaration)
* hover **(limited by goto-type)**
* find-references
* rename-references (falls out of find-references)

There are 3 major things being introduced here:

* `TypeInferenceBuilder::string_annotations` is a `FxHashSet` of exprs
which were determined to be string annotations during inference. It's
bubbled up in `extras` to hopefully minimize the overhead as in most
contexts it's empty.
* Very happy to hear if this is too hacky and if I should do something
better, but it's IMO important that we get an authoritative answer on
whether something is a string annotation or not.
* `SemanticModel::enter_string_annotation` checks if the expr was marked
by `TypeInferenceBuilder::string_annotations` and then parses the subast
and produces a sub-SemanticModel that sets
`SemanticModel::in_string_annotation_expr`. This expr will be used by
the model whenever we need to query e.g. the scope of the current
expression (otherwise the code will constantly panic as the subast nodes
are not in the current File's AST)
* This hazard consequently encouraged me to refactor a bunch of code to
replace uses of file/db with SemanticModel to minimize hazards (it is no
longer as safe to randomly materialize a SemanticModel in the middle of
analysis, you need to thread through the one you have in case it has
`in_string_annotation_expr` set).
* `GotoTarget::StringAnnotationSubexpr` (and a semantic-tokens impl)
which involves invoking `SemanticModel::enter_string_annotation` before
invoking the same kind of subroutine a normal expression would.
* goto-type (and consequently displaying the type in hover) is the main
hole here, because we can only get the type iff the string annotation is
the entire subexpression (i.e. we can get the type of `"int"` but not
the parts of `"int | str"`). This is shippable IMO.

## Test Plan

Messed around in IDE, wrote a ton of tests.
2025-11-25 13:31:04 +00:00
Micha Reiser
15cb41c1f9 [ty] Add 'remove unused ignore comment' code action (#21582)
## Summary

This PR adds a code action to remove unused ignore comments.

This PR also includes some infrastructure boilerplate to set up code
actions in the editor:

* Extend `snapshot-diagnostics` to render fixes
* Render fixes when using `--output-format=full`
* Hook up edits and the code action request in the LSP
* Add the `Unnecessary` tag to `unused-ignore-comment` diagnostics
* Group multiple unused codes into a single diagnostic

The same fix can be used on the CLI once we add `ty fix` 

Note: `unused-ignore-comment` is currently disabled by default.


https://github.com/user-attachments/assets/f9e21087-3513-4156-85d7-a90b1a7a3489
2025-11-25 08:08:21 -05:00
Micha Reiser
eddb9ad38d [ty] Refactor CheckSuppressionContext to use DiagnosticGuard (#21587) 2025-11-25 10:54:42 +00:00
Alex Waygood
b19ddca69b [ty] Improve several "Did you mean?" suggestions (#21597) 2025-11-25 10:29:01 +00:00
Micha Reiser
747c39a26a [ty] Add more and update existing projects in ty_benchmark (#21536) 2025-11-25 08:58:34 +01:00
250 changed files with 204813 additions and 3750 deletions

View File

@@ -67,7 +67,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e26ebfb78d372b8b091e1cb1d6fc522e135474c1"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
ecosystem-analyzer \
--repository ruff \

View File

@@ -52,7 +52,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e26ebfb78d372b8b091e1cb1d6fc522e135474c1"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
ecosystem-analyzer \
--verbose \

View File

@@ -1,5 +1,40 @@
# Changelog
## 0.14.7
Released on 2025-11-28.
### Preview features
- \[`flake8-bandit`\] Handle string literal bindings in suspicious-url-open-usage (`S310`) ([#21469](https://github.com/astral-sh/ruff/pull/21469))
- \[`pylint`\] Fix `PLR1708` false positives on nested functions ([#21177](https://github.com/astral-sh/ruff/pull/21177))
- \[`pylint`\] Fix suppression for empty dict without tuple key annotation (`PLE1141`) ([#21290](https://github.com/astral-sh/ruff/pull/21290))
- \[`ruff`\] Add rule `RUF066` to detect unnecessary class properties ([#21535](https://github.com/astral-sh/ruff/pull/21535))
- \[`ruff`\] Catch more dummy variable uses (`RUF052`) ([#19799](https://github.com/astral-sh/ruff/pull/19799))
### Bug fixes
- [server] Set severity for non-rule diagnostics ([#21559](https://github.com/astral-sh/ruff/pull/21559))
- \[`flake8-implicit-str-concat`\] Avoid invalid fix in (`ISC003`) ([#21517](https://github.com/astral-sh/ruff/pull/21517))
- \[`parser`\] Fix panic when parsing IPython escape command expressions ([#21480](https://github.com/astral-sh/ruff/pull/21480))
### CLI
- Show partial fixability indicator in statistics output ([#21513](https://github.com/astral-sh/ruff/pull/21513))
### Contributors
- [@mikeleppane](https://github.com/mikeleppane)
- [@senekor](https://github.com/senekor)
- [@ShaharNaveh](https://github.com/ShaharNaveh)
- [@JumboBear](https://github.com/JumboBear)
- [@prakhar1144](https://github.com/prakhar1144)
- [@tsvikas](https://github.com/tsvikas)
- [@danparizher](https://github.com/danparizher)
- [@chirizxc](https://github.com/chirizxc)
- [@AlexWaygood](https://github.com/AlexWaygood)
- [@MichaReiser](https://github.com/MichaReiser)
## 0.14.6
Released on 2025-11-21.

10
Cargo.lock generated
View File

@@ -2859,7 +2859,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.6"
version = "0.14.7"
dependencies = [
"anyhow",
"argfile",
@@ -3117,7 +3117,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.6"
version = "0.14.7"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3472,7 +3472,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.6"
version = "0.14.7"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -4474,6 +4474,7 @@ dependencies = [
"quickcheck_macros",
"ruff_annotate_snippets",
"ruff_db",
"ruff_diagnostics",
"ruff_index",
"ruff_macros",
"ruff_memory_usage",
@@ -4519,6 +4520,7 @@ dependencies = [
"lsp-types",
"regex",
"ruff_db",
"ruff_diagnostics",
"ruff_macros",
"ruff_notebook",
"ruff_python_ast",
@@ -4559,6 +4561,7 @@ dependencies = [
"path-slash",
"regex",
"ruff_db",
"ruff_diagnostics",
"ruff_index",
"ruff_notebook",
"ruff_python_ast",
@@ -4600,6 +4603,7 @@ dependencies = [
"js-sys",
"log",
"ruff_db",
"ruff_diagnostics",
"ruff_notebook",
"ruff_python_formatter",
"ruff_source_file",

View File

@@ -147,8 +147,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.6/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.6/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.7/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -181,7 +181,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.6
rev: v0.14.7
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.14.6"
version = "0.14.7"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -34,9 +34,21 @@ struct ExpandedStatistics<'a> {
code: Option<&'a SecondaryCode>,
name: &'static str,
count: usize,
fixable: bool,
#[serde(rename = "fixable")]
all_fixable: bool,
fixable_count: usize,
}
impl ExpandedStatistics<'_> {
fn any_fixable(&self) -> bool {
self.fixable_count > 0
}
}
/// Accumulator type for grouping diagnostics by code.
/// Format: (`code`, `representative_diagnostic`, `total_count`, `fixable_count`)
type DiagnosticGroup<'a> = (Option<&'a SecondaryCode>, &'a Diagnostic, usize, usize);
pub(crate) struct Printer {
format: OutputFormat,
log_level: LogLevel,
@@ -133,7 +145,7 @@ impl Printer {
if fixables.applicable > 0 {
writeln!(
writer,
"{fix_prefix} {} fixable with the --fix option.",
"{fix_prefix} {} fixable with the `--fix` option.",
fixables.applicable
)?;
}
@@ -256,35 +268,41 @@ impl Printer {
diagnostics: &Diagnostics,
writer: &mut dyn Write,
) -> Result<()> {
let required_applicability = self.unsafe_fixes.required_applicability();
let statistics: Vec<ExpandedStatistics> = diagnostics
.inner
.iter()
.map(|message| (message.secondary_code(), message))
.sorted_by_key(|(code, message)| (*code, message.fixable()))
.fold(
vec![],
|mut acc: Vec<((Option<&SecondaryCode>, &Diagnostic), usize)>, (code, message)| {
if let Some(((prev_code, _prev_message), count)) = acc.last_mut() {
if *prev_code == code {
*count += 1;
return acc;
.sorted_by_key(|diagnostic| diagnostic.secondary_code())
.fold(vec![], |mut acc: Vec<DiagnosticGroup>, diagnostic| {
let is_fixable = diagnostic
.fix()
.is_some_and(|fix| fix.applies(required_applicability));
let code = diagnostic.secondary_code();
if let Some((prev_code, _prev_message, count, fixable_count)) = acc.last_mut() {
if *prev_code == code {
*count += 1;
if is_fixable {
*fixable_count += 1;
}
return acc;
}
acc.push(((code, message), 1));
acc
}
acc.push((code, diagnostic, 1, usize::from(is_fixable)));
acc
})
.iter()
.map(
|&(code, message, count, fixable_count)| ExpandedStatistics {
code,
name: message.name(),
count,
// Backward compatibility: `fixable` is true only when all violations are fixable.
// See: https://github.com/astral-sh/ruff/pull/21513
all_fixable: fixable_count == count,
fixable_count,
},
)
.iter()
.map(|&((code, message), count)| ExpandedStatistics {
code,
name: message.name(),
count,
fixable: if let Some(fix) = message.fix() {
fix.applies(self.unsafe_fixes.required_applicability())
} else {
false
},
})
.sorted_by_key(|statistic| Reverse(statistic.count))
.collect();
@@ -308,13 +326,14 @@ impl Printer {
.map(|statistic| statistic.code.map_or(0, |s| s.len()))
.max()
.unwrap();
let any_fixable = statistics.iter().any(|statistic| statistic.fixable);
let any_fixable = statistics.iter().any(ExpandedStatistics::any_fixable);
let fixable = format!("[{}] ", "*".cyan());
let all_fixable = format!("[{}] ", "*".cyan());
let partially_fixable = format!("[{}] ", "-".cyan());
let unfixable = "[ ] ";
// By default, we mimic Flake8's `--statistics` format.
for statistic in statistics {
for statistic in &statistics {
writeln!(
writer,
"{:>count_width$}\t{:<code_width$}\t{}{}",
@@ -326,8 +345,10 @@ impl Printer {
.red()
.bold(),
if any_fixable {
if statistic.fixable {
&fixable
if statistic.all_fixable {
&all_fixable
} else if statistic.any_fixable() {
&partially_fixable
} else {
unfixable
}

View File

@@ -1043,7 +1043,7 @@ def mvce(keys, values):
----- stdout -----
1 C416 [*] unnecessary-comprehension
Found 1 error.
[*] 1 fixable with the --fix option.
[*] 1 fixable with the `--fix` option.
----- stderr -----
");
@@ -1073,7 +1073,8 @@ def mvce(keys, values):
"code": "C416",
"name": "unnecessary-comprehension",
"count": 1,
"fixable": false
"fixable": false,
"fixable_count": 0
}
]
@@ -1106,7 +1107,8 @@ def mvce(keys, values):
"code": "C416",
"name": "unnecessary-comprehension",
"count": 1,
"fixable": true
"fixable": true,
"fixable_count": 1
}
]
@@ -1114,6 +1116,54 @@ def mvce(keys, values):
"#);
}
#[test]
fn show_statistics_json_partial_fix() {
let mut cmd = RuffCheck::default()
.args([
"--select",
"UP035",
"--statistics",
"--output-format",
"json",
])
.build();
assert_cmd_snapshot!(cmd
.pass_stdin("from typing import List, AsyncGenerator"), @r#"
success: false
exit_code: 1
----- stdout -----
[
{
"code": "UP035",
"name": "deprecated-import",
"count": 2,
"fixable": false,
"fixable_count": 1
}
]
----- stderr -----
"#);
}
#[test]
fn show_statistics_partial_fix() {
let mut cmd = RuffCheck::default()
.args(["--select", "UP035", "--statistics"])
.build();
assert_cmd_snapshot!(cmd
.pass_stdin("from typing import List, AsyncGenerator"), @r"
success: false
exit_code: 1
----- stdout -----
2 UP035 [-] deprecated-import
Found 2 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----
");
}
#[test]
fn show_statistics_syntax_errors() {
let mut cmd = RuffCheck::default()
@@ -1810,7 +1860,7 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
--> -:1:1
Found 2 errors.
[*] 1 fixable with the --fix option.
[*] 1 fixable with the `--fix` option.
----- stderr -----
");
@@ -1853,7 +1903,7 @@ fn check_shows_unsafe_fixes_with_opt_in() {
--> -:1:1
Found 2 errors.
[*] 2 fixable with the --fix option.
[*] 2 fixable with the `--fix` option.
----- stderr -----
");

View File

@@ -120,7 +120,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY310,
},
600,
1070,
);
static FREQTRADE: Benchmark = Benchmark::new(
@@ -223,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
900,
950,
);
#[track_caller]

View File

@@ -1,5 +1,5 @@
use ruff_db::files::FilePath;
use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
use ty_python_semantic::{ModuleName, resolve_module_old, resolve_real_module_old};
use crate::ModuleDb;
use crate::collector::CollectedImport;
@@ -70,13 +70,13 @@ impl<'a> Resolver<'a> {
/// Resolves a module name to a module.
pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_module(self.db, module_name)?;
let module = resolve_module_old(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
/// Resolves a module name to a module (stubs not allowed).
fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_real_module(self.db, module_name)?;
let module = resolve_real_module_old(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.14.6"
version = "0.14.7"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -45,3 +45,22 @@ urllib.request.urlopen(urllib.request.Request(url))
# https://github.com/astral-sh/ruff/issues/15522
map(urllib.request.urlopen, [])
foo = urllib.request.urlopen
# https://github.com/astral-sh/ruff/issues/21462
path = "https://example.com/data.csv"
urllib.request.urlretrieve(path, "data.csv")
url = "https://example.com/api"
urllib.request.Request(url)
# Test resolved f-strings and concatenated string literals
fstring_url = f"https://example.com/data.csv"
urllib.request.urlopen(fstring_url)
urllib.request.Request(fstring_url)
concatenated_url = "https://" + "example.com/data.csv"
urllib.request.urlopen(concatenated_url)
urllib.request.Request(concatenated_url)
nested_concatenated = "http://" + "example.com" + "/data.csv"
urllib.request.urlopen(nested_concatenated)
urllib.request.Request(nested_concatenated)

View File

@@ -0,0 +1,70 @@
import abc
import typing
class User: # Test normal class properties
@property
def name(self): # ERROR: No return
f"{self.first_name} {self.last_name}"
@property
def age(self): # OK: Returning something
return 100
def method(self): # OK: Not a property
x = 1
@property
def nested(self): # ERROR: Property itself doesn't return
def inner():
return 0
@property
def stub(self): ... # OK: A stub; doesn't return anything
class UserMeta(metaclass=abc.ABCMeta): # Test properies inside of an ABC class
@property
@abc.abstractmethod
def abstr_prop1(self): ... # OK: Abstract methods doesn't need to return anything
@property
@abc.abstractmethod
def abstr_prop2(self): # OK: Abstract methods doesn't need to return anything
"""
A cool docstring
"""
@property
def prop1(self): # OK: Returning a value
return 1
@property
def prop2(self): # ERROR: Not returning something (even when we are inside an ABC)
50
def method(self): # OK: Not a property
x = 1
def func(): # OK: Not a property
x = 1
class Proto(typing.Protocol): # Tests for a Protocol class
@property
def prop1(self) -> int: ... # OK: A stub property
class File: # Extra tests for things like yield/yield from/raise
@property
def stream1(self): # OK: Yields something
yield
@property
def stream2(self): # OK: Yields from something
yield from self.stream1
@property
def children(self): # OK: Raises
raise ValueError("File does not have children")

View File

@@ -17,7 +17,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:8:5: F841 [
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
crates/ruff_linter/resources/test/project/project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
Found 7 errors.
[*] 7 potentially fixable with the --fix option.
[*] 7 potentially fixable with the `--fix` option.
```
Running from the project directory itself should exhibit the same behavior:
@@ -32,7 +32,7 @@ examples/docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but n
project/file.py:1:8: F401 [*] `os` imported but unused
project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
Found 7 errors.
[*] 7 potentially fixable with the --fix option.
[*] 7 potentially fixable with the `--fix` option.
```
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
@@ -43,7 +43,7 @@ files:
docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
Found 2 errors.
[*] 2 potentially fixable with the --fix option.
[*] 2 potentially fixable with the `--fix` option.
```
`--config` should force Ruff to use the specified `pyproject.toml` for all files, and resolve
@@ -61,7 +61,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:4:27: F401
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
Found 9 errors.
[*] 9 potentially fixable with the --fix option.
[*] 9 potentially fixable with the `--fix` option.
```
Running from a parent directory should "ignore" the `exclude` (hence, `concepts/file.py` gets
@@ -74,7 +74,7 @@ docs/docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
excluded/script.py:5:5: F841 [*] Local variable `x` is assigned to but never used
Found 4 errors.
[*] 4 potentially fixable with the --fix option.
[*] 4 potentially fixable with the `--fix` option.
```
Passing an excluded directory directly should report errors in the contained files:
@@ -83,7 +83,7 @@ Passing an excluded directory directly should report errors in the contained fil
∴ cargo run -p ruff -- check crates/ruff_linter/resources/test/project/examples/excluded/
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
[*] 1 potentially fixable with the `--fix` option.
```
Unless we `--force-exclude`:

View File

@@ -347,6 +347,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::InvalidArgumentName) {
pep8_naming::rules::invalid_argument_name_function(checker, function_def);
}
if checker.is_rule_enabled(Rule::PropertyWithoutReturn) {
ruff::rules::property_without_return(checker, function_def);
}
}
Stmt::Return(_) => {
if checker.is_rule_enabled(Rule::ReturnInInit) {

View File

@@ -1058,6 +1058,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "063") => rules::ruff::rules::AccessAnnotationsFromClassDict,
(Ruff, "064") => rules::ruff::rules::NonOctalPermissions,
(Ruff, "065") => rules::ruff::rules::LoggingEagerConversion,
(Ruff, "066") => rules::ruff::rules::PropertyWithoutReturn,
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,

View File

@@ -279,3 +279,10 @@ pub(crate) const fn is_extended_snmp_api_path_detection_enabled(settings: &Linte
pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21469
pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}

View File

@@ -10,11 +10,11 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::assert_diagnostics;
use crate::registry::Rule;
use crate::settings::LinterSettings;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_diagnostics, assert_diagnostics_diff};
#[test_case(Rule::Assert, Path::new("S101.py"))]
#[test_case(Rule::BadFilePermissions, Path::new("S103.py"))]
@@ -112,14 +112,19 @@ mod tests {
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
assert_diagnostics_diff!(
snapshot,
Path::new("flake8_bandit").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Disabled,
..LinterSettings::for_rule(rule_code)
},
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
}
);
Ok(())
}

View File

@@ -4,11 +4,16 @@
use itertools::Either;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Arguments, Decorator, Expr, ExprCall, Operator};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing::find_binding_value;
use ruff_text_size::{Ranged, TextRange};
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_suspicious_function_reference_enabled;
use crate::preview::{
is_s310_resolve_string_literal_bindings_enabled, is_suspicious_function_reference_enabled,
};
use crate::settings::LinterSettings;
/// ## What it does
/// Checks for calls to `pickle` functions or modules that wrap them.
@@ -1016,6 +1021,25 @@ fn suspicious_function(
|| has_prefix(chars.skip_while(|c| c.is_whitespace()), "https://")
}
/// Resolves `expr` to its binding and checks if the resolved expression starts with an HTTP or HTTPS prefix.
fn expression_starts_with_http_prefix(
expr: &Expr,
semantic: &SemanticModel,
settings: &LinterSettings,
) -> bool {
let resolved_expression = if is_s310_resolve_string_literal_bindings_enabled(settings)
&& let Some(name_expr) = expr.as_name_expr()
&& let Some(binding_id) = semantic.only_binding(name_expr)
&& let Some(value) = find_binding_value(semantic.binding(binding_id), semantic)
{
value
} else {
expr
};
leading_chars(resolved_expression).is_some_and(has_http_prefix)
}
/// Return the leading characters for an expression, if it's a string literal, f-string, or
/// string concatenation.
fn leading_chars(expr: &Expr) -> Option<impl Iterator<Item = char> + Clone + '_> {
@@ -1139,17 +1163,19 @@ fn suspicious_function(
// URLOpen (`Request`)
["urllib", "request", "Request"] | ["six", "moves", "urllib", "request", "Request"] => {
if let Some(arguments) = arguments {
// If the `url` argument is a string literal or an f-string, allow `http` and `https` schemes.
// If the `url` argument is a string literal (including resolved bindings), allow `http` and `https` schemes.
if arguments.args.iter().all(|arg| !arg.is_starred_expr())
&& arguments
.keywords
.iter()
.all(|keyword| keyword.arg.is_some())
{
if arguments
.find_argument_value("url", 0)
.and_then(leading_chars)
.is_some_and(has_http_prefix)
if let Some(url_expr) = arguments.find_argument_value("url", 0)
&& expression_starts_with_http_prefix(
url_expr,
checker.semantic(),
checker.settings(),
)
{
return;
}
@@ -1186,19 +1212,25 @@ fn suspicious_function(
name.segments() == ["urllib", "request", "Request"]
})
{
if arguments
.find_argument_value("url", 0)
.and_then(leading_chars)
.is_some_and(has_http_prefix)
if let Some(url_expr) = arguments.find_argument_value("url", 0)
&& expression_starts_with_http_prefix(
url_expr,
checker.semantic(),
checker.settings(),
)
{
return;
}
}
}
// If the `url` argument is a string literal, allow `http` and `https` schemes.
// If the `url` argument is a string literal (including resolved bindings), allow `http` and `https` schemes.
Some(expr) => {
if leading_chars(expr).is_some_and(has_http_prefix) {
if expression_starts_with_http_prefix(
expr,
checker.semantic(),
checker.settings(),
) {
return;
}
}

View File

@@ -254,3 +254,84 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
42 | urllib.request.urlopen(urllib.request.Request(url))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:51:1
|
49 | # https://github.com/astral-sh/ruff/issues/21462
50 | path = "https://example.com/data.csv"
51 | urllib.request.urlretrieve(path, "data.csv")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | url = "https://example.com/api"
53 | urllib.request.Request(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:53:1
|
51 | urllib.request.urlretrieve(path, "data.csv")
52 | url = "https://example.com/api"
53 | urllib.request.Request(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
54 |
55 | # Test resolved f-strings and concatenated string literals
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:57:1
|
55 | # Test resolved f-strings and concatenated string literals
56 | fstring_url = f"https://example.com/data.csv"
57 | urllib.request.urlopen(fstring_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 | urllib.request.Request(fstring_url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:58:1
|
56 | fstring_url = f"https://example.com/data.csv"
57 | urllib.request.urlopen(fstring_url)
58 | urllib.request.Request(fstring_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 |
60 | concatenated_url = "https://" + "example.com/data.csv"
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:61:1
|
60 | concatenated_url = "https://" + "example.com/data.csv"
61 | urllib.request.urlopen(concatenated_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 | urllib.request.Request(concatenated_url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:62:1
|
60 | concatenated_url = "https://" + "example.com/data.csv"
61 | urllib.request.urlopen(concatenated_url)
62 | urllib.request.Request(concatenated_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 |
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:65:1
|
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
65 | urllib.request.urlopen(nested_concatenated)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 | urllib.request.Request(nested_concatenated)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:66:1
|
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
65 | urllib.request.urlopen(nested_concatenated)
66 | urllib.request.Request(nested_concatenated)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|

View File

@@ -1,15 +1,15 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
--> S301.py:3:1
|
1 | import pickle
2 |
3 | pickle.loads()
| ^^^^^^^^^^^^^^
|
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 2
--- Added ---
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
--> S301.py:7:5
|
@@ -19,6 +19,7 @@ S301 `pickle` and modules that wrap it can be unsafe when used to deserialize un
8 | foo = pickle.load
|
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
--> S301.py:8:7
|

View File

@@ -1,24 +1,15 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S307 Use of possibly insecure function; consider using `ast.literal_eval`
--> S307.py:3:7
|
1 | import os
2 |
3 | print(eval("1+1")) # S307
| ^^^^^^^^^^^
4 | print(eval("os.getcwd()")) # S307
|
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
S307 Use of possibly insecure function; consider using `ast.literal_eval`
--> S307.py:4:7
|
3 | print(eval("1+1")) # S307
4 | print(eval("os.getcwd()")) # S307
| ^^^^^^^^^^^^^^^^^^^
|
--- Summary ---
Removed: 0
Added: 2
--- Added ---
S307 Use of possibly insecure function; consider using `ast.literal_eval`
--> S307.py:16:5
|
@@ -28,6 +19,7 @@ S307 Use of possibly insecure function; consider using `ast.literal_eval`
17 | foo = eval
|
S307 Use of possibly insecure function; consider using `ast.literal_eval`
--> S307.py:17:7
|

View File

@@ -1,60 +1,37 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:6:5
|
4 | def bad_func():
5 | inject = "harmful_input"
6 | mark_safe(inject)
| ^^^^^^^^^^^^^^^^^
7 | mark_safe("I will add" + inject + "to my string")
8 | mark_safe("I will add %s to my string" % inject)
|
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:7:5
|
5 | inject = "harmful_input"
6 | mark_safe(inject)
7 | mark_safe("I will add" + inject + "to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | mark_safe("I will add %s to my string" % inject)
9 | mark_safe("I will add {} to my string".format(inject))
|
--- Summary ---
Removed: 2
Added: 4
--- Removed ---
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:8:5
--> S308.py:16:1
|
6 | mark_safe(inject)
7 | mark_safe("I will add" + inject + "to my string")
8 | mark_safe("I will add %s to my string" % inject)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 | mark_safe("I will add {} to my string".format(inject))
10 | mark_safe(f"I will add {inject} to my string")
16 | @mark_safe
| ^^^^^^^^^^
17 | def some_func():
18 | return '<script>alert("evil!")</script>'
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:9:5
|
7 | mark_safe("I will add" + inject + "to my string")
8 | mark_safe("I will add %s to my string" % inject)
9 | mark_safe("I will add {} to my string".format(inject))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 | mark_safe(f"I will add {inject} to my string")
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:10:5
--> S308.py:36:1
|
8 | mark_safe("I will add %s to my string" % inject)
9 | mark_safe("I will add {} to my string".format(inject))
10 | mark_safe(f"I will add {inject} to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 |
12 | def good_func():
36 | @mark_safe
| ^^^^^^^^^^
37 | def some_func():
38 | return '<script>alert("evil!")</script>'
|
--- Added ---
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:16:2
|
@@ -64,59 +41,6 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
18 | return '<script>alert("evil!")</script>'
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:26:5
|
24 | def bad_func():
25 | inject = "harmful_input"
26 | mark_safe(inject)
| ^^^^^^^^^^^^^^^^^
27 | mark_safe("I will add" + inject + "to my string")
28 | mark_safe("I will add %s to my string" % inject)
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:27:5
|
25 | inject = "harmful_input"
26 | mark_safe(inject)
27 | mark_safe("I will add" + inject + "to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | mark_safe("I will add %s to my string" % inject)
29 | mark_safe("I will add {} to my string".format(inject))
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:28:5
|
26 | mark_safe(inject)
27 | mark_safe("I will add" + inject + "to my string")
28 | mark_safe("I will add %s to my string" % inject)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29 | mark_safe("I will add {} to my string".format(inject))
30 | mark_safe(f"I will add {inject} to my string")
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:29:5
|
27 | mark_safe("I will add" + inject + "to my string")
28 | mark_safe("I will add %s to my string" % inject)
29 | mark_safe("I will add {} to my string".format(inject))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30 | mark_safe(f"I will add {inject} to my string")
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:30:5
|
28 | mark_safe("I will add %s to my string" % inject)
29 | mark_safe("I will add {} to my string".format(inject))
30 | mark_safe(f"I will add {inject} to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31 |
32 | def good_func():
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:36:2
@@ -127,6 +51,7 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
38 | return '<script>alert("evil!")</script>'
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:42:5
|
@@ -136,6 +61,7 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
43 | foo = mark_safe
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:43:7
|

View File

@@ -1,260 +1,106 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:6:1
|
4 | urllib.request.urlopen(url=f'http://www.google.com')
5 | urllib.request.urlopen(url='http://' + 'www' + '.google.com')
6 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 | urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
8 | urllib.request.urlopen('http://www.google.com')
|
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:7:1
|
5 | urllib.request.urlopen(url='http://' + 'www' + '.google.com')
6 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
7 | urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | urllib.request.urlopen('http://www.google.com')
9 | urllib.request.urlopen(f'http://www.google.com')
|
--- Summary ---
Removed: 8
Added: 2
--- Removed ---
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:10:1
--> S310.py:51:1
|
8 | urllib.request.urlopen('http://www.google.com')
9 | urllib.request.urlopen(f'http://www.google.com')
10 | urllib.request.urlopen('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 | urllib.request.urlopen(url)
49 | # https://github.com/astral-sh/ruff/issues/21462
50 | path = "https://example.com/data.csv"
51 | urllib.request.urlretrieve(path, "data.csv")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | url = "https://example.com/api"
53 | urllib.request.Request(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:11:1
--> S310.py:53:1
|
9 | urllib.request.urlopen(f'http://www.google.com')
10 | urllib.request.urlopen('file:///foo/bar/baz')
11 | urllib.request.urlopen(url)
51 | urllib.request.urlretrieve(path, "data.csv")
52 | url = "https://example.com/api"
53 | urllib.request.Request(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 |
13 | urllib.request.Request(url='http://www.google.com')
54 |
55 | # Test resolved f-strings and concatenated string literals
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:16:1
--> S310.py:57:1
|
14 | urllib.request.Request(url=f'http://www.google.com')
15 | urllib.request.Request(url='http://' + 'www' + '.google.com')
16 | urllib.request.Request(url='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17 | urllib.request.Request(url=f'http://www.google.com', **kwargs)
18 | urllib.request.Request('http://www.google.com')
55 | # Test resolved f-strings and concatenated string literals
56 | fstring_url = f"https://example.com/data.csv"
57 | urllib.request.urlopen(fstring_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 | urllib.request.Request(fstring_url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:17:1
--> S310.py:58:1
|
15 | urllib.request.Request(url='http://' + 'www' + '.google.com')
16 | urllib.request.Request(url='http://www.google.com', **kwargs)
17 | urllib.request.Request(url=f'http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 | urllib.request.Request('http://www.google.com')
19 | urllib.request.Request(f'http://www.google.com')
56 | fstring_url = f"https://example.com/data.csv"
57 | urllib.request.urlopen(fstring_url)
58 | urllib.request.Request(fstring_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 |
60 | concatenated_url = "https://" + "example.com/data.csv"
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:20:1
--> S310.py:61:1
|
18 | urllib.request.Request('http://www.google.com')
19 | urllib.request.Request(f'http://www.google.com')
20 | urllib.request.Request('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21 | urllib.request.Request(url)
60 | concatenated_url = "https://" + "example.com/data.csv"
61 | urllib.request.urlopen(concatenated_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 | urllib.request.Request(concatenated_url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:21:1
--> S310.py:62:1
|
19 | urllib.request.Request(f'http://www.google.com')
20 | urllib.request.Request('file:///foo/bar/baz')
21 | urllib.request.Request(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
22 |
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
60 | concatenated_url = "https://" + "example.com/data.csv"
61 | urllib.request.urlopen(concatenated_url)
62 | urllib.request.Request(concatenated_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 |
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:23:1
--> S310.py:65:1
|
21 | urllib.request.Request(url)
22 |
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
65 | urllib.request.urlopen(nested_concatenated)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 | urllib.request.Request(nested_concatenated)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:24:1
--> S310.py:66:1
|
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
65 | urllib.request.urlopen(nested_concatenated)
66 | urllib.request.Request(nested_concatenated)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:25:1
|
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:26:1
|
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
28 | urllib.request.URLopener().open('http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:27:1
|
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | urllib.request.URLopener().open('http://www.google.com')
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:28:1
|
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
28 | urllib.request.URLopener().open('http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
29 | urllib.request.URLopener().open(f'http://www.google.com')
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:29:1
|
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
28 | urllib.request.URLopener().open('http://www.google.com')
29 | urllib.request.URLopener().open(f'http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:30:1
|
28 | urllib.request.URLopener().open('http://www.google.com')
29 | urllib.request.URLopener().open(f'http://www.google.com')
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
32 | urllib.request.URLopener().open(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:31:1
|
29 | urllib.request.URLopener().open(f'http://www.google.com')
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | urllib.request.URLopener().open(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:32:1
|
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
32 | urllib.request.URLopener().open(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
33 |
34 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:37:1
|
35 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'))
36 | urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
37 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:38:1
|
36 | urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
37 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
38 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:41:1
|
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42 | urllib.request.urlopen(urllib.request.Request(url))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:41:24
|
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42 | urllib.request.urlopen(urllib.request.Request(url))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:42:1
|
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
42 | urllib.request.urlopen(urllib.request.Request(url))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:42:24
|
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
42 | urllib.request.urlopen(urllib.request.Request(url))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
--- Added ---
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:46:5
|
@@ -264,6 +110,7 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
47 | foo = urllib.request.urlopen
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:47:7
|
@@ -271,4 +118,6 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
46 | map(urllib.request.urlopen, [])
47 | foo = urllib.request.urlopen
| ^^^^^^^^^^^^^^^^^^^^^^
48 |
49 | # https://github.com/astral-sh/ruff/issues/21462
|

View File

@@ -1,103 +1,15 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:10:1
|
9 | # Errors
10 | random.Random()
| ^^^^^^^^^^^^^^^
11 | random.random()
12 | random.randrange()
|
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:11:1
|
9 | # Errors
10 | random.Random()
11 | random.random()
| ^^^^^^^^^^^^^^^
12 | random.randrange()
13 | random.randint()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:12:1
|
10 | random.Random()
11 | random.random()
12 | random.randrange()
| ^^^^^^^^^^^^^^^^^^
13 | random.randint()
14 | random.choice()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:13:1
|
11 | random.random()
12 | random.randrange()
13 | random.randint()
| ^^^^^^^^^^^^^^^^
14 | random.choice()
15 | random.choices()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:14:1
|
12 | random.randrange()
13 | random.randint()
14 | random.choice()
| ^^^^^^^^^^^^^^^
15 | random.choices()
16 | random.uniform()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:15:1
|
13 | random.randint()
14 | random.choice()
15 | random.choices()
| ^^^^^^^^^^^^^^^^
16 | random.uniform()
17 | random.triangular()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:16:1
|
14 | random.choice()
15 | random.choices()
16 | random.uniform()
| ^^^^^^^^^^^^^^^^
17 | random.triangular()
18 | random.randbytes()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:17:1
|
15 | random.choices()
16 | random.uniform()
17 | random.triangular()
| ^^^^^^^^^^^^^^^^^^^
18 | random.randbytes()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:18:1
|
16 | random.uniform()
17 | random.triangular()
18 | random.randbytes()
| ^^^^^^^^^^^^^^^^^^
19 |
20 | # Unrelated
|
--- Summary ---
Removed: 0
Added: 2
--- Added ---
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:26:5
|
@@ -107,6 +19,7 @@ S311 Standard pseudo-random generators are not suitable for cryptographic purpos
27 | foo = random.randrange
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:27:7
|

View File

@@ -1,15 +1,15 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:3:1
|
1 | from telnetlib import Telnet
2 |
3 | Telnet("localhost", 23)
| ^^^^^^^^^^^^^^^^^^^^^^^
|
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 3
--- Added ---
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:7:5
|
@@ -19,6 +19,7 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
8 | foo = Telnet
|
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:8:7
|
@@ -30,6 +31,7 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
10 | import telnetlib
|
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:11:5
|
@@ -39,13 +41,3 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
12 |
13 | from typing import Annotated
|
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:14:24
|
13 | from typing import Annotated
14 | foo: Annotated[Telnet, telnetlib.Telnet()]
| ^^^^^^^^^^^^^^^^^^
15 |
16 | def _() -> Telnet: ...
|

View File

@@ -1,26 +1,15 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:3:25
|
1 | from pysnmp.hlapi import CommunityData
2 |
3 | CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
4 | CommunityData("public", mpModel=1) # S508
|
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:4:25
|
3 | CommunityData("public", mpModel=0) # S508
4 | CommunityData("public", mpModel=1) # S508
| ^^^^^^^^^
5 |
6 | CommunityData("public", mpModel=2) # OK
|
--- Summary ---
Removed: 0
Added: 8
--- Added ---
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:18:46
|
@@ -32,6 +21,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:19:58
|
@@ -42,6 +32,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:20:53
|
@@ -53,6 +44,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:21:45
|
@@ -64,6 +56,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:22:58
|
@@ -75,6 +68,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:23:53
|
@@ -86,6 +80,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:24:45
|
@@ -96,6 +91,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:25:43
|

View File

@@ -1,24 +1,15 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:4:12
|
4 | insecure = UsmUserData("securityName") # S509
| ^^^^^^^^^^^
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
|
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:5:16
|
4 | insecure = UsmUserData("securityName") # S509
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
| ^^^^^^^^^^^
6 |
7 | less_insecure = UsmUserData("securityName", "authName", "privName") # OK
|
--- Summary ---
Removed: 0
Added: 4
--- Added ---
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:15:1
|
@@ -30,6 +21,7 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv`
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:16:1
|
@@ -40,6 +32,7 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv`
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:17:1
|
@@ -50,6 +43,7 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv`
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:18:1
|

View File

@@ -25,6 +25,11 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// keyword-only argument, to force callers to be explicit when providing
/// the argument.
///
/// This rule exempts methods decorated with [`@typing.override`][override],
/// since changing the signature of a subclass method that overrides a
/// superclass method may cause type checkers to complain about a violation of
/// the Liskov Substitution Principle.
///
/// ## Example
/// ```python
/// from math import ceil, floor
@@ -89,6 +94,8 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.127")]
pub(crate) struct BooleanDefaultValuePositionalArgument;

View File

@@ -28,7 +28,7 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// the argument.
///
/// Dunder methods that define operators are exempt from this rule, as are
/// setters and `@override` definitions.
/// setters and [`@override`][override] definitions.
///
/// ## Example
///
@@ -93,6 +93,8 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.127")]
pub(crate) struct BooleanTypeHintPositionalArgument;

View File

@@ -17,6 +17,8 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// non-obvious errors, as readers may mistake the argument for the
/// builtin and vice versa.
///
/// Function definitions decorated with [`@override`][override] or
/// [`@overload`][overload] are exempt from this rule by default.
/// Builtins can be marked as exceptions to this rule via the
/// [`lint.flake8-builtins.ignorelist`] configuration option.
///
@@ -48,6 +50,9 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// ## References
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
/// [overload]: https://docs.python.org/3/library/typing.html#typing.overload
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.48")]
pub(crate) struct BuiltinArgumentShadowing {

View File

@@ -1,8 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::whitespace::trailing_comment_start_offset;
use ruff_python_ast::{Expr, ExprStringLiteral, Stmt, StmtExpr};
use ruff_python_semantic::{ScopeKind, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -101,7 +99,7 @@ pub(crate) fn unnecessary_placeholder(checker: &Checker, body: &[Stmt]) {
// Ellipses are significant in protocol methods and abstract methods.
// Specifically, Pyright uses the presence of an ellipsis to indicate that
// a method is a stub, rather than a default implementation.
if in_protocol_or_abstract_method(checker.semantic()) {
if checker.semantic().in_protocol_or_abstract_method() {
return;
}
Placeholder::Ellipsis
@@ -163,21 +161,3 @@ impl std::fmt::Display for Placeholder {
}
}
}
/// Return `true` if the [`SemanticModel`] is in a `typing.Protocol` subclass or an abstract
/// method.
fn in_protocol_or_abstract_method(semantic: &SemanticModel) -> bool {
semantic.current_scopes().any(|scope| match scope.kind {
ScopeKind::Class(class_def) => class_def
.bases()
.iter()
.any(|base| semantic.match_typing_expr(map_subscript(base), "Protocol")),
ScopeKind::Function(function_def) => {
ruff_python_semantic::analyze::visibility::is_abstract(
&function_def.decorator_list,
semantic,
)
}
_ => false,
})
}

View File

@@ -60,6 +60,16 @@ impl Violation for UnusedFunctionArgument {
/// prefixed with an underscore, or some other value that adheres to the
/// [`lint.dummy-variable-rgx`] pattern.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Removing a parameter from a subclass method (or changing a parameter's
/// name) may cause type checkers to complain about a violation of the Liskov
/// Substitution Principle if it means that the method now incompatibly
/// overrides a method defined on a superclass. Explicitly decorating an
/// overriding method with `@override` signals to Ruff that the method is
/// intended to override a superclass method and that a type checker will
/// enforce that it does so; Ruff therefore knows that it should not enforce
/// rules about unused arguments on such methods.
///
/// ## Example
/// ```python
/// class Class:
@@ -76,6 +86,8 @@ impl Violation for UnusedFunctionArgument {
///
/// ## Options
/// - `lint.dummy-variable-rgx`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.168")]
pub(crate) struct UnusedMethodArgument {
@@ -101,6 +113,16 @@ impl Violation for UnusedMethodArgument {
/// prefixed with an underscore, or some other value that adheres to the
/// [`lint.dummy-variable-rgx`] pattern.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Removing a parameter from a subclass method (or changing a parameter's
/// name) may cause type checkers to complain about a violation of the Liskov
/// Substitution Principle if it means that the method now incompatibly
/// overrides a method defined on a superclass. Explicitly decorating an
/// overriding method with `@override` signals to Ruff that the method is
/// intended to override a superclass method and that a type checker will
/// enforce that it does so; Ruff therefore knows that it should not enforce
/// rules about unused arguments on such methods.
///
/// ## Example
/// ```python
/// class Class:
@@ -119,6 +141,8 @@ impl Violation for UnusedMethodArgument {
///
/// ## Options
/// - `lint.dummy-variable-rgx`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.168")]
pub(crate) struct UnusedClassMethodArgument {
@@ -144,6 +168,16 @@ impl Violation for UnusedClassMethodArgument {
/// prefixed with an underscore, or some other value that adheres to the
/// [`lint.dummy-variable-rgx`] pattern.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Removing a parameter from a subclass method (or changing a parameter's
/// name) may cause type checkers to complain about a violation of the Liskov
/// Substitution Principle if it means that the method now incompatibly
/// overrides a method defined on a superclass. Explicitly decorating an
/// overriding method with `@override` signals to Ruff that the method is
/// intended to override a superclass method, and that a type checker will
/// enforce that it does so; Ruff therefore knows that it should not enforce
/// rules about unused arguments on such methods.
///
/// ## Example
/// ```python
/// class Class:
@@ -162,6 +196,8 @@ impl Violation for UnusedClassMethodArgument {
///
/// ## Options
/// - `lint.dummy-variable-rgx`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.168")]
pub(crate) struct UnusedStaticMethodArgument {

View File

@@ -23,7 +23,7 @@ use crate::checkers::ast::Checker;
/// > mixedCase is allowed only in contexts where thats already the
/// > prevailing style (e.g. threading.py), to retain backwards compatibility.
///
/// Methods decorated with `@typing.override` are ignored.
/// Methods decorated with [`@typing.override`][override] are ignored.
///
/// ## Example
/// ```python
@@ -43,6 +43,8 @@ use crate::checkers::ast::Checker;
///
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments
/// [preview]: https://docs.astral.sh/ruff/preview/
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.77")]
pub(crate) struct InvalidArgumentName {

View File

@@ -24,6 +24,11 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
/// to ignore all functions starting with `test_` from this rule, set the
/// [`lint.pep8-naming.extend-ignore-names`] option to `["test_*"]`.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Explicitly decorating a method with `@override` signals to Ruff that the method is intended
/// to override a superclass method, and that a type checker will enforce that it does so. Ruff
/// therefore knows that it should not enforce naming conventions on such methods.
///
/// ## Example
/// ```python
/// def myFunction():
@@ -41,6 +46,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-variable-names
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.77")]
pub(crate) struct InvalidFunctionName {

View File

@@ -169,7 +169,12 @@ impl Violation for UndocumentedPublicClass {
/// If the codebase adheres to a standard format for method docstrings, follow
/// that format for consistency.
///
/// This rule exempts methods decorated with [`@typing.override`][override],
/// since it is a common practice to document a method on a superclass but not
/// on an overriding method in a subclass.
///
/// ## Example
///
/// ```python
/// class Cat(Animal):
/// def greet(self, happy: bool = True):
@@ -180,6 +185,7 @@ impl Violation for UndocumentedPublicClass {
/// ```
///
/// Use instead (in the NumPy docstring format):
///
/// ```python
/// class Cat(Animal):
/// def greet(self, happy: bool = True):
@@ -202,6 +208,7 @@ impl Violation for UndocumentedPublicClass {
/// ```
///
/// Or (in the Google docstring format):
///
/// ```python
/// class Cat(Animal):
/// def greet(self, happy: bool = True):
@@ -227,6 +234,8 @@ impl Violation for UndocumentedPublicClass {
/// - [PEP 287 reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.70")]
pub(crate) struct UndocumentedPublicMethod;

View File

@@ -21,7 +21,7 @@ use crate::rules::pylint::helpers::is_known_dunder_method;
///
/// This rule will detect all methods starting and ending with at least
/// one underscore (e.g., `_str_`), but ignores known dunder methods (like
/// `__init__`), as well as methods that are marked with `@override`.
/// `__init__`), as well as methods that are marked with [`@override`][override].
///
/// Additional dunder methods names can be allowed via the
/// [`lint.pylint.allow-dunder-method-names`] setting.
@@ -42,6 +42,8 @@ use crate::rules::pylint::helpers::is_known_dunder_method;
///
/// ## Options
/// - `lint.pylint.allow-dunder-method-names`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "v0.0.285")]
pub(crate) struct BadDunderMethodName {

View File

@@ -17,6 +17,16 @@ use crate::rules::flake8_unused_arguments::rules::is_not_implemented_stub_with_v
/// Unused `self` parameters are usually a sign of a method that could be
/// replaced by a function, class method, or static method.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Converting an instance method into a static method or class method may
/// cause type checkers to complain about a violation of the Liskov
/// Substitution Principle if it means that the method now incompatibly
/// overrides a method defined on a superclass. Explicitly decorating an
/// overriding method with `@override` signals to Ruff that the method is
/// intended to override a superclass method and that a type checker will
/// enforce that it does so; Ruff therefore knows that it should not enforce
/// rules about unused `self` parameters on such methods.
///
/// ## Example
/// ```python
/// class Person:
@@ -38,6 +48,8 @@ use crate::rules::flake8_unused_arguments::rules::is_not_implemented_stub_with_v
/// def greeting():
/// print("Greetings friend!")
/// ```
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "v0.0.286")]
pub(crate) struct NoSelfUse {

View File

@@ -12,6 +12,16 @@ use crate::checkers::ast::Checker;
/// By default, this rule allows up to five arguments, as configured by the
/// [`lint.pylint.max-args`] option.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Changing the signature of a subclass method may cause type checkers to
/// complain about a violation of the Liskov Substitution Principle if it
/// means that the method now incompatibly overrides a method defined on a
/// superclass. Explicitly decorating an overriding method with `@override`
/// signals to Ruff that the method is intended to override a superclass
/// method and that a type checker will enforce that it does so; Ruff
/// therefore knows that it should not enforce rules about methods having
/// too many arguments.
///
/// ## Why is this bad?
/// Functions with many arguments are harder to understand, maintain, and call.
/// Consider refactoring functions with many arguments into smaller functions
@@ -43,6 +53,8 @@ use crate::checkers::ast::Checker;
///
/// ## Options
/// - `lint.pylint.max-args`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.238")]
pub(crate) struct TooManyArguments {

View File

@@ -21,6 +21,16 @@ use crate::checkers::ast::Checker;
/// with fewer arguments, using objects to group related arguments, or migrating to
/// [keyword-only arguments](https://docs.python.org/3/tutorial/controlflow.html#special-parameters).
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Changing the signature of a subclass method may cause type checkers to
/// complain about a violation of the Liskov Substitution Principle if it
/// means that the method now incompatibly overrides a method defined on a
/// superclass. Explicitly decorating an overriding method with `@override`
/// signals to Ruff that the method is intended to override a superclass
/// method and that a type checker will enforce that it does so; Ruff
/// therefore knows that it should not enforce rules about methods having
/// too many arguments.
///
/// ## Example
///
/// ```python
@@ -41,6 +51,8 @@ use crate::checkers::ast::Checker;
///
/// ## Options
/// - `lint.pylint.max-positional-args`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "v0.1.7")]
pub(crate) struct TooManyPositionalArguments {

View File

@@ -115,6 +115,7 @@ mod tests {
#[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))]
#[test_case(Rule::LoggingEagerConversion, Path::new("RUF065_0.py"))]
#[test_case(Rule::LoggingEagerConversion, Path::new("RUF065_1.py"))]
#[test_case(Rule::PropertyWithoutReturn, Path::new("RUF066.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]

View File

@@ -35,6 +35,7 @@ pub(crate) use non_octal_permissions::*;
pub(crate) use none_not_at_end_of_union::*;
pub(crate) use parenthesize_chained_operators::*;
pub(crate) use post_init_default::*;
pub(crate) use property_without_return::*;
pub(crate) use pytest_raises_ambiguous_pattern::*;
pub(crate) use quadratic_list_summation::*;
pub(crate) use redirected_noqa::*;
@@ -99,6 +100,7 @@ mod non_octal_permissions;
mod none_not_at_end_of_union;
mod parenthesize_chained_operators;
mod post_init_default;
mod property_without_return;
mod pytest_raises_ambiguous_pattern;
mod quadratic_list_summation;
mod redirected_noqa;

View File

@@ -0,0 +1,119 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
use ruff_python_ast::{Expr, Stmt, StmtFunctionDef};
use ruff_python_semantic::analyze::{function_type, visibility};
use crate::checkers::ast::Checker;
use crate::{FixAvailability, Violation};
/// ## What it does
/// Detects class `@property` methods that does not have a `return` statement.
///
/// ## Why is this bad?
/// Property methods are expected to return a computed value, a missing return in a property usually indicates an implementation mistake.
///
/// ## Example
/// ```python
/// class User:
/// @property
/// def full_name(self):
/// f"{self.first_name} {self.last_name}"
/// ```
///
/// Use instead:
/// ```python
/// class User:
/// @property
/// def full_name(self):
/// return f"{self.first_name} {self.last_name}"
/// ```
///
/// ## References
/// - [Python documentation: The property class](https://docs.python.org/3/library/functions.html#property)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.7")]
pub(crate) struct PropertyWithoutReturn {
name: String,
}
impl Violation for PropertyWithoutReturn {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
#[derive_message_formats]
fn message(&self) -> String {
let Self { name } = self;
format!("`{name}` is a property without a `return` statement")
}
}
/// RUF066
pub(crate) fn property_without_return(checker: &Checker, function_def: &StmtFunctionDef) {
let semantic = checker.semantic();
if checker.source_type.is_stub() || semantic.in_protocol_or_abstract_method() {
return;
}
let StmtFunctionDef {
decorator_list,
body,
name,
..
} = function_def;
if !visibility::is_property(decorator_list, [], semantic)
|| visibility::is_overload(decorator_list, semantic)
|| function_type::is_stub(function_def, semantic)
{
return;
}
let mut visitor = PropertyVisitor::default();
visitor.visit_body(body);
if visitor.found {
return;
}
checker.report_diagnostic(
PropertyWithoutReturn {
name: name.to_string(),
},
function_def.identifier(),
);
}
#[derive(Default)]
struct PropertyVisitor {
found: bool,
}
// NOTE: We are actually searching for the presence of
// `yield`/`yield from`/`raise`/`return` statement/expression,
// as having one of those indicates that there's likely no implementation mistake
impl Visitor<'_> for PropertyVisitor {
fn visit_expr(&mut self, expr: &Expr) {
if self.found {
return;
}
match expr {
Expr::Yield(_) | Expr::YieldFrom(_) => self.found = true,
_ => walk_expr(self, expr),
}
}
fn visit_stmt(&mut self, stmt: &Stmt) {
if self.found {
return;
}
match stmt {
Stmt::Return(_) | Stmt::Raise(_) => self.found = true,
Stmt::FunctionDef(_) => {
// Do not recurse into nested functions; they're evaluated separately.
}
_ => walk_stmt(self, stmt),
}
}
}

View File

@@ -0,0 +1,31 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF066 `name` is a property without a `return` statement
--> RUF066.py:7:9
|
5 | class User: # Test normal class properties
6 | @property
7 | def name(self): # ERROR: No return
| ^^^^
8 | f"{self.first_name} {self.last_name}"
|
RUF066 `nested` is a property without a `return` statement
--> RUF066.py:18:9
|
17 | @property
18 | def nested(self): # ERROR: Property itself doesn't return
| ^^^^^^
19 | def inner():
20 | return 0
|
RUF066 `prop2` is a property without a `return` statement
--> RUF066.py:43:9
|
42 | @property
43 | def prop2(self): # ERROR: Not returning something (even when we are inside an ABC)
| ^^^^^
44 | 50
|

View File

@@ -3,12 +3,13 @@ use std::path::Path;
use bitflags::bitflags;
use rustc_hash::FxHashMap;
use ruff_python_ast::helpers::from_relative_import;
use ruff_python_ast::helpers::{from_relative_import, map_subscript};
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use ruff_python_ast::{self as ast, Expr, ExprContext, PySourceType, Stmt};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::Imported;
use crate::analyze::visibility;
use crate::binding::{
Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImport, Import,
SubmoduleImport,
@@ -2153,6 +2154,21 @@ impl<'a> SemanticModel<'a> {
function.range() == function_def.range()
})
}
/// Return `true` if the model is in a `typing.Protocol` subclass or an abstract
/// method.
pub fn in_protocol_or_abstract_method(&self) -> bool {
self.current_scopes().any(|scope| match scope.kind {
ScopeKind::Class(class_def) => class_def
.bases()
.iter()
.any(|base| self.match_typing_expr(map_subscript(base), "Protocol")),
ScopeKind::Function(function_def) => {
visibility::is_abstract(&function_def.decorator_list, self)
}
_ => false,
})
}
}
pub struct ShadowedBinding {

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.14.6"
version = "0.14.7"
publish = false
authors = { workspace = true }
edition = { workspace = true }

315
crates/ty/docs/rules.md generated
View File

@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L127" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L134" target="_blank">View source</a>
</small>
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L171" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L178" target="_blank">View source</a>
</small>
@@ -95,7 +95,7 @@ f(int) # error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L197" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L204" target="_blank">View source</a>
</small>
@@ -126,7 +126,7 @@ a = 1
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L222" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L229" target="_blank">View source</a>
</small>
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L248" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L255" target="_blank">View source</a>
</small>
@@ -184,13 +184,41 @@ class B(A): ...
[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
## `cyclic-type-alias-definition`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-type-alias-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L281" target="_blank">View source</a>
</small>
**What it does**
Checks for type alias definitions that (directly or mutually) refer to themselves.
**Why is it bad?**
Although it is permitted to define a recursive type alias, it is not meaningful
to have a type alias whose expansion can only result in itself, and is therefore not allowed.
**Examples**
```python
type Itself = Itself
type A = B
type B = A
```
## `duplicate-base`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L313" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L342" target="_blank">View source</a>
</small>
@@ -217,7 +245,7 @@ class B(A, A): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L334" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L363" target="_blank">View source</a>
</small>
@@ -329,7 +357,7 @@ def test(): -> "Literal[5]":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L538" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567" target="_blank">View source</a>
</small>
@@ -359,7 +387,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L562" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L591" target="_blank">View source</a>
</small>
@@ -385,7 +413,7 @@ t[3] # IndexError: tuple index out of range
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L366" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L395" target="_blank">View source</a>
</small>
@@ -474,7 +502,7 @@ an atypical memory layout.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L616" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L645" target="_blank">View source</a>
</small>
@@ -501,7 +529,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L656" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L685" target="_blank">View source</a>
</small>
@@ -529,7 +557,7 @@ a: int = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1815" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1948" target="_blank">View source</a>
</small>
@@ -563,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L678" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L707" target="_blank">View source</a>
</small>
@@ -599,7 +627,7 @@ asyncio.run(main())
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L708" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L737" target="_blank">View source</a>
</small>
@@ -623,7 +651,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L759" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788" target="_blank">View source</a>
</small>
@@ -650,7 +678,7 @@ with 1:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L780" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809" target="_blank">View source</a>
</small>
@@ -679,7 +707,7 @@ a: str
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L803" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L832" target="_blank">View source</a>
</small>
@@ -717,13 +745,55 @@ except ZeroDivisionError:
This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes)
## `invalid-explicit-override`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.28">0.0.1-alpha.28</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1645" target="_blank">View source</a>
</small>
**What it does**
Checks for methods that are decorated with `@override` but do not override any method in a superclass.
**Why is this bad?**
Decorating a method with `@override` declares to the type checker that the intention is that it should
override a method from a superclass.
**Example**
```python
from typing import override
class A:
@override
def foo(self): ... # Error raised here
class B(A):
@override
def ffooo(self): ... # Error raised here
class C:
@override
def __repr__(self): ... # fine: overrides `object.__repr__`
class D(A):
@override
def foo(self): ... # fine: overrides `A.foo`
```
## `invalid-generic-class`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L839" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L868" target="_blank">View source</a>
</small>
@@ -756,7 +826,7 @@ class C[U](Generic[T]): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L583" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L612" target="_blank">View source</a>
</small>
@@ -795,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L865" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L894" target="_blank">View source</a>
</small>
@@ -830,7 +900,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L962" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L991" target="_blank">View source</a>
</small>
@@ -864,7 +934,7 @@ class B(metaclass=f): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1943" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2076" target="_blank">View source</a>
</small>
@@ -942,7 +1012,28 @@ classes in Python do indeed behave this way, the strongly held convention is tha
be avoided wherever possible. As part of this check, therefore, ty enforces that `__eq__`
and `__ne__` methods accept `object` as their second argument.
**Why does ty disagree with Ruff about how to write my method?**
Ruff has several rules that will encourage you to rename a parameter, or change its type
signature, if it thinks you're falling into a certain anti-pattern. For example, Ruff's
[ARG002](https://docs.astral.sh/ruff/rules/unused-method-argument/) rule recommends that an
unused parameter should either be removed or renamed to start with `_`. Applying either of
these suggestions can cause ty to start reporting an `invalid-method-override` error if
the function in question is a method on a subclass that overrides a method on a superclass,
and the change would cause the subclass method to no longer accept all argument combinations
that the superclass method accepts.
This can usually be resolved by adding [`@typing.override`][override] to your method
definition. Ruff knows that a method decorated with `@typing.override` is intended to
override a method by the same name on a superclass, and avoids reporting rules like ARG002
for such methods; it knows that the changes recommended by ARG002 would violate the Liskov
Substitution Principle.
Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
[Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle
[override]: https://docs.python.org/3/library/typing.html#typing.override
## `invalid-named-tuple`
@@ -950,7 +1041,7 @@ and `__ne__` methods accept `object` as their second argument.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L512" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541" target="_blank">View source</a>
</small>
@@ -982,7 +1073,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L938" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L967" target="_blank">View source</a>
</small>
@@ -1012,7 +1103,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L989" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1018" target="_blank">View source</a>
</small>
@@ -1062,7 +1153,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1088" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1117" target="_blank">View source</a>
</small>
@@ -1088,7 +1179,7 @@ def f(a: int = ''): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L893" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L922" target="_blank">View source</a>
</small>
@@ -1119,7 +1210,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L448" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L477" target="_blank">View source</a>
</small>
@@ -1153,7 +1244,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1108" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1137" target="_blank">View source</a>
</small>
@@ -1202,7 +1293,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L637" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L666" target="_blank">View source</a>
</small>
@@ -1227,7 +1318,7 @@ def func() -> int:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1151" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1180" target="_blank">View source</a>
</small>
@@ -1285,7 +1376,7 @@ TODO #14889
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L917" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L946" target="_blank">View source</a>
</small>
@@ -1306,13 +1397,60 @@ IntOrStr = TypeAliasType("IntOrStr", int | str) # okay
NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal
```
## `invalid-type-arguments`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412" target="_blank">View source</a>
</small>
**What it does**
Checks for invalid type arguments in explicit type specialization.
**Why is this bad?**
Providing the wrong number of type arguments or type arguments that don't
satisfy the type variable's bounds or constraints will lead to incorrect
type inference and may indicate a misunderstanding of the generic type's
interface.
**Examples**
Using legacy type variables:
```python
from typing import Generic, TypeVar
T1 = TypeVar('T1', int, str)
T2 = TypeVar('T2', bound=int)
class Foo1(Generic[T1]): ...
class Foo2(Generic[T2]): ...
Foo1[bytes] # error: bytes does not satisfy T1's constraints
Foo2[str] # error: str does not satisfy T2's bound
```
Using PEP 695 type variables:
```python
class Foo[T]: ...
class Bar[T, U]: ...
Foo[int, str] # error: too many arguments
Bar[int] # error: too few arguments
```
## `invalid-type-checking-constant`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1190" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1219" target="_blank">View source</a>
</small>
@@ -1342,7 +1480,7 @@ TYPE_CHECKING = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1214" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1243" target="_blank">View source</a>
</small>
@@ -1372,7 +1510,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1266" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1295" target="_blank">View source</a>
</small>
@@ -1406,7 +1544,7 @@ f(10) # Error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1238" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1267" target="_blank">View source</a>
</small>
@@ -1440,7 +1578,7 @@ class C:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1294" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323" target="_blank">View source</a>
</small>
@@ -1475,7 +1613,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1352" target="_blank">View source</a>
</small>
@@ -1500,7 +1638,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1916" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2049" target="_blank">View source</a>
</small>
@@ -1533,7 +1671,7 @@ alice["age"] # KeyError
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1342" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371" target="_blank">View source</a>
</small>
@@ -1562,7 +1700,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1365" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1394" target="_blank">View source</a>
</small>
@@ -1586,7 +1724,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1383" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453" target="_blank">View source</a>
</small>
@@ -1606,13 +1744,46 @@ for i in 34: # TypeError: 'int' object is not iterable
pass
```
## `override-of-final-method`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1618" target="_blank">View source</a>
</small>
**What it does**
Checks for methods on subclasses that override superclass methods decorated with `@final`.
**Why is this bad?**
Decorating a method with `@final` declares to the type checker that it should not be
overridden on any subclass.
**Example**
```python
from typing import final
class A:
@final
def foo(self): ...
class B(A):
def foo(self): ... # Error raised here
```
## `parameter-already-assigned`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1434" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1504" target="_blank">View source</a>
</small>
@@ -1639,7 +1810,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1669" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1802" target="_blank">View source</a>
</small>
@@ -1697,7 +1868,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1791" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1924" target="_blank">View source</a>
</small>
@@ -1727,7 +1898,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1525" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1595" target="_blank">View source</a>
</small>
@@ -1756,7 +1927,7 @@ class B(A): ... # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1570" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1703" target="_blank">View source</a>
</small>
@@ -1783,7 +1954,7 @@ f("foo") # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1548" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1681" target="_blank">View source</a>
</small>
@@ -1811,7 +1982,7 @@ def _(x: int):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1591" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1724" target="_blank">View source</a>
</small>
@@ -1857,7 +2028,7 @@ class A:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1648" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1781" target="_blank">View source</a>
</small>
@@ -1884,7 +2055,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1690" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1823" target="_blank">View source</a>
</small>
@@ -1912,7 +2083,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1712" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1845" target="_blank">View source</a>
</small>
@@ -1937,7 +2108,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1731" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1864" target="_blank">View source</a>
</small>
@@ -1962,7 +2133,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1403" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1473" target="_blank">View source</a>
</small>
@@ -1999,7 +2170,7 @@ b1 < b2 < b1 # exception raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1750" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1883" target="_blank">View source</a>
</small>
@@ -2027,7 +2198,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1772" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1905" target="_blank">View source</a>
</small>
@@ -2052,7 +2223,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L477" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506" target="_blank">View source</a>
</small>
@@ -2093,7 +2264,7 @@ class SubProto(BaseProto, Protocol):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.16">0.0.1-alpha.16</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L292" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L321" target="_blank">View source</a>
</small>
@@ -2120,7 +2291,7 @@ old_func() # emits [deprecated] diagnostic
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ignore-comment-unknown-rule" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L40" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L47" target="_blank">View source</a>
</small>
@@ -2151,7 +2322,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-ignore-comment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L65" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L72" target="_blank">View source</a>
</small>
@@ -2181,7 +2352,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1455" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1525" target="_blank">View source</a>
</small>
@@ -2209,7 +2380,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L145" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L152" target="_blank">View source</a>
</small>
@@ -2241,7 +2412,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1477" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1547" target="_blank">View source</a>
</small>
@@ -2273,7 +2444,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1843" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1976" target="_blank">View source</a>
</small>
@@ -2300,7 +2471,7 @@ cast(int, f()) # Redundant
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1630" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1763" target="_blank">View source</a>
</small>
@@ -2324,7 +2495,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1864" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1997" target="_blank">View source</a>
</small>
@@ -2382,7 +2553,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L726" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L755" target="_blank">View source</a>
</small>
@@ -2421,7 +2592,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1032" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1061" target="_blank">View source</a>
</small>
@@ -2484,7 +2655,7 @@ def foo(x: int | str) -> int | str:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L274" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L303" target="_blank">View source</a>
</small>
@@ -2508,7 +2679,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1503" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1573" target="_blank">View source</a>
</small>
@@ -2536,7 +2707,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unused-ignore-comment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L15" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L22" target="_blank">View source</a>
</small>

View File

@@ -313,7 +313,8 @@ impl MainLoop {
let terminal_settings = db.project().settings(db).terminal();
let display_config = DisplayDiagnosticConfig::default()
.format(terminal_settings.output_format.into())
.color(colored::control::SHOULD_COLORIZE.should_colorize());
.color(colored::control::SHOULD_COLORIZE.should_colorize())
.show_fix_diff(true);
if check_revision == revision {
if db.project().files(db).is_empty() {

View File

@@ -15,7 +15,7 @@ use ty_project::metadata::pyproject::{PyProject, Tool};
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher};
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module};
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module_old};
struct TestCase {
db: ProjectDatabase,
@@ -232,7 +232,8 @@ impl TestCase {
}
fn module<'c>(&'c self, name: &str) -> Module<'c> {
resolve_module(self.db(), &ModuleName::new(name).unwrap()).expect("module to be present")
resolve_module_old(self.db(), &ModuleName::new(name).unwrap())
.expect("module to be present")
}
fn sorted_submodule_names(&self, parent_module_name: &str) -> Vec<String> {
@@ -811,7 +812,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
.with_context(|| "Failed to create __init__.py")?;
std::fs::write(a_original_path.as_std_path(), "").with_context(|| "Failed to create a.py")?;
let sub_a_module = resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap());
let sub_a_module = resolve_module_old(case.db(), &ModuleName::new_static("sub.a").unwrap());
assert_eq!(sub_a_module, None);
case.assert_indexed_project_files([bar]);
@@ -832,7 +833,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
.expect("a.py to exist");
// `import sub.a` should now resolve
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
case.assert_indexed_project_files([bar, init_file, a_file]);
@@ -848,7 +849,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
])?;
let bar = case.system_file(case.project_path("bar.py")).unwrap();
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
let sub_path = case.project_path("sub");
let init_file = case
@@ -870,7 +871,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
case.apply_changes(changes, None);
// `import sub.a` should no longer resolve
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
assert!(!init_file.exists(case.db()));
assert!(!a_file.exists(case.db()));
@@ -890,8 +891,8 @@ fn directory_renamed() -> anyhow::Result<()> {
let bar = case.system_file(case.project_path("bar.py")).unwrap();
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_none());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_none());
let sub_path = case.project_path("sub");
let sub_init = case
@@ -915,9 +916,9 @@ fn directory_renamed() -> anyhow::Result<()> {
case.apply_changes(changes, None);
// `import sub.a` should no longer resolve
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
// `import foo.baz` should now resolve
assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_some());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_some());
// The old paths are no longer tracked
assert!(!sub_init.exists(case.db()));
@@ -950,7 +951,7 @@ fn directory_deleted() -> anyhow::Result<()> {
let bar = case.system_file(case.project_path("bar.py")).unwrap();
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
let sub_path = case.project_path("sub");
@@ -970,7 +971,7 @@ fn directory_deleted() -> anyhow::Result<()> {
case.apply_changes(changes, None);
// `import sub.a` should no longer resolve
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
assert!(!init_file.exists(case.db()));
assert!(!a_file.exists(case.db()));
@@ -999,7 +1000,7 @@ fn search_path() -> anyhow::Result<()> {
let site_packages = case.root_path().join("site_packages");
assert_eq!(
resolve_module(case.db(), &ModuleName::new("a").unwrap()),
resolve_module_old(case.db(), &ModuleName::new("a").unwrap()),
None
);
@@ -1009,7 +1010,7 @@ fn search_path() -> anyhow::Result<()> {
case.apply_changes(changes, None);
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
case.assert_indexed_project_files([case.system_file(case.project_path("bar.py")).unwrap()]);
Ok(())
@@ -1022,7 +1023,7 @@ fn add_search_path() -> anyhow::Result<()> {
let site_packages = case.project_path("site_packages");
std::fs::create_dir_all(site_packages.as_std_path())?;
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_none());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("a").unwrap()).is_none());
// Register site-packages as a search path.
case.update_options(Options {
@@ -1040,7 +1041,7 @@ fn add_search_path() -> anyhow::Result<()> {
case.apply_changes(changes, None);
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
assert!(resolve_module_old(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
Ok(())
}
@@ -1172,7 +1173,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
// Unset the custom typeshed directory.
assert_eq!(
resolve_module(case.db(), &ModuleName::new("os").unwrap()),
resolve_module_old(case.db(), &ModuleName::new("os").unwrap()),
None
);
@@ -1187,7 +1188,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
case.apply_changes(changes, None);
assert!(resolve_module(case.db(), &ModuleName::new("os").unwrap()).is_some());
assert!(resolve_module_old(case.db(), &ModuleName::new("os").unwrap()).is_some());
Ok(())
}
@@ -1410,7 +1411,7 @@ mod unix {
Ok(())
})?;
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
let baz = resolve_module_old(case.db(), &ModuleName::new_static("bar.baz").unwrap())
.expect("Expected bar.baz to exist in site-packages.");
let baz_project = case.project_path("bar/baz.py");
let baz_file = baz.file(case.db()).unwrap();
@@ -1486,7 +1487,7 @@ mod unix {
Ok(())
})?;
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
let baz = resolve_module_old(case.db(), &ModuleName::new_static("bar.baz").unwrap())
.expect("Expected bar.baz to exist in site-packages.");
let baz_file = baz.file(case.db()).unwrap();
let bar_baz = case.project_path("bar/baz.py");
@@ -1591,7 +1592,7 @@ mod unix {
Ok(())
})?;
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
let baz = resolve_module_old(case.db(), &ModuleName::new_static("bar.baz").unwrap())
.expect("Expected bar.baz to exist in site-packages.");
let baz_site_packages_path =
case.project_path(".venv/lib/python3.12/site-packages/bar/baz.py");
@@ -1854,11 +1855,11 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
let mut case = setup([("lib.py", "class Foo: ...")])?;
assert!(
resolve_module(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
resolve_module_old(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
"Expected `lib` module to exist."
);
assert_eq!(
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()),
resolve_module_old(case.db(), &ModuleName::new("Lib").unwrap()),
None,
"Expected `Lib` module not to exist"
);
@@ -1891,13 +1892,13 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
// Resolving `lib` should now fail but `Lib` should now succeed
assert_eq!(
resolve_module(case.db(), &ModuleName::new("lib").unwrap()),
resolve_module_old(case.db(), &ModuleName::new("lib").unwrap()),
None,
"Expected `lib` module to no longer exist."
);
assert!(
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
resolve_module_old(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
"Expected `Lib` module to exist"
);

View File

@@ -143,7 +143,7 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
impl CursorTest {
fn all_symbols(&self, query: &str) -> String {
let symbols = all_symbols(&self.db, &QueryPattern::new(query));
let symbols = all_symbols(&self.db, &QueryPattern::fuzzy(query));
if symbols.is_empty() {
return "No symbols found".to_string();

View File

@@ -0,0 +1,43 @@
use crate::{completion, find_node::covering_node};
use ruff_db::{files::File, parsed::parsed_module};
use ruff_diagnostics::Edit;
use ruff_text_size::TextRange;
use ty_project::Db;
use ty_python_semantic::types::UNRESOLVED_REFERENCE;
/// A `QuickFix` Code Action
#[derive(Debug, Clone)]
pub struct QuickFix {
pub title: String,
pub edits: Vec<Edit>,
pub preferred: bool,
}
pub fn code_actions(
db: &dyn Db,
file: File,
diagnostic_range: TextRange,
diagnostic_id: &str,
) -> Option<Vec<QuickFix>> {
let registry = db.lint_registry();
let Ok(lint_id) = registry.get(diagnostic_id) else {
return None;
};
if lint_id.name() == UNRESOLVED_REFERENCE.name() {
let parsed = parsed_module(db, file).load(db);
let node = covering_node(parsed.syntax().into(), diagnostic_range).node();
let symbol = &node.expr_name()?.id;
let fixes = completion::missing_imports(db, file, &parsed, symbol, node)
.into_iter()
.map(|import| QuickFix {
title: import.label,
edits: vec![import.edit],
preferred: true,
})
.collect();
Some(fixes)
} else {
None
}
}

View File

@@ -4,8 +4,8 @@ use ruff_db::files::File;
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_db::source::source_text;
use ruff_diagnostics::Edit;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_codegen::Stylist;
use ruff_python_parser::{Token, TokenAt, TokenKind, Tokens};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@@ -17,7 +17,7 @@ use ty_python_semantic::{
use crate::docstring::Docstring;
use crate::find_node::covering_node;
use crate::goto::DefinitionsOrTargets;
use crate::goto::Definitions;
use crate::importer::{ImportRequest, Importer};
use crate::symbols::QueryPattern;
use crate::{Db, all_symbols};
@@ -37,9 +37,9 @@ impl<'db> Completions<'db> {
/// the user has typed as part of the next symbol they are writing.
/// This collection will treat it as a query when present, and only
/// add completions that match it.
fn new(db: &'db dyn Db, typed: Option<&str>) -> Completions<'db> {
fn fuzzy(db: &'db dyn Db, typed: Option<&str>) -> Completions<'db> {
let query = typed
.map(QueryPattern::new)
.map(QueryPattern::fuzzy)
.unwrap_or_else(QueryPattern::matches_all_symbols);
Completions {
db,
@@ -48,6 +48,15 @@ impl<'db> Completions<'db> {
}
}
fn exactly(db: &'db dyn Db, symbol: &str) -> Completions<'db> {
let query = QueryPattern::exactly(symbol);
Completions {
db,
items: vec![],
query,
}
}
/// Convert this collection into a simple
/// sequence of completions.
fn into_completions(mut self) -> Vec<Completion<'db>> {
@@ -57,6 +66,21 @@ impl<'db> Completions<'db> {
self.items
}
fn into_imports(mut self) -> Vec<ImportEdit> {
self.items.sort_by(compare_suggestions);
self.items
.dedup_by(|c1, c2| (&c1.name, c1.module_name) == (&c2.name, c2.module_name));
self.items
.into_iter()
.filter_map(|item| {
Some(ImportEdit {
label: format!("import {}.{}", item.module_name?, item.name),
edit: item.import?,
})
})
.collect()
}
/// Attempts to adds the given completion to this collection.
///
/// When added, `true` is returned.
@@ -196,9 +220,7 @@ impl<'db> Completion<'db> {
db: &'db dyn Db,
semantic: SemanticCompletion<'db>,
) -> Completion<'db> {
let definition = semantic
.ty
.and_then(|ty| DefinitionsOrTargets::from_ty(db, ty));
let definition = semantic.ty.and_then(|ty| Definitions::from_ty(db, ty));
let documentation = definition.and_then(|def| def.docstring(db));
let is_type_check_only = semantic.is_type_check_only(db);
Completion {
@@ -369,7 +391,7 @@ pub fn completion<'db>(
return vec![];
}
let mut completions = Completions::new(db, typed.as_deref());
let mut completions = Completions::fuzzy(db, typed.as_deref());
if let Some(import) = ImportStatement::detect(db, file, &parsed, tokens, typed.as_deref()) {
import.add_completions(db, file, &mut completions);
@@ -417,6 +439,25 @@ pub fn completion<'db>(
completions.into_completions()
}
pub(crate) struct ImportEdit {
pub label: String,
pub edit: Edit,
}
pub(crate) fn missing_imports(
db: &dyn Db,
file: File,
parsed: &ParsedModuleRef,
symbol: &str,
node: AnyNodeRef,
) -> Vec<ImportEdit> {
let mut completions = Completions::exactly(db, symbol);
let scoped = ScopedTarget { node };
add_unimported_completions(db, file, parsed, scoped, &mut completions);
completions.into_imports()
}
/// Adds completions derived from keywords.
///
/// This should generally only be used when offering "scoped" completions.
@@ -1315,7 +1356,8 @@ fn find_typed_text(
if last.end() < offset || last.range().is_empty() {
return None;
}
Some(source[last.range()].to_string())
let range = TextRange::new(last.start(), offset);
Some(source[range].to_string())
}
/// Whether the last token is in a place where we should not provide completions.
@@ -1406,6 +1448,24 @@ fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Opt
type_param.name.range.contains_range(range)
}
ast::AnyNodeRef::StmtFor(stmt_for) => stmt_for.target.range().contains_range(range),
// The AST does not produce `ast::AnyNodeRef::Parameter` nodes for keywords
// or otherwise invalid syntax. Rather they are captured in a
// `ast::AnyNodeRef::Parameters` node as "empty space". To ensure
// we still suppress suggestions even when the syntax is technically
// invalid we extract the token under the cursor and check if it makes
// up that "empty space" inside the Parameters Node. If it does, we know
// that we are still binding variables, just that the current state is
// syntatically invalid. Hence we suppress autocomplete suggestons
// also in those cases.
ast::AnyNodeRef::Parameters(params) => {
if !params.range.contains_range(range) {
return false;
}
params
.iter()
.map(|param| param.range())
.all(|r| !r.contains_range(range))
}
_ => false,
})
}
@@ -1633,6 +1693,21 @@ mod tests {
);
}
#[test]
fn inside_token() {
let test = completion_test_builder(
"\
foo_bar_baz = 1
x = foo<CURSOR>bad
",
);
assert_snapshot!(
test.skip_builtins().build().snapshot(),
@"foo_bar_baz",
);
}
#[test]
fn type_keyword_dedup() {
let test = completion_test_builder(
@@ -2821,7 +2896,7 @@ Answer.<CURSOR>
__itemsize__ :: int
__iter__ :: bound method <class 'Answer'>.__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT@__iter__]
__len__ :: bound method <class 'Answer'>.__len__() -> int
__members__ :: MappingProxyType[str, Unknown]
__members__ :: MappingProxyType[str, Answer]
__module__ :: str
__mro__ :: tuple[type, ...]
__name__ :: str
@@ -5347,6 +5422,45 @@ def foo(p<CURSOR>
);
}
#[test]
fn no_completions_in_function_param_keyword() {
let builder = completion_test_builder(
"\
def foo(in<CURSOR>
",
);
assert_snapshot!(
builder.build().snapshot(),
@"<No completions found>",
);
}
#[test]
fn no_completions_in_function_param_multi_keyword() {
let builder = completion_test_builder(
"\
def foo(param, in<CURSOR>
",
);
assert_snapshot!(
builder.build().snapshot(),
@"<No completions found>",
);
}
#[test]
fn no_completions_in_function_param_multi_keyword_middle() {
let builder = completion_test_builder(
"\
def foo(param, in<CURSOR>, param_two
",
);
assert_snapshot!(
builder.build().snapshot(),
@"<No completions found>",
);
}
#[test]
fn no_completions_in_function_type_param() {
let builder = completion_test_builder(

View File

@@ -3,6 +3,7 @@ use crate::references::{ReferencesMode, references};
use crate::{Db, ReferenceTarget};
use ruff_db::files::File;
use ruff_text_size::TextSize;
use ty_python_semantic::SemanticModel;
/// Find all document highlights for a symbol at the given position.
/// Document highlights are limited to the current file only.
@@ -13,9 +14,10 @@ pub fn document_highlights(
) -> Option<Vec<ReferenceTarget>> {
let parsed = ruff_db::parsed::parsed_module(db, file);
let module = parsed.load(db);
let model = SemanticModel::new(db, file);
// Get the definitions for the symbol at the cursor position
let goto_target = find_goto_target(&module, offset)?;
let goto_target = find_goto_target(&model, &module, offset)?;
// Use DocumentHighlights mode which limits search to current file only
references(db, file, &goto_target, ReferencesMode::DocumentHighlights)

View File

@@ -182,6 +182,11 @@ fn documentation_trim(docs: &str) -> String {
/// </code>
/// ```
fn render_markdown(docstring: &str) -> String {
// Here lies a monumemnt to robust parsing and escaping:
// a codefence with SO MANY backticks that surely no one will ever accidentally
// break out of it, even if they're writing python documentation about markdown
// code fences and are showing off how you can use more than 3 backticks.
const FENCE: &str = "```````````";
// TODO: there is a convention that `singletick` is for items that can
// be looked up in-scope while ``multitick`` is for opaque inline code.
// While rendering this we should make note of all the `singletick` locations
@@ -191,9 +196,10 @@ fn render_markdown(docstring: &str) -> String {
let mut first_line = true;
let mut block_indent = 0;
let mut in_doctest = false;
let mut starting_literal = false;
let mut starting_literal = None;
let mut in_literal = false;
let mut in_any_code = false;
let mut temp_owned_line;
for untrimmed_line in docstring.lines() {
// We can assume leading whitespace has been normalized
let mut line = untrimmed_line.trim_start_matches(' ');
@@ -207,7 +213,7 @@ fn render_markdown(docstring: &str) -> String {
output.push_str(" ");
}
// Only push newlines if we're not scanning for a real line
if !starting_literal {
if starting_literal.is_none() {
output.push('\n');
}
}
@@ -219,21 +225,23 @@ fn render_markdown(docstring: &str) -> String {
in_literal = false;
in_any_code = false;
block_indent = 0;
output.push_str("```\n");
output.push_str(FENCE);
output.push('\n');
}
// We previously entered a literal block and we just found our first non-blank line
// So now we're actually in the literal block
if starting_literal && !line.is_empty() {
starting_literal = false;
if let Some(literal) = starting_literal
&& !line.is_empty()
{
starting_literal = None;
in_literal = true;
in_any_code = true;
block_indent = line_indent;
// TODO: I hope people don't have literal blocks about markdown code fence syntax
// TODO: should we not be this aggressive? Let it autodetect?
// TODO: respect `.. code-block::` directives:
// <https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block>
output.push_str("\n```python\n");
output.push('\n');
output.push_str(FENCE);
output.push_str(literal);
output.push('\n');
}
// If we're not in a codeblock and we see something that signals a doctest, start one
@@ -242,25 +250,79 @@ fn render_markdown(docstring: &str) -> String {
in_doctest = true;
in_any_code = true;
// TODO: is there something more specific? `pycon`?
output.push_str("```python\n");
output.push_str(FENCE);
output.push_str("python\n");
}
// If we're not in a codeblock and we see something that signals a literal block, start one
if !in_any_code && let Some(without_lit) = line.strip_suffix("::") {
let trimmed_without_lit = without_lit.trim();
if let Some(character) = trimmed_without_lit.chars().next_back() {
if character.is_whitespace() {
// Remove the marker completely
line = trimmed_without_lit;
} else {
// Only remove the first `:`
line = line.strip_suffix(":").unwrap();
}
let parsed_lit = line
// first check for a line ending with `::`
.strip_suffix("::")
.map(|prefix| (prefix, None))
// if that fails, look for a line ending with `:: lang`
.or_else(|| {
let (prefix, lang) = line.rsplit_once(' ')?;
let prefix = prefix.trim_end().strip_suffix("::")?;
Some((prefix, Some(lang)))
});
if !in_any_code && let Some((without_lit, lang)) = parsed_lit {
let mut without_directive = without_lit;
let mut directive = None;
// Parse out a directive like `.. warning::`
if let Some((prefix, directive_str)) = without_lit.rsplit_once(' ')
&& let Some(without_directive_str) = prefix.strip_suffix("..")
{
directive = Some(directive_str);
without_directive = without_directive_str;
}
// Whether the `::` should become `:` or be erased
let include_colon = if let Some(character) = without_directive.chars().next_back() {
// If lang is set then we're either deleting the whole line or
// the special rendering below will add it itself
lang.is_none() && !character.is_whitespace()
} else {
// Delete whole line
line = trimmed_without_lit;
false
};
if include_colon {
line = line.strip_suffix(":").unwrap();
} else {
line = without_directive.trim_end();
}
starting_literal = true;
starting_literal = match directive {
// Special directives that should be plaintext
Some(
"attention" | "caution" | "danger" | "error" | "hint" | "important" | "note"
| "tip" | "warning" | "admonition" | "versionadded" | "version-added"
| "versionchanged" | "version-changed" | "version-deprecated" | "deprecated"
| "version-removed" | "versionremoved",
) => {
// Render the argument of things like `.. version-added:: 4.0`
let suffix = if let Some(lang) = lang {
format!(" *{lang}*")
} else {
String::new()
};
// We prepend without_directive here out of caution for preserving input.
// This is probably gibberish/invalid syntax? But it's a no-op in normal cases.
temp_owned_line =
format!("**{without_directive}{}:**{suffix}", directive.unwrap());
line = temp_owned_line.as_str();
None
}
// Things that just mean "it's code"
Some(
"code-block" | "sourcecode" | "code" | "testcode" | "testsetup" | "testcleanup",
) => lang.or(Some("python")),
// Unknown (python I guess?)
Some(_) => lang.or(Some("python")),
// default to python
None => lang.or(Some("python")),
};
}
// Add this line's indentation.
@@ -349,7 +411,7 @@ fn render_markdown(docstring: &str) -> String {
block_indent = 0;
in_any_code = false;
in_literal = false;
output.push_str("```");
output.push_str(FENCE);
}
} else {
// Print the line verbatim, it's in code
@@ -360,7 +422,8 @@ fn render_markdown(docstring: &str) -> String {
}
// Flush codeblock
if in_any_code {
output.push_str("\n```");
output.push('\n');
output.push_str(FENCE);
}
output
@@ -730,28 +793,6 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r"
Here _this_ and ___that__ should be escaped
Here *this* and **that** should be untouched
Here `this` and ``that`` should be untouched
Here `_this_` and ``__that__`` should be untouched
Here `_this_` ``__that__`` should be untouched
`_this_too_should_be_untouched_`
Here `_this_```__that__`` should be untouched but this_is_escaped
Here ``_this_```__that__` should be untouched but this_is_escaped
Here `_this_ and _that_ should be escaped (but isn't)
Here _this_ and _that_` should be escaped
`Here _this_ and _that_ should be escaped (but isn't)
Here _this_ and _that_ should be escaped`
Here ```_is_``__a__`_balanced_``_mess_```
Here ```_is_`````__a__``_random_````_mess__````
```_is_`````__a__``_random_````_mess__````
");
assert_snapshot!(docstring.render_markdown(), @r"
Here \_this\_ and \_\_\_that\_\_ should be escaped
Here *this* and **that** should be untouched
@@ -796,24 +837,9 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r#"
Check out this great example code::
x_y = "hello"
if len(x_y) > 4:
print(x_y)
else:
print("too short :(")
print("done")
You love to see it.
"#);
assert_snapshot!(docstring.render_markdown(), @r#"
Check out this great example code:
```python
```````````python
x_y = "hello"
if len(x_y) > 4:
@@ -823,7 +849,7 @@ mod tests {
print("done")
```
```````````
You love to see it.
"#);
}
@@ -849,24 +875,9 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r#"
Check out this great example code ::
x_y = "hello"
if len(x_y) > 4:
print(x_y)
else:
print("too short :(")
print("done")
You love to see it.
"#);
assert_snapshot!(docstring.render_markdown(), @r#"
Check out this great example code :
```python
Check out this great example code
```````````python
x_y = "hello"
if len(x_y) > 4:
@@ -876,7 +887,7 @@ mod tests {
print("done")
```
```````````
You love to see it.
"#);
}
@@ -903,26 +914,10 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r#"
Check out this great example code
::
x_y = "hello"
if len(x_y) > 4:
print(x_y)
else:
print("too short :(")
print("done")
You love to see it.
"#);
assert_snapshot!(docstring.render_markdown(), @r#"
Check out this great example code
&nbsp;&nbsp;&nbsp;&nbsp;
```python
```````````python
x_y = "hello"
if len(x_y) > 4:
@@ -932,7 +927,7 @@ mod tests {
print("done")
```
```````````
You love to see it.
"#);
}
@@ -956,22 +951,9 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r#"
Check out this great example code::
x_y = "hello"
if len(x_y) > 4:
print(x_y)
else:
print("too short :(")
print("done")
You love to see it.
"#);
assert_snapshot!(docstring.render_markdown(), @r#"
Check out this great example code:
```python
```````````python
x_y = "hello"
if len(x_y) > 4:
@@ -980,7 +962,7 @@ mod tests {
print("too short :(")
print("done")
```
```````````
You love to see it.
"#);
}
@@ -1003,22 +985,9 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r#"
Check out this great example code::
x_y = "hello"
if len(x_y) > 4:
print(x_y)
else:
print("too short :(")
print("done")
"#);
assert_snapshot!(docstring.render_markdown(), @r#"
Check out this great example code:
```python
```````````python
x_y = "hello"
if len(x_y) > 4:
@@ -1027,7 +996,224 @@ mod tests {
print("too short :(")
print("done")
```
```````````
"#);
}
// `warning` and several other directives are special languages that should actually
// still be shown as text and not ```code```.
#[test]
fn warning_block() {
let docstring = r#"
The thing you need to understand is that computers are hard.
.. warning::
Now listen here buckaroo you might have seen me say computers are hard,
and though "yeah I know computers are hard but NO you DON'T KNOW.
Listen:
- Computers
- Are
- Hard
Ok!?!?!?
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r#"
The thing you need to understand is that computers are hard.
**warning:**
&nbsp;&nbsp;&nbsp;&nbsp;Now listen here buckaroo you might have seen me say computers are hard,
&nbsp;&nbsp;&nbsp;&nbsp;and though "yeah I know computers are hard but NO you DON'T KNOW.
&nbsp;&nbsp;&nbsp;&nbsp;Listen:
&nbsp;&nbsp;&nbsp;&nbsp;- Computers
&nbsp;&nbsp;&nbsp;&nbsp;- Are
&nbsp;&nbsp;&nbsp;&nbsp;- Hard
&nbsp;&nbsp;&nbsp;&nbsp;Ok!?!?!?
"#);
}
// `warning` and several other directives are special languages that should actually
// still be shown as text and not ```code```.
#[test]
fn version_blocks() {
let docstring = r#"
Some much-updated docs
.. version-added:: 3.0
Function added
.. version-changed:: 4.0
The `spam` argument was added
.. version-changed:: 4.1
The `spam` argument is considered evil now.
You really shouldnt use it
And that's the docs
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
Some much-updated docs
**version-added:** *3.0*
&nbsp;&nbsp;&nbsp;Function added
**version-changed:** *4.0*
&nbsp;&nbsp;&nbsp;The `spam` argument was added
**version-changed:** *4.1*
&nbsp;&nbsp;&nbsp;The `spam` argument is considered evil now.
&nbsp;&nbsp;&nbsp;You really shouldnt use it
And that's the docs
");
}
// I don't know if this is valid syntax but we preserve stuff before non-code blocks like
// `..deprecated ::`
#[test]
fn deprecated_prefix_gunk() {
let docstring = r#"
wow this is some changes .. deprecated:: 1.2.3
x = 2
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
**wow this is some changes deprecated:** *1.2.3*
&nbsp;&nbsp;&nbsp;&nbsp;x = 2
");
}
// `.. code::` is a literal block and the `.. code::` should be deleted
#[test]
fn code_block() {
let docstring = r#"
Here's some code!
.. code::
def main() {
print("hello world!")
}
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r#"
Here's some code!
```````````python
def main() {
print("hello world!")
}
```````````
"#);
}
// `.. code:: rust` is a literal block with rust syntax highlighting
#[test]
fn code_block_lang() {
let docstring = r#"
Here's some Rust code!
.. code:: rust
fn main() {
println!("hello world!");
}
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r#"
Here's some Rust code!
```````````rust
fn main() {
println!("hello world!");
}
```````````
"#);
}
// I don't know if this is valid syntax but we preserve stuff before `..code ::`
#[test]
fn code_block_prefix_gunk() {
let docstring = r#"
wow this is some code.. code:: abc
x = 2
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
wow this is some code
```````````abc
x = 2
```````````
");
}
// `.. asdgfhjkl-unknown::` is treated the same as `.. code::`
#[test]
fn unknown_block() {
let docstring = r#"
Here's some code!
.. asdgfhjkl-unknown::
fn main() {
println!("hello world!");
}
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r#"
Here's some code!
```````````python
fn main() {
println!("hello world!");
}
```````````
"#);
}
// `.. asdgfhjkl-unknown:: rust` is treated the same as `.. code:: rust`
#[test]
fn unknown_block_lang() {
let docstring = r#"
Here's some Rust code!
.. asdgfhjkl-unknown:: rust
fn main() {
print("hello world!")
}
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r#"
Here's some Rust code!
```````````rust
fn main() {
print("hello world!")
}
```````````
"#);
}
@@ -1047,26 +1233,15 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r"
This is a function description
>>> thing.do_thing()
wow it did the thing
>>> thing.do_other_thing()
it sure did the thing
As you can see it did the thing!
");
assert_snapshot!(docstring.render_markdown(), @r"
This is a function description
```python
```````````python
>>> thing.do_thing()
wow it did the thing
>>> thing.do_other_thing()
it sure did the thing
```
```````````
As you can see it did the thing!
");
}
@@ -1087,26 +1262,15 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r"
This is a function description
>>> thing.do_thing()
wow it did the thing
>>> thing.do_other_thing()
it sure did the thing
As you can see it did the thing!
");
assert_snapshot!(docstring.render_markdown(), @r"
This is a function description
```python
```````````python
>>> thing.do_thing()
wow it did the thing
>>> thing.do_other_thing()
it sure did the thing
```
```````````
As you can see it did the thing!
");
}
@@ -1121,20 +1285,13 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r"
>>> thing.do_thing()
wow it did the thing
>>> thing.do_other_thing()
it sure did the thing
");
assert_snapshot!(docstring.render_markdown(), @r"
```python
```````````python
>>> thing.do_thing()
wow it did the thing
>>> thing.do_other_thing()
it sure did the thing
```
```````````
");
}
@@ -1154,26 +1311,15 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r"
This is a function description::
>>> thing.do_thing()
wow it did the thing
>>> thing.do_other_thing()
it sure did the thing
As you can see it did the thing!
");
assert_snapshot!(docstring.render_markdown(), @r"
This is a function description:
```python
```````````python
>>> thing.do_thing()
wow it did the thing
>>> thing.do_other_thing()
it sure did the thing
```
```````````
As you can see it did the thing!
");
}
@@ -1189,22 +1335,14 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_plaintext(), @r"
And so you can see that
>>> thing.do_thing()
wow it did the thing
>>> thing.do_other_thing()
it sure did the thing
");
assert_snapshot!(docstring.render_markdown(), @r"
And so you can see that
```python
```````````python
>>> thing.do_thing()
wow it did the thing
>>> thing.do_other_thing()
it sure did the thing
```
```````````
");
}
@@ -1383,14 +1521,14 @@ mod tests {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;This is a continuation of param2 description.
'param3' -- A parameter without type annotation
```python
```````````python
>>> print repr(foo.__doc__)
'\n This is the second line of the docstring.\n '
>>> foo.__doc__.splitlines()
['', ' This is the second line of the docstring.', ' ']
>>> trim(foo.__doc__)
'This is the second line of the docstring.'
```
```````````
");
}

View File

@@ -3,10 +3,11 @@ use crate::references::{ReferencesMode, references};
use crate::{Db, ReferenceTarget};
use ruff_db::files::File;
use ruff_text_size::TextSize;
use ty_python_semantic::SemanticModel;
/// Find all references to a symbol at the given position.
/// Search for references across all files in the project.
pub fn goto_references(
pub fn find_references(
db: &dyn Db,
file: File,
offset: TextSize,
@@ -14,9 +15,10 @@ pub fn goto_references(
) -> Option<Vec<ReferenceTarget>> {
let parsed = ruff_db::parsed::parsed_module(db, file);
let module = parsed.load(db);
let model = SemanticModel::new(db, file);
// Get the definitions for the symbol at the cursor position
let goto_target = find_goto_target(&module, offset)?;
let goto_target = find_goto_target(&model, &module, offset)?;
let mode = if include_declaration {
ReferencesMode::References
@@ -39,7 +41,7 @@ mod tests {
impl CursorTest {
fn references(&self) -> String {
let Some(mut reference_results) =
goto_references(&self.db, self.cursor.file, self.cursor.offset, true)
find_references(&self.db, self.cursor.file, self.cursor.offset, true)
else {
return "No references found".to_string();
};
@@ -82,7 +84,7 @@ mod tests {
}
#[test]
fn test_parameter_references_in_function() {
fn parameter_references_in_function() {
let test = cursor_test(
"
def calculate_sum(<CURSOR>value: int) -> int:
@@ -147,29 +149,28 @@ result = calculate_sum(value=42)
}
#[test]
#[ignore] // TODO: Enable when nonlocal support is fully implemented in goto.rs
fn test_nonlocal_variable_references() {
fn nonlocal_variable_references() {
let test = cursor_test(
"
def outer_function():
coun<CURSOR>ter = 0
def increment():
nonlocal counter
counter += 1
return counter
def decrement():
nonlocal counter
counter -= 1
return counter
# Use counter in outer scope
initial = counter
increment()
decrement()
final = counter
return increment, decrement
",
);
@@ -181,7 +182,7 @@ def outer_function():
2 | def outer_function():
3 | counter = 0
| ^^^^^^^
4 |
4 |
5 | def increment():
|
@@ -212,7 +213,7 @@ def outer_function():
7 | counter += 1
8 | return counter
| ^^^^^^^
9 |
9 |
10 | def decrement():
|
@@ -243,7 +244,7 @@ def outer_function():
12 | counter -= 1
13 | return counter
| ^^^^^^^
14 |
14 |
15 | # Use counter in outer scope
|
@@ -264,15 +265,14 @@ def outer_function():
18 | decrement()
19 | final = counter
| ^^^^^^^
20 |
20 |
21 | return increment, decrement
|
");
}
#[test]
#[ignore] // TODO: Enable when global support is fully implemented in goto.rs
fn test_global_variable_references() {
fn global_variable_references() {
let test = cursor_test(
"
glo<CURSOR>bal_counter = 0
@@ -389,7 +389,7 @@ final_value = global_counter
}
#[test]
fn test_except_handler_variable_references() {
fn except_handler_variable_references() {
let test = cursor_test(
"
try:
@@ -450,7 +450,7 @@ except ValueError as err:
}
#[test]
fn test_pattern_match_as_references() {
fn pattern_match_as_references() {
let test = cursor_test(
"
match x:
@@ -498,7 +498,7 @@ match x:
}
#[test]
fn test_pattern_match_mapping_rest_references() {
fn pattern_match_mapping_rest_references() {
let test = cursor_test(
"
match data:
@@ -553,7 +553,7 @@ match data:
}
#[test]
fn test_function_definition_references() {
fn function_definition_references() {
let test = cursor_test(
"
def my_func<CURSOR>tion():
@@ -632,7 +632,7 @@ value = my_function
}
#[test]
fn test_class_definition_references() {
fn class_definition_references() {
let test = cursor_test(
"
class My<CURSOR>Class:
@@ -711,7 +711,741 @@ cls = MyClass
}
#[test]
fn test_multi_file_function_references() {
fn references_string_annotation1() {
let test = cursor_test(
r#"
a: "MyCla<CURSOR>ss" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:2:5
|
2 | a: "MyClass" = 1
| ^^^^^^^
3 |
4 | class MyClass:
|
info[references]: Reference 2
--> main.py:4:7
|
2 | a: "MyClass" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
"#);
}
#[test]
fn references_string_annotation2() {
let test = cursor_test(
r#"
a: "None | MyCl<CURSOR>ass" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:2:12
|
2 | a: "None | MyClass" = 1
| ^^^^^^^
3 |
4 | class MyClass:
|
info[references]: Reference 2
--> main.py:4:7
|
2 | a: "None | MyClass" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
"#);
}
#[test]
fn references_string_annotation3() {
let test = cursor_test(
r#"
a: "None |<CURSOR> MyClass" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_string_annotation4() {
let test = cursor_test(
r#"
a: "None | MyClass<CURSOR>" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:2:12
|
2 | a: "None | MyClass" = 1
| ^^^^^^^
3 |
4 | class MyClass:
|
info[references]: Reference 2
--> main.py:4:7
|
2 | a: "None | MyClass" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
"#);
}
#[test]
fn references_string_annotation5() {
let test = cursor_test(
r#"
a: "None | MyClass"<CURSOR> = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_string_annotation_dangling1() {
let test = cursor_test(
r#"
a: "MyCl<CURSOR>ass |" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_string_annotation_dangling2() {
let test = cursor_test(
r#"
a: "MyCl<CURSOR>ass | No" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:2:5
|
2 | a: "MyClass | No" = 1
| ^^^^^^^
3 |
4 | class MyClass:
|
info[references]: Reference 2
--> main.py:4:7
|
2 | a: "MyClass | No" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
"#);
}
#[test]
fn references_string_annotation_dangling3() {
let test = cursor_test(
r#"
a: "MyClass | N<CURSOR>o" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_match_name_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^^
5 | x = ab
|
info[references]: Reference 2
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", ab]:
5 | x = ab
| ^^
|
"#);
}
#[test]
fn references_match_name_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^^
5 | x = ab
|
info[references]: Reference 2
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", ab]:
5 | x = ab
| ^^
|
"#);
}
#[test]
fn references_match_rest_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^^
5 | x = ab
|
info[references]: Reference 2
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", *ab]:
5 | x = ab
| ^^
|
"#);
}
#[test]
fn references_match_rest_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^^
5 | x = ab
|
info[references]: Reference 2
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", *ab]:
5 | x = ab
| ^^
|
"#);
}
#[test]
fn references_match_as_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^^
5 | x = ab
|
info[references]: Reference 2
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
5 | x = ab
| ^^
|
"#);
}
#[test]
fn references_match_as_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^^
5 | x = ab
|
info[references]: Reference 2
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
5 | x = ab
| ^^
|
"#);
}
#[test]
fn references_match_keyword_stmt() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=a<CURSOR>b):
x = ab
"#,
);
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^
11 | x = ab
|
info[references]: Reference 2
--> main.py:11:17
|
9 | match event:
10 | case Click(x, button=ab):
11 | x = ab
| ^^
|
");
}
#[test]
fn references_match_keyword_binding() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^
11 | x = ab
|
info[references]: Reference 2
--> main.py:11:17
|
9 | match event:
10 | case Click(x, button=ab):
11 | x = ab
| ^^
|
");
}
#[test]
fn references_match_class_name() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Cl<CURSOR>ick(x, button=ab):
x = ab
"#,
);
assert_snapshot!(test.references(), @r#"
info[references]: Reference 1
--> main.py:2:7
|
2 | class Click:
| ^^^^^
3 | __match_args__ = ("position", "button")
4 | def __init__(self, pos, btn):
|
info[references]: Reference 2
--> main.py:8:20
|
6 | self.button: str = btn
7 |
8 | def my_func(event: Click):
| ^^^^^
9 | match event:
10 | case Click(x, button=ab):
|
info[references]: Reference 3
--> main.py:10:14
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^^^^
11 | x = ab
|
"#);
}
#[test]
fn references_match_class_field_name() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, but<CURSOR>ton=ab):
x = ab
"#,
);
assert_snapshot!(test.references(), @"No references found");
}
#[test]
fn references_typevar_name_stmt() {
let test = cursor_test(
r#"
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
"#,
);
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
info[references]: Reference 2
--> main.py:2:37
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
info[references]: Reference 3
--> main.py:2:46
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
");
}
#[test]
fn references_typevar_name_binding() {
let test = cursor_test(
r#"
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
"#,
);
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
info[references]: Reference 2
--> main.py:2:37
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
info[references]: Reference 3
--> main.py:2:46
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
");
}
#[test]
fn references_typevar_spec_stmt() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
"#,
);
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:3:15
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^
|
info[references]: Reference 2
--> main.py:3:43
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^
|
info[references]: Reference 3
--> main.py:3:53
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^
|
");
}
#[test]
fn references_typevar_spec_binding() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
"#,
);
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:3:15
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^
|
info[references]: Reference 2
--> main.py:3:43
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^
|
info[references]: Reference 3
--> main.py:3:53
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^
|
");
}
#[test]
fn references_typevar_tuple_stmt() {
let test = cursor_test(
r#"
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
"#,
);
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:2:14
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^
|
info[references]: Reference 2
--> main.py:2:38
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^
|
info[references]: Reference 3
--> main.py:2:50
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^
|
");
}
#[test]
fn references_typevar_tuple_binding() {
let test = cursor_test(
r#"
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
"#,
);
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:2:14
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^
|
info[references]: Reference 2
--> main.py:2:38
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^
|
info[references]: Reference 3
--> main.py:2:50
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^
|
");
}
#[test]
fn multi_file_function_references() {
let test = CursorTest::builder()
.source(
"utils.py",
@@ -737,7 +1471,7 @@ from utils import func
class DataProcessor:
def __init__(self):
self.multiplier = func
def process(self, value):
return func(value)
",
@@ -801,14 +1535,14 @@ class DataProcessor:
}
#[test]
fn test_multi_file_class_attribute_references() {
fn multi_file_class_attribute_references() {
let test = CursorTest::builder()
.source(
"models.py",
"
class MyModel:
a<CURSOR>ttr = 42
def get_attribute(self):
return MyModel.attr
",
@@ -879,7 +1613,7 @@ def process_model():
}
#[test]
fn test_import_alias_references_should_not_resolve_to_original() {
fn import_alias_references_should_not_resolve_to_original() {
let test = CursorTest::builder()
.source(
"original.py",

View File

@@ -190,20 +190,31 @@ pub(crate) enum GotoTarget<'a> {
/// The call of the callable
call: &'a ast::ExprCall,
},
/// Go to on a sub-expression of a string annotation's sub-AST
///
/// ```py
/// x: "int | None"
/// ^^^^
/// ```
///
/// This is equivalent to `GotoTarget::Expression` but the expression
/// isn't actually in the AST.
StringAnnotationSubexpr {
/// The string literal that is a string annotation.
string_expr: &'a ast::ExprStringLiteral,
/// The range to query in the sub-AST for the sub-expression.
subrange: TextRange,
/// If the expression is a Name of some kind this is the name (just a cached result).
name: Option<String>,
},
}
/// The resolved definitions for a `GotoTarget`
#[derive(Debug, Clone)]
pub(crate) enum DefinitionsOrTargets<'db> {
/// We computed actual Definitions we can do followup queries on.
Definitions(Vec<ResolvedDefinition<'db>>),
/// We directly computed a navigation.
///
/// We can't get docs or usefully compute goto-definition for this.
Targets(crate::NavigationTargets),
}
pub(crate) struct Definitions<'db>(pub Vec<ResolvedDefinition<'db>>);
impl<'db> DefinitionsOrTargets<'db> {
impl<'db> Definitions<'db> {
pub(crate) fn from_ty(db: &'db dyn crate::Db, ty: Type<'db>) -> Option<Self> {
let ty_def = ty.definition(db)?;
let resolved = match ty_def {
@@ -219,7 +230,7 @@ impl<'db> DefinitionsOrTargets<'db> {
ResolvedDefinition::Definition(definition)
}
};
Some(DefinitionsOrTargets::Definitions(vec![resolved]))
Some(Definitions(vec![resolved]))
}
/// Get the "goto-declaration" interpretation of this definition
@@ -227,14 +238,9 @@ impl<'db> DefinitionsOrTargets<'db> {
/// In this case it basically returns exactly what was found.
pub(crate) fn declaration_targets(
self,
db: &'db dyn crate::Db,
db: &'db dyn ty_python_semantic::Db,
) -> Option<crate::NavigationTargets> {
match self {
DefinitionsOrTargets::Definitions(definitions) => {
definitions_to_navigation_targets(db, None, definitions)
}
DefinitionsOrTargets::Targets(targets) => Some(targets),
}
definitions_to_navigation_targets(db, None, self.0)
}
/// Get the "goto-definition" interpretation of this definition
@@ -243,14 +249,9 @@ impl<'db> DefinitionsOrTargets<'db> {
/// if the definition we have is found in a stub file.
pub(crate) fn definition_targets(
self,
db: &'db dyn crate::Db,
db: &'db dyn ty_python_semantic::Db,
) -> Option<crate::NavigationTargets> {
match self {
DefinitionsOrTargets::Definitions(definitions) => {
definitions_to_navigation_targets(db, Some(&StubMapper::new(db)), definitions)
}
DefinitionsOrTargets::Targets(targets) => Some(targets),
}
definitions_to_navigation_targets(db, Some(&StubMapper::new(db)), self.0)
}
/// Get the docstring for this definition
@@ -259,13 +260,7 @@ impl<'db> DefinitionsOrTargets<'db> {
/// so this will check both the goto-declarations and goto-definitions (in that order)
/// and return the first one found.
pub(crate) fn docstring(self, db: &'db dyn crate::Db) -> Option<Docstring> {
let definitions = match self {
DefinitionsOrTargets::Definitions(definitions) => definitions,
// Can't find docs for these
// (make more cases DefinitionOrTargets::Definitions to get more docs!)
DefinitionsOrTargets::Targets(_) => return None,
};
for definition in &definitions {
for definition in &self.0 {
// If we got a docstring from the original definition, use it
if let Some(docstring) = definition.docstring(db) {
return Some(Docstring::new(docstring));
@@ -278,7 +273,7 @@ impl<'db> DefinitionsOrTargets<'db> {
let stub_mapper = StubMapper::new(db);
// Try to find the corresponding implementation definition
for definition in stub_mapper.map_definitions(definitions) {
for definition in stub_mapper.map_definitions(self.0) {
if let Some(docstring) = definition.docstring(db) {
return Some(Docstring::new(docstring));
}
@@ -313,6 +308,32 @@ impl GotoTarget<'_> {
let module = import_name(module_name, *component_index);
model.resolve_module_type(Some(module), *level)?
}
GotoTarget::StringAnnotationSubexpr {
string_expr,
subrange,
..
} => {
let (subast, _submodel) = model.enter_string_annotation(string_expr)?;
let submod = subast.syntax();
let subnode = covering_node(submod.into(), *subrange).node();
// The type checker knows the type of the full annotation but nothing else
if AnyNodeRef::from(&*submod.body) == subnode {
string_expr.inferred_type(model)
} else {
// TODO: force the typechecker to tell us its secrets
// (it computes but then immediately discards these types)
return None;
}
}
GotoTarget::BinOp { expression, .. } => {
let (_, ty) = ty_python_semantic::definitions_for_bin_op(model, expression)?;
ty
}
GotoTarget::UnaryOp { expression, .. } => {
let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?;
ty
}
// TODO: Support identifier targets
GotoTarget::PatternMatchRest(_)
| GotoTarget::PatternKeywordArgument(_)
@@ -322,16 +343,6 @@ impl GotoTarget<'_> {
| GotoTarget::TypeParamTypeVarTupleName(_)
| GotoTarget::NonLocal { .. }
| GotoTarget::Globals { .. } => return None,
GotoTarget::BinOp { expression, .. } => {
let (_, ty) =
ty_python_semantic::definitions_for_bin_op(model.db(), model, expression)?;
ty
}
GotoTarget::UnaryOp { expression, .. } => {
let (_, ty) =
ty_python_semantic::definitions_for_unary_op(model.db(), model, expression)?;
ty
}
};
Some(ty)
@@ -343,7 +354,7 @@ impl GotoTarget<'_> {
model: &SemanticModel,
) -> Option<String> {
if let GotoTarget::Call { call, .. } = self {
call_type_simplified_by_overloads(model.db(), model, call)
call_type_simplified_by_overloads(model, call)
} else {
None
}
@@ -365,42 +376,32 @@ impl GotoTarget<'_> {
&self,
model: &SemanticModel<'db>,
alias_resolution: ImportAliasResolution,
) -> Option<DefinitionsOrTargets<'db>> {
use crate::NavigationTarget;
let db = model.db();
let file = model.file();
match self {
GotoTarget::Expression(expression) => {
definitions_for_expression(model, expression).map(DefinitionsOrTargets::Definitions)
}
) -> Option<Definitions<'db>> {
let definitions = match self {
GotoTarget::Expression(expression) => definitions_for_expression(model, *expression),
// For already-defined symbols, they are their own definitions
GotoTarget::FunctionDef(function) => Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(function.definition(model)),
])),
GotoTarget::FunctionDef(function) => Some(vec![ResolvedDefinition::Definition(
function.definition(model),
)]),
GotoTarget::ClassDef(class) => Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(class.definition(model)),
])),
GotoTarget::ClassDef(class) => Some(vec![ResolvedDefinition::Definition(
class.definition(model),
)]),
GotoTarget::Parameter(parameter) => Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(parameter.definition(model)),
])),
GotoTarget::Parameter(parameter) => Some(vec![ResolvedDefinition::Definition(
parameter.definition(model),
)]),
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
GotoTarget::ImportSymbolAlias {
alias, import_from, ..
} => {
let symbol_name = alias.name.as_str();
Some(DefinitionsOrTargets::Definitions(
definitions_for_imported_symbol(
db,
file,
import_from,
symbol_name,
alias_resolution,
),
Some(definitions_for_imported_symbol(
model,
import_from,
symbol_name,
alias_resolution,
))
}
@@ -420,14 +421,9 @@ impl GotoTarget<'_> {
if alias_resolution == ImportAliasResolution::ResolveAliases {
definitions_for_module(model, Some(alias.name.as_str()), 0)
} else {
let alias_range = alias.asname.as_ref().unwrap().range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget {
file,
focus_range: alias_range,
full_range: alias.range(),
}),
))
alias.asname.as_ref().map(|name| {
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
})
}
}
@@ -435,39 +431,44 @@ impl GotoTarget<'_> {
GotoTarget::KeywordArgument {
keyword,
call_expression,
} => Some(DefinitionsOrTargets::Definitions(
definitions_for_keyword_argument(db, file, keyword, call_expression),
} => Some(definitions_for_keyword_argument(
model,
keyword,
call_expression,
)),
// For exception variables, they are their own definitions (like parameters)
GotoTarget::ExceptVariable(except_handler) => {
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(except_handler.definition(model)),
]))
Some(vec![ResolvedDefinition::Definition(
except_handler.definition(model),
)])
}
// For pattern match rest variables, they are their own definitions
// Patterns are glorified assignments but we have to look them up by ident
// because they're not expressions
GotoTarget::PatternMatchRest(pattern_mapping) => {
if let Some(rest_name) = &pattern_mapping.rest {
let range = rest_name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
))
} else {
None
}
pattern_mapping.rest.as_ref().map(|name| {
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
})
}
// For pattern match as names, they are their own definitions
GotoTarget::PatternMatchAsName(pattern_as) => {
if let Some(name) = &pattern_as.name {
let range = name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
))
} else {
None
}
GotoTarget::PatternMatchAsName(pattern_as) => pattern_as.name.as_ref().map(|name| {
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
}),
GotoTarget::PatternKeywordArgument(pattern_keyword) => {
let name = &pattern_keyword.attr;
Some(definitions_for_name(
model,
name.as_str(),
AnyNodeRef::Identifier(name),
))
}
GotoTarget::PatternMatchStarName(pattern_star) => {
pattern_star.name.as_ref().map(|name| {
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
})
}
// For callables, both the definition of the callable and the actual function impl are relevant.
@@ -476,32 +477,82 @@ impl GotoTarget<'_> {
GotoTarget::Call { callable, call } => {
let mut definitions = definitions_for_callable(model, call);
let expr_definitions =
definitions_for_expression(model, callable).unwrap_or_default();
definitions_for_expression(model, *callable).unwrap_or_default();
definitions.extend(expr_definitions);
if definitions.is_empty() {
None
} else {
Some(DefinitionsOrTargets::Definitions(definitions))
Some(definitions)
}
}
GotoTarget::BinOp { expression, .. } => {
let (definitions, _) =
ty_python_semantic::definitions_for_bin_op(db, model, expression)?;
ty_python_semantic::definitions_for_bin_op(model, expression)?;
Some(DefinitionsOrTargets::Definitions(definitions))
Some(definitions)
}
GotoTarget::UnaryOp { expression, .. } => {
let (definitions, _) =
ty_python_semantic::definitions_for_unary_op(db, model, expression)?;
ty_python_semantic::definitions_for_unary_op(model, expression)?;
Some(DefinitionsOrTargets::Definitions(definitions))
Some(definitions)
}
_ => None,
}
// String annotations sub-expressions require us to recurse into the sub-AST
GotoTarget::StringAnnotationSubexpr {
string_expr,
subrange,
..
} => {
let (subast, submodel) = model.enter_string_annotation(string_expr)?;
let subexpr = covering_node(subast.syntax().into(), *subrange)
.node()
.as_expr_ref()?;
definitions_for_expression(&submodel, subexpr)
}
// nonlocal and global are essentially loads, but again they're statements,
// so we need to look them up by ident
GotoTarget::NonLocal { identifier } | GotoTarget::Globals { identifier } => {
Some(definitions_for_name(
model,
identifier.as_str(),
AnyNodeRef::Identifier(identifier),
))
}
// These are declarations of sorts, but they're stmts and not exprs, so look up by ident.
GotoTarget::TypeParamTypeVarName(type_var) => {
let name = &type_var.name;
Some(definitions_for_name(
model,
name.as_str(),
AnyNodeRef::Identifier(name),
))
}
GotoTarget::TypeParamParamSpecName(name) => {
let name = &name.name;
Some(definitions_for_name(
model,
name.as_str(),
AnyNodeRef::Identifier(name),
))
}
GotoTarget::TypeParamTypeVarTupleName(name) => {
let name = &name.name;
Some(definitions_for_name(
model,
name.as_str(),
AnyNodeRef::Identifier(name),
))
}
};
definitions.map(Definitions)
}
/// Returns the text representation of this goto target.
@@ -519,6 +570,7 @@ impl GotoTarget<'_> {
ast::ExprRef::Attribute(attr) => Some(Cow::Borrowed(attr.attr.as_str())),
_ => None,
},
GotoTarget::StringAnnotationSubexpr { name, .. } => name.as_deref().map(Cow::Borrowed),
GotoTarget::FunctionDef(function) => Some(Cow::Borrowed(function.name.as_str())),
GotoTarget::ClassDef(class) => Some(Cow::Borrowed(class.name.as_str())),
GotoTarget::Parameter(parameter) => Some(Cow::Borrowed(parameter.name.as_str())),
@@ -579,6 +631,7 @@ impl GotoTarget<'_> {
/// Creates a `GotoTarget` from a `CoveringNode` and an offset within the node
pub(crate) fn from_covering_node<'a>(
model: &SemanticModel,
covering_node: &crate::find_node::CoveringNode<'a>,
offset: TextSize,
tokens: &Tokens,
@@ -778,6 +831,31 @@ impl GotoTarget<'_> {
Some(GotoTarget::Expression(unary.into()))
}
node @ AnyNodeRef::ExprStringLiteral(string_expr) => {
// Check if we've clicked on a sub-GotoTarget inside a string annotation's sub-AST
if let Some((subast, submodel)) = model.enter_string_annotation(string_expr)
&& let Some(GotoTarget::Expression(subexpr)) = find_goto_target_impl(
&submodel,
subast.tokens(),
subast.syntax().into(),
offset,
)
{
let name = match subexpr {
ast::ExprRef::Name(name) => Some(name.id.to_string()),
ast::ExprRef::Attribute(attr) => Some(attr.attr.to_string()),
_ => None,
};
Some(GotoTarget::StringAnnotationSubexpr {
string_expr,
subrange: subexpr.range(),
name,
})
} else {
node.as_expr_ref().map(GotoTarget::Expression)
}
}
node => {
// Check if this is seemingly a callable being invoked (the `x` in `x(...)`)
let parent = covering_node.parent();
@@ -813,6 +891,7 @@ impl Ranged for GotoTarget<'_> {
GotoTarget::ImportModuleComponent {
component_range, ..
} => *component_range,
GotoTarget::StringAnnotationSubexpr { subrange, .. } => *subrange,
GotoTarget::ImportModuleAlias { alias } => alias.asname.as_ref().unwrap().range,
GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range,
GotoTarget::KeywordArgument { keyword, .. } => keyword.arg.as_ref().unwrap().range,
@@ -832,9 +911,9 @@ impl Ranged for GotoTarget<'_> {
}
/// Converts a collection of `ResolvedDefinition` items into `NavigationTarget` items.
fn convert_resolved_definitions_to_targets(
db: &dyn crate::Db,
definitions: Vec<ty_python_semantic::ResolvedDefinition<'_>>,
fn convert_resolved_definitions_to_targets<'db>(
db: &'db dyn ty_python_semantic::Db,
definitions: Vec<ty_python_semantic::ResolvedDefinition<'db>>,
) -> Vec<crate::NavigationTarget> {
definitions
.into_iter()
@@ -869,14 +948,16 @@ fn convert_resolved_definitions_to_targets(
/// Shared helper to get definitions for an expr (that is presumably a name/attr)
fn definitions_for_expression<'db>(
model: &SemanticModel<'db>,
expression: &ruff_python_ast::ExprRef<'_>,
expression: ruff_python_ast::ExprRef<'_>,
) -> Option<Vec<ResolvedDefinition<'db>>> {
match expression {
ast::ExprRef::Name(name) => Some(definitions_for_name(model.db(), model.file(), name)),
ast::ExprRef::Name(name) => Some(definitions_for_name(
model,
name.id.as_str(),
expression.into(),
)),
ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute(
model.db(),
model.file(),
attribute,
model, attribute,
)),
_ => None,
}
@@ -887,7 +968,7 @@ fn definitions_for_callable<'db>(
call: &ast::ExprCall,
) -> Vec<ResolvedDefinition<'db>> {
// Attempt to refine to a specific call
let signature_info = call_signature_details(model.db(), model, call);
let signature_info = call_signature_details(model, call);
signature_info
.into_iter()
.filter_map(|signature| signature.definition.map(ResolvedDefinition::Definition))
@@ -896,7 +977,7 @@ fn definitions_for_callable<'db>(
/// Shared helper to map and convert resolved definitions into navigation targets.
fn definitions_to_navigation_targets<'db>(
db: &dyn crate::Db,
db: &dyn ty_python_semantic::Db,
stub_mapper: Option<&StubMapper<'db>>,
mut definitions: Vec<ty_python_semantic::ResolvedDefinition<'db>>,
) -> Option<crate::NavigationTargets> {
@@ -911,12 +992,21 @@ fn definitions_to_navigation_targets<'db>(
}
}
pub(crate) fn find_goto_target(
parsed: &ParsedModuleRef,
pub(crate) fn find_goto_target<'a>(
model: &'a SemanticModel,
parsed: &'a ParsedModuleRef,
offset: TextSize,
) -> Option<GotoTarget<'_>> {
let token = parsed
.tokens()
) -> Option<GotoTarget<'a>> {
find_goto_target_impl(model, parsed.tokens(), parsed.syntax().into(), offset)
}
pub(crate) fn find_goto_target_impl<'a>(
model: &'a SemanticModel,
tokens: &'a Tokens,
syntax: AnyNodeRef<'a>,
offset: TextSize,
) -> Option<GotoTarget<'a>> {
let token = tokens
.at_offset(offset)
.max_by_key(|token| match token.kind() {
TokenKind::Name
@@ -937,26 +1027,24 @@ pub(crate) fn find_goto_target(
return None;
}
let covering_node = covering_node(parsed.syntax().into(), token.range())
let covering_node = covering_node(syntax, token.range())
.find_first(|node| {
node.is_identifier() || node.is_expression() || node.is_stmt_import_from()
})
.ok()?;
GotoTarget::from_covering_node(&covering_node, offset, parsed.tokens())
GotoTarget::from_covering_node(model, &covering_node, offset, tokens)
}
/// Helper function to resolve a module name and create a navigation target.
fn definitions_for_module<'db>(
model: &SemanticModel,
model: &SemanticModel<'db>,
module: Option<&str>,
level: u32,
) -> Option<DefinitionsOrTargets<'db>> {
) -> Option<Vec<ResolvedDefinition<'db>>> {
let module = model.resolve_module(module, level)?;
let file = module.file(model.db())?;
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Module(file),
]))
Some(vec![ResolvedDefinition::Module(file)])
}
/// Helper function to extract module component information from a dotted module name

View File

@@ -16,9 +16,9 @@ pub fn goto_declaration(
offset: TextSize,
) -> Option<RangedValue<NavigationTargets>> {
let module = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&module, offset)?;
let model = SemanticModel::new(db, file);
let goto_target = find_goto_target(&model, &module, offset)?;
let declaration_targets = goto_target
.get_definition_targets(&model, ImportAliasResolution::ResolveAliases)?
.declaration_targets(db)?;
@@ -889,6 +889,190 @@ def another_helper(path):
");
}
#[test]
fn goto_declaration_string_annotation1() {
let test = cursor_test(
r#"
a: "MyCla<CURSOR>ss" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:4:7
|
2 | a: "MyClass" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
info: Source
--> main.py:2:5
|
2 | a: "MyClass" = 1
| ^^^^^^^
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn goto_declaration_string_annotation2() {
let test = cursor_test(
r#"
a: "None | MyCl<CURSOR>ass" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:4:7
|
2 | a: "None | MyClass" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
info: Source
--> main.py:2:12
|
2 | a: "None | MyClass" = 1
| ^^^^^^^
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn goto_declaration_string_annotation3() {
let test = cursor_test(
r#"
a: "None |<CURSOR> MyClass" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_declaration(), @"No goto target found");
}
#[test]
fn goto_declaration_string_annotation4() {
let test = cursor_test(
r#"
a: "None | MyClass<CURSOR>" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:4:7
|
2 | a: "None | MyClass" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
info: Source
--> main.py:2:12
|
2 | a: "None | MyClass" = 1
| ^^^^^^^
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn goto_declaration_string_annotation5() {
let test = cursor_test(
r#"
a: "None | MyClass"<CURSOR> = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_declaration(), @"No goto target found");
}
#[test]
fn goto_declaration_string_annotation_dangling1() {
let test = cursor_test(
r#"
a: "MyCl<CURSOR>ass |" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_declaration(), @"No goto target found");
}
#[test]
fn goto_declaration_string_annotation_dangling2() {
let test = cursor_test(
r#"
a: "MyCl<CURSOR>ass | No" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:4:7
|
2 | a: "MyClass | No" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
info: Source
--> main.py:2:5
|
2 | a: "MyClass | No" = 1
| ^^^^^^^
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn goto_declaration_string_annotation_dangling3() {
let test = cursor_test(
r#"
a: "MyClass | N<CURSOR>o" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_declaration(), @"No goto target found");
}
#[test]
fn goto_declaration_nested_instance_attribute() {
let test = cursor_test(
@@ -1070,6 +1254,45 @@ def outer():
"#);
}
#[test]
fn goto_declaration_nonlocal_stmt() {
let test = cursor_test(
r#"
def outer():
xy = "outer_value"
def inner():
nonlocal x<CURSOR>y
xy = "modified"
return x # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
// Should find the variable declaration in the outer scope, not the nonlocal statement
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:3:5
|
2 | def outer():
3 | xy = "outer_value"
| ^^
4 |
5 | def inner():
|
info: Source
--> main.py:6:18
|
5 | def inner():
6 | nonlocal xy
| ^^
7 | xy = "modified"
8 | return x # Should find the nonlocal x declaration in outer scope
|
"#);
}
#[test]
fn goto_declaration_global_binding() {
let test = cursor_test(
@@ -1104,6 +1327,41 @@ def function():
"#);
}
#[test]
fn goto_declaration_global_stmt() {
let test = cursor_test(
r#"
global_var = "global_value"
def function():
global global_<CURSOR>var
global_var = "modified"
return global_var # Should find the global variable declaration
"#,
);
// Should find the global variable declaration, not the global statement
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:2:1
|
2 | global_var = "global_value"
| ^^^^^^^^^^
3 |
4 | def function():
|
info: Source
--> main.py:5:12
|
4 | def function():
5 | global global_var
| ^^^^^^^^^^
6 | global_var = "modified"
7 | return global_var # Should find the global variable declaration
|
"#);
}
#[test]
fn goto_declaration_inherited_attribute() {
let test = cursor_test(
@@ -1139,6 +1397,486 @@ def function():
");
}
#[test]
fn goto_declaration_match_name_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^^
5 | x = ab
|
info: Source
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^^
5 | x = ab
|
"#);
}
#[test]
fn goto_declaration_match_name_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^^
5 | x = ab
|
info: Source
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", ab]:
5 | x = ab
| ^^
|
"#);
}
#[test]
fn goto_declaration_match_rest_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^^
5 | x = ab
|
info: Source
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^^
5 | x = ab
|
"#);
}
#[test]
fn goto_declaration_match_rest_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^^
5 | x = ab
|
info: Source
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", *ab]:
5 | x = ab
| ^^
|
"#);
}
#[test]
fn goto_declaration_match_as_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^^
5 | x = ab
|
info: Source
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^^
5 | x = ab
|
"#);
}
#[test]
fn goto_declaration_match_as_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^^
5 | x = ab
|
info: Source
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
5 | x = ab
| ^^
|
"#);
}
#[test]
fn goto_declaration_match_keyword_stmt() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=a<CURSOR>b):
x = ab
"#,
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^
11 | x = ab
|
info: Source
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^
11 | x = ab
|
");
}
#[test]
fn goto_declaration_match_keyword_binding() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^
11 | x = ab
|
info: Source
--> main.py:11:17
|
9 | match event:
10 | case Click(x, button=ab):
11 | x = ab
| ^^
|
");
}
#[test]
fn goto_declaration_match_class_name() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Cl<CURSOR>ick(x, button=ab):
x = ab
"#,
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:2:7
|
2 | class Click:
| ^^^^^
3 | __match_args__ = ("position", "button")
4 | def __init__(self, pos, btn):
|
info: Source
--> main.py:10:14
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^^^^
11 | x = ab
|
"#);
}
#[test]
fn goto_declaration_match_class_field_name() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, but<CURSOR>ton=ab):
x = ab
"#,
);
assert_snapshot!(test.goto_declaration(), @"No goto target found");
}
#[test]
fn goto_declaration_typevar_name_stmt() {
let test = cursor_test(
r#"
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
"#,
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
info: Source
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
");
}
#[test]
fn goto_declaration_typevar_name_binding() {
let test = cursor_test(
r#"
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
"#,
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
info: Source
--> main.py:2:37
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
");
}
#[test]
fn goto_declaration_typevar_spec_stmt() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
"#,
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:3:15
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^
|
info: Source
--> main.py:3:15
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^
|
");
}
#[test]
fn goto_declaration_typevar_spec_binding() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
"#,
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:3:15
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^
|
info: Source
--> main.py:3:43
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^
|
");
}
#[test]
fn goto_declaration_typevar_tuple_stmt() {
let test = cursor_test(
r#"
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
"#,
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:14
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^
|
info: Source
--> main.py:2:14
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^
|
");
}
#[test]
fn goto_declaration_typevar_tuple_binding() {
let test = cursor_test(
r#"
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
"#,
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:14
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^
|
info: Source
--> main.py:2:38
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^
|
");
}
#[test]
fn goto_declaration_property_getter_setter() {
let test = cursor_test(

View File

@@ -17,8 +17,8 @@ pub fn goto_definition(
offset: TextSize,
) -> Option<RangedValue<NavigationTargets>> {
let module = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&module, offset)?;
let model = SemanticModel::new(db, file);
let goto_target = find_goto_target(&model, &module, offset)?;
let definition_targets = goto_target
.get_definition_targets(&model, ImportAliasResolution::ResolveAliases)?
.definition_targets(db)?;

View File

@@ -11,9 +11,9 @@ pub fn goto_type_definition(
offset: TextSize,
) -> Option<RangedValue<NavigationTargets>> {
let module = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&module, offset)?;
let model = SemanticModel::new(db, file);
let goto_target = find_goto_target(&model, &module, offset)?;
let ty = goto_target.inferred_type(&model)?;
tracing::debug!("Inferred type of covering node is {}", ty.display(db));
@@ -744,6 +744,502 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]
fn goto_type_string_annotation1() {
let test = cursor_test(
r#"
a: "MyCla<CURSOR>ss" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> main.py:4:7
|
2 | a: "MyClass" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
info: Source
--> main.py:2:5
|
2 | a: "MyClass" = 1
| ^^^^^^^
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn goto_type_string_annotation2() {
let test = cursor_test(
r#"
a: "None | MyCl<CURSOR>ass" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_string_annotation3() {
let test = cursor_test(
r#"
a: "None |<CURSOR> MyClass" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> main.py:4:7
|
2 | a: "None | MyClass" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
info: Source
--> main.py:2:4
|
2 | a: "None | MyClass" = 1
| ^^^^^^^^^^^^^^^^
3 |
4 | class MyClass:
|
info[goto-type-definition]: Type definition
--> stdlib/types.pyi:950:11
|
948 | if sys.version_info >= (3, 10):
949 | @final
950 | class NoneType:
| ^^^^^^^^
951 | """The type of the None singleton."""
|
info: Source
--> main.py:2:4
|
2 | a: "None | MyClass" = 1
| ^^^^^^^^^^^^^^^^
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn goto_type_string_annotation4() {
let test = cursor_test(
r#"
a: "None | MyClass<CURSOR>" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_string_annotation5() {
let test = cursor_test(
r#"
a: "None | MyClass"<CURSOR> = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> main.py:4:7
|
2 | a: "None | MyClass" = 1
3 |
4 | class MyClass:
| ^^^^^^^
5 | """some docs"""
|
info: Source
--> main.py:2:4
|
2 | a: "None | MyClass" = 1
| ^^^^^^^^^^^^^^^^
3 |
4 | class MyClass:
|
info[goto-type-definition]: Type definition
--> stdlib/types.pyi:950:11
|
948 | if sys.version_info >= (3, 10):
949 | @final
950 | class NoneType:
| ^^^^^^^^
951 | """The type of the None singleton."""
|
info: Source
--> main.py:2:4
|
2 | a: "None | MyClass" = 1
| ^^^^^^^^^^^^^^^^
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn goto_type_string_annotation_dangling1() {
let test = cursor_test(
r#"
a: "MyCl<CURSOR>ass |" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:2:4
|
2 | a: "MyClass |" = 1
| ^^^^^^^^^^^
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn goto_type_string_annotation_dangling2() {
let test = cursor_test(
r#"
a: "MyCl<CURSOR>ass | No" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_string_annotation_dangling3() {
let test = cursor_test(
r#"
a: "MyClass | N<CURSOR>o" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_match_name_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_match_name_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]
fn goto_type_match_rest_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_match_rest_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]
fn goto_type_match_as_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_match_as_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]
fn goto_type_match_keyword_stmt() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=a<CURSOR>b):
x = ab
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_match_keyword_binding() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]
fn goto_type_match_class_name() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Cl<CURSOR>ick(x, button=ab):
x = ab
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> main.py:2:7
|
2 | class Click:
| ^^^^^
3 | __match_args__ = ("position", "button")
4 | def __init__(self, pos, btn):
|
info: Source
--> main.py:10:14
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^^^^
11 | x = ab
|
"#);
}
#[test]
fn goto_type_match_class_field_name() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, but<CURSOR>ton=ab):
x = ab
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_typevar_name_stmt() {
let test = cursor_test(
r#"
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
info: Source
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
");
}
#[test]
fn goto_type_typevar_name_binding() {
let test = cursor_test(
r#"
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
info: Source
--> main.py:2:37
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^
|
");
}
#[test]
fn goto_type_typevar_spec_stmt() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_typevar_spec_binding() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]
fn goto_type_typevar_tuple_stmt() {
let test = cursor_test(
r#"
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_typevar_tuple_binding() {
let test = cursor_test(
r#"
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]
fn goto_type_on_keyword_argument() {
let test = cursor_test(
@@ -842,6 +1338,118 @@ f(**kwargs<CURSOR>)
"#);
}
#[test]
fn goto_type_nonlocal_binding() {
let test = cursor_test(
r#"
def outer():
x = "outer_value"
def inner():
nonlocal x
x = "modified"
return x<CURSOR> # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
// Should find the variable declaration in the outer scope, not the nonlocal statement
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:915:7
|
914 | @disjoint_base
915 | class str(Sequence[str]):
| ^^^
916 | """str(object='') -> str
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:8:16
|
6 | nonlocal x
7 | x = "modified"
8 | return x # Should find the nonlocal x declaration in outer scope
| ^
9 |
10 | return inner
|
"#);
}
#[test]
fn goto_type_nonlocal_stmt() {
let test = cursor_test(
r#"
def outer():
xy = "outer_value"
def inner():
nonlocal x<CURSOR>y
xy = "modified"
return x # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
// Should find the variable declaration in the outer scope, not the nonlocal statement
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_global_binding() {
let test = cursor_test(
r#"
global_var = "global_value"
def function():
global global_var
global_var = "modified"
return global_<CURSOR>var # Should find the global variable declaration
"#,
);
// Should find the global variable declaration, not the global statement
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:915:7
|
914 | @disjoint_base
915 | class str(Sequence[str]):
| ^^^
916 | """str(object='') -> str
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:7:12
|
5 | global global_var
6 | global_var = "modified"
7 | return global_var # Should find the global variable declaration
| ^^^^^^^^^^
|
"#);
}
#[test]
fn goto_type_global_stmt() {
let test = cursor_test(
r#"
global_var = "global_value"
def function():
global global_<CURSOR>var
global_var = "modified"
return global_var # Should find the global variable declaration
"#,
);
// Should find the global variable declaration, not the global statement
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_of_expression_with_builtin() {
let test = cursor_test(

View File

@@ -11,7 +11,8 @@ use ty_python_semantic::{DisplaySettings, SemanticModel};
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover<'_>>> {
let parsed = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&parsed, offset)?;
let model = SemanticModel::new(db, file);
let goto_target = find_goto_target(&model, &parsed, offset)?;
if let GotoTarget::Expression(expr) = goto_target {
if expr.is_literal_expr() {
@@ -19,7 +20,6 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
}
}
let model = SemanticModel::new(db, file);
let docs = goto_target
.get_definition_targets(
&model,
@@ -904,6 +904,191 @@ mod tests {
");
}
#[test]
fn hover_string_annotation1() {
let test = cursor_test(
r#"
a: "MyCla<CURSOR>ss" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.hover(), @r#"
MyClass
---------------------------------------------
some docs
---------------------------------------------
```python
MyClass
```
---
some docs
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
|
2 | a: "MyClass" = 1
| ^^^^^-^
| | |
| | Cursor offset
| source
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn hover_string_annotation2() {
let test = cursor_test(
r#"
a: "None | MyCl<CURSOR>ass" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.hover(), @r#"
some docs
---------------------------------------------
some docs
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:12
|
2 | a: "None | MyClass" = 1
| ^^^^-^^
| | |
| | Cursor offset
| source
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn hover_string_annotation3() {
let test = cursor_test(
r#"
a: "None |<CURSOR> MyClass" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_string_annotation4() {
let test = cursor_test(
r#"
a: "None | MyClass<CURSOR>" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.hover(), @r#"
some docs
---------------------------------------------
some docs
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:12
|
2 | a: "None | MyClass" = 1
| ^^^^^^^- Cursor offset
| |
| source
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn hover_string_annotation5() {
let test = cursor_test(
r#"
a: "None | MyClass"<CURSOR> = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_string_annotation_dangling1() {
let test = cursor_test(
r#"
a: "MyCl<CURSOR>ass |" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_string_annotation_dangling2() {
let test = cursor_test(
r#"
a: "MyCl<CURSOR>ass | No" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.hover(), @r#"
some docs
---------------------------------------------
some docs
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
|
2 | a: "MyClass | No" = 1
| ^^^^-^^
| | |
| | Cursor offset
| source
3 |
4 | class MyClass:
|
"#);
}
#[test]
fn hover_string_annotation_dangling3() {
let test = cursor_test(
r#"
a: "MyClass | N<CURSOR>o" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_overload_type_disambiguated1() {
let test = CursorTest::builder()
@@ -1463,6 +1648,509 @@ def ab(a: int, *, c: int):
");
}
#[test]
fn hover_nonlocal_binding() {
let test = cursor_test(
r#"
def outer():
x = "outer_value"
def inner():
nonlocal x
x = "modified"
return x<CURSOR> # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
// Should find the variable declaration in the outer scope, not the nonlocal statement
assert_snapshot!(test.hover(), @r#"
Literal["modified"]
---------------------------------------------
```python
Literal["modified"]
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:8:16
|
6 | nonlocal x
7 | x = "modified"
8 | return x # Should find the nonlocal x declaration in outer scope
| ^- Cursor offset
| |
| source
9 |
10 | return inner
|
"#);
}
#[test]
fn hover_nonlocal_stmt() {
let test = cursor_test(
r#"
def outer():
xy = "outer_value"
def inner():
nonlocal x<CURSOR>y
xy = "modified"
return x # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
// Should find the variable declaration in the outer scope, not the nonlocal statement
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_global_binding() {
let test = cursor_test(
r#"
global_var = "global_value"
def function():
global global_var
global_var = "modified"
return global_<CURSOR>var # Should find the global variable declaration
"#,
);
// Should find the global variable declaration, not the global statement
assert_snapshot!(test.hover(), @r#"
Literal["modified"]
---------------------------------------------
```python
Literal["modified"]
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:7:12
|
5 | global global_var
6 | global_var = "modified"
7 | return global_var # Should find the global variable declaration
| ^^^^^^^-^^
| | |
| | Cursor offset
| source
|
"#);
}
#[test]
fn hover_global_stmt() {
let test = cursor_test(
r#"
global_var = "global_value"
def function():
global global_<CURSOR>var
global_var = "modified"
return global_var # Should find the global variable declaration
"#,
);
// Should find the global variable declaration, not the global statement
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_match_name_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_match_name_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.hover(), @r#"
@Todo
---------------------------------------------
```python
@Todo
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", ab]:
5 | x = ab
| ^-
| ||
| |Cursor offset
| source
|
"#);
}
#[test]
fn hover_match_rest_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_match_rest_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.hover(), @r#"
@Todo
---------------------------------------------
```python
@Todo
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", *ab]:
5 | x = ab
| ^-
| ||
| |Cursor offset
| source
|
"#);
}
#[test]
fn hover_match_as_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_match_as_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.hover(), @r#"
@Todo
---------------------------------------------
```python
@Todo
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:17
|
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
5 | x = ab
| ^-
| ||
| |Cursor offset
| source
|
"#);
}
#[test]
fn hover_match_keyword_stmt() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=a<CURSOR>b):
x = ab
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_match_keyword_binding() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.hover(), @r"
@Todo
---------------------------------------------
```python
@Todo
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:11:17
|
9 | match event:
10 | case Click(x, button=ab):
11 | x = ab
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_match_class_name() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Cl<CURSOR>ick(x, button=ab):
x = ab
"#,
);
assert_snapshot!(test.hover(), @r"
<class 'Click'>
---------------------------------------------
```python
<class 'Click'>
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:10:14
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^-^^
| | |
| | Cursor offset
| source
11 | x = ab
|
");
}
#[test]
fn hover_match_class_field_name() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, but<CURSOR>ton=ab):
x = ab
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_typevar_name_stmt() {
let test = cursor_test(
r#"
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
"#,
);
assert_snapshot!(test.hover(), @r"
AB@Alias1 (invariant)
---------------------------------------------
```python
AB@Alias1 (invariant)
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_typevar_name_binding() {
let test = cursor_test(
r#"
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
"#,
);
assert_snapshot!(test.hover(), @r"
AB@Alias1 (invariant)
---------------------------------------------
```python
AB@Alias1 (invariant)
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:37
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_typevar_spec_stmt() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_typevar_spec_binding() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
"#,
);
assert_snapshot!(test.hover(), @r"
(
...
) -> tuple[typing.ParamSpec]
---------------------------------------------
```python
(
...
) -> tuple[typing.ParamSpec]
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:3:43
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_typevar_tuple_stmt() {
let test = cursor_test(
r#"
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_typevar_tuple_binding() {
let test = cursor_test(
r#"
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
"#,
);
assert_snapshot!(test.hover(), @r"
@Todo
---------------------------------------------
```python
@Todo
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:38
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_module_import() {
let mut test = cursor_test(

View File

@@ -1946,6 +1946,131 @@ mod tests {
"#);
}
#[test]
fn test_match_name_binding() {
let mut test = inlay_hint_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ab]:
x = ab
"#,
);
assert_snapshot!(test.inlay_hints(), @r#"
def my_func(command: str):
match command.split():
case ["get", ab]:
x[: @Todo] = ab
"#);
}
#[test]
fn test_match_rest_binding() {
let mut test = inlay_hint_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *ab]:
x = ab
"#,
);
assert_snapshot!(test.inlay_hints(), @r#"
def my_func(command: str):
match command.split():
case ["get", *ab]:
x[: @Todo] = ab
"#);
}
#[test]
fn test_match_as_binding() {
let mut test = inlay_hint_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as ab]:
x = ab
"#,
);
assert_snapshot!(test.inlay_hints(), @r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as ab]:
x[: @Todo] = ab
"#);
}
#[test]
fn test_match_keyword_binding() {
let mut test = inlay_hint_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
x = ab
"#,
);
assert_snapshot!(test.inlay_hints(), @r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
x[: @Todo] = ab
"#);
}
#[test]
fn test_typevar_name_binding() {
let mut test = inlay_hint_test(
r#"
type Alias1[AB: int = bool] = tuple[AB, list[AB]]
"#,
);
assert_snapshot!(test.inlay_hints(), @"type Alias1[AB: int = bool] = tuple[AB, list[AB]]");
}
#[test]
fn test_typevar_spec_binding() {
let mut test = inlay_hint_test(
r#"
from typing import Callable
type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
"#,
);
assert_snapshot!(test.inlay_hints(), @r"
from typing import Callable
type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
");
}
#[test]
fn test_typevar_tuple_binding() {
let mut test = inlay_hint_test(
r#"
type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
"#,
);
assert_snapshot!(test.inlay_hints(), @"type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]");
}
#[test]
fn test_many_literals() {
let mut test = inlay_hint_test(
@@ -6294,6 +6419,22 @@ mod tests {
");
}
#[test]
fn test_literal_type_alias_inlay_hint() {
let mut test = inlay_hint_test(
"
from typing import Literal
a = Literal['a', 'b', 'c']",
);
assert_snapshot!(test.inlay_hints(), @r"
from typing import Literal
a[: <typing.Literal special form>] = Literal['a', 'b', 'c']
");
}
struct InlayHintLocationDiagnostic {
source: FileRange,
target: FileRange,

View File

@@ -3,15 +3,16 @@
reason = "Prefer System trait methods over std methods in ty crates"
)]
mod all_symbols;
mod code_action;
mod completion;
mod doc_highlights;
mod docstring;
mod document_symbols;
mod find_node;
mod find_references;
mod goto;
mod goto_declaration;
mod goto_definition;
mod goto_references;
mod goto_type_definition;
mod hover;
mod importer;
@@ -27,11 +28,12 @@ mod symbols;
mod workspace_symbols;
pub use all_symbols::{AllSymbolInfo, all_symbols};
pub use code_action::{QuickFix, code_actions};
pub use completion::{Completion, CompletionKind, CompletionSettings, completion};
pub use doc_highlights::document_highlights;
pub use document_symbols::document_symbols;
pub use find_references::find_references;
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
pub use goto_references::goto_references;
pub use hover::hover;
pub use inlay_hints::{
InlayHintKind, InlayHintLabel, InlayHintSettings, InlayHintTextEdit, inlay_hints,

View File

@@ -122,10 +122,10 @@ fn references_for_file(
) {
let parsed = ruff_db::parsed::parsed_module(db, file);
let module = parsed.load(db);
let model = SemanticModel::new(db, file);
let mut finder = LocalReferencesFinder {
db,
file,
model: &model,
target_definitions,
references,
mode,
@@ -157,8 +157,7 @@ fn is_symbol_externally_visible(goto_target: &GotoTarget<'_>) -> bool {
/// AST visitor to find all references to a specific symbol by comparing semantic definitions
struct LocalReferencesFinder<'a> {
db: &'a dyn Db,
file: File,
model: &'a SemanticModel<'a>,
tokens: &'a Tokens,
target_definitions: &'a [NavigationTarget],
references: &'a mut Vec<ReferenceTarget>,
@@ -220,6 +219,11 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> {
self.check_identifier_reference(name);
}
}
AnyNodeRef::PatternMatchStar(pattern_star) if self.should_include_declaration() => {
if let Some(name) = &pattern_star.name {
self.check_identifier_reference(name);
}
}
AnyNodeRef::PatternMatchMapping(pattern_mapping)
if self.should_include_declaration() =>
{
@@ -227,6 +231,31 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> {
self.check_identifier_reference(rest_name);
}
}
AnyNodeRef::TypeParamParamSpec(param_spec) if self.should_include_declaration() => {
self.check_identifier_reference(&param_spec.name);
}
AnyNodeRef::TypeParamTypeVarTuple(param_tuple) if self.should_include_declaration() => {
self.check_identifier_reference(&param_tuple.name);
}
AnyNodeRef::TypeParamTypeVar(param_var) if self.should_include_declaration() => {
self.check_identifier_reference(&param_var.name);
}
AnyNodeRef::ExprStringLiteral(string_expr) if self.should_include_declaration() => {
// Highlight the sub-AST of a string annotation
if let Some((sub_ast, sub_model)) = self.model.enter_string_annotation(string_expr)
{
let mut sub_finder = LocalReferencesFinder {
model: &sub_model,
target_definitions: self.target_definitions,
references: self.references,
mode: self.mode,
tokens: sub_ast.tokens(),
target_text: self.target_text,
ancestors: Vec::new(),
};
sub_finder.visit_expr(sub_ast.expr());
}
}
AnyNodeRef::Alias(alias) if self.should_include_declaration() => {
// Handle import alias declarations
if let Some(asname) = &alias.asname {
@@ -285,15 +314,13 @@ impl LocalReferencesFinder<'_> {
// the node is fine here. Offsets matter only for import statements
// where the identifier might be a multi-part module name.
let offset = covering_node.node().start();
if let Some(goto_target) =
GotoTarget::from_covering_node(covering_node, offset, self.tokens)
GotoTarget::from_covering_node(self.model, covering_node, offset, self.tokens)
{
// Get the definitions for this goto target
let model = SemanticModel::new(self.db, self.file);
if let Some(current_definitions_nav) = goto_target
.get_definition_targets(&model, ImportAliasResolution::PreserveAliases)
.and_then(|definitions| definitions.declaration_targets(self.db))
.get_definition_targets(self.model, ImportAliasResolution::PreserveAliases)
.and_then(|definitions| definitions.declaration_targets(self.model.db()))
{
let current_definitions: Vec<NavigationTarget> =
current_definitions_nav.into_iter().collect();
@@ -302,7 +329,7 @@ impl LocalReferencesFinder<'_> {
// Determine if this is a read or write reference
let kind = self.determine_reference_kind(covering_node);
let target =
ReferenceTarget::new(self.file, covering_node.node().range(), kind);
ReferenceTarget::new(self.model.file(), covering_node.node().range(), kind);
self.references.push(target);
}
}

View File

@@ -12,7 +12,7 @@ pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option<ruff_text
let model = SemanticModel::new(db, file);
// Get the definitions for the symbol at the offset
let goto_target = find_goto_target(&module, offset)?;
let goto_target = find_goto_target(&model, &module, offset)?;
// Don't allow renaming of import module components
if matches!(
@@ -59,9 +59,10 @@ pub fn rename(
) -> Option<Vec<ReferenceTarget>> {
let parsed = ruff_db::parsed::parsed_module(db, file);
let module = parsed.load(db);
let model = SemanticModel::new(db, file);
// Get the definitions for the symbol at the offset
let goto_target = find_goto_target(&module, offset)?;
let goto_target = find_goto_target(&model, &module, offset)?;
// Clients shouldn't call us with an empty new name, but just in case...
if new_name.is_empty() {
@@ -338,6 +339,546 @@ class DataProcessor:
");
}
#[test]
fn rename_string_annotation1() {
let test = cursor_test(
r#"
a: "MyCla<CURSOR>ss" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.rename("MyNewClass"), @r#"
info[rename]: Rename symbol (found 2 locations)
--> main.py:2:5
|
2 | a: "MyClass" = 1
| ^^^^^^^
3 |
4 | class MyClass:
| -------
5 | """some docs"""
|
"#);
}
#[test]
fn rename_string_annotation2() {
let test = cursor_test(
r#"
a: "None | MyCl<CURSOR>ass" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.rename("MyNewClass"), @r#"
info[rename]: Rename symbol (found 2 locations)
--> main.py:2:12
|
2 | a: "None | MyClass" = 1
| ^^^^^^^
3 |
4 | class MyClass:
| -------
5 | """some docs"""
|
"#);
}
#[test]
fn rename_string_annotation3() {
let test = cursor_test(
r#"
a: "None |<CURSOR> MyClass" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.rename("MyNewClass"), @"Cannot rename");
}
#[test]
fn rename_string_annotation4() {
let test = cursor_test(
r#"
a: "None | MyClass<CURSOR>" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.rename("MyNewClass"), @r#"
info[rename]: Rename symbol (found 2 locations)
--> main.py:2:12
|
2 | a: "None | MyClass" = 1
| ^^^^^^^
3 |
4 | class MyClass:
| -------
5 | """some docs"""
|
"#);
}
#[test]
fn rename_string_annotation5() {
let test = cursor_test(
r#"
a: "None | MyClass"<CURSOR> = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.rename("MyNewClass"), @"Cannot rename");
}
#[test]
fn rename_string_annotation_dangling1() {
let test = cursor_test(
r#"
a: "MyCl<CURSOR>ass |" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.rename("MyNewClass"), @"Cannot rename");
}
#[test]
fn rename_string_annotation_dangling2() {
let test = cursor_test(
r#"
a: "MyCl<CURSOR>ass | No" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.rename("MyNewClass"), @r#"
info[rename]: Rename symbol (found 2 locations)
--> main.py:2:5
|
2 | a: "MyClass | No" = 1
| ^^^^^^^
3 |
4 | class MyClass:
| -------
5 | """some docs"""
|
"#);
}
#[test]
fn rename_string_annotation_dangling3() {
let test = cursor_test(
r#"
a: "MyClass | N<CURSOR>o" = 1
class MyClass:
"""some docs"""
"#,
);
assert_snapshot!(test.rename("MyNewClass"), @"Cannot rename");
}
#[test]
fn rename_match_name_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.rename("XY"), @r#"
info[rename]: Rename symbol (found 2 locations)
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^^
5 | x = ab
| --
|
"#);
}
#[test]
fn rename_match_name_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.rename("XY"), @r#"
info[rename]: Rename symbol (found 2 locations)
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^^
5 | x = ab
| --
|
"#);
}
#[test]
fn rename_match_rest_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.rename("XY"), @r#"
info[rename]: Rename symbol (found 2 locations)
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^^
5 | x = ab
| --
|
"#);
}
#[test]
fn rename_match_rest_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", *ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.rename("XY"), @r#"
info[rename]: Rename symbol (found 2 locations)
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^^
5 | x = ab
| --
|
"#);
}
#[test]
fn rename_match_as_stmt() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as a<CURSOR>b]:
x = ab
"#,
);
assert_snapshot!(test.rename("XY"), @r#"
info[rename]: Rename symbol (found 2 locations)
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^^
5 | x = ab
| --
|
"#);
}
#[test]
fn rename_match_as_binding() {
let test = cursor_test(
r#"
def my_func(command: str):
match command.split():
case ["get", ("a" | "b") as ab]:
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.rename("XY"), @r#"
info[rename]: Rename symbol (found 2 locations)
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^^
5 | x = ab
| --
|
"#);
}
#[test]
fn rename_match_keyword_stmt() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=a<CURSOR>b):
x = ab
"#,
);
assert_snapshot!(test.rename("XY"), @r"
info[rename]: Rename symbol (found 2 locations)
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^
11 | x = ab
| --
|
");
}
#[test]
fn rename_match_keyword_binding() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
x = a<CURSOR>b
"#,
);
assert_snapshot!(test.rename("XY"), @r"
info[rename]: Rename symbol (found 2 locations)
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^
11 | x = ab
| --
|
");
}
#[test]
fn rename_match_class_name() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Cl<CURSOR>ick(x, button=ab):
x = ab
"#,
);
assert_snapshot!(test.rename("XY"), @r#"
info[rename]: Rename symbol (found 3 locations)
--> main.py:2:7
|
2 | class Click:
| ^^^^^
3 | __match_args__ = ("position", "button")
4 | def __init__(self, pos, btn):
|
::: main.py:8:20
|
6 | self.button: str = btn
7 |
8 | def my_func(event: Click):
| -----
9 | match event:
10 | case Click(x, button=ab):
| -----
11 | x = ab
|
"#);
}
#[test]
fn rename_match_class_field_name() {
let test = cursor_test(
r#"
class Click:
__match_args__ = ("position", "button")
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, but<CURSOR>ton=ab):
x = ab
"#,
);
assert_snapshot!(test.rename("XY"), @"Cannot rename");
}
#[test]
fn rename_typevar_name_stmt() {
let test = cursor_test(
r#"
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
"#,
);
assert_snapshot!(test.rename("XY"), @r"
info[rename]: Rename symbol (found 3 locations)
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^ -- --
|
");
}
#[test]
fn rename_typevar_name_binding() {
let test = cursor_test(
r#"
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
"#,
);
assert_snapshot!(test.rename("XY"), @r"
info[rename]: Rename symbol (found 3 locations)
--> main.py:2:13
|
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
| ^^ -- --
|
");
}
#[test]
fn rename_typevar_spec_stmt() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
"#,
);
assert_snapshot!(test.rename("XY"), @r"
info[rename]: Rename symbol (found 3 locations)
--> main.py:3:15
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^ -- --
|
");
}
#[test]
fn rename_typevar_spec_binding() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
"#,
);
assert_snapshot!(test.rename("XY"), @r"
info[rename]: Rename symbol (found 3 locations)
--> main.py:3:15
|
2 | from typing import Callable
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
| ^^ -- --
|
");
}
#[test]
fn rename_typevar_tuple_stmt() {
let test = cursor_test(
r#"
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
"#,
);
assert_snapshot!(test.rename("XY"), @r"
info[rename]: Rename symbol (found 3 locations)
--> main.py:2:14
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^ -- --
|
");
}
#[test]
fn rename_typevar_tuple_binding() {
let test = cursor_test(
r#"
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
"#,
);
assert_snapshot!(test.rename("XY"), @r"
info[rename]: Rename symbol (found 3 locations)
--> main.py:2:14
|
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
| ^^ -- --
|
");
}
#[test]
fn test_cannot_rename_import_module_component() {
// Test that we cannot rename parts of module names in import statements

View File

@@ -187,9 +187,9 @@ impl Deref for SemanticTokens {
/// Pass None to get tokens for the entire file.
pub fn semantic_tokens(db: &dyn Db, file: File, range: Option<TextRange>) -> SemanticTokens {
let parsed = parsed_module(db, file).load(db);
let semantic_model = SemanticModel::new(db, file);
let model = SemanticModel::new(db, file);
let mut visitor = SemanticTokenVisitor::new(&semantic_model, file, range);
let mut visitor = SemanticTokenVisitor::new(&model, range);
visitor.visit_body(parsed.suite());
SemanticTokens::new(visitor.tokens)
@@ -197,8 +197,7 @@ pub fn semantic_tokens(db: &dyn Db, file: File, range: Option<TextRange>) -> Sem
/// AST visitor that collects semantic tokens.
struct SemanticTokenVisitor<'db> {
semantic_model: &'db SemanticModel<'db>,
file: File,
model: &'db SemanticModel<'db>,
tokens: Vec<SemanticToken>,
in_class_scope: bool,
in_type_annotation: bool,
@@ -207,14 +206,9 @@ struct SemanticTokenVisitor<'db> {
}
impl<'db> SemanticTokenVisitor<'db> {
fn new(
semantic_model: &'db SemanticModel<'db>,
file: File,
range_filter: Option<TextRange>,
) -> Self {
fn new(model: &'db SemanticModel<'db>, range_filter: Option<TextRange>) -> Self {
Self {
semantic_model,
file,
model,
tokens: Vec::new(),
in_class_scope: false,
in_target_creating_definition: false,
@@ -265,7 +259,7 @@ impl<'db> SemanticTokenVisitor<'db> {
fn classify_name(&self, name: &ast::ExprName) -> (SemanticTokenType, SemanticTokenModifier) {
// First try to classify the token based on its definition kind.
let definition = definition_for_name(self.semantic_model.db(), self.file, name);
let definition = definition_for_name(self.model, name);
if let Some(definition) = definition {
let name_str = name.id.as_str();
@@ -275,7 +269,7 @@ impl<'db> SemanticTokenVisitor<'db> {
}
// Fall back to type-based classification.
let ty = name.inferred_type(self.semantic_model);
let ty = name.inferred_type(self.model);
let name_str = name.id.as_str();
self.classify_from_type_and_name_str(ty, name_str)
}
@@ -286,7 +280,7 @@ impl<'db> SemanticTokenVisitor<'db> {
name_str: &str,
) -> Option<(SemanticTokenType, SemanticTokenModifier)> {
let mut modifiers = SemanticTokenModifier::empty();
let db = self.semantic_model.db();
let db = self.model.db();
let model = SemanticModel::new(db, definition.file(db));
match definition.kind(db) {
@@ -712,12 +706,12 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
for alias in &import.names {
if let Some(asname) = &alias.asname {
// For aliased imports (from X import Y as Z), classify Z based on what Y is
let ty = alias.inferred_type(self.semantic_model);
let ty = alias.inferred_type(self.model);
let (token_type, modifiers) = self.classify_from_alias_type(ty, asname);
self.add_token(asname, token_type, modifiers);
} else {
// For direct imports (from X import Y), use semantic classification
let ty = alias.inferred_type(self.semantic_model);
let ty = alias.inferred_type(self.model);
let (token_type, modifiers) =
self.classify_from_alias_type(ty, &alias.name);
self.add_token(&alias.name, token_type, modifiers);
@@ -837,7 +831,7 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
self.visit_expr(&attr.value);
// Then add token for the attribute name (e.g., 'path' in 'os.path')
let ty = expr.inferred_type(self.semantic_model);
let ty = expr.inferred_type(self.model);
let (token_type, modifiers) =
Self::classify_from_type_for_attribute(ty, &attr.attr);
self.add_token(&attr.attr, token_type, modifiers);
@@ -881,6 +875,17 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
self.visit_expr(&named.value);
}
ast::Expr::StringLiteral(string_expr) => {
// Highlight the sub-AST of a string annotation
if let Some((sub_ast, sub_model)) = self.model.enter_string_annotation(string_expr)
{
let mut sub_visitor = SemanticTokenVisitor::new(&sub_model, None);
sub_visitor.visit_expr(sub_ast.expr());
self.tokens.extend(sub_visitor.tokens);
} else {
walk_expr(self, expr);
}
}
_ => {
// For all other expression types, let the default visitor handle them
walk_expr(self, expr);
@@ -1055,12 +1060,33 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
);
}
}
ast::Pattern::MatchStar(pattern_star) => {
// Just the one ident here
if let Some(rest_name) = &pattern_star.name {
self.add_token(
rest_name.range(),
SemanticTokenType::Variable,
SemanticTokenModifier::empty(),
);
}
}
_ => {
// For all other pattern types, use the default walker
ruff_python_ast::visitor::source_order::walk_pattern(self, pattern);
}
}
}
fn visit_comprehension(&mut self, comp: &ast::Comprehension) {
self.in_target_creating_definition = true;
self.visit_expr(&comp.target);
self.in_target_creating_definition = false;
self.visit_expr(&comp.iter);
for if_clause in &comp.ifs {
self.visit_expr(if_clause);
}
}
}
#[cfg(test)]
@@ -1564,6 +1590,50 @@ from mymodule import CONSTANT, my_function, MyClass
"#);
}
#[test]
fn test_str_annotation() {
let test = SemanticTokenTest::new(
r#"
x: int = 1
y: "int" = 1
z = "int"
w1: "int | str" = "hello"
w2: "int | sr" = "hello"
w3: "int | " = "hello"
w4: "float"
w5: "float
"#,
);
let tokens = test.highlight_file();
assert_snapshot!(test.to_snapshot(&tokens), @r#"
"x" @ 1..2: Variable [definition]
"int" @ 4..7: Class
"1" @ 10..11: Number
"y" @ 12..13: Variable [definition]
"int" @ 16..19: Class
"1" @ 23..24: Number
"z" @ 25..26: Variable [definition]
"\"int\"" @ 29..34: String
"w1" @ 35..37: Variable [definition]
"int" @ 40..43: Class
"str" @ 46..49: Class
"\"hello\"" @ 53..60: String
"w2" @ 61..63: Variable [definition]
"int" @ 66..69: Class
"sr" @ 72..74: Variable
"\"hello\"" @ 78..85: String
"w3" @ 86..88: Variable [definition]
"\"int | \"" @ 90..98: String
"\"hello\"" @ 101..108: String
"w4" @ 109..111: Variable [definition]
"float" @ 114..119: Class
"w5" @ 121..123: Variable [definition]
"float" @ 126..131: Class
"#);
}
#[test]
fn test_attribute_classification() {
let test = SemanticTokenTest::new(
@@ -2425,6 +2495,7 @@ def process_data(data):
"rest" @ 154..158: Variable
"person" @ 181..187: Variable
"first" @ 202..207: Variable
"remaining" @ 210..219: Variable
"sequence" @ 224..232: Variable
"print" @ 246..251: Function
"First: " @ 254..261: String
@@ -2595,6 +2666,50 @@ with open("file.txt") as f:
"#);
}
#[test]
fn test_comprehensions() {
let test = SemanticTokenTest::new(
r#"
list_comp = [x for x in range(10) if x % 2 == 0]
set_comp = {x for x in range(10)}
dict_comp = {k: v for k, v in zip(["a", "b"], [1, 2])}
generator = (x for x in range(10))
"#,
);
let tokens = test.highlight_file();
assert_snapshot!(test.to_snapshot(&tokens), @r#"
"list_comp" @ 1..10: Variable [definition]
"x" @ 14..15: Variable
"x" @ 20..21: Variable [definition]
"range" @ 25..30: Class
"10" @ 31..33: Number
"x" @ 38..39: Variable
"2" @ 42..43: Number
"0" @ 47..48: Number
"set_comp" @ 50..58: Variable [definition]
"x" @ 62..63: Variable
"x" @ 68..69: Variable [definition]
"range" @ 73..78: Class
"10" @ 79..81: Number
"dict_comp" @ 84..93: Variable [definition]
"k" @ 97..98: Variable
"v" @ 100..101: Variable
"k" @ 106..107: Variable [definition]
"v" @ 109..110: Variable [definition]
"zip" @ 114..117: Class
"\"a\"" @ 119..122: String
"\"b\"" @ 124..127: String
"1" @ 131..132: Number
"2" @ 134..135: Number
"generator" @ 139..148: Variable [definition]
"x" @ 152..153: Variable
"x" @ 158..159: Variable [definition]
"range" @ 163..168: Class
"10" @ 169..171: Number
"#);
}
/// Regression test for <https://github.com/astral-sh/ty/issues/1406>
#[test]
fn test_invalid_kwargs() {

View File

@@ -7,7 +7,7 @@
//! and overloads.
use crate::docstring::Docstring;
use crate::goto::DefinitionsOrTargets;
use crate::goto::Definitions;
use crate::{Db, find_node::covering_node};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
@@ -74,7 +74,7 @@ pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option<Signa
// Get signature details from the semantic analyzer.
let signature_details: Vec<CallSignatureDetails<'_>> =
call_signature_details(db, &model, call_expr);
call_signature_details(&model, call_expr);
if signature_details.is_empty() {
return None;
@@ -214,8 +214,7 @@ fn get_callable_documentation(
db: &dyn crate::Db,
definition: Option<Definition>,
) -> Option<Docstring> {
DefinitionsOrTargets::Definitions(vec![ResolvedDefinition::Definition(definition?)])
.docstring(db)
Definitions(vec![ResolvedDefinition::Definition(definition?)]).docstring(db)
}
/// Create `ParameterDetails` objects from parameter label offsets.

View File

@@ -11,12 +11,12 @@ use crate::cached_vendored_root;
/// other language server providers (like hover, completion, and signature help) to find
/// docstrings for functions that resolve to stubs.
pub(crate) struct StubMapper<'db> {
db: &'db dyn crate::Db,
db: &'db dyn ty_python_semantic::Db,
cached_vendored_root: Option<SystemPathBuf>,
}
impl<'db> StubMapper<'db> {
pub(crate) fn new(db: &'db dyn crate::Db) -> Self {
pub(crate) fn new(db: &'db dyn ty_python_semantic::Db) -> Self {
let cached_vendored_root = cached_vendored_root(db);
Self {
db,

View File

@@ -23,11 +23,12 @@ use crate::completion::CompletionKind;
pub struct QueryPattern {
re: Option<Regex>,
original: String,
original_is_exact: bool,
}
impl QueryPattern {
/// Create a new query pattern from a literal search string given.
pub fn new(literal_query_string: &str) -> QueryPattern {
pub fn fuzzy(literal_query_string: &str) -> QueryPattern {
let mut pattern = "(?i)".to_string();
for ch in literal_query_string.chars() {
pattern.push_str(&regex::escape(ch.encode_utf8(&mut [0; 4])));
@@ -41,6 +42,16 @@ impl QueryPattern {
QueryPattern {
re: Regex::new(&pattern).ok(),
original: literal_query_string.to_string(),
original_is_exact: false,
}
}
/// Create a new query
pub fn exactly(symbol: &str) -> QueryPattern {
QueryPattern {
re: None,
original: symbol.to_string(),
original_is_exact: true,
}
}
@@ -49,6 +60,7 @@ impl QueryPattern {
QueryPattern {
re: None,
original: String::new(),
original_is_exact: false,
}
}
@@ -59,6 +71,8 @@ impl QueryPattern {
pub fn is_match_symbol_name(&self, symbol_name: &str) -> bool {
if let Some(ref re) = self.re {
re.is_match(symbol_name)
} else if self.original_is_exact {
symbol_name == self.original
} else {
// This is a degenerate case. The only way
// we should get here is if the query string
@@ -75,13 +89,13 @@ impl QueryPattern {
/// incorrectly. That is, it's possible that this query will match all
/// inputs but this still returns `false`.
pub fn will_match_everything(&self) -> bool {
self.re.is_none()
self.re.is_none() && self.original.is_empty()
}
}
impl From<&str> for QueryPattern {
fn from(literal_query_string: &str) -> QueryPattern {
QueryPattern::new(literal_query_string)
QueryPattern::fuzzy(literal_query_string)
}
}
@@ -565,7 +579,7 @@ impl SourceOrderVisitor<'_> for SymbolVisitor {
#[cfg(test)]
mod tests {
fn matches(query: &str, symbol: &str) -> bool {
super::QueryPattern::new(query).is_match_symbol_name(symbol)
super::QueryPattern::fuzzy(query).is_match_symbol_name(symbol)
}
#[test]

View File

@@ -12,7 +12,7 @@ pub fn workspace_symbols(db: &dyn Db, query: &str) -> Vec<WorkspaceSymbolInfo> {
let project = db.project();
let query = QueryPattern::new(query);
let query = QueryPattern::fuzzy(query);
let files = project.files(db);
let results = std::sync::Mutex::new(Vec::new());
{

View File

@@ -13,6 +13,7 @@ license = { workspace = true }
[dependencies]
ruff_db = { workspace = true }
ruff_annotate_snippets = { workspace = true }
ruff_diagnostics = { workspace = true }
ruff_index = { workspace = true, features = ["salsa"] }
ruff_macros = { workspace = true }
ruff_memory_usage = { workspace = true }

View File

@@ -0,0 +1,26 @@
try:
type name_4 = name_1
finally:
from .. import name_3
try:
pass
except* 0:
pass
else:
def name_1() -> name_4:
pass
@name_1
def name_3():
pass
finally:
try:
pass
except* 0:
assert name_3
finally:
@name_3
class name_1:
pass

View File

@@ -0,0 +1,5 @@
class foo[_: foo](object): ...
[_] = (foo,) = foo
def foo(): ...

View File

@@ -0,0 +1,20 @@
name_3: Foo = 0
name_4 = 0
if _0:
type name_3 = name_5
type name_4 = name_3
_1: name_3
def name_1(_2: name_4):
pass
match 0:
case name_1._3:
pass
case 1:
type name_5 = name_4
case name_5:
pass
name_3 = name_5

View File

@@ -35,6 +35,8 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
`LiteralString` cannot be parameterized.
<!-- snapshot-diagnostics -->
```py
from typing_extensions import LiteralString
@@ -42,7 +44,6 @@ from typing_extensions import LiteralString
a: LiteralString[str]
# error: [invalid-type-form]
# error: [unresolved-reference] "Name `foo` used when not defined"
b: LiteralString["foo"]
```

View File

@@ -260,15 +260,13 @@ class Shape:
@classmethod
def bar(cls: type[Self]) -> Self:
# TODO: type[Shape]
reveal_type(cls) # revealed: @Todo(unsupported type[X] special form)
reveal_type(cls) # revealed: type[Self@bar]
return cls()
class Circle(Shape): ...
reveal_type(Shape().foo()) # revealed: Shape
# TODO: Shape
reveal_type(Shape.bar()) # revealed: Unknown
reveal_type(Shape.bar()) # revealed: Shape
```
## Attributes

View File

@@ -222,10 +222,10 @@ reveal_type(r) # revealed: dict[int | str, int | str]
## Incorrect collection literal assignments are complained about
```py
# error: [invalid-assignment] "Object of type `list[Unknown | int]` is not assignable to `list[str]`"
# error: [invalid-assignment] "Object of type `list[str | int]` is not assignable to `list[str]`"
a: list[str] = [1, 2, 3]
# error: [invalid-assignment] "Object of type `set[Unknown | int | str]` is not assignable to `set[int]`"
# error: [invalid-assignment] "Object of type `set[int | str]` is not assignable to `set[int]`"
b: set[int] = {1, 2, "3"}
```
@@ -422,7 +422,7 @@ reveal_type(d) # revealed: list[int | tuple[int, int]]
e: list[int] = f(True)
reveal_type(e) # revealed: list[int]
# error: [invalid-assignment] "Object of type `list[str]` is not assignable to `list[int]`"
# error: [invalid-assignment] "Object of type `list[int | str]` is not assignable to `list[int]`"
g: list[int] = f("a")
# error: [invalid-assignment] "Object of type `list[str]` is not assignable to `tuple[int]`"
@@ -459,12 +459,12 @@ reveal_type(b) # revealed: TD
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
# error: [invalid-key] "Unknown key "y" for TypedDict `TD`"
# error: [invalid-assignment] "Object of type `Unknown | dict[Unknown | str, Unknown | int]` is not assignable to `TD`"
# error: [invalid-assignment] "Object of type `TD | dict[Unknown | str, Unknown | int]` is not assignable to `TD`"
c: TD = f([{"y": 0}, {"x": 1}])
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
# error: [invalid-key] "Unknown key "y" for TypedDict `TD`"
# error: [invalid-assignment] "Object of type `Unknown | dict[Unknown | str, Unknown | int]` is not assignable to `TD | None`"
# error: [invalid-assignment] "Object of type `TD | None | dict[Unknown | str, Unknown | int]` is not assignable to `TD | None`"
c: TD | None = f([{"y": 0}, {"x": 1}])
```

View File

@@ -61,8 +61,7 @@ async def main():
result = await task
# TODO: this should be `int`
reveal_type(result) # revealed: Unknown
reveal_type(result) # revealed: int
```
### `asyncio.gather`
@@ -79,9 +78,8 @@ async def main():
task("B"),
)
# TODO: these should be `int`
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
```
## Under the hood

View File

@@ -2383,6 +2383,34 @@ class B:
reveal_type(B().x) # revealed: Unknown | Literal[1]
reveal_type(A().x) # revealed: Unknown | Literal[1]
class Base:
def flip(self) -> "Sub":
return Sub()
class Sub(Base):
# error: [invalid-method-override]
def flip(self) -> "Base":
return Base()
class C2:
def __init__(self, x: Sub):
self.x = x
def replace_with(self, other: "C2"):
self.x = other.x.flip()
reveal_type(C2(Sub()).x) # revealed: Unknown | Base
class C3:
def __init__(self, x: Sub):
self.x = [x]
def replace_with(self, other: "C3"):
self.x = [self.x[0].flip()]
# TODO: should be `Unknown | list[Unknown | Sub] | list[Unknown | Base]`
reveal_type(C3(Sub()).x) # revealed: Unknown | list[Unknown | Sub] | list[Divergent]
```
And cycles between many attributes:
@@ -2432,6 +2460,30 @@ class ManyCycles:
reveal_type(self.x5) # revealed: Unknown | int
reveal_type(self.x6) # revealed: Unknown | int
reveal_type(self.x7) # revealed: Unknown | int
class ManyCycles2:
def __init__(self: "ManyCycles2"):
self.x1 = [0]
self.x2 = [1]
self.x3 = [1]
def f1(self: "ManyCycles2"):
# TODO: should be Unknown | list[Unknown | int] | list[Divergent]
reveal_type(self.x3) # revealed: Unknown | list[Unknown | int] | list[Divergent] | list[Divergent]
self.x1 = [self.x2] + [self.x3]
self.x2 = [self.x1] + [self.x3]
self.x3 = [self.x1] + [self.x2]
def f2(self: "ManyCycles2"):
self.x1 = self.x2 + self.x3
self.x2 = self.x1 + self.x3
self.x3 = self.x1 + self.x2
def f3(self: "ManyCycles2"):
self.x1 = self.x2 + self.x3
self.x2 = self.x1 + self.x3
self.x3 = self.x1 + self.x2
```
This case additionally tests our union/intersection simplification logic:
@@ -2598,7 +2650,7 @@ reveal_type(C().x) # revealed: int
```py
import enum
reveal_type(enum.Enum.__members__) # revealed: MappingProxyType[str, Unknown]
reveal_type(enum.Enum.__members__) # revealed: MappingProxyType[str, Enum]
class Answer(enum.Enum):
NO = 0
@@ -2606,17 +2658,23 @@ class Answer(enum.Enum):
reveal_type(Answer.NO) # revealed: Literal[Answer.NO]
reveal_type(Answer.NO.value) # revealed: Literal[0]
reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown]
reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Answer]
```
## Divergent inferred implicit instance attribute types
If an implicit attribute is defined recursively and type inference diverges, the divergent part is
filled in with the dynamic type `Divergent`. Types containing `Divergent` can be seen as "cheap"
recursive types: they are not true recursive types based on recursive type theory, so no unfolding
is performed when you use them.
```py
class C:
def f(self, other: "C"):
self.x = (other.x, 1)
reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
reveal_type(C().x[0]) # revealed: Unknown | Divergent
```
This also works if the tuple is not constructed directly:
@@ -2655,11 +2713,11 @@ And it also works for homogeneous tuples:
def make_homogeneous_tuple(x: T) -> tuple[T, ...]:
return (x, x)
class E:
def f(self, other: "E"):
class F:
def f(self, other: "F"):
self.x = make_homogeneous_tuple(other.x)
reveal_type(E().x) # revealed: Unknown | tuple[Divergent, ...]
reveal_type(F().x) # revealed: Unknown | tuple[Divergent, ...]
```
## Attributes of standard library modules that aren't yet defined

View File

@@ -268,8 +268,8 @@ class A:
A(f(1))
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `list[int | str]`, found `list[list[Unknown]]`"
# error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `list[int | None]`, found `list[list[Unknown]]`"
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `list[int | str]`, found `list[int | None | list[Unknown]] & list[int | str | list[Unknown]] & list[list[Unknown]]`"
# error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `list[int | None]`, found `list[int | None | list[Unknown]] & list[int | str | list[Unknown]] & list[list[Unknown]]`"
A(f([]))
```

View File

@@ -57,7 +57,7 @@ type("Foo", Base, {})
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
type("Foo", (1, 2), {})
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`"
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
type("Foo", (Base,), {b"attr": 1})
```

View File

@@ -277,6 +277,6 @@ def _(flag: bool):
x = f({"x": 1})
reveal_type(x) # revealed: int
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[Unknown | str, Unknown | int]`"
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int] & dict[Unknown | str, Unknown | int]`"
f({"y": 1})
```

View File

@@ -210,9 +210,7 @@ class BuilderMeta2(type):
) -> BuilderMeta2:
# revealed: <super: <class 'BuilderMeta2'>, <class 'BuilderMeta2'>>
s = reveal_type(super())
# TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501)
# revealed: Unknown
return reveal_type(s.__new__(cls, name, bases, dct))
return reveal_type(s.__new__(cls, name, bases, dct)) # revealed: BuilderMeta2
class Foo[T]:
x: T
@@ -395,6 +393,14 @@ class E(Enum):
reveal_type(super(E, E.X)) # revealed: <super: <class 'E'>, E>
```
## `type[Self]`
```py
class Foo:
def method(self):
super(self.__class__, self)
```
## Descriptor Behavior with Super
Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can

View File

@@ -32,6 +32,39 @@ reveal_type(p.x) # revealed: Unknown | int
reveal_type(p.y) # revealed: Unknown | int
```
## Self-referential bare type alias
```toml
[environment]
python-version = "3.12" # typing.TypeAliasType
```
```py
from typing import Union, TypeAliasType, Sequence, Mapping
A = list["A" | None]
def f(x: A):
# TODO: should be `list[A | None]`?
reveal_type(x) # revealed: list[Divergent]
# TODO: should be `A | None`?
reveal_type(x[0]) # revealed: Divergent
JSONPrimitive = Union[str, int, float, bool, None]
JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]')
```
## Self-referential legacy type variables
```py
from typing import Generic, TypeVar
B = TypeVar("B", bound="Base")
class Base(Generic[B]):
pass
```
## Parameter default values
This is a regression test for <https://github.com/astral-sh/ty/issues/1402>. When a parameter has a

View File

@@ -15,10 +15,8 @@ reveal_type(Color.RED) # revealed: Literal[Color.RED]
reveal_type(Color.RED.name) # revealed: Literal["RED"]
reveal_type(Color.RED.value) # revealed: Literal[1]
# TODO: Should be `Color` or `Literal[Color.RED]`
reveal_type(Color["RED"]) # revealed: Unknown
# TODO: Could be `Literal[Color.RED]` to be more precise
reveal_type(Color["RED"]) # revealed: Color
reveal_type(Color(1)) # revealed: Color
reveal_type(Color.RED in Color) # revealed: bool

View File

@@ -1,6 +1,6 @@
# Tests for the `@typing(_extensions).final` decorator
## Cannot subclass
## Cannot subclass a class decorated with `@final`
Don't do this:
@@ -29,3 +29,456 @@ class H(
G,
): ...
```
## Cannot override a method decorated with `@final`
<!-- snapshot-diagnostics -->
```pyi
from typing_extensions import final, Callable, TypeVar
def lossy_decorator(fn: Callable) -> Callable: ...
class Parent:
@final
def foo(self): ...
@final
@property
def my_property1(self) -> int: ...
@property
@final
def my_property2(self) -> int: ...
@final
@classmethod
def class_method1(cls) -> int: ...
@classmethod
@final
def class_method2(cls) -> int: ...
@final
@staticmethod
def static_method1() -> int: ...
@staticmethod
@final
def static_method2() -> int: ...
@lossy_decorator
@final
def decorated_1(self): ...
@final
@lossy_decorator
def decorated_2(self): ...
class Child(Parent):
# explicitly test the concise diagnostic message,
# which is different to the verbose diagnostic summary message:
#
# error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
def foo(self): ...
@property
def my_property1(self) -> int: ... # error: [override-of-final-method]
@property
def my_property2(self) -> int: ... # error: [override-of-final-method]
@classmethod
def class_method1(cls) -> int: ... # error: [override-of-final-method]
@staticmethod
def static_method1() -> int: ... # error: [override-of-final-method]
@classmethod
def class_method2(cls) -> int: ... # error: [override-of-final-method]
@staticmethod
def static_method2() -> int: ... # error: [override-of-final-method]
def decorated_1(self): ... # TODO: should emit [override-of-final-method]
@lossy_decorator
def decorated_2(self): ... # TODO: should emit [override-of-final-method]
class OtherChild(Parent): ...
class Grandchild(OtherChild):
@staticmethod
# TODO: we should emit a Liskov violation here too
# error: [override-of-final-method]
def foo(): ...
@property
# TODO: we should emit a Liskov violation here too
# error: [override-of-final-method]
def my_property1(self) -> str: ...
# TODO: we should emit a Liskov violation here too
# error: [override-of-final-method]
class_method1 = None
# Diagnostic edge case: `final` is very far away from the method definition in the source code:
T = TypeVar("T")
def identity(x: T) -> T: ...
class Foo:
@final
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
def bar(self): ...
class Baz(Foo):
def bar(self): ... # error: [override-of-final-method]
```
## Diagnostic edge case: superclass with `@final` method has the same name as the subclass
<!-- snapshot-diagnostics -->
`module1.py`:
```py
from typing import final
class Foo:
@final
def f(self): ...
```
`module2.py`:
```py
import module1
class Foo(module1.Foo):
def f(self): ... # error: [override-of-final-method]
```
## Overloaded methods decorated with `@final`
In a stub file, `@final` should be applied to the first overload. In a runtime file, `@final` should
only be applied to the implementation function.
<!-- snapshot-diagnostics -->
`stub.pyi`:
```pyi
from typing import final, overload
class Good:
@overload
@final
def bar(self, x: str) -> str: ...
@overload
def bar(self, x: int) -> int: ...
@final
@overload
def baz(self, x: str) -> str: ...
@overload
def baz(self, x: int) -> int: ...
class ChildOfGood(Good):
@overload
def bar(self, x: str) -> str: ...
@overload
def bar(self, x: int) -> int: ... # error: [override-of-final-method]
@overload
def baz(self, x: str) -> str: ...
@overload
def baz(self, x: int) -> int: ... # error: [override-of-final-method]
class Bad:
@overload
def bar(self, x: str) -> str: ...
@overload
@final
# error: [invalid-overload]
def bar(self, x: int) -> int: ...
@overload
def baz(self, x: str) -> str: ...
@final
@overload
# error: [invalid-overload]
def baz(self, x: int) -> int: ...
class ChildOfBad(Bad):
@overload
def bar(self, x: str) -> str: ...
@overload
def bar(self, x: int) -> int: ... # error: [override-of-final-method]
@overload
def baz(self, x: str) -> str: ...
@overload
def baz(self, x: int) -> int: ... # error: [override-of-final-method]
```
`main.py`:
```py
from typing import overload, final
class Good:
@overload
def f(self, x: str) -> str: ...
@overload
def f(self, x: int) -> int: ...
@final
def f(self, x: int | str) -> int | str:
return x
class ChildOfGood(Good):
@overload
def f(self, x: str) -> str: ...
@overload
def f(self, x: int) -> int: ...
# error: [override-of-final-method]
def f(self, x: int | str) -> int | str:
return x
class Bad:
@overload
@final
def f(self, x: str) -> str: ...
@overload
def f(self, x: int) -> int: ...
# error: [invalid-overload]
def f(self, x: int | str) -> int | str:
return x
@final
@overload
def g(self, x: str) -> str: ...
@overload
def g(self, x: int) -> int: ...
# error: [invalid-overload]
def g(self, x: int | str) -> int | str:
return x
@overload
def h(self, x: str) -> str: ...
@overload
@final
def h(self, x: int) -> int: ...
# error: [invalid-overload]
def h(self, x: int | str) -> int | str:
return x
@overload
def i(self, x: str) -> str: ...
@final
@overload
def i(self, x: int) -> int: ...
# error: [invalid-overload]
def i(self, x: int | str) -> int | str:
return x
class ChildOfBad(Bad):
# TODO: these should all cause us to emit Liskov violations as well
f = None # error: [override-of-final-method]
g = None # error: [override-of-final-method]
h = None # error: [override-of-final-method]
i = None # error: [override-of-final-method]
```
## Edge case: the function is decorated with `@final` but originally defined elsewhere
As of 2025-11-26, pyrefly emits a diagnostic on this, but mypy and pyright do not. For mypy and
pyright to emit a diagnostic, the superclass definition decorated with `@final` must be a literal
function definition: an assignment definition where the right-hand side of the assignment is a
`@final-decorated` function is not sufficient for them to consider the superclass definition as
being `@final`.
For now, we choose to follow mypy's and pyright's behaviour here, in order to maximise compatibility
with other type checkers. We may decide to change this in the future, however, as it would simplify
our implementation. Mypy's and pyright's behaviour here is also arguably inconsistent with their
treatment of other type qualifiers such as `Final`. As discussed in
<https://discuss.python.org/t/imported-final-variable/82429>, both type checkers view the `Final`
type qualifier as travelling *across* scopes.
```py
from typing import final
class A:
@final
def method(self) -> None: ...
class B:
method = A.method
class C(B):
def method(self) -> None: ... # no diagnostic here (see prose discussion above)
```
## Constructor methods are also checked
```py
from typing import final
class A:
@final
def __init__(self) -> None: ...
class B(A):
def __init__(self) -> None: ... # error: [override-of-final-method]
```
## Only the first `@final` violation is reported
(Don't do this.)
<!-- snapshot-diagnostics -->
```py
from typing import final
class A:
@final
def f(self): ...
class B(A):
@final
def f(self): ... # error: [override-of-final-method]
class C(B):
@final
# we only emit one error here, not two
def f(self): ... # error: [override-of-final-method]
```
## For when you just really want to drive the point home
```py
from typing import final, Final
@final
@final
@final
@final
@final
@final
class A:
@final
@final
@final
@final
@final
def method(self): ...
@final
@final
@final
@final
@final
class B:
method: Final = A.method
class C(A): # error: [subclass-of-final-class]
def method(self): ... # error: [override-of-final-method]
class D(B): # error: [subclass-of-final-class]
# TODO: we should emit a diagnostic here
def method(self): ...
```
## An `@final` method is overridden by an implicit instance attribute
```py
from typing import final, Any
class Parent:
@final
def method(self) -> None: ...
class Child(Parent):
def __init__(self) -> None:
self.method: Any = 42 # TODO: we should emit `[override-of-final-method]` here
```
## A possibly-undefined `@final` method is overridden
<!-- snapshot-diagnostics -->
```py
from typing import final
def coinflip() -> bool:
return False
class A:
if coinflip():
@final
def method1(self) -> None: ...
else:
def method1(self) -> None: ...
if coinflip():
def method2(self) -> None: ...
else:
@final
def method2(self) -> None: ...
if coinflip():
@final
def method3(self) -> None: ...
else:
@final
def method3(self) -> None: ...
if coinflip():
def method4(self) -> None: ...
elif coinflip():
@final
def method4(self) -> None: ...
else:
def method4(self) -> None: ...
class B(A):
def method1(self) -> None: ... # error: [override-of-final-method]
def method2(self) -> None: ... # error: [override-of-final-method]
def method3(self) -> None: ... # error: [override-of-final-method]
def method4(self) -> None: ... # error: [override-of-final-method]
# Possible overrides of possibly `@final` methods...
class C(A):
if coinflip():
# TODO: the autofix here introduces invalid syntax because there are now no
# statements inside the `if:` branch
# (but it might still be a useful autofix in an IDE context?)
def method1(self) -> None: ... # error: [override-of-final-method]
else:
pass
if coinflip():
def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
else:
def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
if coinflip():
def method3(self) -> None: ... # error: [override-of-final-method]
def method4(self) -> None: ... # error: [override-of-final-method]
```

View File

@@ -145,8 +145,8 @@ reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]]
The specialization must match the generic types:
```py
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: Unknown
# error: [invalid-type-arguments] "Too many type arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: C[Unknown]
```
If the type variable has an upper bound, the specialized type must satisfy that bound:
@@ -164,13 +164,11 @@ class IntSubclass(int): ...
reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `str`"
reveal_type(Bounded[str]()) # revealed: Unknown
# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded`"
reveal_type(Bounded[str]()) # revealed: Bounded[Unknown]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `int | str`"
reveal_type(Bounded[int | str]()) # revealed: Unknown
# error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded`"
reveal_type(Bounded[int | str]()) # revealed: Bounded[Unknown]
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass]
@@ -197,9 +195,8 @@ reveal_type(Constrained[str]()) # revealed: Constrained[str]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to class `Constrained` is incorrect: Expected `int | str`, found `object`"
reveal_type(Constrained[object]()) # revealed: Unknown
# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `ConstrainedT@Constrained`"
reveal_type(Constrained[object]()) # revealed: Constrained[Unknown]
```
If the type variable has a default, it can be omitted:
@@ -273,7 +270,7 @@ class C(Generic[T]):
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@@ -289,7 +286,7 @@ class C(Generic[T]):
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@@ -308,7 +305,7 @@ class C(Generic[T]):
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@@ -327,7 +324,7 @@ class C(Generic[T]):
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
class D(Generic[T]):
@@ -338,7 +335,7 @@ class D(Generic[T]):
reveal_type(D(1)) # revealed: D[int]
# error: [invalid-assignment] "Object of type `D[str]` is not assignable to `D[int]`"
# error: [invalid-assignment] "Object of type `D[int | str]` is not assignable to `D[int]`"
wrong_innards: D[int] = D("five")
```
@@ -454,7 +451,7 @@ reveal_type(C(1, 1)) # revealed: C[int]
reveal_type(C(1, "string")) # revealed: C[int]
reveal_type(C(1, True)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)
```
@@ -732,6 +729,14 @@ class Base(Generic[T]): ...
class Sub(Base["Sub"]): ...
reveal_type(Sub) # revealed: <class 'Sub'>
U = TypeVar("U")
class Base2(Generic[T, U]): ...
# TODO: no error
# error: [unsupported-base] "Unsupported class base with type `<class 'Base2[Sub2, U@Sub2]'> | <class 'Base2[Sub2[Unknown], U@Sub2]'>`"
class Sub2(Base2["Sub2", U]): ...
```
#### Without string forward references
@@ -756,6 +761,8 @@ from typing_extensions import Generic, TypeVar
T = TypeVar("T")
# TODO: no error "Unsupported class base with type `<class 'list[Derived[T@Derived]]'> | <class 'list[@Todo]'>`"
# error: [unsupported-base]
class Derived(list[Derived[T]], Generic[T]): ...
```

View File

@@ -106,7 +106,7 @@ def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None:
def takes_in_type(x: type[T]) -> type[T]:
return x
reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form)
reveal_type(takes_in_type(int)) # revealed: type[int]
```
This also works when passing in arguments that are subclasses of the parameter type.

View File

@@ -383,8 +383,7 @@ def constrained(f: T):
## Meta-type
The meta-type of a typevar is the same as the meta-type of the upper bound, or the union of the
meta-types of the constraints:
The meta-type of a typevar is `type[T]`.
```py
from typing import TypeVar
@@ -392,22 +391,22 @@ from typing import TypeVar
T_normal = TypeVar("T_normal")
def normal(x: T_normal):
reveal_type(type(x)) # revealed: type
reveal_type(type(x)) # revealed: type[T_normal@normal]
T_bound_object = TypeVar("T_bound_object", bound=object)
def bound_object(x: T_bound_object):
reveal_type(type(x)) # revealed: type
reveal_type(type(x)) # revealed: type[T_bound_object@bound_object]
T_bound_int = TypeVar("T_bound_int", bound=int)
def bound_int(x: T_bound_int):
reveal_type(type(x)) # revealed: type[int]
reveal_type(type(x)) # revealed: type[T_bound_int@bound_int]
T_constrained = TypeVar("T_constrained", int, str)
def constrained(x: T_constrained):
reveal_type(type(x)) # revealed: type[int] | type[str]
reveal_type(type(x)) # revealed: type[T_constrained@constrained]
```
## Cycles
@@ -444,6 +443,22 @@ class G(Generic[T]):
reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]]
```
An invalid specialization in a recursive bound doesn't cause a panic:
```py
from typing import TypeVar, Generic
# error: [invalid-type-arguments]
T = TypeVar("T", bound="Node[int]")
class Node(Generic[T]):
pass
# error: [invalid-type-arguments]
def _(n: Node[str]):
reveal_type(n) # revealed: Node[Unknown]
```
### Defaults
```toml

View File

@@ -61,8 +61,8 @@ def _(a: C[int], b: C[Literal[5]]):
The specialization must match the generic types:
```py
# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2"
reveal_type(C[int, int]) # revealed: Unknown
# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2"
reveal_type(C[int, int]) # revealed: C[Unknown]
```
And non-generic types cannot be specialized:
@@ -88,13 +88,11 @@ class IntSubclass(int): ...
reveal_type(Bounded[int]) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]) # revealed: Bounded[IntSubclass]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `str`"
reveal_type(Bounded[str]) # revealed: Unknown
# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[str]) # revealed: Bounded[Unknown]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `int | str`"
reveal_type(Bounded[int | str]) # revealed: Unknown
# error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[int | str]) # revealed: Bounded[Unknown]
reveal_type(BoundedByUnion[int]) # revealed: BoundedByUnion[int]
reveal_type(BoundedByUnion[IntSubclass]) # revealed: BoundedByUnion[IntSubclass]
@@ -119,9 +117,8 @@ reveal_type(Constrained[str]) # revealed: Constrained[str]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]) # revealed: Constrained[int | str]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument is incorrect: Expected `int | str`, found `object`"
reveal_type(Constrained[object]) # revealed: Unknown
# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `T@Constrained`"
reveal_type(Constrained[object]) # revealed: Constrained[Unknown]
```
If the type variable has a default, it can be omitted:

View File

@@ -135,8 +135,8 @@ reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]]
The specialization must match the generic types:
```py
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: Unknown
# error: [invalid-type-arguments] "Too many type arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: C[Unknown]
```
If the type variable has an upper bound, the specialized type must satisfy that bound:
@@ -149,13 +149,11 @@ class IntSubclass(int): ...
reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `str`"
reveal_type(Bounded[str]()) # revealed: Unknown
# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[str]()) # revealed: Bounded[Unknown]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `int | str`"
reveal_type(Bounded[int | str]()) # revealed: Unknown
# error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[int | str]()) # revealed: Bounded[Unknown]
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass]
@@ -180,9 +178,8 @@ reveal_type(Constrained[str]()) # revealed: Constrained[str]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to class `Constrained` is incorrect: Expected `int | str`, found `object`"
reveal_type(Constrained[object]()) # revealed: Unknown
# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `T@Constrained`"
reveal_type(Constrained[object]()) # revealed: Constrained[Unknown]
```
If the type variable has a default, it can be omitted:
@@ -249,7 +246,7 @@ class C[T]:
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@@ -263,7 +260,7 @@ class C[T]:
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@@ -280,7 +277,7 @@ class C[T]:
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@@ -297,7 +294,7 @@ class C[T]:
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
class D[T]:
@@ -310,7 +307,7 @@ class D[T]:
reveal_type(D(1)) # revealed: D[int]
# error: [invalid-assignment] "Object of type `D[str]` is not assignable to `D[int]`"
# error: [invalid-assignment] "Object of type `D[int | str]` is not assignable to `D[int]`"
wrong_innards: D[int] = D("five")
```
@@ -395,7 +392,7 @@ reveal_type(C(1, 1)) # revealed: C[int]
reveal_type(C(1, "string")) # revealed: C[int]
reveal_type(C(1, True)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)
```

View File

@@ -101,7 +101,7 @@ def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None:
def takes_in_type[T](x: type[T]) -> type[T]:
return x
reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form)
reveal_type(takes_in_type(int)) # revealed: type[int]
```
This also works when passing in arguments that are subclasses of the parameter type.

View File

@@ -754,21 +754,20 @@ def constrained[T: (Callable[[], int], Callable[[], str])](f: T):
## Meta-type
The meta-type of a typevar is the same as the meta-type of the upper bound, or the union of the
meta-types of the constraints:
The meta-type of a typevar is `type[T]`.
```py
def normal[T](x: T):
reveal_type(type(x)) # revealed: type
reveal_type(type(x)) # revealed: type[T@normal]
def bound_object[T: object](x: T):
reveal_type(type(x)) # revealed: type
reveal_type(type(x)) # revealed: type[T@bound_object]
def bound_int[T: int](x: T):
reveal_type(type(x)) # revealed: type[int]
reveal_type(type(x)) # revealed: type[T@bound_int]
def constrained[T: (int, str)](x: T):
reveal_type(type(x)) # revealed: type[int] | type[str]
reveal_type(type(x)) # revealed: type[T@constrained]
```
## Cycles
@@ -834,6 +833,18 @@ class G[T: list[G]]:
reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]]
```
An invalid specialization in a recursive bound doesn't cause a panic:
```py
# error: [invalid-type-arguments]
class Node[T: "Node[int]"]:
pass
# error: [invalid-type-arguments]
def _(n: Node[str]):
reveal_type(n) # revealed: Node[Unknown]
```
### Defaults
Defaults can be generic, but can only refer to earlier typevars:

View File

@@ -110,6 +110,11 @@ static_assert(not has_member(C(), "non_existent"))
### Class objects
```toml
[environment]
python-version = "3.12"
```
Class-level attributes can also be accessed through the class itself:
```py
@@ -153,6 +158,18 @@ static_assert(has_member(D, "meta_base_attr"))
static_assert(has_member(D, "meta_attr"))
static_assert(has_member(D, "base_attr"))
static_assert(has_member(D, "class_attr"))
def _(x: type[D]):
static_assert(has_member(x, "meta_base_attr"))
static_assert(has_member(x, "meta_attr"))
static_assert(has_member(x, "base_attr"))
static_assert(has_member(x, "class_attr"))
def _[T: D](x: type[T]):
static_assert(has_member(x, "meta_base_attr"))
static_assert(has_member(x, "meta_attr"))
static_assert(has_member(x, "base_attr"))
static_assert(has_member(x, "class_attr"))
```
### Generic classes
@@ -170,6 +187,40 @@ static_assert(has_member(C[int], "base_attr"))
static_assert(has_member(C[int](), "base_attr"))
```
Generic classes can also have metaclasses:
```py
class Meta(type):
FOO = 42
class E(Generic[T], metaclass=Meta): ...
static_assert(has_member(E[int], "FOO"))
def f(x: type[E[str]]):
static_assert(has_member(x, "FOO"))
```
### `type[Any]` and `Any`
`type[Any]` has all members of `type`.
```py
from typing import Any
from ty_extensions import has_member, static_assert
def f(x: type[Any]):
static_assert(has_member(x, "__base__"))
static_assert(has_member(x, "__qualname__"))
```
`Any` has all members of `object`, since it is a subtype of `object`:
```py
def f(x: Any):
static_assert(has_member(x, "__repr__"))
```
### Other instance-like types
```py

View File

@@ -191,13 +191,13 @@ def _(
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
# TODO should be Unknown | int
reveal_type(type_var_or_int) # revealed: typing.TypeVar | int
reveal_type(type_var_or_int) # revealed: T@TypeVarOrInt | int
# TODO should be int | Unknown
reveal_type(int_or_type_var) # revealed: int | typing.TypeVar
reveal_type(int_or_type_var) # revealed: int | T@IntOrTypeVar
# TODO should be Unknown | None
reveal_type(type_var_or_none) # revealed: typing.TypeVar | None
reveal_type(type_var_or_none) # revealed: T@TypeVarOrNone | None
# TODO should be None | Unknown
reveal_type(none_or_type_var) # revealed: None | typing.TypeVar
reveal_type(none_or_type_var) # revealed: None | T@NoneOrTypeVar
```
If a type is unioned with itself in a value expression, the result is just that type. No
@@ -366,7 +366,9 @@ def g(obj: Y):
reveal_type(obj) # revealed: list[int | str]
```
## Generic types
## Generic implicit type aliases
### Functionality
Implicit type aliases can also be generic:
@@ -388,73 +390,62 @@ ListOrTuple = list[T] | tuple[T, ...]
ListOrTupleLegacy = Union[list[T], tuple[T, ...]]
MyCallable = Callable[P, T]
AnnotatedType = Annotated[T, "tag"]
TransparentAlias = T
MyOptional = T | None
# TODO: Consider displaying this as `<class 'list[T]'>`, … instead? (and similar for some others below)
reveal_type(MyList) # revealed: <class 'list[typing.TypeVar]'>
reveal_type(MyDict) # revealed: <class 'dict[typing.TypeVar, typing.TypeVar]'>
reveal_type(MyList) # revealed: <class 'list[T@MyList]'>
reveal_type(MyDict) # revealed: <class 'dict[T@MyDict, U@MyDict]'>
reveal_type(MyType) # revealed: GenericAlias
reveal_type(IntAndType) # revealed: <class 'tuple[int, typing.TypeVar]'>
reveal_type(Pair) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
reveal_type(Sum) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
reveal_type(IntAndType) # revealed: <class 'tuple[int, T@IntAndType]'>
reveal_type(Pair) # revealed: <class 'tuple[T@Pair, T@Pair]'>
reveal_type(Sum) # revealed: <class 'tuple[T@Sum, U@Sum]'>
reveal_type(ListOrTuple) # revealed: types.UnionType
reveal_type(ListOrTupleLegacy) # revealed: types.UnionType
reveal_type(MyCallable) # revealed: GenericAlias
reveal_type(MyCallable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
reveal_type(AnnotatedType) # revealed: <typing.Annotated special form>
reveal_type(TransparentAlias) # revealed: typing.TypeVar
reveal_type(MyOptional) # revealed: types.UnionType
def _(
list_of_ints: MyList[int],
dict_str_to_int: MyDict[str, int],
# TODO: no error here
# error: [invalid-type-form] "`typing.TypeVar` is not a generic class"
subclass_of_int: MyType[int],
int_and_str: IntAndType[str],
pair_of_ints: Pair[int],
int_and_bytes: Sum[int, bytes],
list_or_tuple: ListOrTuple[int],
list_or_tuple_legacy: ListOrTupleLegacy[int],
# TODO: no error here
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[str, bytes]`?"
my_callable: MyCallable[[str, bytes], int],
annotated_int: AnnotatedType[int],
transparent_alias: TransparentAlias[int],
optional_int: MyOptional[int],
):
# TODO: This should be `list[int]`
reveal_type(list_of_ints) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `dict[str, int]`
reveal_type(dict_str_to_int) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `type[int]`
reveal_type(subclass_of_int) # revealed: Unknown
# TODO: This should be `tuple[int, str]`
reveal_type(int_and_str) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `tuple[int, int]`
reveal_type(pair_of_ints) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `tuple[int, bytes]`
reveal_type(int_and_bytes) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `list[int] | tuple[int, ...]`
reveal_type(list_or_tuple) # revealed: @Todo(Generic specialization of types.UnionType)
# TODO: This should be `list[int] | tuple[int, ...]`
reveal_type(list_or_tuple_legacy) # revealed: @Todo(Generic specialization of types.UnionType)
reveal_type(list_of_ints) # revealed: list[int]
reveal_type(dict_str_to_int) # revealed: dict[str, int]
reveal_type(subclass_of_int) # revealed: type[int]
reveal_type(int_and_str) # revealed: tuple[int, str]
reveal_type(pair_of_ints) # revealed: tuple[int, int]
reveal_type(int_and_bytes) # revealed: tuple[int, bytes]
reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...]
reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...]
# TODO: This should be `(str, bytes) -> int`
reveal_type(my_callable) # revealed: @Todo(Generic specialization of typing.Callable)
# TODO: This should be `int`
reveal_type(annotated_int) # revealed: @Todo(Generic specialization of typing.Annotated)
reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
reveal_type(annotated_int) # revealed: int
reveal_type(transparent_alias) # revealed: int
reveal_type(optional_int) # revealed: int | None
```
Generic implicit type aliases can be partially specialized:
```py
U = TypeVar("U")
DictStrTo = MyDict[str, U]
reveal_type(DictStrTo) # revealed: GenericAlias
reveal_type(DictStrTo) # revealed: <class 'dict[str, U@DictStrTo]'>
def _(
# TODO: No error here
# error: [invalid-type-form] "Invalid subscript of object of type `GenericAlias` in type expression"
dict_str_to_int: DictStrTo[int],
):
# TODO: This should be `dict[str, int]`
reveal_type(dict_str_to_int) # revealed: Unknown
reveal_type(dict_str_to_int) # revealed: dict[str, int]
```
Using specializations of generic implicit type aliases in other implicit type aliases works as
@@ -464,43 +455,118 @@ expected:
IntsOrNone = MyList[int] | None
IntsOrStrs = Pair[int] | Pair[str]
ListOfPairs = MyList[Pair[str]]
ListOrTupleOfInts = ListOrTuple[int]
AnnotatedInt = AnnotatedType[int]
SubclassOfInt = MyType[int]
CallableIntToStr = MyCallable[[int], str]
reveal_type(IntsOrNone) # revealed: UnionType
reveal_type(IntsOrStrs) # revealed: UnionType
reveal_type(ListOfPairs) # revealed: GenericAlias
reveal_type(IntsOrNone) # revealed: types.UnionType
reveal_type(IntsOrStrs) # revealed: types.UnionType
reveal_type(ListOfPairs) # revealed: <class 'list[tuple[str, str]]'>
reveal_type(ListOrTupleOfInts) # revealed: types.UnionType
reveal_type(AnnotatedInt) # revealed: <typing.Annotated special form>
reveal_type(SubclassOfInt) # revealed: GenericAlias
reveal_type(CallableIntToStr) # revealed: @Todo(Callable[..] specialized with ParamSpec)
def _(
# TODO: This should not be an error
# error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression"
ints_or_none: IntsOrNone,
# TODO: This should not be an error
# error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression"
ints_or_strs: IntsOrStrs,
list_of_pairs: ListOfPairs,
list_or_tuple_of_ints: ListOrTupleOfInts,
annotated_int: AnnotatedInt,
subclass_of_int: SubclassOfInt,
callable_int_to_str: CallableIntToStr,
):
# TODO: This should be `list[int] | None`
reveal_type(ints_or_none) # revealed: Unknown
# TODO: This should be `tuple[int, int] | tuple[str, str]`
reveal_type(ints_or_strs) # revealed: Unknown
# TODO: This should be `list[tuple[str, str]]`
reveal_type(list_of_pairs) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
reveal_type(ints_or_none) # revealed: list[int] | None
reveal_type(ints_or_strs) # revealed: tuple[int, int] | tuple[str, str]
reveal_type(list_of_pairs) # revealed: list[tuple[str, str]]
reveal_type(list_or_tuple_of_ints) # revealed: list[int] | tuple[int, ...]
reveal_type(annotated_int) # revealed: int
reveal_type(subclass_of_int) # revealed: type[int]
# TODO: This should be `(int, /) -> str`
reveal_type(callable_int_to_str) # revealed: @Todo(Callable[..] specialized with ParamSpec)
```
If a generic implicit type alias is used unspecialized in a type expression, we treat it as an
`Unknown` specialization:
A generic implicit type alias can also be used in another generic implicit type alias:
```py
from typing_extensions import Any
B = TypeVar("B", bound=int)
MyOtherList = MyList[T]
MyOtherType = MyType[T]
TypeOrList = MyType[B] | MyList[B]
reveal_type(MyOtherList) # revealed: <class 'list[T@MyOtherList]'>
reveal_type(MyOtherType) # revealed: GenericAlias
reveal_type(TypeOrList) # revealed: types.UnionType
def _(
list_of_ints: MyOtherList[int],
subclass_of_int: MyOtherType[int],
type_or_list: TypeOrList[Any],
):
reveal_type(list_of_ints) # revealed: list[int]
reveal_type(subclass_of_int) # revealed: type[int]
reveal_type(type_or_list) # revealed: type[Any] | list[Any]
```
If a generic implicit type alias is used unspecialized in a type expression, we use the default
specialization. For type variables without defaults, this is `Unknown`:
```py
def _(
my_list: MyList,
my_dict: MyDict,
list_unknown: MyList,
dict_unknown: MyDict,
subclass_of_unknown: MyType,
int_and_unknown: IntAndType,
pair_of_unknown: Pair,
unknown_and_unknown: Sum,
list_or_tuple: ListOrTuple,
list_or_tuple_legacy: ListOrTupleLegacy,
my_callable: MyCallable,
annotated_unknown: AnnotatedType,
optional_unknown: MyOptional,
):
# TODO: Should be `list[Unknown]`
reveal_type(my_list) # revealed: list[typing.TypeVar]
# TODO: Should be `dict[Unknown, Unknown]`
reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar]
# TODO: This should be `list[Unknown]`
reveal_type(list_unknown) # revealed: list[T@MyList]
# TODO: This should be `dict[Unknown, Unknown]`
reveal_type(dict_unknown) # revealed: dict[T@MyDict, U@MyDict]
# TODO: Should be `type[Unknown]`
reveal_type(subclass_of_unknown) # revealed: type[T@MyType]
# TODO: Should be `tuple[int, Unknown]`
reveal_type(int_and_unknown) # revealed: tuple[int, T@IntAndType]
# TODO: Should be `tuple[Unknown, Unknown]`
reveal_type(pair_of_unknown) # revealed: tuple[T@Pair, T@Pair]
# TODO: Should be `tuple[Unknown, Unknown]`
reveal_type(unknown_and_unknown) # revealed: tuple[T@Sum, U@Sum]
# TODO: Should be `list[Unknown] | tuple[Unknown, ...]`
reveal_type(list_or_tuple) # revealed: list[T@ListOrTuple] | tuple[T@ListOrTuple, ...]
# TODO: Should be `list[Unknown] | tuple[Unknown, ...]`
reveal_type(list_or_tuple_legacy) # revealed: list[T@ListOrTupleLegacy] | tuple[T@ListOrTupleLegacy, ...]
# TODO: Should be `(...) -> Unknown`
reveal_type(my_callable) # revealed: (...) -> typing.TypeVar
reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
# TODO: Should be `Unknown`
reveal_type(annotated_unknown) # revealed: T@AnnotatedType
# TODO: Should be `Unknown | None`
reveal_type(optional_unknown) # revealed: T@MyOptional | None
```
For a type variable with a default, we use the default type:
```py
T_default = TypeVar("T_default", default=int)
MyListWithDefault = list[T_default]
def _(
list_of_str: MyListWithDefault[str],
list_of_int: MyListWithDefault,
):
reveal_type(list_of_str) # revealed: list[str]
# TODO: this should be `list[int]`
reveal_type(list_of_int) # revealed: list[T_default@MyListWithDefault]
```
(Generic) implicit type aliases can be used as base classes:
@@ -522,37 +588,209 @@ reveal_mro(Derived1)
GenericBaseAlias = GenericBase[T]
# TODO: No error here
# error: [non-subscriptable] "Cannot subscript object of type `<class 'GenericBase[typing.TypeVar]'>` with no `__class_getitem__` method"
class Derived2(GenericBaseAlias[int]):
pass
```
### Imported aliases
Generic implicit type aliases can be imported from other modules and specialized:
`my_types.py`:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
MyList = list[T]
```
`main.py`:
```py
from my_types import MyList
import my_types as mt
def _(
list_of_ints1: MyList[int],
list_of_ints2: mt.MyList[int],
):
reveal_type(list_of_ints1) # revealed: list[int]
reveal_type(list_of_ints2) # revealed: list[int]
```
### In stringified annotations
Generic implicit type aliases can be specialized in stringified annotations:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
MyList = list[T]
def _(
list_of_ints: "MyList[int]",
):
reveal_type(list_of_ints) # revealed: list[int]
```
### Tuple unpacking
```toml
[environment]
python-version = "3.11"
```
```py
from typing import TypeVar
T = TypeVar("T")
U = TypeVar("U")
V = TypeVar("V")
X = tuple[T, *tuple[U, ...], V]
Y = X[T, tuple[int, str, U], bytes]
def g(obj: Y[bool, range]):
reveal_type(obj) # revealed: tuple[bool, *tuple[tuple[int, str, range], ...], bytes]
```
### Error cases
A generic alias that is already fully specialized cannot be specialized again:
```py
ListOfInts = list[int]
# TODO: this should be an error
# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
def _(doubly_specialized: ListOfInts[int]):
# TODO: this should be `Unknown`
reveal_type(doubly_specialized) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should ideally be `list[Unknown]` or `Unknown`
reveal_type(doubly_specialized) # revealed: list[int]
```
Specializing a generic implicit type alias with an incorrect number of type arguments also results
in an error:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
U = TypeVar("U")
MyList = list[T]
MyDict = dict[T, U]
def _(
# TODO: this should be an error
# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2"
list_too_many_args: MyList[int, str],
# TODO: this should be an error
# error: [invalid-type-arguments] "No type argument provided for required type variable `U`"
dict_too_few_args: MyDict[int],
):
# TODO: this should be `Unknown`
reveal_type(list_too_many_args) # revealed: @Todo(specialized generic alias in type expression)
# TODO: this should be `Unknown`
reveal_type(dict_too_few_args) # revealed: @Todo(specialized generic alias in type expression)
reveal_type(list_too_many_args) # revealed: list[Unknown]
reveal_type(dict_too_few_args) # revealed: dict[Unknown, Unknown]
```
Trying to specialize a non-name node results in an error:
```py
from ty_extensions import TypeOf
IntOrStr = int | str
def this_does_not_work() -> TypeOf[IntOrStr]:
raise NotImplementedError()
def _(
# TODO: Better error message (of kind `invalid-type-form`)?
# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
specialized: this_does_not_work()[int],
):
reveal_type(specialized) # revealed: int | str
```
Similarly, if you try to specialize a union type without a binding context, we emit an error:
```py
# TODO: Better error message (of kind `invalid-type-form`)?
# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
x: (list[T] | set[T])[int]
def _():
# TODO: `list[Unknown] | set[Unknown]` might be better
reveal_type(x) # revealed: list[typing.TypeVar] | set[typing.TypeVar]
```
### Multiple definitions
#### Shadowed definitions
When a generic type alias shadows a definition from an outer scope, the inner definition is used:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
MyAlias = list[T]
def outer():
MyAlias = set[T]
def _(x: MyAlias[int]):
reveal_type(x) # revealed: set[int]
```
#### Statically known conditions
```py
from typing_extensions import TypeVar
T = TypeVar("T")
if True:
MyAlias1 = list[T]
else:
MyAlias1 = set[T]
if False:
MyAlias2 = list[T]
else:
MyAlias2 = set[T]
def _(
x1: MyAlias1[int],
x2: MyAlias2[int],
):
reveal_type(x1) # revealed: list[int]
reveal_type(x2) # revealed: set[int]
```
#### Statically unknown conditions
If several definitions are visible, we emit an error:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
def flag() -> bool:
return True
if flag():
MyAlias = list[T]
else:
MyAlias = set[T]
# It is questionable whether this should be supported or not. It might also be reasonable to
# emit an error here (e.g. "Invalid subscript of object of type `<class 'list[T@MyAlias]'> |
# <class 'set[T@MyAlias]'>` in type expression"). If we ever choose to do so, the revealed
# type should probably be `Unknown`.
def _(x: MyAlias[int]):
reveal_type(x) # revealed: list[int] | set[int]
```
## `Literal`s
@@ -642,8 +880,7 @@ Deprecated = Annotated[T, "deprecated attribute"]
class C:
old: Deprecated[int]
# TODO: Should be `int`
reveal_type(C().old) # revealed: @Todo(Generic specialization of typing.Annotated)
reveal_type(C().old) # revealed: int
```
If the metadata argument is missing, we emit an error (because this code fails at runtime), but
@@ -1298,3 +1535,21 @@ def _(
reveal_type(recursive_dict3) # revealed: dict[Divergent, int]
reveal_type(recursive_dict4) # revealed: dict[Divergent, int]
```
### Self-referential generic implicit type aliases
```py
from typing import TypeVar
T = TypeVar("T")
NestedDict = dict[str, "NestedDict[T] | T"]
NestedList = list["NestedList[T] | None"]
def _(
nested_dict_int: NestedDict[int],
nested_list_str: NestedList[str],
):
reveal_type(nested_dict_int) # revealed: dict[str, Divergent]
reveal_type(nested_list_str) # revealed: list[Divergent]
```

View File

@@ -38,7 +38,7 @@ See: <https://github.com/astral-sh/ty/issues/113>
from pkg.sub import A
# TODO: This should be `<class 'A'>`
reveal_type(A) # revealed: Never
reveal_type(A) # revealed: Divergent
```
`pkg/outer.py`:

View File

@@ -0,0 +1,630 @@
# Support for Resolving Imports In Workspaces
Python packages have fairly rigid structures that we rely on when resolving imports and merging
namespace packages or stub packages. These rules go out the window when analyzing some random local
python file in some random workspace, and so we need to be more tolerant of situations that wouldn't
fly in a published package, cases where we're not configured as well as we'd like, or cases where
two projects in a monorepo have conflicting definitions (but we want to analyze both at once).
## Invalid Names
While you can't syntactically refer to a module with an invalid name (i.e. one with a `-`, or that
has the same name as a keyword) there are plenty of situations where a module with an invalid name
can be run. For instance `python my-script.py` and `python my-proj/main.py` both work, even though
we might in the course of analyzing the code compute the module name `my-script` or `my-proj.main`.
Also, a sufficiently motivated programmer can technically use `importlib.import_module` which takes
strings and does in fact allow syntactically invalid module names.
### Current File Is Invalid Module Name
Relative and absolute imports should resolve fine in a file that isn't a valid module name.
`my-main.py`:
```py
# TODO: there should be no errors in this file
# error: [unresolved-import]
from .mod1 import x
# error: [unresolved-import]
from . import mod2
import mod3
reveal_type(x) # revealed: Unknown
reveal_type(mod2.y) # revealed: Unknown
reveal_type(mod3.z) # revealed: int
```
`mod1.py`:
```py
x: int = 1
```
`mod2.py`:
```py
y: int = 2
```
`mod3.py`:
```py
z: int = 2
```
### Current Directory Is Invalid Module Name
Relative and absolute imports should resolve fine in a dir that isn't a valid module name.
`my-tests/main.py`:
```py
# TODO: there should be no errors in this file
# error: [unresolved-import]
from .mod1 import x
# error: [unresolved-import]
from . import mod2
import mod3
reveal_type(x) # revealed: Unknown
reveal_type(mod2.y) # revealed: Unknown
reveal_type(mod3.z) # revealed: int
```
`my-tests/mod1.py`:
```py
x: int = 1
```
`my-tests/mod2.py`:
```py
y: int = 2
```
`mod3.py`:
```py
z: int = 2
```
### Current Directory Is Invalid Package Name
Relative and absolute imports should resolve fine in a dir that isn't a valid package name, even if
it contains an `__init__.py`:
`my-tests/__init__.py`:
```py
```
`my-tests/main.py`:
```py
# TODO: there should be no errors in this file
# error: [unresolved-import]
from .mod1 import x
# error: [unresolved-import]
from . import mod2
import mod3
reveal_type(x) # revealed: Unknown
reveal_type(mod2.y) # revealed: Unknown
reveal_type(mod3.z) # revealed: int
```
`my-tests/mod1.py`:
```py
x: int = 1
```
`my-tests/mod2.py`:
```py
y: int = 2
```
`mod3.py`:
```py
z: int = 2
```
## Multiple Projects
It's common for a monorepo to define many separate projects that may or may not depend on eachother
and are stitched together with a package manager like `uv` or `poetry`, often as editables. In this
case, especially when running as an LSP, we want to be able to analyze all of the projects at once,
allowing us to reuse results between projects, without getting confused about things that only make
sense when analyzing the project separately.
The following tests will feature two projects, `a` and `b` where the "real" packages are found under
`src/` subdirectories (and we've been configured to understand that), but each project also contains
other python files in their roots or subdirectories that contains python files which relatively
import eachother and also absolutely import the main package of the project. All of these imports
*should* resolve.
Often the fact that there is both an `a` and `b` project seemingly won't matter, but many possible
solutions will misbehave under these conditions, as e.g. if both define a `main.py` and test code
has `import main`, we need to resolve each project's main as appropriate.
One key hint we will have in these situations is the existence of a `pyproject.toml`, so the
following examples include them in case they help.
### Tests Directory With Overlapping Names
Here we have fairly typical situation where there are two projects `aproj` and `bproj` where the
"real" packages are found under `src/` subdirectories, but each project also contains a `tests/`
directory that contains python files which relatively import eachother and also absolutely import
the package they test. All of these imports *should* resolve.
```toml
[environment]
# This is similar to what we would compute for installed editables
extra-paths = ["aproj/src/", "bproj/src/"]
```
`aproj/tests/test1.py`:
```py
from .setup import x
from . import setup
from a import y
import a
reveal_type(x) # revealed: int
reveal_type(setup.x) # revealed: int
reveal_type(y) # revealed: int
reveal_type(a.y) # revealed: int
```
`aproj/tests/setup.py`:
```py
x: int = 1
```
`aproj/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`aproj/src/a/__init__.py`:
```py
y: int = 10
```
`bproj/tests/test1.py`:
```py
from .setup import x
from . import setup
from b import y
import b
reveal_type(x) # revealed: str
reveal_type(setup.x) # revealed: str
reveal_type(y) # revealed: str
reveal_type(b.y) # revealed: str
```
`bproj/tests/setup.py`:
```py
x: str = "2"
```
`bproj/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`bproj/src/b/__init__.py`:
```py
y: str = "20"
```
### Tests Directory With Ambiguous Project Directories
The same situation as the previous test but instead of the project `a` being in a directory `aproj`
to disambiguate, we now need to avoid getting confused about whether `a/` or `a/src/a/` is the
package `a` while still resolving imports.
```toml
[environment]
# This is similar to what we would compute for installed editables
extra-paths = ["a/src/", "b/src/"]
```
`a/tests/test1.py`:
```py
# TODO: there should be no errors in this file.
# error: [unresolved-import]
from .setup import x
# error: [unresolved-import]
from . import setup
from a import y
import a
reveal_type(x) # revealed: Unknown
reveal_type(setup.x) # revealed: Unknown
reveal_type(y) # revealed: int
reveal_type(a.y) # revealed: int
```
`a/tests/setup.py`:
```py
x: int = 1
```
`a/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`a/src/a/__init__.py`:
```py
y: int = 10
```
`b/tests/test1.py`:
```py
# TODO: there should be no errors in this file
# error: [unresolved-import]
from .setup import x
# error: [unresolved-import]
from . import setup
from b import y
import b
reveal_type(x) # revealed: Unknown
reveal_type(setup.x) # revealed: Unknown
reveal_type(y) # revealed: str
reveal_type(b.y) # revealed: str
```
`b/tests/setup.py`:
```py
x: str = "2"
```
`b/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`b/src/b/__init__.py`:
```py
y: str = "20"
```
### Tests Package With Ambiguous Project Directories
The same situation as the previous test but `tests/__init__.py` is also defined, in case that
complicates the situation.
```toml
[environment]
extra-paths = ["a/src/", "b/src/"]
```
`a/tests/test1.py`:
```py
# TODO: there should be no errors in this file.
# error: [unresolved-import]
from .setup import x
# error: [unresolved-import]
from . import setup
from a import y
import a
reveal_type(x) # revealed: Unknown
reveal_type(setup.x) # revealed: Unknown
reveal_type(y) # revealed: int
reveal_type(a.y) # revealed: int
```
`a/tests/__init__.py`:
```py
```
`a/tests/setup.py`:
```py
x: int = 1
```
`a/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`a/src/a/__init__.py`:
```py
y: int = 10
```
`b/tests/test1.py`:
```py
# TODO: there should be no errors in this file
# error: [unresolved-import]
from .setup import x
# error: [unresolved-import]
from . import setup
from b import y
import b
reveal_type(x) # revealed: Unknown
reveal_type(setup.x) # revealed: Unknown
reveal_type(y) # revealed: str
reveal_type(b.y) # revealed: str
```
`b/tests/__init__.py`:
```py
```
`b/tests/setup.py`:
```py
x: str = "2"
```
`b/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`b/src/b/__init__.py`:
```py
y: str = "20"
```
### Tests Directory Absolute Importing `main.py`
Here instead of defining packages we have a couple simple applications with a `main.py` and tests
that `import main` and expect that to work.
`a/tests/test1.py`:
```py
from .setup import x
from . import setup
from main import y
import main
reveal_type(x) # revealed: int
reveal_type(setup.x) # revealed: int
reveal_type(y) # revealed: int
reveal_type(main.y) # revealed: int
```
`a/tests/setup.py`:
```py
x: int = 1
```
`a/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`a/main.py`:
```py
y: int = 10
```
`b/tests/test1.py`:
```py
from .setup import x
from . import setup
from main import y
import main
reveal_type(x) # revealed: str
reveal_type(setup.x) # revealed: str
reveal_type(y) # revealed: str
reveal_type(main.y) # revealed: str
```
`b/tests/setup.py`:
```py
x: str = "2"
```
`b/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`b/main.py`:
```py
y: str = "20"
```
### Tests Package Absolute Importing `main.py`
The same as the previous case but `tests/__init__.py` exists in case that causes different issues.
`a/tests/test1.py`:
```py
from .setup import x
from . import setup
from main import y
import main
reveal_type(x) # revealed: int
reveal_type(setup.x) # revealed: int
reveal_type(y) # revealed: int
reveal_type(main.y) # revealed: int
```
`a/tests/__init__.py`:
```py
```
`a/tests/setup.py`:
```py
x: int = 1
```
`a/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`a/main.py`:
```py
y: int = 10
```
`b/tests/test1.py`:
```py
from .setup import x
from . import setup
from main import y
import main
reveal_type(x) # revealed: str
reveal_type(setup.x) # revealed: str
reveal_type(y) # revealed: str
reveal_type(main.y) # revealed: str
```
`b/tests/__init__.py`:
```py
```
`b/tests/setup.py`:
```py
x: str = "2"
```
`b/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`b/main.py`:
```py
y: str = "20"
```
### `main.py` absolute importing private package
In this case each project has a `main.py` that defines a "private" `utils` package and absolute
imports it.
`a/main.py`:
```py
from utils import x
import utils
reveal_type(x) # revealed: int
reveal_type(utils.x) # revealed: int
```
`a/utils/__init__.py`:
```py
x: int = 1
```
`a/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`b/main.py`:
```py
from utils import x
import utils
reveal_type(x) # revealed: str
reveal_type(utils.x) # revealed: str
```
`b/utils/__init__.py`:
```py
x: str = "2"
```
`b/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```

View File

@@ -0,0 +1,64 @@
# numpy
```toml
[environment]
python-version = "3.14"
```
## numpy's `dtype`
numpy functions often accept a `dtype` parameter. For example, one of `np.array`'s overloads accepts
a `dtype` parameter of type `DTypeLike | None`. Here, we build up something that resembles numpy's
internals in order to model the type `DTypeLike`. Many details have been left out.
`mini_numpy.py`:
```py
from typing import TypeVar, Generic, Any, Protocol, TypeAlias, runtime_checkable, final
import builtins
_ItemT_co = TypeVar("_ItemT_co", default=Any, covariant=True)
class generic(Generic[_ItemT_co]):
@property
def dtype(self) -> _DTypeT_co:
raise NotImplementedError
_BoolItemT_co = TypeVar("_BoolItemT_co", bound=builtins.bool, default=builtins.bool, covariant=True)
class bool(generic[_BoolItemT_co], Generic[_BoolItemT_co]): ...
@final
class object_(generic): ...
_ScalarT = TypeVar("_ScalarT", bound=generic)
_ScalarT_co = TypeVar("_ScalarT_co", bound=generic, default=Any, covariant=True)
@final
class dtype(Generic[_ScalarT_co]): ...
_DTypeT_co = TypeVar("_DTypeT_co", bound=dtype, default=dtype, covariant=True)
@runtime_checkable
class _SupportsDType(Protocol[_DTypeT_co]):
@property
def dtype(self) -> _DTypeT_co: ...
_DTypeLike: TypeAlias = type[_ScalarT] | dtype[_ScalarT] | _SupportsDType[dtype[_ScalarT]]
DTypeLike: TypeAlias = _DTypeLike[Any] | str | None
```
Now we can make sure that a function which accepts `DTypeLike | None` works as expected:
```py
import mini_numpy as np
def accepts_dtype(dtype: np.DTypeLike | None) -> None: ...
accepts_dtype(dtype=np.bool)
accepts_dtype(dtype=np.dtype[np.bool])
accepts_dtype(dtype=object)
accepts_dtype(dtype=np.object_)
accepts_dtype(dtype="U")
```

View File

@@ -0,0 +1,314 @@
# `typing.override`
## Basics
Decorating a method with `typing.override` decorator is an explicit indication to a type checker
that the method is intended to override a method on a superclass. If the decorated method does not
in fact override anything, a type checker should report a diagnostic on that method.
<!-- snapshot-diagnostics -->
```pyi
from typing_extensions import override, Callable, TypeVar
def lossy_decorator(fn: Callable) -> Callable: ...
class A:
@override
def __repr__(self): ... # fine: overrides `object.__repr__`
class Parent:
def foo(self): ...
@property
def my_property1(self) -> int: ...
@property
def my_property2(self) -> int: ...
baz = None
@classmethod
def class_method1(cls) -> int: ...
@staticmethod
def static_method1() -> int: ...
@classmethod
def class_method2(cls) -> int: ...
@staticmethod
def static_method2() -> int: ...
@lossy_decorator
def decorated_1(self): ...
@lossy_decorator
def decorated_2(self): ...
@lossy_decorator
def decorated_3(self): ...
class Child(Parent):
@override
def foo(self): ... # fine: overrides `Parent.foo`
@property
@override
def my_property1(self) -> int: ... # fine: overrides `Parent.my_property1`
@override
@property
def my_property2(self) -> int: ... # fine: overrides `Parent.my_property2`
@override
def baz(self): ... # fine: overrides `Parent.baz`
@classmethod
@override
def class_method1(cls) -> int: ... # fine: overrides `Parent.class_method1`
@staticmethod
@override
def static_method1() -> int: ... # fine: overrides `Parent.static_method1`
@override
@classmethod
def class_method2(cls) -> int: ... # fine: overrides `Parent.class_method2`
@override
@staticmethod
def static_method2() -> int: ... # fine: overrides `Parent.static_method2`
@override
def decorated_1(self): ... # fine: overrides `Parent.decorated_1`
@override
@lossy_decorator
def decorated_2(self): ... # fine: overrides `Parent.decorated_2`
@lossy_decorator
@override
def decorated_3(self): ... # fine: overrides `Parent.decorated_3`
class OtherChild(Parent): ...
class Grandchild(OtherChild):
@override
def foo(self): ... # fine: overrides `Parent.foo`
@override
@property
def bar(self) -> int: ... # fine: overrides `Parent.bar`
@override
def baz(self): ... # fine: overrides `Parent.baz`
@classmethod
@override
def class_method1(cls) -> int: ... # fine: overrides `Parent.class_method1`
@staticmethod
@override
def static_method1() -> int: ... # fine: overrides `Parent.static_method1`
@override
@classmethod
def class_method2(cls) -> int: ... # fine: overrides `Parent.class_method2`
@override
@staticmethod
def static_method2() -> int: ... # fine: overrides `Parent.static_method2`
@override
def decorated_1(self): ... # fine: overrides `Parent.decorated_1`
@override
@lossy_decorator
def decorated_2(self): ... # fine: overrides `Parent.decorated_2`
@lossy_decorator
@override
def decorated_3(self): ... # fine: overrides `Parent.decorated_3`
class Invalid:
@override
def ___reprrr__(self): ... # error: [invalid-explicit-override]
@override
@classmethod
def foo(self): ... # error: [invalid-explicit-override]
@classmethod
@override
def bar(self): ... # error: [invalid-explicit-override]
@staticmethod
@override
def baz(): ... # error: [invalid-explicit-override]
@override
@staticmethod
def eggs(): ... # error: [invalid-explicit-override]
@property
@override
def bad_property1(self) -> int: ... # TODO: should emit `invalid-explicit-override` here
@override
@property
def bad_property2(self) -> int: ... # TODO: should emit `invalid-explicit-override` here
@lossy_decorator
@override
def lossy(self): ... # TODO: should emit `invalid-explicit-override` here
@override
@lossy_decorator
def lossy2(self): ... # TODO: should emit `invalid-explicit-override` here
# TODO: all overrides in this class should cause us to emit *Liskov* violations,
# but not `@override` violations
class LiskovViolatingButNotOverrideViolating(Parent):
@override
@property
def foo(self) -> int: ...
@override
def my_property1(self) -> int: ...
@staticmethod
@override
def class_method1() -> int: ...
@classmethod
@override
def static_method1(cls) -> int: ...
# Diagnostic edge case: `override` is very far away from the method definition in the source code:
T = TypeVar("T")
def identity(x: T) -> T: ...
class Foo:
@override
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
@identity
def bar(self): ... # error: [invalid-explicit-override]
```
## Overloads
The typing spec states that for an overloaded method, `@override` should only be applied to the
implementation function. However, we nonetheless respect the decorator in this situation, even
though we also emit `invalid-overload` on these methods.
```py
from typing_extensions import override, overload
class Spam:
@overload
def foo(self, x: str) -> str: ...
@overload
def foo(self, x: int) -> int: ...
@override
def foo(self, x: str | int) -> str | int: # error: [invalid-explicit-override]
return x
@overload
@override
def bar(self, x: str) -> str: ...
@overload
@override
def bar(self, x: int) -> int: ...
@override
# error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation"
# error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation"
# error: [invalid-explicit-override]
def bar(self, x: str | int) -> str | int:
return x
@overload
@override
def baz(self, x: str) -> str: ...
@overload
def baz(self, x: int) -> int: ...
# error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation"
# error: [invalid-explicit-override]
def baz(self, x: str | int) -> str | int:
return x
```
In a stub file, `@override` should always be applied to the first overload. Even if it isn't, we
always emit `invalid-explicit-override` diagnostics on the first overload.
`module.pyi`:
```pyi
from typing_extensions import override, overload
class Spam:
@overload
def foo(self, x: str) -> str: ... # error: [invalid-explicit-override]
@overload
@override
# error: [invalid-overload] "`@override` decorator should be applied only to the first overload"
def foo(self, x: int) -> int: ...
@overload
@override
def bar(self, x: str) -> str: ... # error: [invalid-explicit-override]
@overload
@override
# error: [invalid-overload] "`@override` decorator should be applied only to the first overload"
def bar(self, x: int) -> int: ...
@overload
@override
def baz(self, x: str) -> str: ... # error: [invalid-explicit-override]
@overload
def baz(self, x: int) -> int: ...
```
## Classes inheriting from `Any`
```py
from typing_extensions import Any, override
from does_not_exist import SomethingUnknown # error: [unresolved-import]
class Parent1(Any): ...
class Parent2(SomethingUnknown): ...
class Child1(Parent1):
@override
def bar(self): ... # fine
class Child2(Parent2):
@override
def bar(self): ... # fine
```
## Override of a synthesized method
```pyi
from typing_extensions import NamedTuple, TypedDict, override, Any, Self
from dataclasses import dataclass
@dataclass(order=True)
class ParentDataclass:
x: int
class Child(ParentDataclass):
@override
def __lt__(self, other: ParentDataclass) -> bool: ... # fine
class MyNamedTuple(NamedTuple):
x: int
@override
# TODO: this raises an exception at runtime (which we should emit a diagnostic for).
# It shouldn't be an `invalid-explicit-override` diagnostic, however.
def _asdict(self, /) -> dict[str, Any]: ...
class MyNamedTupleParent(NamedTuple):
x: int
class MyNamedTupleChild(MyNamedTupleParent):
@override
def _asdict(self, /) -> dict[str, Any]: ... # fine
class MyTypedDict(TypedDict):
x: int
@override
# TODO: it's invalid to define a method on a `TypedDict` class,
# so we should emit a diagnostic here.
# It shouldn't be an `invalid-explicit-override` diagnostic, however.
def copy(self) -> Self: ...
class Grandparent(Any): ...
class Parent(Grandparent, NamedTuple): # error: [invalid-named-tuple]
x: int
class Child(Parent):
@override
def foo(self): ... # fine because `Any` is in the MRO
```

View File

@@ -96,6 +96,45 @@ def _(x: MyAlias):
reveal_type(x) # revealed: int | ((str, /) -> int)
```
## Generic aliases
A more comprehensive set of tests can be found in
[`implicit_type_aliases.md`](./implicit_type_aliases.md). If the implementations ever diverge, we
may need to duplicate more tests here.
### Basic
```py
from typing import TypeAlias, TypeVar
T = TypeVar("T")
MyList: TypeAlias = list[T]
ListOrSet: TypeAlias = list[T] | set[T]
reveal_type(MyList) # revealed: <class 'list[T]'>
reveal_type(ListOrSet) # revealed: types.UnionType
def _(list_of_int: MyList[int], list_or_set_of_str: ListOrSet[str]):
reveal_type(list_of_int) # revealed: list[int]
reveal_type(list_or_set_of_str) # revealed: list[str] | set[str]
```
### Stringified generic alias
```py
from typing import TypeAlias, TypeVar
T = TypeVar("T")
U = TypeVar("U")
TotallyStringifiedPEP613: TypeAlias = "dict[T, U]"
TotallyStringifiedPartiallySpecialized: TypeAlias = "TotallyStringifiedPEP613[U, int]"
def f(x: "TotallyStringifiedPartiallySpecialized[str]"):
reveal_type(x) # revealed: @Todo(Generic stringified PEP-613 type alias)
```
## Subscripted generic alias in union
```py
@@ -107,8 +146,7 @@ Alias1: TypeAlias = list[T] | set[T]
MyAlias: TypeAlias = int | Alias1[str]
def _(x: MyAlias):
# TODO: int | list[str] | set[str]
reveal_type(x) # revealed: int | @Todo(Specialization of union type alias)
reveal_type(x) # revealed: int | list[str] | set[str]
```
## Imported
@@ -144,17 +182,46 @@ def _(x: IntOrStr):
## Cyclic
```py
from typing import TypeAlias
from typing import TypeAlias, TypeVar, Union
from types import UnionType
RecursiveTuple: TypeAlias = tuple[int | "RecursiveTuple", str]
def _(rec: RecursiveTuple):
# TODO should be `tuple[int | RecursiveTuple, str]`
reveal_type(rec) # revealed: tuple[Divergent, str]
RecursiveHomogeneousTuple: TypeAlias = tuple[int | "RecursiveHomogeneousTuple", ...]
def _(rec: RecursiveHomogeneousTuple):
# TODO should be `tuple[int | RecursiveHomogeneousTuple, ...]`
reveal_type(rec) # revealed: tuple[Divergent, ...]
ClassInfo: TypeAlias = type | UnionType | tuple["ClassInfo", ...]
reveal_type(ClassInfo) # revealed: types.UnionType
def my_isinstance(obj: object, classinfo: ClassInfo) -> bool:
# TODO should be `type | UnionType | tuple[ClassInfo, ...]`
reveal_type(classinfo) # revealed: type | UnionType | tuple[Divergent, ...]
return isinstance(obj, classinfo)
K = TypeVar("K")
V = TypeVar("V")
NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]]
def _(nested: NestedDict[str, int]):
# TODO should be `dict[str, int | NestedDict[str, int]]`
reveal_type(nested) # revealed: dict[@Todo(specialized recursive generic type alias), Divergent]
my_isinstance(1, int)
my_isinstance(1, int | str)
my_isinstance(1, (int, str))
my_isinstance(1, (int, (str, float)))
my_isinstance(1, (int, (str | float)))
# error: [invalid-argument-type]
my_isinstance(1, 1)
# TODO should be an invalid-argument-type error
my_isinstance(1, (int, (str, 1)))
```
## Conditionally imported on Python < 3.10

View File

@@ -106,6 +106,29 @@ def _(flag: bool):
```py
type ListOrSet[T] = list[T] | set[T]
reveal_type(ListOrSet.__type_params__) # revealed: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
type Tuple1[T] = tuple[T]
def _(cond: bool):
Generic = ListOrSet if cond else Tuple1
def _(x: Generic[int]):
reveal_type(x) # revealed: list[int] | set[int] | tuple[int]
try:
class Foo[T]:
x: T
def foo(self) -> T:
return self.x
...
except Exception:
class Foo[T]:
x: T
def foo(self) -> T:
return self.x
def f(x: Foo[int]):
reveal_type(x.foo()) # revealed: int
```
## In unions and intersections
@@ -244,6 +267,47 @@ def f(x: IntOr, y: OrInt):
reveal_type(x) # revealed: Never
if not isinstance(y, int):
reveal_type(y) # revealed: Never
# error: [cyclic-type-alias-definition] "Cyclic definition of `Itself`"
type Itself = Itself
def foo(
# this is a very strange thing to do, but this is a regression test to ensure it doesn't panic
Itself: Itself,
):
x: Itself
reveal_type(Itself) # revealed: Divergent
# A type alias defined with invalid recursion behaves as a dynamic type.
foo(42)
foo("hello")
# error: [cyclic-type-alias-definition] "Cyclic definition of `A`"
type A = B
# error: [cyclic-type-alias-definition] "Cyclic definition of `B`"
type B = A
def bar(B: B):
x: B
reveal_type(B) # revealed: Divergent
# error: [cyclic-type-alias-definition] "Cyclic definition of `G`"
type G[T] = G[T]
# error: [cyclic-type-alias-definition] "Cyclic definition of `H`"
type H[T] = I[T]
# error: [cyclic-type-alias-definition] "Cyclic definition of `I`"
type I[T] = H[T]
# It's not possible to create an element of this type, but it's not an error for now
type DirectRecursiveList[T] = list[DirectRecursiveList[T]]
# TODO: this should probably be a cyclic-type-alias-definition error
type Foo[T] = list[T] | Bar[T]
type Bar[T] = int | Foo[T]
def _(x: Bar[int]):
# TODO: should be `int | list[int]`
reveal_type(x) # revealed: int | list[int] | Any
```
### With legacy generic
@@ -327,7 +391,7 @@ class C(P[T]):
pass
reveal_type(C[int]()) # revealed: C[int]
reveal_type(C()) # revealed: C[Divergent]
reveal_type(C()) # revealed: C[C[Divergent]]
```
### Union inside generic

Some files were not shown because too many files have changed in this diff Show More