Compare commits

...

40 Commits

Author SHA1 Message Date
David Peter
ab736b22d0 [ty] Implicit type aliases: Instantiation and attribute access 2025-11-28 09:41:34 +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
Shunsuke Shibayama
dd15656deb [ty] fix ty playground initialization and vite optimization issues (#21471)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-11-25 07:42:56 +00:00
Alex Waygood
adf095e889 [ty] Extend Liskov checks to also cover classmethods and staticmethods (#21598)
## Summary

Building on https://github.com/astral-sh/ruff/pull/21436.

There's nothing conceptually more complicated about this, it just
requires its own set of tests and its own subdiagnostic hint.

I also uncovered another inconsistency between mypy/pyright/pyrefly,
which is fun. In this case, I suggest we go with pyright's behaviour.

## Test Plan

mdtests/snapshots
2025-11-24 23:14:06 +00:00
Alex Waygood
bfd65c4215 Dogfood ty on the scripts directory (#21617)
## Summary

This PR sets up CI jobs to run ty from the `main` branch on the files
and subdirectories in our `scripts` directory

## Test Plan

Both these commands pass for me locally:
- `uv run --project=./scripts cargo run -p ty check --project=./scripts`
- `uv run --project=./scripts/ty_benchmark cargo run -p ty check
--project=./scripts/ty_benchmark`
2025-11-24 23:13:44 +00:00
Jack O'Connor
0631e72187 [ty] support generic aliases in type[...], like type[C[int]] (#21552)
Closes https://github.com/astral-sh/ty/issues/1101.
2025-11-24 13:56:42 -08:00
Alex Waygood
bab688b76c [ty] Retain the function-like-ness of Callable types when binding self (#21614)
## Summary

For something like this:

```py
from typing import Callable

def my_lossy_decorator(fn: Callable[..., int]) -> Callable[..., int]:
    return fn

class MyClass:
    @my_lossy_decorator
    def method(self) -> int:
        return 42
```

we will currently infer the type of `MyClass.method` as a function-like
`Callable`, but we will infer the type of `MyClass().method` as a
`Callable` that is _not_ function-like. That's because a `CallableType`
currently "forgets" whether it was function-like or not during the
`bound_self` transformation:


a57e291311/crates/ty_python_semantic/src/types.rs (L10985-L10987)

This seems incorrect, and it's quite different to what we do when
binding the `self` parameter of `FunctionLiteral` types: `BoundMethod`
types are all seen as subtypes of function-like `Callable` supertypes --
here's `BoundMethodType::into_callable_type`:


a57e291311/crates/ty_python_semantic/src/types.rs (L10844-L10860)

The bug here is also causing lots of false positives in the ecosystem
report on https://github.com/astral-sh/ruff/pull/21611: a decorated
method on a subclass is currently not seen as validly overriding an
undecorated method with the same signature on a superclass, because the
undecorated superclass method is seen as function-like after binding
`self` whereas the decorated subclass method is not.

Fixing the bug required adding a new API in `protocol_class.rs`, because
it turns out that for our purposes in protocol subtyping/assignability,
we really do want a callable type to forget its function-like-ness when
binding `self`.

I initially tried out this change without changing anything in
`protocol_class.rs`. However, it resulted in many ecosystem false
positives and new false positives on the typing conformance test suite.
This is because it would mean that no protocol with a `__call__` method
would ever be seen as a subtype of a `Callable` type, since the
`__call__` method on the protocol would be seen as being function-like
whereas the `Callable` type would not be seen as function-like.

## Test Plan

Added an mdtest that fails on `main`
2025-11-24 21:14:03 +00:00
Douglas Creager
7e277667d1 [ty] Distinguish "unconstrained" from "constrained to any type" (#21539)
Before, we would collapse any constraint of the form `Never ≤ T ≤
object` down to the "always true" constraint set. This is correct in
terms of BDD semantics, but loses information, since "not constraining a
typevar at all" is different than "constraining a typevar to take on any
type". Once we get to specialization inference, we should fall back on
the typevar's default for the former, but not for the latter.

This is much easier to support now that we have a sequent map, since we
need to treat `¬(Never ≤ T ≤ object)` as being impossible, and prune it
when we walk through BDD paths, just like we do for other impossible
combinations.
2025-11-24 15:23:09 -05:00
Alex Waygood
d379f3826f Disable ty workspace diagnostics for VSCode users (#21620) 2025-11-24 20:06:09 +00:00
Matthew Mckee
6f9265d78d [ty] Double click to insert inlay hint (#21600)
<!--
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

Resolves
https://github.com/astral-sh/ty/issues/317#issuecomment-3567398107.

I can't get the auto import working great.

I haven't added many places where we specify that the type display is
invalid syntax.

## Test Plan

Nothing yet
2025-11-24 19:48:30 +00:00
231 changed files with 197815 additions and 2551 deletions

View File

@@ -284,6 +284,10 @@ jobs:
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
- name: Dogfood ty on py-fuzzer
run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
- name: Dogfood ty on the scripts directory
run: uv run --project=./scripts cargo run -p ty check --project=./scripts
- name: Dogfood ty on ty_benchmark
run: uv run --project=./scripts/ty_benchmark cargo run -p ty check --project=./scripts/ty_benchmark
# Check for broken links in the documentation.
- run: cargo doc --all --no-deps
env:

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

@@ -5,5 +5,6 @@
"rust-analyzer.check.command": "clippy",
"search.exclude": {
"**/*.snap": true
}
},
"ty.diagnosticMode": "openFilesOnly"
}

4
Cargo.lock generated
View File

@@ -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

@@ -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

@@ -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

@@ -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 {

282
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#L126" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L133" 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#L170" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L177" 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#L196" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L203" 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#L221" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L228" 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#L247" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L254" 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#L280" 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#L312" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L341" 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#L333" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L362" 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#L537" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L566" 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#L561" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L590" 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#L365" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L394" 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#L615" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L644" 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#L655" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L684" 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#L1814" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1920" 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#L677" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L706" 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#L707" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L736" 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#L758" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L787" 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#L779" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L808" 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#L802" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L831" 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#L1617" 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#L838" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L867" 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#L582" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L611" 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#L864" target="_blank">View source</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>
</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#L961" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L990" 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#L1942" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2048" 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#L511" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L540" 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#L937" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L966" 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#L988" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1017" 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#L1087" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1116" 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#L892" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L921" 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#L447" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L476" 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#L1107" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1136" 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#L636" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L665" 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#L1150" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1179" 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#L916" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L945" 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#L1411" 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#L1189" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1218" 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#L1213" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1242" 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#L1265" target="_blank">View source</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>
</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#L1237" target="_blank">View source</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>
</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#L1293" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1322" 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#L1322" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1351" 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#L1915" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2021" 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#L1341" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1370" 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#L1364" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1393" 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#L1382" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1452" target="_blank">View source</a>
</small>
@@ -1612,7 +1750,7 @@ for i in 34: # TypeError: 'int' object is not iterable
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#L1433" target="_blank">View source</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>
</small>
@@ -1639,7 +1777,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#L1668" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1774" target="_blank">View source</a>
</small>
@@ -1697,7 +1835,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#L1790" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1896" target="_blank">View source</a>
</small>
@@ -1727,7 +1865,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#L1524" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1594" target="_blank">View source</a>
</small>
@@ -1756,7 +1894,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#L1569" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1675" target="_blank">View source</a>
</small>
@@ -1783,7 +1921,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#L1547" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1653" target="_blank">View source</a>
</small>
@@ -1811,7 +1949,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#L1590" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1696" target="_blank">View source</a>
</small>
@@ -1857,7 +1995,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#L1647" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1753" target="_blank">View source</a>
</small>
@@ -1884,7 +2022,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#L1689" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1795" target="_blank">View source</a>
</small>
@@ -1912,7 +2050,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#L1711" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1817" target="_blank">View source</a>
</small>
@@ -1937,7 +2075,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#L1730" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1836" target="_blank">View source</a>
</small>
@@ -1962,7 +2100,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#L1402" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472" target="_blank">View source</a>
</small>
@@ -1999,7 +2137,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#L1749" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1855" target="_blank">View source</a>
</small>
@@ -2027,7 +2165,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#L1771" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1877" target="_blank">View source</a>
</small>
@@ -2052,7 +2190,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#L476" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L505" target="_blank">View source</a>
</small>
@@ -2093,7 +2231,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#L291" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L320" target="_blank">View source</a>
</small>
@@ -2120,7 +2258,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 +2289,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 +2319,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#L1454" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1524" target="_blank">View source</a>
</small>
@@ -2209,7 +2347,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#L144" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L151" target="_blank">View source</a>
</small>
@@ -2241,7 +2379,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#L1476" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1546" target="_blank">View source</a>
</small>
@@ -2273,7 +2411,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#L1842" 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>
@@ -2300,7 +2438,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#L1629" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1735" target="_blank">View source</a>
</small>
@@ -2324,7 +2462,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#L1863" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1969" target="_blank">View source</a>
</small>
@@ -2382,7 +2520,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#L725" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L754" target="_blank">View source</a>
</small>
@@ -2421,7 +2559,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#L1031" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1060" target="_blank">View source</a>
</small>
@@ -2484,7 +2622,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#L273" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L302" target="_blank">View source</a>
</small>
@@ -2508,7 +2646,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#L1502" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1572" target="_blank">View source</a>
</small>
@@ -2536,7 +2674,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

@@ -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};
@@ -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.
@@ -369,7 +393,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 +441,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 +1358,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 +1450,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 +1695,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(
@@ -5347,6 +5424,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

@@ -190,6 +190,24 @@ 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`
@@ -227,7 +245,7 @@ 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) => {
@@ -243,7 +261,7 @@ 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) => {
@@ -313,6 +331,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 +366,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 +377,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
}
@@ -367,14 +401,9 @@ impl GotoTarget<'_> {
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)
}
GotoTarget::Expression(expression) => definitions_for_expression(model, *expression)
.map(DefinitionsOrTargets::Definitions),
// For already-defined symbols, they are their own definitions
GotoTarget::FunctionDef(function) => Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(function.definition(model)),
@@ -395,8 +424,7 @@ impl GotoTarget<'_> {
let symbol_name = alias.name.as_str();
Some(DefinitionsOrTargets::Definitions(
definitions_for_imported_symbol(
db,
file,
model,
import_from,
symbol_name,
alias_resolution,
@@ -423,7 +451,7 @@ impl GotoTarget<'_> {
let alias_range = alias.asname.as_ref().unwrap().range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget {
file,
file: model.file(),
focus_range: alias_range,
full_range: alias.range(),
}),
@@ -436,7 +464,7 @@ impl GotoTarget<'_> {
keyword,
call_expression,
} => Some(DefinitionsOrTargets::Definitions(
definitions_for_keyword_argument(db, file, keyword, call_expression),
definitions_for_keyword_argument(model, keyword, call_expression),
)),
// For exception variables, they are their own definitions (like parameters)
@@ -451,7 +479,10 @@ impl GotoTarget<'_> {
if let Some(rest_name) = &pattern_mapping.rest {
let range = rest_name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
crate::NavigationTargets::single(NavigationTarget::new(
model.file(),
range,
)),
))
} else {
None
@@ -463,7 +494,10 @@ impl GotoTarget<'_> {
if let Some(name) = &pattern_as.name {
let range = name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
crate::NavigationTargets::single(NavigationTarget::new(
model.file(),
range,
)),
))
} else {
None
@@ -476,7 +510,7 @@ 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() {
@@ -488,19 +522,45 @@ impl GotoTarget<'_> {
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))
}
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))
}
_ => 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)
.map(DefinitionsOrTargets::Definitions)
}
GotoTarget::NonLocal { identifier } | GotoTarget::Globals { identifier } => {
Some(DefinitionsOrTargets::Definitions(definitions_for_name(
model,
identifier.as_str(),
AnyNodeRef::Identifier(identifier),
)))
}
// TODO: implement these
GotoTarget::PatternKeywordArgument(..)
| GotoTarget::PatternMatchStarName(..)
| GotoTarget::TypeParamTypeVarName(..)
| GotoTarget::TypeParamParamSpecName(..)
| GotoTarget::TypeParamTypeVarTupleName(..) => None,
}
}
@@ -519,6 +579,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 +640,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 +840,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 +900,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 +920,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 +957,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 +977,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 +986,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 +1001,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,18 +1036,18 @@ 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>> {

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(

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

@@ -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 references to a symbol at the given position.
/// Search for references across all files in the project.
@@ -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
@@ -147,7 +149,6 @@ result = calculate_sum(value=42)
}
#[test]
#[ignore] // TODO: Enable when nonlocal support is fully implemented in goto.rs
fn test_nonlocal_variable_references() {
let test = cursor_test(
"
@@ -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,14 +265,13 @@ 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() {
let test = cursor_test(
"
@@ -710,6 +710,194 @@ cls = MyClass
");
}
#[test]
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 test_multi_file_function_references() {
let test = CursorTest::builder()

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,226 @@ 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_on_keyword_argument() {
let test = cursor_test(
@@ -842,6 +1062,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,117 @@ 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_module_import() {
let mut test = cursor_test(

View File

@@ -6,8 +6,8 @@ use ruff_db::parsed::parsed_module;
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword, Expr, ExprUnaryOp, Stmt, UnaryOp};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::types::Type;
use ty_python_semantic::types::ide_support::inlay_hint_call_argument_details;
use ty_python_semantic::types::{Type, TypeDetail};
use ty_python_semantic::{HasType, SemanticModel};
#[derive(Debug, Clone)]
@@ -15,10 +15,12 @@ pub struct InlayHint {
pub position: TextSize,
pub kind: InlayHintKind,
pub label: InlayHintLabel,
pub text_edits: Vec<InlayHintTextEdit>,
}
impl InlayHint {
fn variable_type(position: TextSize, ty: Type, db: &dyn Db) -> Self {
fn variable_type(expr: &Expr, ty: Type, db: &dyn Db, allow_edits: bool) -> Self {
let position = expr.range().end();
// Render the type to a string, and get subspans for all the types that make it up
let details = ty.display(db).to_string_parts();
@@ -34,7 +36,7 @@ impl InlayHint {
let mut label_parts = vec![": ".into()];
for (target, detail) in details.targets.iter().zip(&details.details) {
match detail {
ty_python_semantic::types::TypeDetail::Type(ty) => {
TypeDetail::Type(ty) => {
let start = target.start().to_usize();
let end = target.end().to_usize();
// If we skipped over some bytes, push them with no target
@@ -50,9 +52,9 @@ impl InlayHint {
offset = end;
}
}
ty_python_semantic::types::TypeDetail::SignatureStart
| ty_python_semantic::types::TypeDetail::SignatureEnd
| ty_python_semantic::types::TypeDetail::Parameter(_) => {
TypeDetail::SignatureStart
| TypeDetail::SignatureEnd
| TypeDetail::Parameter(_) => {
// Don't care about these
}
}
@@ -62,10 +64,20 @@ impl InlayHint {
label_parts.push(details.label[offset..details.label.len()].into());
}
let text_edits = if details.is_valid_syntax && allow_edits {
vec![InlayHintTextEdit {
range: TextRange::new(position, position),
new_text: format!(": {}", details.label),
}]
} else {
vec![]
};
Self {
position,
kind: InlayHintKind::Type,
label: InlayHintLabel { parts: label_parts },
text_edits,
}
}
@@ -83,6 +95,7 @@ impl InlayHint {
position,
kind: InlayHintKind::CallArgumentName,
label: InlayHintLabel { parts: label_parts },
text_edits: vec![],
}
}
@@ -175,6 +188,12 @@ impl From<&str> for InlayHintLabelPart {
}
}
#[derive(Debug, Clone)]
pub struct InlayHintTextEdit {
pub range: TextRange,
pub new_text: String,
}
pub fn inlay_hints(
db: &dyn Db,
file: File,
@@ -234,6 +253,7 @@ struct InlayHintVisitor<'a, 'db> {
in_assignment: bool,
range: TextRange,
settings: &'a InlayHintSettings,
in_no_edits_allowed: bool,
}
impl<'a, 'db> InlayHintVisitor<'a, 'db> {
@@ -245,15 +265,16 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
in_assignment: false,
range,
settings,
in_no_edits_allowed: false,
}
}
fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) {
fn add_type_hint(&mut self, expr: &Expr, ty: Type<'db>, allow_edits: bool) {
if !self.settings.variable_types {
return;
}
let inlay_hint = InlayHint::variable_type(position, ty, self.db);
let inlay_hint = InlayHint::variable_type(expr, ty, self.db, allow_edits);
self.hints.push(inlay_hint);
}
@@ -297,9 +318,13 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
match stmt {
Stmt::Assign(assign) => {
self.in_assignment = !type_hint_is_excessive_for_expr(&assign.value);
if !annotations_are_valid_syntax(assign) {
self.in_no_edits_allowed = true;
}
for target in &assign.targets {
self.visit_expr(target);
}
self.in_no_edits_allowed = false;
self.in_assignment = false;
self.visit_expr(&assign.value);
@@ -325,7 +350,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
if self.in_assignment {
if name.ctx.is_store() {
let ty = expr.inferred_type(&self.model);
self.add_type_hint(expr.range().end(), ty);
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
}
}
source_order::walk_expr(self, expr);
@@ -334,7 +359,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
if self.in_assignment {
if attribute.ctx.is_store() {
let ty = expr.inferred_type(&self.model);
self.add_type_hint(expr.range().end(), ty);
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
}
}
source_order::walk_expr(self, expr);
@@ -420,6 +445,22 @@ fn type_hint_is_excessive_for_expr(expr: &Expr) -> bool {
}
}
fn annotations_are_valid_syntax(stmt_assign: &ruff_python_ast::StmtAssign) -> bool {
if stmt_assign.targets.len() > 1 {
return false;
}
if stmt_assign
.targets
.iter()
.any(|target| matches!(target, Expr::Tuple(_)))
{
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
@@ -427,6 +468,7 @@ mod tests {
use crate::NavigationTarget;
use crate::tests::IntoDiagnostic;
use insta::{assert_snapshot, internals::SettingsBindDropGuard};
use itertools::Itertools;
use ruff_db::{
diagnostic::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig,
@@ -517,12 +559,15 @@ mod tests {
fn inlay_hints_with_settings(&mut self, settings: &InlayHintSettings) -> String {
let hints = inlay_hints(&self.db, self.file, self.range, settings);
let mut buf = source_text(&self.db, self.file).as_str().to_string();
let mut inlay_hint_buf = source_text(&self.db, self.file).as_str().to_string();
let mut text_edit_buf = inlay_hint_buf.clone();
let mut tbd_diagnostics = Vec::new();
let mut offset = 0;
let mut edit_offset = 0;
for hint in hints {
let end_position = hint.position.to_usize() + offset;
let mut hint_str = "[".to_string();
@@ -538,36 +583,65 @@ mod tests {
hint_str.push_str(part.text());
}
for edit in hint.text_edits {
let start = edit.range.start().to_usize() + edit_offset;
let end = edit.range.end().to_usize() + edit_offset;
text_edit_buf.replace_range(start..end, &edit.new_text);
if start == end {
edit_offset += edit.new_text.len();
} else {
edit_offset += edit.new_text.len() - edit.range.len().to_usize();
}
}
hint_str.push(']');
offset += hint_str.len();
buf.insert_str(end_position, &hint_str);
inlay_hint_buf.insert_str(end_position, &hint_str);
}
self.db.write_file("main2.py", &buf).unwrap();
self.db.write_file("main2.py", &inlay_hint_buf).unwrap();
let inlayed_file =
system_path_to_file(&self.db, "main2.py").expect("newly written file to existing");
let diagnostics = tbd_diagnostics.into_iter().map(|(label_range, target)| {
let location_diagnostics = tbd_diagnostics.into_iter().map(|(label_range, target)| {
InlayHintLocationDiagnostic::new(FileRange::new(inlayed_file, label_range), &target)
});
let mut rendered_diagnostics = self.render_diagnostics(diagnostics);
let mut rendered_diagnostics = location_diagnostics
.map(|diagnostic| self.render_diagnostic(diagnostic))
.join("");
if !rendered_diagnostics.is_empty() {
rendered_diagnostics = format!(
"{}{}",
crate::MarkupKind::PlainText.horizontal_line(),
rendered_diagnostics
.strip_suffix("\n")
.unwrap_or(&rendered_diagnostics)
);
}
format!("{buf}{rendered_diagnostics}",)
let rendered_edit_diagnostic = if edit_offset != 0 {
let edit_diagnostic = InlayHintEditDiagnostic::new(text_edit_buf);
let text_edit_buf = self.render_diagnostic(edit_diagnostic);
format!(
"{}{}",
crate::MarkupKind::PlainText.horizontal_line(),
text_edit_buf
)
} else {
String::new()
};
format!("{inlay_hint_buf}{rendered_diagnostics}{rendered_edit_diagnostic}",)
}
fn render_diagnostics<I, D>(&self, diagnostics: I) -> String
fn render_diagnostic<D>(&self, diagnostic: D) -> String
where
I: IntoIterator<Item = D>,
D: IntoDiagnostic,
{
use std::fmt::Write;
@@ -578,10 +652,8 @@ mod tests {
.color(false)
.format(DiagnosticFormat::Full);
for diagnostic in diagnostics {
let diag = diagnostic.into_diagnostic();
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
}
let diag = diagnostic.into_diagnostic();
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
buf
}
@@ -728,6 +800,20 @@ mod tests {
10 | bb[: Literal[b"foo"]] = aa
| ^^^^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def i(x: int, /) -> int:
return x
x = 1
y: Literal[1] = x
z: int = i(1)
w: int = z
aa = b'foo'
bb: Literal[b"foo"] = aa
"#);
}
@@ -1321,6 +1407,20 @@ mod tests {
10 | w[: tuple[int, str]] = z
| ^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def i(x: int, /) -> int:
return x
def s(x: str, /) -> str:
return x
x = (1, 'abc')
y: tuple[Literal[1], Literal["abc"]] = x
z: tuple[int, str] = (i(1), s('abc'))
w: tuple[int, str] = z
"#);
}
@@ -1654,6 +1754,18 @@ mod tests {
8 | w[: int] = z
| ^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def i(x: int, /) -> int:
return x
x: int = 1
y: Literal[1] = x
z: int = i(1)
w: int = z
"#);
}
@@ -1691,6 +1803,15 @@ mod tests {
| ^^^
5 | z = x
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def i(x: int, /) -> int:
return x
x: int = i(1)
z = x
"#);
}
@@ -1810,6 +1931,18 @@ mod tests {
8 | a.y[: int] = int(3)
| ^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class A:
def __init__(self, y):
self.x: int = int(1)
self.y: Unknown = y
a: A = A(2)
a.y: int = int(3)
"#);
}
@@ -2640,6 +2773,22 @@ mod tests {
12 | k[: list[Unknown | int | float]] = [-1, -2.0]
| ^^^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
a: list[Unknown | int] = [1, 2]
b: list[Unknown | float] = [1.0, 2.0]
c: list[Unknown | bool] = [True, False]
d: list[Unknown | None] = [None, None]
e: list[Unknown | str] = ["hel", "lo"]
f: list[Unknown | str] = ['the', 're']
g: list[Unknown | str] = [f"{ft}", f"{ft}"]
h: list[Unknown | Template] = [t"wow %d", t"wow %d"]
i: list[Unknown | bytes] = [b'/x01', b'/x02']
j: list[Unknown | int | float] = [+1, +2.0]
k: list[Unknown | int | float] = [-1, -2.0]
"#);
}
@@ -2811,6 +2960,19 @@ mod tests {
9 | c[: MyClass], d[: MyClass] = (MyClass(), MyClass())
| ^^^^^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class MyClass:
def __init__(self):
self.x: int = 1
x: MyClass = MyClass()
y: tuple[MyClass, MyClass] = (MyClass(), MyClass())
a, b = MyClass(), MyClass()
c, d = (MyClass(), MyClass())
"#);
}
@@ -3681,6 +3843,20 @@ mod tests {
10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "…
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class MyClass[T, U]:
def __init__(self, x: list[T], y: tuple[U, U]):
self.x = x
self.y = y
x: MyClass[Unknown | int, str] = MyClass([42], ("a", "b"))
y: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]] = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b")))
a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))
c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b")))
"#);
}
@@ -3836,6 +4012,20 @@ mod tests {
10 | foo([x=]val.y)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
self.x: int = 1
self.y: int = 2
val: MyClass = MyClass()
foo(val.x)
foo(val.y)
");
}
@@ -3901,6 +4091,20 @@ mod tests {
10 | foo([x=]x.y)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
self.x: int = 1
self.y: int = 2
x: MyClass = MyClass()
foo(x.x)
foo(x.y)
");
}
@@ -3969,6 +4173,22 @@ mod tests {
12 | foo([x=]val.y())
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
def x() -> int:
return 1
def y() -> int:
return 2
val: MyClass = MyClass()
foo(val.x())
foo(val.y())
");
}
@@ -4043,6 +4263,24 @@ mod tests {
14 | foo([x=]val.y()[1])
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
from typing import List
def foo(x: int): pass
class MyClass:
def __init__(self):
def x() -> List[int]:
return 1
def y() -> List[int]:
return 2
val: MyClass = MyClass()
foo(val.x()[0])
foo(val.y()[1])
");
}
@@ -4193,6 +4431,17 @@ mod tests {
7 | foo([x=]y[0])
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
x: list[Unknown | int] = [1]
y: list[Unknown | int] = [2]
foo(x[0])
foo(y[0])
"#);
}
@@ -4378,6 +4627,15 @@ mod tests {
5 | f[: Foo] = Foo([x=]1)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class Foo:
def __init__(self, x: int): pass
Foo(1)
f: Foo = Foo(1)
");
}
@@ -4450,6 +4708,15 @@ mod tests {
5 | f[: Foo] = Foo([x=]1)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class Foo:
def __new__(cls, x: int): pass
Foo(1)
f: Foo = Foo(1)
");
}
@@ -5197,6 +5464,15 @@ mod tests {
| ^^^^^^^^^^^^^
5 | my_func(x="hello")
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
from typing import LiteralString
def my_func(x: LiteralString):
y: LiteralString = x
my_func(x="hello")
"#);
}
@@ -5339,6 +5615,23 @@ mod tests {
13 | y[: Literal[1, 2, 3, "hello"] | None] = x
| ^^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def branch(cond: int):
if cond < 10:
x = 1
elif cond < 20:
x = 2
elif cond < 30:
x = 3
elif cond < 40:
x = "hello"
else:
x = None
y: Literal[1, 2, 3, "hello"] | None = x
"#);
}
@@ -5454,6 +5747,13 @@ mod tests {
3 | y[: type[list[str]]] = type(x)
| ^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def f(x: list[str]):
y: type[list[str]] = type(x)
"#);
}
@@ -5493,6 +5793,16 @@ mod tests {
6 | ab[: property] = F.whatever
| ^^^^^^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class F:
@property
def whatever(self): ...
ab: property = F.whatever
");
}
@@ -5810,6 +6120,196 @@ mod tests {
");
}
#[test]
fn test_function_signature_inlay_hint() {
let mut test = inlay_hint_test(
"
def foo(x: int, *y: bool, z: str | int | list[str]): ...
a = foo",
);
assert_snapshot!(test.inlay_hints(), @r#"
def foo(x: int, *y: bool, z: str | int | list[str]): ...
a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main2.py:4:16
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2591:7
|
2590 | @final
2591 | class bool(int):
| ^^^^
2592 | """Returns True when the argument is true, False otherwise.
2593 | The builtins True and False are the only two instances of the class bool.
|
info: Source
--> main2.py:4:25
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> 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
--> main2.py:4:37
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main2.py:4:43
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2802:7
|
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:4:49
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> 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
--> main2.py:4:54
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main2.py:4:63
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^^^^^
|
"#);
}
#[test]
fn test_module_inlay_hint() {
let mut test = inlay_hint_test(
"
import foo
a = foo",
);
test.with_extra_file("foo.py", "'''Foo module'''");
assert_snapshot!(test.inlay_hints(), @r"
import foo
a[: <module 'foo'>] = foo
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> foo.py:1:1
|
1 | '''Foo module'''
| ^^^^^^^^^^^^^^^^
|
info: Source
--> main2.py:4:5
|
2 | import foo
3 |
4 | a[: <module 'foo'>] = foo
| ^^^^^^^^^^^^^^
|
");
}
#[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,
@@ -5847,4 +6347,31 @@ mod tests {
main
}
}
struct InlayHintEditDiagnostic {
file_content: String,
}
impl InlayHintEditDiagnostic {
fn new(file_content: String) -> Self {
Self { file_content }
}
}
impl IntoDiagnostic for InlayHintEditDiagnostic {
fn into_diagnostic(self) -> Diagnostic {
let mut main = Diagnostic::new(
DiagnosticId::Lint(LintName::of("inlay-hint-edit")),
Severity::Info,
"File after edits".to_string(),
);
main.sub(SubDiagnostic::new(
SubDiagnosticSeverity::Info,
format!("{}\n{}", "Source", self.file_content),
));
main
}
}
}

View File

@@ -3,6 +3,7 @@
reason = "Prefer System trait methods over std methods in ty crates"
)]
mod all_symbols;
mod code_action;
mod completion;
mod doc_highlights;
mod docstring;
@@ -27,13 +28,16 @@ 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 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, inlay_hints};
pub use inlay_hints::{
InlayHintKind, InlayHintLabel, InlayHintSettings, InlayHintTextEdit, inlay_hints,
};
pub use markup::MarkupKind;
pub use references::ReferencesMode;
pub use rename::{can_rename, rename};

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>,
@@ -227,6 +226,22 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> {
self.check_identifier_reference(rest_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 +300,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 +315,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,162 @@ 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 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);
@@ -1061,6 +1066,17 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
}
}
}
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 +1580,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(
@@ -2595,6 +2655,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

@@ -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;

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

@@ -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

@@ -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:
@@ -2611,12 +2663,18 @@ reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown]
## 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

@@ -237,4 +237,26 @@ class Matrix:
Matrix() < Matrix()
```
## `self`-binding behaviour of function-like `Callable`s
Binding the `self` parameter of a function-like `Callable` creates a new `Callable` that is also
function-like:
`main.py`:
```py
from typing import Callable
def my_lossy_decorator(fn: Callable[..., int]) -> Callable[..., int]:
return fn
class MyClass:
@my_lossy_decorator
def method(self) -> int:
return 42
reveal_type(MyClass().method) # revealed: (...) -> int
reveal_type(MyClass().method.__name__) # revealed: str
```
[`tensorbase`]: https://github.com/pytorch/pytorch/blob/f3913ea641d871f04fa2b6588a77f63efeeb9f10/torch/_tensor.py#L1084-L1092

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

@@ -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

@@ -171,7 +171,7 @@ class Config:
import generic_a
import generic_b
# TODO should be error: [invalid-assignment] "Object of type `<class 'generic_b.Container[int]'>` is not assignable to `type[generic_a.Container[int]]`"
# error: [invalid-assignment] "Object of type `<class 'generic_b.Container[int]'>` is not assignable to `type[generic_a.Container[int]]`"
container: type[generic_a.Container[int]] = generic_b.Container[int]
```

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

@@ -444,6 +444,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

@@ -834,6 +834,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

@@ -22,8 +22,10 @@ from ty_extensions import ConstraintSet, generic_context
# fmt: off
def unbounded[T]():
# revealed: ty_extensions.Specialization[T@unbounded = object]
# revealed: ty_extensions.Specialization[T@unbounded = Unknown]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.always()))
# revealed: ty_extensions.Specialization[T@unbounded = object]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, object)))
# revealed: None
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.never()))
@@ -88,6 +90,7 @@ that makes the test succeed.
from typing import Any
def bounded_by_gradual[T: Any]():
# TODO: revealed: ty_extensions.Specialization[T@bounded_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@bounded_by_gradual = object]
reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.always()))
# revealed: None
@@ -168,12 +171,16 @@ from typing import Any
# fmt: off
def constrained_by_gradual[T: (Base, Any)]():
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Unknown]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.always()))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = object]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.always()))
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, object)))
# revealed: None
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.never()))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Base)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
@@ -181,14 +188,14 @@ def constrained_by_gradual[T: (Base, Any)]():
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Unrelated)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Super]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Super)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Super]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Super, T, Super)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = object]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Sub, T, object)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Sub]
@@ -288,7 +295,7 @@ class Unrelated: ...
# fmt: off
def mutually_bound[T: Base, U]():
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = object]
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Unknown]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.never()))
@@ -296,7 +303,7 @@ def mutually_bound[T: Base, U]():
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Base]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, U, T)))
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = object]
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = Unknown]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, T, Sub)))
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = Sub]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, T, Sub) & ConstraintSet.range(Never, U, T)))

View File

@@ -153,6 +153,12 @@ 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 f(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"))
```
### Generic classes
@@ -170,6 +176,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

@@ -11,17 +11,36 @@ valid type for use in a type expression:
```py
MyInt = int
reveal_type(MyInt) # revealed: <class 'int'>
reveal_type(MyInt(1)) # revealed: int
def f(x: MyInt):
reveal_type(x) # revealed: int
f(1)
```
This also works for generic aliases:
```py
ListOfStr = list[str]
reveal_type(ListOfStr) # revealed: <class 'list[str]'>
reveal_type(ListOfStr(["a", "b"])) # revealed: list[str]
def g(x: ListOfStr):
reveal_type(x) # revealed: list[str]
g(["a", "b"])
```
## None
```py
MyNone = None
reveal_type(MyNone) # revealed: None
def g(x: MyNone):
reveal_type(x) # revealed: None
@@ -215,6 +234,13 @@ def _(int_or_int: IntOrInt, list_of_int_or_list_of_int: ListOfIntOrListOfInt):
reveal_type(list_of_int_or_list_of_int) # revealed: list[int]
```
`types.UnionType` instances can not be instantiated:
```py
# error: [call-non-callable] "Object of type `UnionType` is not callable"
IntOrStr()
```
`NoneType` has no special or-operator behavior, so this is an error:
```py
@@ -619,7 +645,7 @@ def _(weird: IntLiteral1[int]):
## `Annotated`
Basic usage:
### Basic usage
```py
from typing import Annotated
@@ -646,10 +672,61 @@ class C:
reveal_type(C().old) # revealed: @Todo(Generic specialization of typing.Annotated)
```
### Special members
The `__origin__` attribute on an instance of `Annotated` can be used to access the underlying type.
We currently do not model this precisely:
```py
from typing import Annotated
StrWithMetadata = Annotated[str, "metadata", 1, 2, 3]
reveal_type(StrWithMetadata.__origin__) # revealed: type | TypeAliasType
```
At runtime, the `__metadata__` attribute contains the metadata elements `('metadata', 1, 2, 3)`, but
we do not model the type of this attribute precisely:
```py
reveal_type(StrWithMetadata.__metadata__) # revealed: Any
```
### Attribute access and instantiation
A class can be instantiated through an `Annotated` alias, and attributes can be accessed on the
created instance:
```py
from __future__ import annotations
from typing import Annotated, Union
class Foo:
attribute: int = 1
FooWithMetadata = Annotated[Foo, "metadata"]
c = FooWithMetadata()
reveal_type(c) # revealed: Foo
reveal_type(c.attribute) # revealed: int
```
However, accessing attributes on the alias itself currently yields `Any`, instead of the attribute's
type:
```py
reveal_type(FooWithMetadata.attribute) # revealed: Any
```
### Error cases
If the metadata argument is missing, we emit an error (because this code fails at runtime), but
still use the first element as the type, when used in annotations:
```py
from typing import Annotated
# error: [invalid-type-form] "Special form `typing.Annotated` expected at least 2 arguments (one type and at least one metadata element)"
WronglyAnnotatedInt = Annotated[int]

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

@@ -523,3 +523,63 @@ class Baz(NamedTuple):
class Spam(Baz):
def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
```
## Staticmethods and classmethods
Methods decorated with `@staticmethod` or `@classmethod` are checked in much the same way as other
methods.
<!-- snapshot-diagnostics -->
```pyi
class Parent:
def instance_method(self, x: int) -> int: ...
@classmethod
def class_method(cls, x: int) -> int: ...
@staticmethod
def static_method(x: int) -> int: ...
class BadChild1(Parent):
@staticmethod
def instance_method(self, x: int) -> int: ... # error: [invalid-method-override]
# TODO: we should emit `invalid-method-override` here.
# Although the method has the same signature as `Parent.class_method`
# when accessed on instances, it does not have the same signature as
# `Parent.class_method` when accessed on the class object itself
def class_method(cls, x: int) -> int: ...
def static_method(x: int) -> int: ... # error: [invalid-method-override]
class BadChild2(Parent):
# TODO: we should emit `invalid-method-override` here.
# Although the method has the same signature as `Parent.class_method`
# when accessed on instances, it does not have the same signature as
# `Parent.class_method` when accessed on the class object itself.
#
# Note that whereas `BadChild1.class_method` is reported as a Liskov violation by
# mypy, pyright and pyrefly, pyright is the only one of those three to report a
# Liskov violation on this method as of 2025-11-23.
@classmethod
def instance_method(self, x: int) -> int: ...
@staticmethod
def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
@classmethod
def static_method(x: int) -> int: ... # error: [invalid-method-override]
class BadChild3(Parent):
@classmethod
def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
@staticmethod
def static_method(x: bool) -> object: ... # error: [invalid-method-override]
class GoodChild1(Parent):
@classmethod
def class_method(cls, x: int) -> int: ...
@staticmethod
def static_method(x: int) -> int: ...
class GoodChild2(Parent):
@classmethod
def class_method(cls, x: object) -> bool: ...
@staticmethod
def static_method(x: object) -> bool: ...
```

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

@@ -144,17 +144,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: @Todo(specialized generic alias in type expression)
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

View File

@@ -2,10 +2,7 @@
Regression test for <https://github.com/astral-sh/ty/issues/1377>.
The code is an excerpt from <https://github.com/Gobot1234/steam.py> that is minimal enough to
trigger the iteration count mismatch bug in Salsa.
<!-- expect-panic: execute: too many cycle iterations -->
The code is an excerpt from <https://github.com/Gobot1234/steam.py>.
```toml
[environment]

View File

@@ -34,5 +34,11 @@ error[invalid-key]: Unknown key "Retries" for TypedDict `Config`
| TypedDict `Config`
|
info: rule `invalid-key` is enabled by default
4 | retries: int
5 |
6 | def _(config: Config) -> None:
- config["Retries"] = 30.0 # error: [invalid-key]
7 + config["retries"] = 30.0 # error: [invalid-key]
note: This is an unsafe fix and may change runtime behavior
```

View File

@@ -42,6 +42,12 @@ error[invalid-key]: Unknown key "surname" for TypedDict `Person`
| TypedDict `Person` in union type `Person | Animal`
|
info: rule `invalid-key` is enabled by default
11 | def _(being: Person | Animal) -> None:
12 | # error: [invalid-key]
13 | # error: [invalid-key]
- being["surname"] = "unknown"
14 + being["name"] = "unknown"
note: This is an unsafe fix and may change runtime behavior
```
@@ -57,5 +63,11 @@ error[invalid-key]: Unknown key "surname" for TypedDict `Animal`
| TypedDict `Animal` in union type `Person | Animal`
|
info: rule `invalid-key` is enabled by default
11 | def _(being: Person | Animal) -> None:
12 | # error: [invalid-key]
13 | # error: [invalid-key]
- being["surname"] = "unknown"
14 + being["name"] = "unknown"
note: This is an unsafe fix and may change runtime behavior
```

View File

@@ -34,29 +34,37 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
# Diagnostics
```
error[invalid-type-form]: Variable of type `<module 'datetime'>` is not allowed in a type expression
error[invalid-type-form]: Module `datetime` is not valid in a type expression
--> src/foo.py:3:10
|
1 | import datetime
2 |
3 | def f(x: datetime): ... # error: [invalid-type-form]
| ^^^^^^^^
| ^^^^^^^^ Did you mean to use the module's member `datetime.datetime`?
|
info: Did you mean to use the module's member `datetime.datetime` instead?
info: rule `invalid-type-form` is enabled by default
1 | import datetime
2 |
- def f(x: datetime): ... # error: [invalid-type-form]
3 + def f(x: datetime.datetime): ... # error: [invalid-type-form]
note: This is an unsafe fix and may change runtime behavior
```
```
error[invalid-type-form]: Variable of type `<module 'PIL.Image'>` is not allowed in a type expression
error[invalid-type-form]: Module `PIL.Image` is not valid in a type expression
--> src/bar.py:3:10
|
1 | from PIL import Image
2 |
3 | def g(x: Image): ... # error: [invalid-type-form]
| ^^^^^
| ^^^^^ Did you mean to use the module's member `Image.Image`?
|
info: Did you mean to use the module's member `Image.Image` instead?
info: rule `invalid-type-form` is enabled by default
1 | from PIL import Image
2 |
- def g(x: Image): ... # error: [invalid-type-form]
3 + def g(x: Image.Image): ... # error: [invalid-type-form]
note: This is an unsafe fix and may change runtime behavior
```

View File

@@ -23,13 +23,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assi
# Diagnostics
```
error[invalid-assignment]: Object of type `tuple[Literal["a"], Literal["b"]]` is not assignable to `int`
error[invalid-assignment]: Object of type `Literal["a"]` is not assignable to `int`
--> src/mdtest_snippet.py:4:1
|
2 | y: str
3 |
4 | x, y = ("a", "b") # error: [invalid-assignment]
| - ^^^^^^^^^^ Incompatible value of type `tuple[Literal["a"], Literal["b"]]`
| - ^^^^^^^^^^ Incompatible value of type `Literal["a"]`
| |
| Declared type `int`
5 |
@@ -40,13 +40,13 @@ info: rule `invalid-assignment` is enabled by default
```
```
error[invalid-assignment]: Object of type `tuple[Literal[0], Literal[0]]` is not assignable to `str`
error[invalid-assignment]: Object of type `Literal[0]` is not assignable to `str`
--> src/mdtest_snippet.py:6:4
|
4 | x, y = ("a", "b") # error: [invalid-assignment]
5 |
6 | x, y = (0, 0) # error: [invalid-assignment]
| - ^^^^^^ Incompatible value of type `tuple[Literal[0], Literal[0]]`
| - ^^^^^^ Incompatible value of type `Literal[0]`
| |
| Declared type `str`
|

View File

@@ -0,0 +1,220 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Staticmethods and classmethods
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## mdtest_snippet.pyi
```
1 | class Parent:
2 | def instance_method(self, x: int) -> int: ...
3 | @classmethod
4 | def class_method(cls, x: int) -> int: ...
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
7 |
8 | class BadChild1(Parent):
9 | @staticmethod
10 | def instance_method(self, x: int) -> int: ... # error: [invalid-method-override]
11 | # TODO: we should emit `invalid-method-override` here.
12 | # Although the method has the same signature as `Parent.class_method`
13 | # when accessed on instances, it does not have the same signature as
14 | # `Parent.class_method` when accessed on the class object itself
15 | def class_method(cls, x: int) -> int: ...
16 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
17 |
18 | class BadChild2(Parent):
19 | # TODO: we should emit `invalid-method-override` here.
20 | # Although the method has the same signature as `Parent.class_method`
21 | # when accessed on instances, it does not have the same signature as
22 | # `Parent.class_method` when accessed on the class object itself.
23 | #
24 | # Note that whereas `BadChild1.class_method` is reported as a Liskov violation by
25 | # mypy, pyright and pyrefly, pyright is the only one of those three to report a
26 | # Liskov violation on this method as of 2025-11-23.
27 | @classmethod
28 | def instance_method(self, x: int) -> int: ...
29 | @staticmethod
30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
31 | @classmethod
32 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
33 |
34 | class BadChild3(Parent):
35 | @classmethod
36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
37 | @staticmethod
38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override]
39 |
40 | class GoodChild1(Parent):
41 | @classmethod
42 | def class_method(cls, x: int) -> int: ...
43 | @staticmethod
44 | def static_method(x: int) -> int: ...
45 |
46 | class GoodChild2(Parent):
47 | @classmethod
48 | def class_method(cls, x: object) -> bool: ...
49 | @staticmethod
50 | def static_method(x: object) -> bool: ...
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `instance_method`
--> src/mdtest_snippet.pyi:10:9
|
8 | class BadChild1(Parent):
9 | @staticmethod
10 | def instance_method(self, x: int) -> int: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.instance_method`
11 | # TODO: we should emit `invalid-method-override` here.
12 | # Although the method has the same signature as `Parent.class_method`
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Parent:
2 | def instance_method(self, x: int) -> int: ...
| ------------------------------------ `Parent.instance_method` defined here
3 | @classmethod
4 | def class_method(cls, x: int) -> int: ...
|
info: `BadChild1.instance_method` is a staticmethod but `Parent.instance_method` is an instance method
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `static_method`
--> src/mdtest_snippet.pyi:16:9
|
14 | # `Parent.class_method` when accessed on the class object itself
15 | def class_method(cls, x: int) -> int: ...
16 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method`
17 |
18 | class BadChild2(Parent):
|
::: src/mdtest_snippet.pyi:6:9
|
4 | def class_method(cls, x: int) -> int: ...
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
| ---------------------------- `Parent.static_method` defined here
7 |
8 | class BadChild1(Parent):
|
info: `BadChild1.static_method` is an instance method but `Parent.static_method` is a staticmethod
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `class_method`
--> src/mdtest_snippet.pyi:30:9
|
28 | def instance_method(self, x: int) -> int: ...
29 | @staticmethod
30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.class_method`
31 | @classmethod
32 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
|
::: src/mdtest_snippet.pyi:4:9
|
2 | def instance_method(self, x: int) -> int: ...
3 | @classmethod
4 | def class_method(cls, x: int) -> int: ...
| -------------------------------- `Parent.class_method` defined here
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
|
info: `BadChild2.class_method` is a staticmethod but `Parent.class_method` is a classmethod
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `static_method`
--> src/mdtest_snippet.pyi:32:9
|
30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
31 | @classmethod
32 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method`
33 |
34 | class BadChild3(Parent):
|
::: src/mdtest_snippet.pyi:6:9
|
4 | def class_method(cls, x: int) -> int: ...
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
| ---------------------------- `Parent.static_method` defined here
7 |
8 | class BadChild1(Parent):
|
info: `BadChild2.static_method` is a classmethod but `Parent.static_method` is a staticmethod
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `class_method`
--> src/mdtest_snippet.pyi:36:9
|
34 | class BadChild3(Parent):
35 | @classmethod
36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.class_method`
37 | @staticmethod
38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override]
|
::: src/mdtest_snippet.pyi:4:9
|
2 | def instance_method(self, x: int) -> int: ...
3 | @classmethod
4 | def class_method(cls, x: int) -> int: ...
| -------------------------------- `Parent.class_method` defined here
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `static_method`
--> src/mdtest_snippet.pyi:38:9
|
36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
37 | @staticmethod
38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method`
39 |
40 | class GoodChild1(Parent):
|
::: src/mdtest_snippet.pyi:6:9
|
4 | def class_method(cls, x: int) -> int: ...
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
| ---------------------------- `Parent.static_method` defined here
7 |
8 | class BadChild1(Parent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,52 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: literal_string.md - `LiteralString` - Usages - Parameterized
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import LiteralString
2 |
3 | # error: [invalid-type-form]
4 | a: LiteralString[str]
5 |
6 | # error: [invalid-type-form]
7 | b: LiteralString["foo"]
```
# Diagnostics
```
error[invalid-type-form]: `LiteralString` expects no type parameter
--> src/mdtest_snippet.py:4:4
|
3 | # error: [invalid-type-form]
4 | a: LiteralString[str]
| ^^^^^^^^^^^^^^^^^^
5 |
6 | # error: [invalid-type-form]
|
info: rule `invalid-type-form` is enabled by default
```
```
error[invalid-type-form]: `LiteralString` expects no type parameter
--> src/mdtest_snippet.py:7:4
|
6 | # error: [invalid-type-form]
7 | b: LiteralString["foo"]
| -------------^^^^^^^
| |
| Did you mean `Literal`?
|
info: rule `invalid-type-form` is enabled by default
```

View File

@@ -312,6 +312,15 @@ info[unused-ignore-comment]: Unused blanket `type: ignore` directive
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
73 | ): ...
|
help: Remove the unused suppression comment
69 | class D(
70 | A,
71 | # error: [unused-ignore-comment]
- A, # type: ignore[duplicate-base]
72 + A,
73 | ): ...
74 |
75 | # error: [duplicate-base]
```
@@ -356,5 +365,13 @@ info[unused-ignore-comment]: Unused blanket `type: ignore` directive
82 |
83 | # fmt: on
|
help: Remove the unused suppression comment
78 | A
79 | ):
80 | # error: [unused-ignore-comment]
- x: int # type: ignore[duplicate-base]
81 + x: int
82 |
83 | # fmt: on
```

View File

@@ -0,0 +1,293 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: override.md - `typing.override` - Basics
mdtest path: crates/ty_python_semantic/resources/mdtest/override.md
---
# Python source files
## mdtest_snippet.pyi
```
1 | from typing_extensions import override, Callable, TypeVar
2 |
3 | def lossy_decorator(fn: Callable) -> Callable: ...
4 |
5 | class A:
6 | @override
7 | def __repr__(self): ... # fine: overrides `object.__repr__`
8 |
9 | class Parent:
10 | def foo(self): ...
11 | @property
12 | def my_property1(self) -> int: ...
13 | @property
14 | def my_property2(self) -> int: ...
15 | baz = None
16 | @classmethod
17 | def class_method1(cls) -> int: ...
18 | @staticmethod
19 | def static_method1() -> int: ...
20 | @classmethod
21 | def class_method2(cls) -> int: ...
22 | @staticmethod
23 | def static_method2() -> int: ...
24 | @lossy_decorator
25 | def decorated_1(self): ...
26 | @lossy_decorator
27 | def decorated_2(self): ...
28 | @lossy_decorator
29 | def decorated_3(self): ...
30 |
31 | class Child(Parent):
32 | @override
33 | def foo(self): ... # fine: overrides `Parent.foo`
34 | @property
35 | @override
36 | def my_property1(self) -> int: ... # fine: overrides `Parent.my_property1`
37 | @override
38 | @property
39 | def my_property2(self) -> int: ... # fine: overrides `Parent.my_property2`
40 | @override
41 | def baz(self): ... # fine: overrides `Parent.baz`
42 | @classmethod
43 | @override
44 | def class_method1(cls) -> int: ... # fine: overrides `Parent.class_method1`
45 | @staticmethod
46 | @override
47 | def static_method1() -> int: ... # fine: overrides `Parent.static_method1`
48 | @override
49 | @classmethod
50 | def class_method2(cls) -> int: ... # fine: overrides `Parent.class_method2`
51 | @override
52 | @staticmethod
53 | def static_method2() -> int: ... # fine: overrides `Parent.static_method2`
54 | @override
55 | def decorated_1(self): ... # fine: overrides `Parent.decorated_1`
56 | @override
57 | @lossy_decorator
58 | def decorated_2(self): ... # fine: overrides `Parent.decorated_2`
59 | @lossy_decorator
60 | @override
61 | def decorated_3(self): ... # fine: overrides `Parent.decorated_3`
62 |
63 | class OtherChild(Parent): ...
64 |
65 | class Grandchild(OtherChild):
66 | @override
67 | def foo(self): ... # fine: overrides `Parent.foo`
68 | @override
69 | @property
70 | def bar(self) -> int: ... # fine: overrides `Parent.bar`
71 | @override
72 | def baz(self): ... # fine: overrides `Parent.baz`
73 | @classmethod
74 | @override
75 | def class_method1(cls) -> int: ... # fine: overrides `Parent.class_method1`
76 | @staticmethod
77 | @override
78 | def static_method1() -> int: ... # fine: overrides `Parent.static_method1`
79 | @override
80 | @classmethod
81 | def class_method2(cls) -> int: ... # fine: overrides `Parent.class_method2`
82 | @override
83 | @staticmethod
84 | def static_method2() -> int: ... # fine: overrides `Parent.static_method2`
85 | @override
86 | def decorated_1(self): ... # fine: overrides `Parent.decorated_1`
87 | @override
88 | @lossy_decorator
89 | def decorated_2(self): ... # fine: overrides `Parent.decorated_2`
90 | @lossy_decorator
91 | @override
92 | def decorated_3(self): ... # fine: overrides `Parent.decorated_3`
93 |
94 | class Invalid:
95 | @override
96 | def ___reprrr__(self): ... # error: [invalid-explicit-override]
97 | @override
98 | @classmethod
99 | def foo(self): ... # error: [invalid-explicit-override]
100 | @classmethod
101 | @override
102 | def bar(self): ... # error: [invalid-explicit-override]
103 | @staticmethod
104 | @override
105 | def baz(): ... # error: [invalid-explicit-override]
106 | @override
107 | @staticmethod
108 | def eggs(): ... # error: [invalid-explicit-override]
109 | @property
110 | @override
111 | def bad_property1(self) -> int: ... # TODO: should emit `invalid-explicit-override` here
112 | @override
113 | @property
114 | def bad_property2(self) -> int: ... # TODO: should emit `invalid-explicit-override` here
115 | @lossy_decorator
116 | @override
117 | def lossy(self): ... # TODO: should emit `invalid-explicit-override` here
118 | @override
119 | @lossy_decorator
120 | def lossy2(self): ... # TODO: should emit `invalid-explicit-override` here
121 |
122 | # TODO: all overrides in this class should cause us to emit *Liskov* violations,
123 | # but not `@override` violations
124 | class LiskovViolatingButNotOverrideViolating(Parent):
125 | @override
126 | @property
127 | def foo(self) -> int: ...
128 | @override
129 | def my_property1(self) -> int: ...
130 | @staticmethod
131 | @override
132 | def class_method1() -> int: ...
133 | @classmethod
134 | @override
135 | def static_method1(cls) -> int: ...
136 |
137 | # Diagnostic edge case: `override` is very far away from the method definition in the source code:
138 |
139 | T = TypeVar("T")
140 |
141 | def identity(x: T) -> T: ...
142 |
143 | class Foo:
144 | @override
145 | @identity
146 | @identity
147 | @identity
148 | @identity
149 | @identity
150 | @identity
151 | @identity
152 | @identity
153 | @identity
154 | @identity
155 | @identity
156 | @identity
157 | @identity
158 | @identity
159 | @identity
160 | @identity
161 | @identity
162 | @identity
163 | def bar(self): ... # error: [invalid-explicit-override]
```
# Diagnostics
```
error[invalid-explicit-override]: Method `___reprrr__` is decorated with `@override` but does not override anything
--> src/mdtest_snippet.pyi:95:5
|
94 | class Invalid:
95 | @override
| ---------
96 | def ___reprrr__(self): ... # error: [invalid-explicit-override]
| ^^^^^^^^^^^
97 | @override
98 | @classmethod
|
info: No `___reprrr__` definitions were found on any superclasses of `Invalid`
info: rule `invalid-explicit-override` is enabled by default
```
```
error[invalid-explicit-override]: Method `foo` is decorated with `@override` but does not override anything
--> src/mdtest_snippet.pyi:97:5
|
95 | @override
96 | def ___reprrr__(self): ... # error: [invalid-explicit-override]
97 | @override
| ---------
98 | @classmethod
99 | def foo(self): ... # error: [invalid-explicit-override]
| ^^^
100 | @classmethod
101 | @override
|
info: No `foo` definitions were found on any superclasses of `Invalid`
info: rule `invalid-explicit-override` is enabled by default
```
```
error[invalid-explicit-override]: Method `bar` is decorated with `@override` but does not override anything
--> src/mdtest_snippet.pyi:101:5
|
99 | def foo(self): ... # error: [invalid-explicit-override]
100 | @classmethod
101 | @override
| ---------
102 | def bar(self): ... # error: [invalid-explicit-override]
| ^^^
103 | @staticmethod
104 | @override
|
info: No `bar` definitions were found on any superclasses of `Invalid`
info: rule `invalid-explicit-override` is enabled by default
```
```
error[invalid-explicit-override]: Method `baz` is decorated with `@override` but does not override anything
--> src/mdtest_snippet.pyi:104:5
|
102 | def bar(self): ... # error: [invalid-explicit-override]
103 | @staticmethod
104 | @override
| ---------
105 | def baz(): ... # error: [invalid-explicit-override]
| ^^^
106 | @override
107 | @staticmethod
|
info: No `baz` definitions were found on any superclasses of `Invalid`
info: rule `invalid-explicit-override` is enabled by default
```
```
error[invalid-explicit-override]: Method `eggs` is decorated with `@override` but does not override anything
--> src/mdtest_snippet.pyi:106:5
|
104 | @override
105 | def baz(): ... # error: [invalid-explicit-override]
106 | @override
| ---------
107 | @staticmethod
108 | def eggs(): ... # error: [invalid-explicit-override]
| ^^^^
109 | @property
110 | @override
|
info: No `eggs` definitions were found on any superclasses of `Invalid`
info: rule `invalid-explicit-override` is enabled by default
```
```
error[invalid-explicit-override]: Method `bar` is decorated with `@override` but does not override anything
--> src/mdtest_snippet.pyi:163:9
|
161 | @identity
162 | @identity
163 | def bar(self): ... # error: [invalid-explicit-override]
| ^^^
|
::: src/mdtest_snippet.pyi:144:5
|
143 | class Foo:
144 | @override
| ---------
145 | @identity
146 | @identity
|
info: No `bar` definitions were found on any superclasses of `Foo`
info: rule `invalid-explicit-override` is enabled by default
```

View File

@@ -0,0 +1,109 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: ty_ignore.md - Suppressing errors with `ty: ignore` - Multiple unused comments
mdtest path: crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md
---
# Python source files
## mdtest_snippet.py
```
1 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive"
2 | a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference]
3 |
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
6 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
7 |
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
9 | a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
```
# Diagnostics
```
info[unused-ignore-comment]: Unused `ty: ignore` directive
--> src/mdtest_snippet.py:2:13
|
1 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive"
2 | a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 |
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
help: Remove the unused suppression comment
1 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive"
- a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference]
2 + a = 10 / 2
3 |
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
```
```
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment'
--> src/mdtest_snippet.py:6:26
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
6 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
| ^^^^^^^^^^^^^^^^^^
7 |
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
help: Remove the unused suppression code
3 |
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
- a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
6 + a = 10 / 0 # ty: ignore[division-by-zero, unresolved-reference]
7 |
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
9 | a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
```
```
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'unresolved-reference'
--> src/mdtest_snippet.py:6:64
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
6 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
| ^^^^^^^^^^^^^^^^^^^^
7 |
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
help: Remove the unused suppression code
3 |
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
- a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
6 + a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero]
7 |
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
9 | a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
```
```
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'
--> src/mdtest_snippet.py:9:26
|
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
9 | a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Remove the unused suppression codes
6 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
7 |
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
- a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
9 + a = 10 / 0 # ty: ignore[division-by-zero]
```

View File

@@ -0,0 +1,72 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: type_ignore.md - Suppressing errors with `type: ignore` - Nested comments
mdtest path: crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md
---
# Python source files
## mdtest_snippet.py
```
1 | # fmt: off
2 | a = test /
3 | + 2 # fmt: skip # type: ignore
4 |
5 | a = test /
6 | + 2 # type: ignore # fmt: skip
7 |
8 | a = (3
9 | # error: [unused-ignore-comment]
10 | + 2) # ty:ignore[division-by-zero] # fmt: skip
11 |
12 | a = (3
13 | # error: [unused-ignore-comment]
14 | + 2) # fmt: skip # ty:ignore[division-by-zero]
```
# Diagnostics
```
info[unused-ignore-comment]: Unused `ty: ignore` directive
--> src/mdtest_snippet.py:10:9
|
8 | a = (3
9 | # error: [unused-ignore-comment]
10 | + 2) # ty:ignore[division-by-zero] # fmt: skip
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 |
12 | a = (3
|
help: Remove the unused suppression comment
7 |
8 | a = (3
9 | # error: [unused-ignore-comment]
- + 2) # ty:ignore[division-by-zero] # fmt: skip
10 + + 2) # fmt: skip
11 |
12 | a = (3
13 | # error: [unused-ignore-comment]
```
```
info[unused-ignore-comment]: Unused `ty: ignore` directive
--> src/mdtest_snippet.py:14:21
|
12 | a = (3
13 | # error: [unused-ignore-comment]
14 | + 2) # fmt: skip # ty:ignore[division-by-zero]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Remove the unused suppression comment
11 |
12 | a = (3
13 | # error: [unused-ignore-comment]
- + 2) # fmt: skip # ty:ignore[division-by-zero]
14 + + 2) # fmt: skip
```

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