Compare commits

...

57 Commits

Author SHA1 Message Date
Micha Reiser
68c8cb93d6 Use internal Salsa APIs to make it compile 2025-12-26 11:20:00 +01:00
Micha Reiser
4bd405e1c5 [ty] Test performance and memory impact of shrinking Salsa's DB key 2025-12-26 11:09:33 +01:00
Micha Reiser
9693375e10 [ty] Reduce monomorphization (#22195) 2025-12-26 10:02:20 +01:00
Matthew Mckee
1ec3503cc3 [ty] Fix playground inlay hint location (#22200) 2025-12-26 09:20:57 +01:00
Alex Waygood
19b10993e1 [ty] Automatically re-run ecosystem-analyzer workflow on subsequent pushes to a PR, if the PR has the ecosystem-analyzer label (#22179)
## Summary

This PR reworks our ecosystem-analyzer workflow so that it automatically
reruns if a PR with the `ecosystem-analyzer` label has new commits
pushed to it, or is reopened after previously being closed. It's
currently easy to forget that you need to remove and re-add the label to
trigger a fresh workflow run, which can then mean that there are stale
(misleading) results in the PR comment posted by the bot. It also means
that it takes longer for CI to finish than it would otherwise, because
it might be a few minutes after pushing new commits to the PR before you
remember that you also need to remove and re-add the label.

To write this PR, I consulted:
- The GitHub workflow trigger documentation:
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request
- This Stack Overflow answer:
https://stackoverflow.com/a/59588725/13990016

## Test Plan

I experimented with pushing commits to this PR and closing/reopening it,
and both of these actions triggered fresh runs of the ecosystem-analyzer
worfklow when the label was present on the PR. However, removing the
label again meant that the workflow was no longer triggered by these
actions.
2025-12-25 17:41:19 +00:00
Micha Reiser
014abe1ee1 [ty] Fix completion in decorators with missing declaration (#22177) 2025-12-25 15:05:47 +00:00
Simon Lamon
dd3a985109 [ty] Spell out "method resolution order" in unsupported-base subdiagnostic (#22194) 2025-12-25 10:26:44 +00:00
Micha Reiser
12f5ea51e3 [ty] Invert dependencies of ty_combine and ty_python_semantic (#22191) 2025-12-25 10:06:06 +01:00
Alex Waygood
f9afcc400c [ty] Improve diagnostic when a user tries to access a function attribute on a Callable type (#22182)
## Summary

Other type checkers allow you to access all `FunctionType` attributes on
any object with a `Callable` type. ty does not, because this is
demonstrably unsound, but this is often a source of confusion for users.
And there were lots of diagnostics in the ecosystem report for
https://github.com/astral-sh/ruff/pull/22145 that were complaining that
"Object of type `(...) -> Unknown` has no attribute `__name__`", for
example.

The discrepancy between what ty does here and what other type checkers
do is discussed a bit in https://github.com/astral-sh/ty/issues/1495.
You can see that there have been lots of issues closed as duplicates of
that issue; we should probably also add an FAQ entry for it.

Anyway, this PR adds a subdiagnostic to help users out when they hit
this diagnostic. Unfortunately something I did meant that rustfmt
increased the indentation of the whole of this huge closure, so this PR
is best reviewed with the "No whitespace" option selected for viewing
the diff.

## Test Plan

Snapshot added
2025-12-24 15:47:11 -05:00
Alex Waygood
768c5a2285 [ty] Use Type::string_literal() more (#22184) 2025-12-24 20:06:57 +00:00
Alex Waygood
139149f87b [ty] Improve diagnostic when callable is used in a type expression instead of collections.abc.Callable or typing.Callable (#22180) 2025-12-24 19:18:51 +00:00
Charlie Marsh
2de4464e92 [ty] Fix implementation of Top[Callable[..., object]] (#22145)
## Summary

Add a proper representation for the `Callable` top type, and use it to
get `callable()` narrowing right.

Closes https://github.com/astral-sh/ty/issues/1426.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-12-24 12:49:09 -05:00
Micha Reiser
ded4d4bbe9 [ty] Fix classification of module in import x as y (#22175) 2025-12-24 18:25:29 +01:00
Micha Reiser
eef403f6cf Revert "[ty] Fix completion in decorator without class or function definition" (#22176) 2025-12-24 17:27:21 +01:00
Micha Reiser
adef89eb7c [ty] Fix completion in decorator without class or function definition 2025-12-24 16:57:36 +01:00
Micha Reiser
e3498121b4 [ty] Fix module resolution on network drives (#22173) 2025-12-24 16:36:18 +01:00
Brent Westbrook
5e7fc9a4e1 Render the entire diagnostic message in all output formats (#22164)
Summary
--

This PR fixes https://github.com/astral-sh/ty/issues/2186 by replacing
uses of
`Diagnostic::body` with [`Diagnostic::concise_message`][d]. The initial
report
was only about ty's GitHub and GitLab output formats, but I think it
makes sense
to prefer `concise_message` in the other output formats too. Ruff
currently only
sets the primary message on its diagnostics, which is why this has no
effect on
Ruff, and ty currently only supports the GitHub and GitLab formats that
used
`body`, but removing `body` should help to avoid this problem as Ruff
starts to
use more diagnostic features or ty gets new output formats.

Test Plan
--

Updated existing GitLab and GitHub CLI tests to have `reveal_type`
diagnostics

[d]:
https://github.com/astral-sh/ruff/blob/395bf106ab/crates/ruff_db/src/diagnostic/mod.rs#L185
[t]:
https://github.com/astral-sh/ruff/blob/395bf106ab/crates/ruff/tests/cli/lint.rs#L3509
2025-12-24 10:28:24 -05:00
Alex Waygood
3c5956e93d [ty] Include the specialization of a generic TypedDict as part of its display (#22174)
## Summary

This is the easy bit of https://github.com/astral-sh/ty/issues/2190

## Test Plan

mdtests updated
2025-12-24 14:39:46 +00:00
Charlie Marsh
81f34fbc8e [ty] Store un-widened type in Place (#22093)
## Summary

See: https://github.com/astral-sh/ruff/pull/22025#discussion_r2632724156
2025-12-23 23:19:57 -05:00
Charlie Marsh
184f487c84 [ty] Add a dedicated diagnostic for TypedDict deletions (#22123)
## Summary

Provides a message like:

```
  error[invalid-argument-type]: Cannot delete required key "name" from TypedDict `Movie`
    --> test.py:15:7
     |
  15 | del m["name"]
     |       ^^^^^^
     |
  info: Field defined here
   --> test.py:4:5
    |
  4 |     name: str
    |     --------- `name` declared as required here; consider making it `NotRequired`
    |
  info: Only keys marked as `NotRequired` (or in a TypedDict with `total=False`) can be deleted
```
2025-12-24 03:49:42 +00:00
Charlie Marsh
969c8a547e [ty] Synthesize __delitem__ for TypedDict to allow deleting non-required keys (#22122)
## Summary

TypedDict now synthesizes a proper `__delitem__` method that...

- ...allows deletion of `NotRequired` keys and keys in `total=False`
TypedDicts.
- ...rejects deletion of required keys (synthesizes `__delitem__(k:
Never)`).
2025-12-24 03:39:54 +00:00
Charlie Marsh
acdda78189 [ty] Fix @staticmethod combined with other decorators incorrectly binding self (#22128)
## Summary

We already had `CallableTypeKind::ClassMethodLike` to track callables
that behave like `classmethods` (always bind the first argument). This
PR adds the symmetric `CallableTypeKind::StaticMethodLike` for callables
that behave like `staticmethods` (never bind `self`).

Closes https://github.com/astral-sh/ty/issues/2114.
2025-12-24 03:35:09 +00:00
Charlie Marsh
c28c1f534d [ty] Check __delitem__ instead of __getitem__ for del x[k] (#22121)
## Summary

Previously, `del x[k]` incorrectly required the object to have a
`__getitem__` method. This was wrong because deletion only needs
`__delitem__`, which is independent of `__getitem__`.

Closes https://github.com/astral-sh/ty/issues/1799.
2025-12-24 03:34:20 +00:00
Charlie Marsh
b723917463 [ty] Support tuple narrowing based on member checks (#22167)
## Summary

Closes https://github.com/astral-sh/ty/issues/2179.
2025-12-23 20:15:50 -05:00
Charlie Marsh
5decf94644 [ty] Synthesize a _fields attribute for NamedTuples (#22163)
## Summary

Closes #2176.
2025-12-23 21:38:41 +00:00
Charlie Marsh
89a55dd09f [ty] Synthesize a _replace method for NamedTuples (#22153)
## Summary

Closes https://github.com/astral-sh/ty/issues/2170.
2025-12-23 16:33:55 -05:00
Shunsuke Shibayama
8710a8c4ac [ty] don't expand type aliases in implicit tuple aliases (#22015)
## Summary

This PR fixes https://github.com/astral-sh/ty/issues/1848.

```python
T = tuple[int, 'U']

class C(set['U']):
    pass

type U = T | C
```

The reason why the fixed point iteration did not converge was because
the types stored in the implicit tuple type alias `Specialization`
changed each time.

```
1st: <class 'tuple[int, C]'>
2nd: <class 'tuple[int, tuple[int, C] | C]'>
3rd: <class 'tuple[int, tuple[int, tuple[int, C] | C] | C]'>
...
```

And this was because `UnionType::from_elements` was used when creating
union types for tuple operations, which causes type aliases inside to be
expanded.
This PR replaces these with `UnionType::from_elements_leave_aliases`.

## Test Plan

New corpus test
2025-12-23 13:22:54 -08:00
Jack O'Connor
e245c1d76e [ty] narrow tagged unions of TypedDict (#22104)
Identify and narrow cases like this:

```py
class Foo(TypedDict):
    tag: Literal["foo"]

class Bar(TypedDict):
    tag: Literal["bar"]

def _(union: Foo | Bar):
    if union["tag"] == "foo":
        reveal_type(union)  # Foo
```

Fixes part of https://github.com/astral-sh/ty/issues/1479.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-12-23 19:30:08 +00:00
Charlie Marsh
4c175fa0e1 [ty] Bind self with instance in __get__ (#22155)
## Summary

See: https://github.com/astral-sh/ruff/pull/22153/changes#r2641788438.
2025-12-23 11:25:58 -08:00
Micha Reiser
ed64c4d943 [ty] Abort printing diagnostics when pressing Ctrl+C (#22083) 2025-12-23 18:03:58 +01:00
Matthew Mckee
f1e6c9c3a0 [ty] Use markdown for completions documentation (#21752) 2025-12-23 17:07:53 +01:00
Wizzerinus | Alex K.
d9fe996e64 [ty] Support custom builtins (#22021) 2025-12-23 13:48:14 +00:00
Matthew Mckee
5ea30c4c53 Show both ty.toml and pyproject.toml examples in configuration reference (#22144)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-12-23 09:49:44 +01:00
Micha Reiser
ccc9132f73 [ty] Use ModuleName::new_static in more places (#22156) 2025-12-23 09:24:49 +01:00
Ibraheem Ahmed
65021fcee9 [ty] Support type inference between protocol instances (#22120) 2025-12-23 09:24:01 +01:00
Micha Reiser
aa21b70a8b [ty] Fix path of instrumented benchnmark binary (#22157) 2025-12-23 09:12:18 +01:00
Vincent Ging Ho Yim
22ce0c8a51 Decrease Markdown heading level (#22152) 2025-12-23 07:44:45 +00:00
Micha Reiser
d6a7c9b4ed [ty] Add respect-type-ignore-comments configuration option (#22137) 2025-12-23 08:36:51 +01:00
Charlie Marsh
4745d15fff [ty] Respect debug text interpolation in f-strings (#22151)
## Summary

Per @carljm's comment, we just fall back to `str`.

Closes https://github.com/astral-sh/ty/issues/2151.
2025-12-22 20:21:28 -05:00
Shunsuke Shibayama
06db474f20 [ty] stabilize union-type ordering in fixed-point iteration (#22070)
## Summary

This PR fixes https://github.com/astral-sh/ty/issues/2085.

Based on the reported code, the panicking MRE is:

```python
class Test:
    def __init__(self, x: int):
        self.left = x
        self.right = x
    def method(self):
        self.left, self.right = self.right, self.left
        if self.right:
            self.right = self.right
```

The type inference (`implicit_attribute_inner`) for `self.right`
proceeds as follows:

```
0: Divergent(Id(6c07))
1: Unknown | int | (Divergent(Id(1c00)) & ~AlwaysFalsy)
2: Unknown | int | (Divergent(Id(6c07)) & ~AlwaysFalsy) | (Divergent(Id(1c00)) & ~AlwaysFalsy)
3: Unknown | int | (Divergent(Id(1c00)) & ~AlwaysFalsy) | (Divergent(Id(6c07)) & ~AlwaysFalsy)
4: Unknown | int | (Divergent(Id(6c07)) & ~AlwaysFalsy) | (Divergent(Id(1c00)) & ~AlwaysFalsy)
...
```

The problem is that the order of union types is not stable between
cycles. To solve this, when unioning the previous union type with the
current union type, we should use the previous type as the base and add
only the new elements in this cycle (In the current implementation, this
unioning order was reversed).

## Test Plan

New corpus test
2025-12-22 16:16:03 -08:00
Charlie Marsh
664686bdbc [ty] Exclude parameterized tuple types from narrowing when disjoint from comparison values (#22129)
## Summary

IIUC, tuples with a known structure (`tuple_spec`) use the standard
tuple `__eq__` which only returns `True` for other tuples, so they can
be safely excluded when disjoint from string literals or other non-tuple
types.

Closes https://github.com/astral-sh/ty/issues/2140.
2025-12-22 20:44:49 +00:00
William Woodruff
4a937543b9 Ecosystem report: publish site via astral-sh-bot (#22142) 2025-12-22 12:57:49 -05:00
Micha Reiser
ec034fc359 [ty] Add new diagnosticMode: off (#22073) 2025-12-22 16:46:02 +01:00
Micha Reiser
29d7f22c1f [ty] Add ty.configuration and ty.configurationFile options (#22053) 2025-12-22 16:13:20 +01:00
Micha Reiser
8fc4349fd3 [ty] Split suppression.rs into multiple smaller modules (#22141) 2025-12-22 16:08:56 +01:00
Matthew Mckee
816b19c4a6 [ty] Rename set_invalid_syntax to set_invalid_type_annotation (#22140) 2025-12-22 14:29:03 +00:00
Peter Law
87406b43ea Fix iter example in usafe fixes doc (#22118)
## Summary

This appears to have been a copy/paste error from the list example, as
the subscript is not present in the original next/iter example only in
the case where the error case is shown. While in the specific example
code the subscript actually has no effect, it does make the example
slightly confusing.

Consider the following variations, first the example from the docs
unchanged and second the same code but not hitting the intended error
case (due to using a non-empty collection):
```console
$ python3 -c 'next(iter(range(0)))[0]'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
StopIteration

$ python3 -c 'next(iter(range(1)))[0]'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
TypeError: 'int' object is not subscriptable
```

## Test Plan

Not directly tested, however see inline snippets above.
2025-12-22 09:25:28 -05:00
Matthew Mckee
422e99ea70 [ty] Add inlay hint request time log (#22138) 2025-12-22 15:08:49 +01:00
Micha Reiser
ea6730f546 [ty] Speed-up instrumented benchmarks (#22133) 2025-12-22 15:06:22 +01:00
Matthew Mckee
a46835c224 [ty] Set flag to avoid type[T@f] being inserted when you double-click on the inlay (#22139) 2025-12-22 14:00:45 +00:00
Micha Reiser
884e83591e [ty] Update salsa (#22072) 2025-12-22 13:22:54 +01:00
Alex Waygood
6b3dd28e63 [ty] Make a server snapshot less painful to update (#22132) 2025-12-22 12:13:58 +00:00
Harutaka Kawamura
572f57aa3c Fix GitHub Actions output format for multi-line diagnostics (#22108) 2025-12-22 10:08:07 +01:00
Micha Reiser
ed423e0ae2 [ty] Speedup ty-walltime benchmarks (#22126)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-12-22 08:44:17 +01:00
Charlie Marsh
fee4e2d72a [ty] Distribute type[] over unions (#22115)
## Summary

Closes https://github.com/astral-sh/ty/issues/2121.
2025-12-21 18:45:29 -05:00
Will Duke
b6e84eca16 [ty] Document invalid-syntax-in-forward-annotation and escape-character-in-forward-annotation (#22130)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-12-21 19:35:44 +00:00
Micha Reiser
b4c2825afd [ty] Move module resolver code into its own crate (#22106) 2025-12-21 11:00:34 +00:00
193 changed files with 7040 additions and 3081 deletions

View File

@@ -4,10 +4,12 @@
self-hosted-runner:
# Various runners we use that aren't recognized out-of-the-box by actionlint:
labels:
- depot-ubuntu-24.04-4
- depot-ubuntu-latest-8
- depot-ubuntu-22.04-16
- depot-ubuntu-22.04-32
- depot-windows-2022-16
- depot-ubuntu-22.04-arm-4
- github-windows-2025-x86_64-8
- github-windows-2025-x86_64-16
- codspeed-macro

View File

@@ -952,7 +952,7 @@ jobs:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
run: cargo codspeed build --features "codspeed,ruff_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
- name: "Run benchmarks"
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
@@ -960,9 +960,9 @@ jobs:
mode: simulation
run: cargo codspeed run
benchmarks-instrumented-ty:
name: "benchmarks instrumented (ty)"
runs-on: ubuntu-24.04
benchmarks-instrumented-ty-build:
name: "benchmarks instrumented ty (build)"
runs-on: depot-ubuntu-24.04-4
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
@@ -971,9 +971,83 @@ jobs:
needs.determine_changes.outputs.ty == 'true'
)
timeout-minutes: 20
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build -m instrumentation --features "codspeed,ty_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
- name: "Upload benchmark binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: benchmarks-instrumented-ty-binary
path: target/codspeed/simulation/ruff_benchmark
retention-days: 1
benchmarks-instrumented-ty-run:
name: "benchmarks instrumented ty (${{ matrix.benchmark }})"
runs-on: ubuntu-24.04
needs: benchmarks-instrumented-ty-build
timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
strategy:
fail-fast: false
matrix:
benchmark:
- "check_file|micro|anyio"
- "attrs|hydra|datetype"
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: "Install codspeed"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-codspeed
- name: "Download benchmark binary"
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v4.3.0
with:
name: benchmarks-instrumented-ty-binary
path: target/codspeed/simulation/ruff_benchmark
- name: "Restore binary permissions"
run: chmod +x target/codspeed/simulation/ruff_benchmark/ty
- name: "Run benchmarks"
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
with:
mode: simulation
run: cargo codspeed run --bench ty "${{ matrix.benchmark }}"
benchmarks-walltime-build:
name: "benchmarks walltime (build)"
# We only run this job if `github.repository == 'astral-sh/ruff'`,
# so hardcoding depot here is fine
runs-on: depot-ubuntu-22.04-arm-4
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -994,49 +1068,51 @@ jobs:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
run: cargo codspeed build -m walltime --features "codspeed,ty_walltime" --profile profiling --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
- name: "Upload benchmark binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
mode: simulation
run: cargo codspeed run
name: benchmarks-walltime-binary
path: target/codspeed/walltime/ruff_benchmark
retention-days: 1
benchmarks-walltime:
name: "benchmarks walltime (${{ matrix.benchmarks }})"
benchmarks-walltime-run:
name: "benchmarks walltime (${{ matrix.benchmark }})"
runs-on: codspeed-macro
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main') }}
needs: benchmarks-walltime-build
timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
strategy:
matrix:
benchmarks:
- "medium|multithreaded"
- "small|large"
benchmark:
- colour_science
- "pandas|tanjun|altair"
- "static_frame|sympy"
- "pydantic|multithreaded|freqtrade"
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark
- name: "Download benchmark binary"
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: benchmarks-walltime-binary
path: target/codspeed/walltime/ruff_benchmark
- name: "Restore binary permissions"
run: chmod +x target/codspeed/walltime/ruff_benchmark/ty_walltime
- name: "Run benchmarks"
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
@@ -1047,4 +1123,4 @@ jobs:
CODSPEED_PERF_ENABLED: false
with:
mode: walltime
run: cargo codspeed run --bench ty_walltime "${{ matrix.benchmarks }}"
run: cargo codspeed run --bench ty_walltime -m walltime "${{ matrix.benchmark }}"

View File

@@ -4,7 +4,13 @@ permissions: {}
on:
pull_request:
types: [labeled]
# The default for `pull_request` is to trigger on `synchronize`, `opened` and `reopened`.
# We also add `labeled` here so that the workflow triggers when a label is initially added.
types:
- labeled
- synchronize
- opened
- reopened
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
@@ -23,7 +29,7 @@ jobs:
name: Compute diagnostic diff
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
timeout-minutes: 20
if: contains(github.event.label.name, 'ecosystem-analyzer')
if: contains( github.event.pull_request.labels.*.name, 'ecosystem-analyzer')
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:

View File

@@ -1,3 +1,7 @@
# This workflow is a cron job that generates a report describing
# all diagnostics ty emits across the whole ecosystem. The report
# is uploaded to https://ty-ecosystem-ext.pages.dev/ on a weekly basis.
name: ty ecosystem-report
permissions: {}
@@ -14,7 +18,6 @@ env:
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
jobs:
ty-ecosystem-report:
@@ -31,12 +34,12 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
enable-cache: true
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
lookup-only: false
- name: Install Rust toolchain
run: rustup show
@@ -70,11 +73,10 @@ jobs:
ecosystem-diagnostics.json \
--output dist/index.html
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
id: deploy
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
# NOTE: astral-sh-bot uses this artifact to publish the ecosystem report.
# Make sure to update the bot if you rename the artifact.
- name: "Upload ecosystem report"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages deploy dist --project-name=ty-ecosystem --branch main --commit-hash ${GITHUB_SHA}
name: full-report
path: dist/

48
Cargo.lock generated
View File

@@ -3129,6 +3129,7 @@ dependencies = [
"salsa",
"schemars",
"serde",
"ty_module_resolver",
"ty_python_semantic",
"zip",
]
@@ -3619,8 +3620,8 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=0298d2951e45ccf1450f1a09fb379dc7e48ddee9#0298d2951e45ccf1450f1a09fb379dc7e48ddee9"
dependencies = [
"boxcar",
"compact_str",
@@ -3644,13 +3645,13 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=0298d2951e45ccf1450f1a09fb379dc7e48ddee9#0298d2951e45ccf1450f1a09fb379dc7e48ddee9"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=0298d2951e45ccf1450f1a09fb379dc7e48ddee9#0298d2951e45ccf1450f1a09fb379dc7e48ddee9"
dependencies = [
"proc-macro2",
"quote",
@@ -4375,6 +4376,7 @@ dependencies = [
"tracing-flame",
"tracing-subscriber",
"ty_combine",
"ty_module_resolver",
"ty_project",
"ty_python_semantic",
"ty_server",
@@ -4389,7 +4391,6 @@ dependencies = [
"ordermap",
"ruff_db",
"ruff_python_ast",
"ty_python_semantic",
]
[[package]]
@@ -4407,8 +4408,8 @@ dependencies = [
"tempfile",
"toml",
"ty_ide",
"ty_module_resolver",
"ty_project",
"ty_python_semantic",
"walkdir",
]
@@ -4438,11 +4439,35 @@ dependencies = [
"salsa",
"smallvec",
"tracing",
"ty_module_resolver",
"ty_project",
"ty_python_semantic",
"ty_vendored",
]
[[package]]
name = "ty_module_resolver"
version = "0.0.0"
dependencies = [
"anyhow",
"camino",
"compact_str",
"get-size2",
"insta",
"ruff_db",
"ruff_memory_usage",
"ruff_python_ast",
"ruff_python_stdlib",
"rustc-hash",
"salsa",
"strum",
"strum_macros",
"tempfile",
"thiserror 2.0.17",
"tracing",
"ty_vendored",
]
[[package]]
name = "ty_project"
version = "0.0.0"
@@ -4477,6 +4502,7 @@ dependencies = [
"toml",
"tracing",
"ty_combine",
"ty_module_resolver",
"ty_python_semantic",
"ty_static",
"ty_vendored",
@@ -4529,11 +4555,11 @@ dependencies = [
"strsim",
"strum",
"strum_macros",
"tempfile",
"test-case",
"thiserror 2.0.17",
"tracing",
"ty_python_semantic",
"ty_combine",
"ty_module_resolver",
"ty_static",
"ty_test",
"ty_vendored",
@@ -4572,6 +4598,7 @@ dependencies = [
"tracing-subscriber",
"ty_combine",
"ty_ide",
"ty_module_resolver",
"ty_project",
"ty_python_semantic",
]
@@ -4612,6 +4639,7 @@ dependencies = [
"thiserror 2.0.17",
"toml",
"tracing",
"ty_module_resolver",
"ty_python_semantic",
"ty_static",
"ty_vendored",

View File

@@ -45,6 +45,7 @@ ty = { path = "crates/ty" }
ty_combine = { path = "crates/ty_combine" }
ty_completion_eval = { path = "crates/ty_completion_eval" }
ty_ide = { path = "crates/ty_ide" }
ty_module_resolver = { path = "crates/ty_module_resolver" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { path = "crates/ty_python_semantic" }
ty_server = { path = "crates/ty_server" }
@@ -146,7 +147,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "55e5e7d32fa3fc189276f35bb04c9438f9aedbd1", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "0298d2951e45ccf1450f1a09fb379dc7e48ddee9", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -14,6 +14,6 @@ info:
success: false
exit_code: 1
----- stdout -----
::error title=Ruff (unformatted),file=[TMP]/input.py,line=1,col=1,endLine=2,endColumn=1::input.py:1:1: unformatted: File would be reformatted
::error title=Ruff (unformatted),file=[TMP]/input.py,line=1,endLine=2::input.py:1:1: unformatted: File would be reformatted
----- stderr -----

View File

@@ -19,32 +19,32 @@ doctest = false
[[bench]]
name = "linter"
harness = false
required-features = ["instrumented"]
required-features = ["ruff_instrumented"]
[[bench]]
name = "lexer"
harness = false
required-features = ["instrumented"]
required-features = ["ruff_instrumented"]
[[bench]]
name = "parser"
harness = false
required-features = ["instrumented"]
required-features = ["ruff_instrumented"]
[[bench]]
name = "formatter"
harness = false
required-features = ["instrumented"]
required-features = ["ruff_instrumented"]
[[bench]]
name = "ty"
harness = false
required-features = ["instrumented"]
required-features = ["ty_instrumented"]
[[bench]]
name = "ty_walltime"
harness = false
required-features = ["walltime"]
required-features = ["ty_walltime"]
[dependencies]
ruff_db = { workspace = true, features = ["testing"] }
@@ -67,25 +67,32 @@ tracing = { workspace = true }
workspace = true
[features]
default = ["instrumented", "walltime"]
# Enables the benchmark that should only run with codspeed's instrumented runner
instrumented = [
default = ["ty_instrumented", "ty_walltime", "ruff_instrumented"]
# Enables the ruff instrumented benchmarks
ruff_instrumented = [
"criterion",
"ruff_linter",
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"mimalloc",
"tikv-jemallocator",
]
# Enables the ty instrumented benchmarks
ty_instrumented = [
"criterion",
"ty_project",
"ruff_python_trivia",
]
codspeed = ["codspeed-criterion-compat"]
# Enables benchmark that should only run with codspeed's walltime runner.
walltime = ["ruff_db/os", "ty_project", "divan"]
# Enables the ty_walltime benchmarks
ty_walltime = ["ruff_db/os", "ty_project", "divan"]
[target.'cfg(target_os = "windows")'.dev-dependencies]
mimalloc = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true, optional = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dev-dependencies]
tikv-jemallocator = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
tikv-jemallocator = { workspace = true, optional = true }
[dev-dependencies]
rustc-hash = { workspace = true }

View File

@@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
13100,
13106,
);
static TANJUN: Benchmark = Benchmark::new(
@@ -235,30 +235,55 @@ fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) {
});
}
#[bench(args=[&ALTAIR, &FREQTRADE, &TANJUN], sample_size=2, sample_count=3)]
fn small(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
#[bench(sample_size = 2, sample_count = 3)]
fn altair(bencher: Bencher) {
run_single_threaded(bencher, &ALTAIR);
}
#[bench(args=[&COLOUR_SCIENCE, &PANDAS, &STATIC_FRAME], sample_size=1, sample_count=3)]
fn medium(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
#[bench(sample_size = 2, sample_count = 3)]
fn freqtrade(bencher: Bencher) {
run_single_threaded(bencher, &FREQTRADE);
}
#[bench(args=[&SYMPY, &PYDANTIC], sample_size=1, sample_count=2)]
fn large(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
#[bench(sample_size = 2, sample_count = 3)]
fn tanjun(bencher: Bencher) {
run_single_threaded(bencher, &TANJUN);
}
#[bench(args=[&ALTAIR], sample_size=3, sample_count=8)]
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
#[bench(sample_size = 2, sample_count = 3)]
fn pydantic(bencher: Bencher) {
run_single_threaded(bencher, &PYDANTIC);
}
#[bench(sample_size = 1, sample_count = 3)]
fn static_frame(bencher: Bencher) {
run_single_threaded(bencher, &STATIC_FRAME);
}
#[bench(sample_size = 1, sample_count = 2)]
fn colour_science(bencher: Bencher) {
run_single_threaded(bencher, &COLOUR_SCIENCE);
}
#[bench(sample_size = 1, sample_count = 2)]
fn pandas(bencher: Bencher) {
run_single_threaded(bencher, &PANDAS);
}
#[bench(sample_size = 1, sample_count = 2)]
fn sympy(bencher: Bencher) {
run_single_threaded(bencher, &SYMPY);
}
#[bench(sample_size = 3, sample_count = 8)]
fn multithreaded(bencher: Bencher) {
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
bencher
.with_inputs(|| benchmark.setup_iteration())
.with_inputs(|| ALTAIR.setup_iteration())
.bench_local_values(|db| {
thread_pool.install(|| {
check_project(&db, benchmark.project.name, benchmark.max_diagnostics);
check_project(&db, ALTAIR.project.name, ALTAIR.max_diagnostics);
db
})
});

View File

@@ -1,6 +1,6 @@
use std::path::PathBuf;
#[cfg(feature = "instrumented")]
#[cfg(any(feature = "ty_instrumented", feature = "ruff_instrumented"))]
pub mod criterion;
pub mod real_world_projects;

View File

@@ -0,0 +1,51 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
/// Signals a [`CancellationToken`] that it should be canceled.
#[derive(Debug, Clone)]
pub struct CancellationTokenSource {
cancelled: Arc<AtomicBool>,
}
impl Default for CancellationTokenSource {
fn default() -> Self {
Self::new()
}
}
impl CancellationTokenSource {
pub fn new() -> Self {
Self {
cancelled: Arc::new(AtomicBool::new(false)),
}
}
pub fn is_cancellation_requested(&self) -> bool {
self.cancelled.load(std::sync::atomic::Ordering::Relaxed)
}
/// Creates a new token that uses this source.
pub fn token(&self) -> CancellationToken {
CancellationToken {
cancelled: self.cancelled.clone(),
}
}
/// Requests cancellation for operations using this token.
pub fn cancel(&self) {
self.cancelled
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
/// Token signals whether an operation should be canceled.
#[derive(Debug, Clone)]
pub struct CancellationToken {
cancelled: Arc<AtomicBool>,
}
impl CancellationToken {
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(std::sync::atomic::Ordering::Relaxed)
}
}

View File

@@ -1,4 +1,4 @@
use std::{fmt::Formatter, path::Path, sync::Arc};
use std::{borrow::Cow, fmt::Formatter, path::Path, sync::Arc};
use ruff_diagnostics::{Applicability, Fix};
use ruff_source_file::{LineColumn, SourceCode, SourceFile};
@@ -11,6 +11,7 @@ pub use self::render::{
ceil_char_boundary,
github::{DisplayGithubDiagnostics, GithubRenderer},
};
use crate::cancellation::CancellationToken;
use crate::{Db, files::File};
mod render;
@@ -410,11 +411,6 @@ impl Diagnostic {
self.id().is_invalid_syntax()
}
/// Returns the message body to display to the user.
pub fn body(&self) -> &str {
self.primary_message()
}
/// Returns the message of the first sub-diagnostic with a `Help` severity.
///
/// Note that this is used as the fix title/suggestion for some of Ruff's output formats, but in
@@ -1312,6 +1308,8 @@ pub struct DisplayDiagnosticConfig {
show_fix_diff: bool,
/// The lowest applicability that should be shown when reporting diagnostics.
fix_applicability: Applicability,
cancellation_token: Option<CancellationToken>,
}
impl DisplayDiagnosticConfig {
@@ -1385,6 +1383,20 @@ impl DisplayDiagnosticConfig {
pub fn fix_applicability(&self) -> Applicability {
self.fix_applicability
}
pub fn with_cancellation_token(
mut self,
token: Option<CancellationToken>,
) -> DisplayDiagnosticConfig {
self.cancellation_token = token;
self
}
pub fn is_canceled(&self) -> bool {
self.cancellation_token
.as_ref()
.is_some_and(|token| token.is_cancelled())
}
}
impl Default for DisplayDiagnosticConfig {
@@ -1398,6 +1410,7 @@ impl Default for DisplayDiagnosticConfig {
show_fix_status: false,
show_fix_diff: false,
fix_applicability: Applicability::Safe,
cancellation_token: None,
}
}
}
@@ -1474,6 +1487,15 @@ pub enum ConciseMessage<'a> {
Custom(&'a str),
}
impl<'a> ConciseMessage<'a> {
pub fn to_str(&self) -> Cow<'a, str> {
match self {
ConciseMessage::MainDiagnostic(s) | ConciseMessage::Custom(s) => Cow::Borrowed(s),
ConciseMessage::Both { .. } => Cow::Owned(self.to_string()),
}
}
}
impl std::fmt::Display for ConciseMessage<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
@@ -1490,6 +1512,16 @@ impl std::fmt::Display for ConciseMessage<'_> {
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for ConciseMessage<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
/// A diagnostic message string.
///
/// This is, for all intents and purposes, equivalent to a `Box<str>`.

View File

@@ -52,7 +52,7 @@ impl AzureRenderer<'_> {
f,
"code={code};]{body}",
code = diag.secondary_code_or_id(),
body = diag.body(),
body = diag.concise_message(),
)?;
}

View File

@@ -28,6 +28,10 @@ impl<'a> ConciseRenderer<'a> {
let sep = fmt_styled(":", stylesheet.separator);
for diag in diagnostics {
if self.config.is_canceled() {
return Ok(());
}
if let Some(span) = diag.primary_span() {
write!(
f,

View File

@@ -53,6 +53,10 @@ impl<'a> FullRenderer<'a> {
.hyperlink(stylesheet.hyperlink);
for diag in diagnostics {
if self.config.is_canceled() {
return Ok(());
}
let resolved = Resolved::new(self.resolver, diag, self.config);
let renderable = resolved.to_renderable(self.config.context);
for diag in renderable.diagnostics.iter() {

View File

@@ -49,14 +49,26 @@ impl<'a> GithubRenderer<'a> {
}
.unwrap_or_default();
write!(
f,
",line={row},col={column},endLine={end_row},endColumn={end_column}::",
row = start_location.line,
column = start_location.column,
end_row = end_location.line,
end_column = end_location.column,
)?;
// GitHub Actions workflow commands have constraints on error annotations:
// - `col` and `endColumn` cannot be set if `line` and `endLine` are different
// See: https://github.com/astral-sh/ruff/issues/22074
if start_location.line == end_location.line {
write!(
f,
",line={row},col={column},endLine={end_row},endColumn={end_column}::",
row = start_location.line,
column = start_location.column,
end_row = end_location.line,
end_column = end_location.column,
)?;
} else {
write!(
f,
",line={row},endLine={end_row}::",
row = start_location.line,
end_row = end_location.line,
)?;
}
write!(
f,
@@ -75,7 +87,7 @@ impl<'a> GithubRenderer<'a> {
write!(f, "{id}:", id = diagnostic.id())?;
}
writeln!(f, " {}", diagnostic.body())?;
writeln!(f, " {}", diagnostic.concise_message())?;
}
Ok(())

View File

@@ -98,7 +98,7 @@ impl Serialize for SerializedMessages<'_> {
}
fingerprints.insert(message_fingerprint);
let description = diagnostic.body();
let description = diagnostic.concise_message();
let check_name = diagnostic.secondary_code_or_id();
let severity = match diagnostic.severity() {
Severity::Info => "info",

View File

@@ -6,7 +6,7 @@ use ruff_notebook::NotebookIndex;
use ruff_source_file::{LineColumn, OneIndexed};
use ruff_text_size::Ranged;
use crate::diagnostic::{Diagnostic, DiagnosticSource, DisplayDiagnosticConfig};
use crate::diagnostic::{ConciseMessage, Diagnostic, DiagnosticSource, DisplayDiagnosticConfig};
use super::FileResolver;
@@ -101,7 +101,7 @@ pub(super) fn diagnostic_to_json<'a>(
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
url: diagnostic.documentation_url(),
message: diagnostic.body(),
message: diagnostic.concise_message(),
fix,
cell: notebook_cell_index,
location: start_location.map(JsonLocation::from),
@@ -113,7 +113,7 @@ pub(super) fn diagnostic_to_json<'a>(
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
url: diagnostic.documentation_url(),
message: diagnostic.body(),
message: diagnostic.concise_message(),
fix,
cell: notebook_cell_index,
location: Some(start_location.unwrap_or_default().into()),
@@ -226,7 +226,7 @@ pub(crate) struct JsonDiagnostic<'a> {
filename: Option<&'a str>,
fix: Option<JsonFix<'a>>,
location: Option<JsonLocation>,
message: &'a str,
message: ConciseMessage<'a>,
noqa_row: Option<OneIndexed>,
url: Option<&'a str>,
}

View File

@@ -56,17 +56,17 @@ impl<'a> JunitRenderer<'a> {
start_location: location,
} = diagnostic;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(diagnostic.body());
status.set_message(diagnostic.concise_message().to_str());
if let Some(location) = location {
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.line,
col = location.column,
body = diagnostic.body()
body = diagnostic.concise_message()
));
} else {
status.set_description(diagnostic.body());
status.set_description(diagnostic.concise_message().to_str());
}
let code = diagnostic

View File

@@ -55,7 +55,7 @@ impl PylintRenderer<'_> {
f,
"{path}:{row}: [{code}] {body}",
path = filename,
body = diagnostic.body()
body = diagnostic.concise_message()
)?;
}

View File

@@ -5,7 +5,7 @@ use ruff_diagnostics::{Edit, Fix};
use ruff_source_file::{LineColumn, SourceCode};
use ruff_text_size::Ranged;
use crate::diagnostic::Diagnostic;
use crate::diagnostic::{ConciseMessage, Diagnostic};
use super::FileResolver;
@@ -76,7 +76,7 @@ fn diagnostic_to_rdjson<'a>(
let edits = diagnostic.fix().map(Fix::edits).unwrap_or_default();
RdjsonDiagnostic {
message: diagnostic.body(),
message: diagnostic.concise_message(),
location,
code: RdjsonCode {
value: diagnostic
@@ -155,7 +155,7 @@ struct RdjsonDiagnostic<'a> {
code: RdjsonCode<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
location: Option<RdjsonLocation<'a>>,
message: &'a str,
message: ConciseMessage<'a>,
#[serde(skip_serializing_if = "Vec::is_empty")]
suggestions: Vec<RdjsonSuggestion<'a>>,
}

View File

@@ -2,5 +2,5 @@
source: crates/ruff_db/src/diagnostic/render/github.rs
expression: env.render_diagnostics(&diagnostics)
---
::error title=ty (invalid-syntax),file=/syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
::error title=ty (invalid-syntax),file=/syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline
::error title=ty (invalid-syntax),file=/syntax_errors.py,line=1,endLine=2::syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
::error title=ty (invalid-syntax),file=/syntax_errors.py,line=3,endLine=4::syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline

View File

@@ -12,6 +12,7 @@ use std::hash::BuildHasherDefault;
use std::num::NonZeroUsize;
use ty_static::EnvVars;
pub mod cancellation;
pub mod diagnostic;
pub mod display;
pub mod file_revision;

View File

@@ -275,16 +275,16 @@ impl OsSystem {
/// instead of at least one system call for each component between `path` and `prefix`.
///
/// However, using `canonicalize` to resolve the path's casing doesn't work in two cases:
/// * if `path` is a symlink because `canonicalize` then returns the symlink's target and not the symlink's source path.
/// * on Windows: If `path` is a mapped network drive because `canonicalize` then returns the UNC path
/// (e.g. `Z:\` is mapped to `\\server\share` and `canonicalize` then returns `\\?\UNC\server\share`).
/// * if `path` is a symlink, `canonicalize` returns the symlink's target and not the symlink's source path.
/// * on Windows: If `path` is a mapped network drive, `canonicalize` returns the UNC path
/// (e.g. `Z:\` is mapped to `\\server\share` and `canonicalize` returns `\\?\UNC\server\share`).
///
/// Symlinks and mapped network drives should be rare enough that this fast path is worth trying first,
/// even if it comes at a cost for those rare use cases.
fn path_exists_case_sensitive_fast(&self, path: &SystemPath) -> Option<bool> {
// This is a more forgiving version of `dunce::simplified` that removes all `\\?\` prefixes on Windows.
// We use this more forgiving version because we don't intend on using either path for anything other than comparison
// and the prefix is only relevant when passing the path to other programs and its longer than 200 something
// and the prefix is only relevant when passing the path to other programs and it's longer than 200 something
// characters.
fn simplify_ignore_verbatim(path: &SystemPath) -> &SystemPath {
if cfg!(windows) {
@@ -298,9 +298,7 @@ impl OsSystem {
}
}
let simplified = simplify_ignore_verbatim(path);
let Ok(canonicalized) = simplified.as_std_path().canonicalize() else {
let Ok(canonicalized) = path.as_std_path().canonicalize() else {
// The path doesn't exist or can't be accessed. The path doesn't exist.
return Some(false);
};
@@ -309,12 +307,13 @@ impl OsSystem {
// The original path is valid UTF8 but the canonicalized path isn't. This definitely suggests
// that a symlink is involved. Fall back to the slow path.
tracing::debug!(
"Falling back to the slow case-sensitive path existence check because the canonicalized path of `{simplified}` is not valid UTF-8"
"Falling back to the slow case-sensitive path existence check because the canonicalized path of `{path}` is not valid UTF-8"
);
return None;
};
let simplified_canonicalized = simplify_ignore_verbatim(&canonicalized);
let simplified = simplify_ignore_verbatim(path);
// Test if the paths differ by anything other than casing. If so, that suggests that
// `path` pointed to a symlink (or some other none reversible path normalization happened).

View File

@@ -37,7 +37,7 @@ pub fn assert_const_function_query_was_not_run<Db, Q, QDb, R>(
let event = events.iter().find(|event| {
if let salsa::EventKind::WillExecute { database_key } = event.kind {
db.ingredient_debug_name(database_key.ingredient_index()) == query_name
db.ingredient_debug_name(database_key.ingredient_index(db.zalsa())) == query_name
} else {
false
}
@@ -89,7 +89,7 @@ where
let event = events.iter().find(|event| {
if let salsa::EventKind::WillExecute { database_key } = event.kind {
db.ingredient_debug_name(database_key.ingredient_index()) == query_name
db.ingredient_debug_name(database_key.ingredient_index(db.zalsa())) == query_name
&& database_key.key_index() == input.as_id()
} else {
false

View File

@@ -1,11 +1,13 @@
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
use std::borrow::Cow;
use std::{fmt::Write, path::PathBuf};
use anyhow::bail;
use itertools::Itertools;
use pretty_assertions::StrComparison;
use std::{fmt::Write, path::PathBuf};
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
use ruff_python_trivia::textwrap;
use ty_project::metadata::Options;
use crate::{
@@ -165,62 +167,69 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
let _ = writeln!(output, "**Default value**: `{}`", field.default);
output.push('\n');
let _ = writeln!(output, "**Type**: `{}`", field.value_type);
output.push('\n');
output.push_str("**Example usage**:\n\n");
output.push_str(&format_example(
"pyproject.toml",
&format_header(
field.scope,
field.example,
parents,
ConfigurationFile::PyprojectToml,
),
field.example,
));
output.push('\n');
}
fn format_example(title: &str, header: &str, content: &str) -> String {
if header.is_empty() {
format!("```toml title=\"{title}\"\n{content}\n```\n",)
} else {
format!("```toml title=\"{title}\"\n{header}\n{content}\n```\n",)
for configuration_file in [ConfigurationFile::PyprojectToml, ConfigurationFile::TyToml] {
let (header, example) =
format_snippet(field.scope, field.example, parents, configuration_file);
output.push_str(&format_tab(configuration_file.name(), &header, &example));
output.push('\n');
}
}
fn format_tab(tab_name: &str, header: &str, content: &str) -> String {
let header = if header.is_empty() {
String::new()
} else {
format!("\n {header}")
};
format!(
"=== \"{}\"\n\n ```toml{}\n{}\n ```\n",
tab_name,
header,
textwrap::indent(content, " ")
)
}
/// Format the TOML header for the example usage for a given option.
///
/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`.
fn format_header(
/// For example: `[tool.ty.rules]`.
fn format_snippet<'a>(
scope: Option<&str>,
example: &str,
example: &'a str,
parents: &[Set],
configuration: ConfigurationFile,
) -> String {
let tool_parent = match configuration {
ConfigurationFile::PyprojectToml => Some("tool.ty"),
ConfigurationFile::TyToml => None,
};
) -> (String, Cow<'a, str>) {
let mut example = Cow::Borrowed(example);
let header = tool_parent
let header = configuration
.parent_table()
.into_iter()
.chain(parents.iter().filter_map(|parent| parent.name()))
.chain(scope)
.join(".");
// Rewrite examples starting with `[tool.ty]` or `[[tool.ty]]` to their `ty.toml` equivalent.
if matches!(configuration, ConfigurationFile::TyToml) {
example = example.replace("[tool.ty.", "[").into();
}
// Ex) `[[tool.ty.xx]]`
if example.starts_with(&format!("[[{header}")) {
return String::new();
return (String::new(), example);
}
// Ex) `[tool.ty.rules]`
if example.starts_with(&format!("[{header}")) {
return String::new();
return (String::new(), example);
}
if header.is_empty() {
String::new()
(String::new(), example)
} else {
format!("[{header}]")
(format!("[{header}]"), example)
}
}
@@ -243,10 +252,25 @@ impl Visit for CollectOptionsVisitor {
#[derive(Debug, Copy, Clone)]
enum ConfigurationFile {
PyprojectToml,
#[expect(dead_code)]
TyToml,
}
impl ConfigurationFile {
const fn name(self) -> &'static str {
match self {
Self::PyprojectToml => "pyproject.toml",
Self::TyToml => "ty.toml",
}
}
const fn parent_table(self) -> Option<&'static str> {
match self {
Self::PyprojectToml => Some("tool.ty"),
Self::TyToml => None,
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;

View File

@@ -16,6 +16,7 @@ ruff_linter = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ty_module_resolver = { workspace = true }
ty_python_semantic = { workspace = true }
anyhow = { workspace = true }

View File

@@ -3,7 +3,7 @@ use ruff_python_ast::visitor::source_order::{
SourceOrderVisitor, walk_expr, walk_module, walk_stmt,
};
use ruff_python_ast::{self as ast, Expr, Mod, Stmt};
use ty_python_semantic::ModuleName;
use ty_module_resolver::ModuleName;
/// Collect all imports for a given Python file.
#[derive(Default, Debug)]

View File

@@ -7,10 +7,11 @@ use ruff_db::files::{File, Files};
use ruff_db::system::{OsSystem, System, SystemPathBuf};
use ruff_db::vendored::{VendoredFileSystem, VendoredFileSystemBuilder};
use ruff_python_ast::PythonVersion;
use ty_module_resolver::{SearchPathSettings, SearchPaths};
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{
Db, Program, ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionSource,
PythonVersionWithSource, SearchPathSettings, SysPrefixPathOrigin, default_lint_registry,
AnalysisSettings, Db, Program, ProgramSettings, PythonEnvironment, PythonPlatform,
PythonVersionSource, PythonVersionWithSource, SysPrefixPathOrigin, default_lint_registry,
};
static EMPTY_VENDORED: std::sync::LazyLock<VendoredFileSystem> = std::sync::LazyLock::new(|| {
@@ -26,6 +27,7 @@ pub struct ModuleDb {
files: Files,
system: OsSystem,
rule_selection: Arc<RuleSelection>,
analysis_settings: Arc<AnalysisSettings>,
}
impl ModuleDb {
@@ -85,6 +87,13 @@ impl SourceDb for ModuleDb {
}
}
#[salsa::db]
impl ty_module_resolver::Db for ModuleDb {
fn search_paths(&self) -> &SearchPaths {
Program::get(self).search_paths(self)
}
}
#[salsa::db]
impl Db for ModuleDb {
fn should_check_file(&self, file: File) -> bool {
@@ -102,6 +111,10 @@ impl Db for ModuleDb {
fn verbose(&self) -> bool {
false
}
fn analysis_settings(&self) -> &AnalysisSettings {
&self.analysis_settings
}
}
#[salsa::db]

View File

@@ -1,6 +1,6 @@
use ruff_db::files::{File, FilePath, system_path_to_file};
use ruff_db::system::SystemPath;
use ty_python_semantic::{
use ty_module_resolver::{
ModuleName, resolve_module, resolve_module_confident, resolve_real_module,
resolve_real_module_confident,
};

View File

@@ -197,7 +197,7 @@ impl Display for RuleCodeAndBody<'_> {
f,
"{fix}{body}",
fix = format_args!("[{}] ", "*".cyan()),
body = self.message.body(),
body = self.message.concise_message(),
);
}
}
@@ -208,14 +208,14 @@ impl Display for RuleCodeAndBody<'_> {
f,
"{code} {body}",
code = code.red().bold(),
body = self.message.body(),
body = self.message.concise_message(),
)
} else {
write!(
f,
"{code}: {body}",
code = self.message.id().as_str().red().bold(),
body = self.message.body(),
body = self.message.concise_message(),
)
}
}

View File

@@ -334,7 +334,7 @@ impl<'a> SarifResult<'a> {
rule_id: RuleCode::from(diagnostic),
level: "error".to_string(),
message: SarifMessage {
text: diagnostic.body().to_string(),
text: diagnostic.concise_message().to_string(),
},
fixes: Self::fix(diagnostic, &uri).into_iter().collect(),
locations: vec![SarifLocation {

View File

@@ -3,3 +3,6 @@
def foo(): ...
@@
def foo(): ...
@test
@
class Test

View File

@@ -477,6 +477,17 @@ impl<'src> Parser<'src> {
}
}
pub(super) fn parse_missing_name(&mut self) -> ast::ExprName {
let identifier = self.parse_missing_identifier();
ast::ExprName {
range: identifier.range,
id: identifier.id,
ctx: ExprContext::Invalid,
node_index: AtomicNodeIndex::NONE,
}
}
/// Parses an identifier.
///
/// For an invalid identifier, the `id` field will be an empty string.
@@ -524,16 +535,20 @@ impl<'src> Parser<'src> {
node_index: AtomicNodeIndex::NONE,
}
} else {
self.add_error(
ParseErrorType::OtherError("Expected an identifier".into()),
range,
);
self.parse_missing_identifier()
}
}
ast::Identifier {
id: Name::empty(),
range: self.missing_node_range(),
node_index: AtomicNodeIndex::NONE,
}
fn parse_missing_identifier(&mut self) -> ast::Identifier {
self.add_error(
ParseErrorType::OtherError("Expected an identifier".into()),
self.current_token_range(),
);
ast::Identifier {
id: Name::empty(),
range: self.missing_node_range(),
node_index: AtomicNodeIndex::NONE,
}
}

View File

@@ -2782,13 +2782,20 @@ impl<'src> Parser<'src> {
// def foo(): ...
// @@
// def foo(): ...
// @test
// @
// class Test
while self.at(TokenKind::At) {
progress.assert_progressing(self);
let decorator_start = self.node_start();
self.bump(TokenKind::At);
let parsed_expr = self.parse_named_expression_or_higher(ExpressionContext::default());
let parsed_expr = if self.at(TokenKind::Def) || self.at(TokenKind::Class) {
Expr::Name(self.parse_missing_name()).into()
} else {
self.parse_named_expression_or_higher(ExpressionContext::default())
};
if self.options.target_version < PythonVersion::PY39 {
// test_ok decorator_expression_dotted_ident_py38
@@ -2914,21 +2921,27 @@ impl<'src> Parser<'src> {
self.current_token_range(),
);
// TODO(dhruvmanila): It seems that this recovery drops all the parsed
// decorators. Maybe we could convert them into statement expression
// with a flag indicating that this expression is part of a decorator.
// It's only possible to keep them if it's a function or class definition.
// We could possibly keep them if there's indentation error:
//
// ```python
// @decorator
// @decorator
// def foo(): ...
// ```
//
// Or, parse it as a binary expression where the left side is missing.
// We would need to convert each decorator into a binary expression.
self.parse_statement()
let range = self.node_range(start);
ast::StmtFunctionDef {
node_index: AtomicNodeIndex::default(),
range,
is_async: false,
decorator_list: decorators,
name: ast::Identifier {
id: Name::empty(),
range: self.missing_node_range(),
node_index: AtomicNodeIndex::NONE,
},
type_params: None,
parameters: Box::new(ast::Parameters {
range: self.missing_node_range(),
..ast::Parameters::default()
}),
returns: None,
body: vec![],
}
.into()
}
}
}

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/decorator_missing_expression.py
---
## AST
@@ -8,40 +7,57 @@ input_file: crates/ruff_python_parser/resources/inline/err/decorator_missing_exp
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..51,
range: 0..70,
body: [
AnnAssign(
StmtAnnAssign {
FunctionDef(
StmtFunctionDef {
node_index: NodeIndex(None),
range: 5..15,
target: Call(
ExprCall {
range: 0..15,
is_async: false,
decorator_list: [
Decorator {
range: 0..1,
node_index: NodeIndex(None),
range: 5..10,
func: Name(
expression: Name(
ExprName {
node_index: NodeIndex(None),
range: 5..8,
id: Name("foo"),
ctx: Load,
range: 1..1,
id: Name(""),
ctx: Invalid,
},
),
arguments: Arguments {
range: 8..10,
},
],
name: Identifier {
id: Name("foo"),
range: 5..8,
node_index: NodeIndex(None),
},
type_params: None,
parameters: Parameters {
range: 8..10,
node_index: NodeIndex(None),
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
args: [],
keywords: [],
range: 12..15,
value: EllipsisLiteral(
ExprEllipsisLiteral {
node_index: NodeIndex(None),
range: 12..15,
},
),
},
},
),
annotation: EllipsisLiteral(
ExprEllipsisLiteral {
node_index: NodeIndex(None),
range: 12..15,
},
),
value: None,
simple: false,
),
],
},
),
FunctionDef(
@@ -161,6 +177,46 @@ Module(
],
},
),
ClassDef(
StmtClassDef {
node_index: NodeIndex(None),
range: 51..69,
decorator_list: [
Decorator {
range: 51..56,
node_index: NodeIndex(None),
expression: Name(
ExprName {
node_index: NodeIndex(None),
range: 52..56,
id: Name("test"),
ctx: Load,
},
),
},
Decorator {
range: 57..58,
node_index: NodeIndex(None),
expression: Name(
ExprName {
node_index: NodeIndex(None),
range: 58..58,
id: Name(""),
ctx: Invalid,
},
),
},
],
name: Identifier {
id: Name("Test"),
range: 65..69,
node_index: NodeIndex(None),
},
type_params: None,
arguments: None,
body: [],
},
),
],
},
)
@@ -169,15 +225,7 @@ Module(
|
1 | @def foo(): ...
| ^^^ Syntax Error: Expected an identifier, but found a keyword `def` that cannot be used here
2 | @
3 | def foo(): ...
|
|
1 | @def foo(): ...
| ^^^ Syntax Error: Expected newline, found name
| ^^^ Syntax Error: Expected an identifier
2 | @
3 | def foo(): ...
|
@@ -199,6 +247,7 @@ Module(
4 | @@
| ^ Syntax Error: Expected an expression
5 | def foo(): ...
6 | @test
|
@@ -208,4 +257,23 @@ Module(
4 | @@
| ^ Syntax Error: Expected an expression
5 | def foo(): ...
6 | @test
7 | @
|
|
5 | def foo(): ...
6 | @test
7 | @
| ^ Syntax Error: Expected an expression
8 | class Test
|
|
6 | @test
7 | @
8 | class Test
| ^ Syntax Error: Expected `:`, found newline
|

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/decorator_unexpected_token.py
---
## AST
@@ -10,6 +9,44 @@ Module(
node_index: NodeIndex(None),
range: 0..34,
body: [
FunctionDef(
StmtFunctionDef {
node_index: NodeIndex(None),
range: 0..4,
is_async: false,
decorator_list: [
Decorator {
range: 0..4,
node_index: NodeIndex(None),
expression: Name(
ExprName {
node_index: NodeIndex(None),
range: 1..4,
id: Name("foo"),
ctx: Load,
},
),
},
],
name: Identifier {
id: Name(""),
range: 4..4,
node_index: NodeIndex(None),
},
type_params: None,
parameters: Parameters {
range: 4..4,
node_index: NodeIndex(None),
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [],
},
),
With(
StmtWith {
node_index: NodeIndex(None),
@@ -46,6 +83,44 @@ Module(
],
},
),
FunctionDef(
StmtFunctionDef {
node_index: NodeIndex(None),
range: 23..27,
is_async: false,
decorator_list: [
Decorator {
range: 23..27,
node_index: NodeIndex(None),
expression: Name(
ExprName {
node_index: NodeIndex(None),
range: 24..27,
id: Name("foo"),
ctx: Load,
},
),
},
],
name: Identifier {
id: Name(""),
range: 27..27,
node_index: NodeIndex(None),
},
type_params: None,
parameters: Parameters {
range: 27..27,
node_index: NodeIndex(None),
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [],
},
),
Assign(
StmtAssign {
node_index: NodeIndex(None),

View File

@@ -243,7 +243,7 @@ fn to_lsp_diagnostic(
) -> (usize, lsp_types::Diagnostic) {
let diagnostic_range = diagnostic.range().unwrap_or_default();
let name = diagnostic.name();
let body = diagnostic.body().to_string();
let body = diagnostic.concise_message().to_string();
let fix = diagnostic.fix();
let suggestion = diagnostic.first_help_text();
let code = diagnostic.secondary_code();

View File

@@ -241,7 +241,7 @@ impl Workspace {
let range = msg.range().unwrap_or_default();
ExpandedMessage {
code: msg.secondary_code_or_id().to_string(),
message: msg.body().to_string(),
message: msg.concise_message().to_string(),
start_location: source_code
.source_location(range.start(), self.position_encoding)
.into(),

View File

@@ -42,6 +42,7 @@ wild = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing"] }
ruff_python_trivia = { workspace = true }
ty_module_resolver = { workspace = true }
dunce = { workspace = true }
insta = { workspace = true, features = ["filters"] }

View File

@@ -20,11 +20,59 @@ Valid severities are:
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.rules]
possibly-unresolved-reference = "warn"
division-by-zero = "ignore"
```
=== "pyproject.toml"
```toml
[tool.ty.rules]
possibly-unresolved-reference = "warn"
division-by-zero = "ignore"
```
=== "ty.toml"
```toml
[rules]
possibly-unresolved-reference = "warn"
division-by-zero = "ignore"
```
---
## `analysis`
### `respect-type-ignore-comments`
Whether ty should respect `type: ignore` comments.
When set to `false`, `type: ignore` comments are treated like any other normal
comment and can't be used to suppress ty errors (you have to use `ty: ignore` instead).
Setting this option can be useful when using ty alongside other type checkers or when
you prefer using `ty: ignore` over `type: ignore`.
Defaults to `true`.
**Default value**: `true`
**Type**: `bool`
**Example usage**:
=== "pyproject.toml"
```toml
[tool.ty.analysis]
# Disable support for `type: ignore` comments
respect-type-ignore-comments = false
```
=== "ty.toml"
```toml
[analysis]
# Disable support for `type: ignore` comments
respect-type-ignore-comments = false
```
---
@@ -47,10 +95,19 @@ configuration setting.
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.environment]
extra-paths = ["./shared/my-search-path"]
```
=== "pyproject.toml"
```toml
[tool.ty.environment]
extra-paths = ["./shared/my-search-path"]
```
=== "ty.toml"
```toml
[environment]
extra-paths = ["./shared/my-search-path"]
```
---
@@ -78,10 +135,19 @@ This option can be used to point to virtual or system Python environments.
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.environment]
python = "./custom-venv-location/.venv"
```
=== "pyproject.toml"
```toml
[tool.ty.environment]
python = "./custom-venv-location/.venv"
```
=== "ty.toml"
```toml
[environment]
python = "./custom-venv-location/.venv"
```
---
@@ -105,11 +171,21 @@ If no platform is specified, ty will use the current platform:
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.environment]
# Tailor type stubs and conditionalized type definitions to windows.
python-platform = "win32"
```
=== "pyproject.toml"
```toml
[tool.ty.environment]
# Tailor type stubs and conditionalized type definitions to windows.
python-platform = "win32"
```
=== "ty.toml"
```toml
[environment]
# Tailor type stubs and conditionalized type definitions to windows.
python-platform = "win32"
```
---
@@ -139,10 +215,19 @@ to reflect the differing contents of the standard library across Python versions
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.environment]
python-version = "3.12"
```
=== "pyproject.toml"
```toml
[tool.ty.environment]
python-version = "3.12"
```
=== "ty.toml"
```toml
[environment]
python-version = "3.12"
```
---
@@ -167,11 +252,21 @@ it will also be included in the first party search path.
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.environment]
# Multiple directories (priority order)
root = ["./src", "./lib", "./vendor"]
```
=== "pyproject.toml"
```toml
[tool.ty.environment]
# Multiple directories (priority order)
root = ["./src", "./lib", "./vendor"]
```
=== "ty.toml"
```toml
[environment]
# Multiple directories (priority order)
root = ["./src", "./lib", "./vendor"]
```
---
@@ -187,10 +282,19 @@ bundled as a zip file in the binary
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.environment]
typeshed = "/path/to/custom/typeshed"
```
=== "pyproject.toml"
```toml
[tool.ty.environment]
typeshed = "/path/to/custom/typeshed"
```
=== "ty.toml"
```toml
[environment]
typeshed = "/path/to/custom/typeshed"
```
---
@@ -240,15 +344,29 @@ If not specified, defaults to `[]` (excludes no files).
**Example usage**:
```toml title="pyproject.toml"
[[tool.ty.overrides]]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
=== "pyproject.toml"
```toml
[[tool.ty.overrides]]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
=== "ty.toml"
```toml
[[overrides]]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
---
@@ -268,13 +386,25 @@ If not specified, defaults to `["**"]` (matches all files).
**Example usage**:
```toml title="pyproject.toml"
[[tool.ty.overrides]]
include = [
"src",
"tests",
]
```
=== "pyproject.toml"
```toml
[[tool.ty.overrides]]
include = [
"src",
"tests",
]
```
=== "ty.toml"
```toml
[[overrides]]
include = [
"src",
"tests",
]
```
---
@@ -292,13 +422,25 @@ severity levels or disable them entirely.
**Example usage**:
```toml title="pyproject.toml"
[[tool.ty.overrides]]
include = ["src"]
=== "pyproject.toml"
[tool.ty.overrides.rules]
possibly-unresolved-reference = "ignore"
```
```toml
[[tool.ty.overrides]]
include = ["src"]
[tool.ty.overrides.rules]
possibly-unresolved-reference = "ignore"
```
=== "ty.toml"
```toml
[[overrides]]
include = ["src"]
[overrides.rules]
possibly-unresolved-reference = "ignore"
```
---
@@ -358,15 +500,29 @@ to re-include `dist` use `exclude = ["!dist"]`
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.src]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
=== "pyproject.toml"
```toml
[tool.ty.src]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
=== "ty.toml"
```toml
[src]
exclude = [
"generated",
"*.proto",
"tests/fixtures/**",
"!tests/fixtures/important.py" # Include this one file
]
```
---
@@ -399,13 +555,25 @@ matches `<project_root>/src` and not `<project_root>/test/src`).
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.src]
include = [
"src",
"tests",
]
```
=== "pyproject.toml"
```toml
[tool.ty.src]
include = [
"src",
"tests",
]
```
=== "ty.toml"
```toml
[src]
include = [
"src",
"tests",
]
```
---
@@ -421,10 +589,19 @@ Enabled by default.
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.src]
respect-ignore-files = false
```
=== "pyproject.toml"
```toml
[tool.ty.src]
respect-ignore-files = false
```
=== "ty.toml"
```toml
[src]
respect-ignore-files = false
```
---
@@ -450,10 +627,19 @@ it will also be included in the first party search path.
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.src]
root = "./app"
```
=== "pyproject.toml"
```toml
[tool.ty.src]
root = "./app"
```
=== "ty.toml"
```toml
[src]
root = "./app"
```
---
@@ -471,11 +657,21 @@ Defaults to `false`.
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.terminal]
# Error if ty emits any warning-level diagnostics.
error-on-warning = true
```
=== "pyproject.toml"
```toml
[tool.ty.terminal]
# Error if ty emits any warning-level diagnostics.
error-on-warning = true
```
=== "ty.toml"
```toml
[terminal]
# Error if ty emits any warning-level diagnostics.
error-on-warning = true
```
---
@@ -491,10 +687,19 @@ Defaults to `full`.
**Example usage**:
```toml title="pyproject.toml"
[tool.ty.terminal]
output-format = "concise"
```
=== "pyproject.toml"
```toml
[tool.ty.terminal]
output-format = "concise"
```
=== "ty.toml"
```toml
[terminal]
output-format = "concise"
```
---

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

@@ -8,7 +8,7 @@
Default level: <a href="../../rules#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#L511" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L538" target="_blank">View source</a>
</small>
@@ -80,7 +80,7 @@ def test(): -> "int":
Default level: <a href="../../rules#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#L135" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L137" target="_blank">View source</a>
</small>
@@ -98,13 +98,44 @@ Calling a non-callable object will raise a `TypeError` at runtime.
4() # TypeError: 'int' object is not callable
```
## `call-top-callable`
<small>
Default level: <a href="../../rules#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.7">0.0.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-top-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L155" target="_blank">View source</a>
</small>
**What it does**
Checks for calls to objects typed as `Top[Callable[..., T]]` (the infinite union of all
callable types with return type `T`).
**Why is this bad?**
When an object is narrowed to `Top[Callable[..., object]]` (e.g., via `callable(x)` or
`isinstance(x, Callable)`), we know the object is callable, but we don't know its
precise signature. This type represents the set of all possible callable types
(including, e.g., functions that take no arguments and functions that require arguments),
so no specific set of arguments can be guaranteed to be valid.
**Examples**
```python
def f(x: object):
if callable(x):
x() # error: We know `x` is callable, but not what arguments it accepts
```
## `conflicting-argument-forms`
<small>
Default level: <a href="../../rules#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#L179" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L206" target="_blank">View source</a>
</small>
@@ -136,7 +167,7 @@ f(int) # error
Default level: <a href="../../rules#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#L205" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L232" target="_blank">View source</a>
</small>
@@ -167,7 +198,7 @@ a = 1
Default level: <a href="../../rules#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#L230" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L257" target="_blank">View source</a>
</small>
@@ -199,7 +230,7 @@ class C(A, B): ...
Default level: <a href="../../rules#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#L256" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L283" target="_blank">View source</a>
</small>
@@ -231,7 +262,7 @@ class B(A): ...
Default level: <a href="../../rules#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#L282" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L309" target="_blank">View source</a>
</small>
@@ -259,7 +290,7 @@ type B = A
Default level: <a href="../../rules#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#L326" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L353" target="_blank">View source</a>
</small>
@@ -286,7 +317,7 @@ old_func() # emits [deprecated] diagnostic
Default level: <a href="../../rules#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#L304" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L331" target="_blank">View source</a>
</small>
@@ -315,7 +346,7 @@ false positives it can produce.
Default level: <a href="../../rules#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#L347" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L374" target="_blank">View source</a>
</small>
@@ -342,7 +373,7 @@ class B(A, A): ...
Default level: <a href="../../rules#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#L368" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L395" target="_blank">View source</a>
</small>
@@ -380,11 +411,24 @@ class A: # Crash at runtime
Default level: <a href="../../rules#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%20escape-character-in-forward-annotation" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L120" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L154" target="_blank">View source</a>
</small>
TODO #14889
**What it does**
Checks for forward annotations that contain escape characters.
**Why is this bad?**
Static analysis tools like ty can't analyze type annotations that contain escape characters.
**Example**
```python
def foo() -> "intt\b": ...
```
## `fstring-type-annotation`
@@ -423,7 +467,7 @@ def test(): -> "int":
Default level: <a href="../../rules#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#L47" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L50" target="_blank">View source</a>
</small>
@@ -485,7 +529,7 @@ def test(): -> "Literal[5]":
Default level: <a href="../../rules#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#L594" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L621" target="_blank">View source</a>
</small>
@@ -515,7 +559,7 @@ class C(A, B): ...
Default level: <a href="../../rules#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#L618" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L645" target="_blank">View source</a>
</small>
@@ -541,7 +585,7 @@ t[3] # IndexError: tuple index out of range
Default level: <a href="../../rules#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#L400" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L427" target="_blank">View source</a>
</small>
@@ -630,7 +674,7 @@ an atypical memory layout.
Default level: <a href="../../rules#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#L672" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L699" target="_blank">View source</a>
</small>
@@ -657,7 +701,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../../rules#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#L712" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739" target="_blank">View source</a>
</small>
@@ -685,7 +729,7 @@ a: int = ''
Default level: <a href="../../rules#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#L2015" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2042" target="_blank">View source</a>
</small>
@@ -719,7 +763,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../../rules#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#L734" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L761" target="_blank">View source</a>
</small>
@@ -755,7 +799,7 @@ asyncio.run(main())
Default level: <a href="../../rules#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#L764" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L791" target="_blank">View source</a>
</small>
@@ -779,7 +823,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../../rules#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#L815" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L842" target="_blank">View source</a>
</small>
@@ -806,7 +850,7 @@ with 1:
Default level: <a href="../../rules#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#L836" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L863" target="_blank">View source</a>
</small>
@@ -835,7 +879,7 @@ a: str
Default level: <a href="../../rules#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#L859" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L886" target="_blank">View source</a>
</small>
@@ -879,7 +923,7 @@ except ZeroDivisionError:
Default level: <a href="../../rules#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#L1685" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1712" target="_blank">View source</a>
</small>
@@ -921,7 +965,7 @@ class D(A):
Default level: <a href="../../rules#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.35">0.0.1-alpha.35</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-frozen-dataclass-subclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2241" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2268" target="_blank">View source</a>
</small>
@@ -965,7 +1009,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: <a href="../../rules#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#L895" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L922" target="_blank">View source</a>
</small>
@@ -1003,7 +1047,7 @@ class D(Generic[U, T]): ...
Default level: <a href="../../rules#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#L72" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L75" target="_blank">View source</a>
</small>
@@ -1033,7 +1077,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../../rules#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#L639" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L666" target="_blank">View source</a>
</small>
@@ -1072,7 +1116,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: <a href="../../rules#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#L926" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L953" target="_blank">View source</a>
</small>
@@ -1107,7 +1151,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../../rules#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#L1023" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1050" target="_blank">View source</a>
</small>
@@ -1141,7 +1185,7 @@ class B(metaclass=f): ...
Default level: <a href="../../rules#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#L2143" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2170" target="_blank">View source</a>
</small>
@@ -1248,7 +1292,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: <a href="../../rules#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#L546" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L573" target="_blank">View source</a>
</small>
@@ -1302,7 +1346,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: <a href="../../rules#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#L999" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1026" target="_blank">View source</a>
</small>
@@ -1332,7 +1376,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: <a href="../../rules#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#L1050" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1077" target="_blank">View source</a>
</small>
@@ -1382,7 +1426,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../../rules#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#L1149" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176" target="_blank">View source</a>
</small>
@@ -1408,7 +1452,7 @@ def f(a: int = ''): ...
Default level: <a href="../../rules#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#L954" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981" target="_blank">View source</a>
</small>
@@ -1439,7 +1483,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: <a href="../../rules#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#L482" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L509" target="_blank">View source</a>
</small>
@@ -1473,7 +1517,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../../rules#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#L1169" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1196" target="_blank">View source</a>
</small>
@@ -1522,7 +1566,7 @@ def g():
Default level: <a href="../../rules#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#L693" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L720" target="_blank">View source</a>
</small>
@@ -1547,7 +1591,7 @@ def func() -> int:
Default level: <a href="../../rules#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#L1212" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1239" target="_blank">View source</a>
</small>
@@ -1597,7 +1641,45 @@ Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.
</small>
TODO #14889
**What it does**
Checks for string-literal annotations where the string cannot be
parsed as a Python expression.
**Why is this bad?**
Type annotations are expected to be Python expressions that
describe the expected type of a variable, parameter, attribute or
`return` statement.
Type annotations are permitted to be string-literal expressions, in
order to enable forward references to names not yet defined.
However, it must be possible to parse the contents of that string
literal as a normal Python expression.
**Example**
```python
def foo() -> "intstance of C":
return 42
class C: ...
```
Use instead:
```python
def foo() -> "C":
return 42
class C: ...
```
**References**
- [Typing spec: The meaning of annotations](https://typing.python.org/en/latest/spec/annotations.html#the-meaning-of-annotations)
- [Typing spec: String annotations](https://typing.python.org/en/latest/spec/annotations.html#string-annotations)
## `invalid-type-alias-type`
@@ -1605,7 +1687,7 @@ TODO #14889
Default level: <a href="../../rules#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#L978" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1005" target="_blank">View source</a>
</small>
@@ -1632,7 +1714,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: <a href="../../rules#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#L1444" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471" target="_blank">View source</a>
</small>
@@ -1679,7 +1761,7 @@ Bar[int] # error: too few arguments
Default level: <a href="../../rules#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#L1251" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1278" target="_blank">View source</a>
</small>
@@ -1709,7 +1791,7 @@ TYPE_CHECKING = ''
Default level: <a href="../../rules#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#L1275" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1302" target="_blank">View source</a>
</small>
@@ -1739,7 +1821,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../../rules#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#L1327" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1354" target="_blank">View source</a>
</small>
@@ -1773,7 +1855,7 @@ f(10) # Error
Default level: <a href="../../rules#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#L1299" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1326" target="_blank">View source</a>
</small>
@@ -1807,7 +1889,7 @@ class C:
Default level: <a href="../../rules#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#L1355" target="_blank">View source</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>
</small>
@@ -1842,7 +1924,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: <a href="../../rules#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#L1384" target="_blank">View source</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>
@@ -1867,7 +1949,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../../rules#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#L2116" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2143" target="_blank">View source</a>
</small>
@@ -1900,7 +1982,7 @@ alice["age"] # KeyError
Default level: <a href="../../rules#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#L1403" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1430" target="_blank">View source</a>
</small>
@@ -1929,7 +2011,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../../rules#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#L1426" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453" target="_blank">View source</a>
</small>
@@ -1953,7 +2035,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../../rules#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#L1485" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1512" target="_blank">View source</a>
</small>
@@ -1979,7 +2061,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1658" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1685" target="_blank">View source</a>
</small>
@@ -2012,7 +2094,7 @@ class B(A):
Default level: <a href="../../rules#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#L1536" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563" target="_blank">View source</a>
</small>
@@ -2039,7 +2121,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../../rules#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#L1869" 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>
@@ -2066,7 +2148,7 @@ f(x=1) # Error raised here
Default level: <a href="../../rules#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#L1557" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1584" target="_blank">View source</a>
</small>
@@ -2094,7 +2176,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: <a href="../../rules#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#L153" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L180" target="_blank">View source</a>
</small>
@@ -2126,7 +2208,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../../rules#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.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#L1579" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1606" target="_blank">View source</a>
</small>
@@ -2163,7 +2245,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../../rules#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#L1609" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1636" target="_blank">View source</a>
</small>
@@ -2227,7 +2309,7 @@ def test(): -> "int":
Default level: <a href="../../rules#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#L2043" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2070" target="_blank">View source</a>
</small>
@@ -2254,7 +2336,7 @@ cast(int, f()) # Redundant
Default level: <a href="../../rules#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#L1991" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2018" target="_blank">View source</a>
</small>
@@ -2284,7 +2366,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: <a href="../../rules#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#L1635" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1662" target="_blank">View source</a>
</small>
@@ -2313,7 +2395,7 @@ class B(A): ... # Error raised here
Default level: <a href="../../rules#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/0.0.1-alpha.30">0.0.1-alpha.30</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20super-call-in-named-tuple-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1803" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1830" target="_blank">View source</a>
</small>
@@ -2347,7 +2429,7 @@ class F(NamedTuple):
Default level: <a href="../../rules#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#L1743" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1770" target="_blank">View source</a>
</small>
@@ -2374,7 +2456,7 @@ f("foo") # Error raised here
Default level: <a href="../../rules#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#L1721" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1748" target="_blank">View source</a>
</small>
@@ -2402,7 +2484,7 @@ def _(x: int):
Default level: <a href="../../rules#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#L1764" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1791" target="_blank">View source</a>
</small>
@@ -2448,7 +2530,7 @@ class A:
Default level: <a href="../../rules#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#L1830" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1857" target="_blank">View source</a>
</small>
@@ -2472,7 +2554,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: <a href="../../rules#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#L1848" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1875" target="_blank">View source</a>
</small>
@@ -2499,7 +2581,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../../rules#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#L1890" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1917" target="_blank">View source</a>
</small>
@@ -2527,7 +2609,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../../rules#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#L2064" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2091" target="_blank">View source</a>
</small>
@@ -2585,7 +2667,7 @@ def g():
Default level: <a href="../../rules#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#L1912" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1939" target="_blank">View source</a>
</small>
@@ -2610,7 +2692,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../../rules#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#L1931" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1958" target="_blank">View source</a>
</small>
@@ -2635,7 +2717,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../../rules#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#L782" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809" target="_blank">View source</a>
</small>
@@ -2674,7 +2756,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../../rules#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#L1505" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1532" target="_blank">View source</a>
</small>
@@ -2711,7 +2793,7 @@ b1 < b2 < b1 # exception raised here
Default level: <a href="../../rules#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#L1950" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1977" target="_blank">View source</a>
</small>
@@ -2739,7 +2821,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: <a href="../../rules#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#L22" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L25" target="_blank">View source</a>
</small>
@@ -2770,7 +2852,7 @@ a = 20 / 2
Default level: <a href="../../rules#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#L1093" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1120" target="_blank">View source</a>
</small>
@@ -2833,7 +2915,7 @@ def foo(x: int | str) -> int | str:
Default level: <a href="../../rules#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#L1972" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1999" target="_blank">View source</a>
</small>

View File

@@ -10,9 +10,9 @@ use ty_static::EnvVars;
use std::fmt::Write;
use std::process::{ExitCode, Termination};
use std::sync::Mutex;
use anyhow::Result;
use std::sync::Mutex;
use crate::args::{CheckCommand, Command, TerminalColor};
use crate::logging::{VerbosityLevel, setup_tracing};
@@ -22,6 +22,7 @@ use clap::{CommandFactory, Parser};
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use rayon::ThreadPoolBuilder;
use ruff_db::cancellation::{CancellationToken, CancellationTokenSource};
use ruff_db::diagnostic::{
Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity,
};
@@ -121,7 +122,9 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let force_exclude = args.force_exclude();
let mut project_metadata = match &config_file {
Some(config_file) => ProjectMetadata::from_config_file(config_file.clone(), &system)?,
Some(config_file) => {
ProjectMetadata::from_config_file(config_file.clone(), &project_path, &system)?
}
None => ProjectMetadata::discover(&project_path, &system)?,
};
@@ -225,6 +228,11 @@ struct MainLoop {
printer: Printer,
project_options_overrides: ProjectOptionsOverrides,
/// Cancellation token that gets set by Ctrl+C.
/// Used for long-running operations on the main thread. Operations on background threads
/// use Salsa's cancellation mechanism.
cancellation_token: CancellationToken,
}
impl MainLoop {
@@ -234,6 +242,9 @@ impl MainLoop {
) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);
let cancellation_token_source = CancellationTokenSource::new();
let cancellation_token = cancellation_token_source.token();
(
Self {
sender: sender.clone(),
@@ -241,8 +252,12 @@ impl MainLoop {
watcher: None,
project_options_overrides,
printer,
cancellation_token,
},
MainLoopCancellationToken {
sender,
source: cancellation_token_source,
},
MainLoopCancellationToken { sender },
)
}
@@ -314,6 +329,7 @@ impl MainLoop {
let display_config = DisplayDiagnosticConfig::default()
.format(terminal_settings.output_format.into())
.color(colored::control::SHOULD_COLORIZE.should_colorize())
.with_cancellation_token(Some(self.cancellation_token.clone()))
.show_fix_diff(true);
if check_revision == revision {
@@ -357,19 +373,21 @@ impl MainLoop {
)?;
}
if is_human_readable {
writeln!(
self.printer.stream_for_failure_summary(),
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
)?;
}
if !self.cancellation_token.is_cancelled() {
if is_human_readable {
writeln!(
self.printer.stream_for_failure_summary(),
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
)?;
}
if exit_status.is_internal_error() {
tracing::warn!(
"A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."
);
if exit_status.is_internal_error() {
tracing::warn!(
"A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."
);
}
}
if self.watcher.is_none() {
@@ -496,10 +514,12 @@ impl ty_project::ProgressReporter for IndicatifReporter {
#[derive(Debug)]
struct MainLoopCancellationToken {
sender: crossbeam_channel::Sender<MainLoopMessage>,
source: CancellationTokenSource,
}
impl MainLoopCancellationToken {
fn stop(self) {
self.source.cancel();
self.sender.send(MainLoopMessage::Exit).unwrap();
}
}

View File

@@ -0,0 +1,43 @@
use insta_cmd::assert_cmd_snapshot;
use crate::CliTest;
/// ty ignores `type: ignore` comments when setting `respect-type-ignore-comments=false`
#[test]
fn respect_type_ignore_comments_is_turned_off() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
y = a + 5 # type: ignore
"#,
)?;
// Assert that there's an `unresolved-reference` diagnostic (error).
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
");
assert_cmd_snapshot!(case.command().arg("--config").arg("analysis.respect-type-ignore-comments=false"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `a` used when not defined
--> test.py:2:5
|
2 | y = a + 5 # type: ignore
| ^
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
");
Ok(())
}

View File

@@ -133,7 +133,7 @@ fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
1 | bad-option=true
| ^^^^^^^^^^
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `overrides`
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `analysis`, `overrides`
Usage: ty <COMMAND>

View File

@@ -1,3 +1,4 @@
mod analysis_options;
mod config_option;
mod exit_code;
mod file_selection;
@@ -658,6 +659,8 @@ fn gitlab_diagnostics() -> anyhow::Result<()> {
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
from typing_extensions import reveal_type
reveal_type('str'.lower()) # [revealed-type]
"#,
)?;
@@ -708,6 +711,25 @@ fn gitlab_diagnostics() -> anyhow::Result<()> {
}
}
}
},
{
"check_name": "revealed-type",
"description": "revealed-type: Revealed type: `LiteralString`",
"severity": "info",
"fingerprint": "[FINGERPRINT]",
"location": {
"path": "test.py",
"positions": {
"begin": {
"line": 5,
"column": 13
},
"end": {
"line": 5,
"column": 26
}
}
}
}
]
----- stderr -----
@@ -723,6 +745,8 @@ fn github_diagnostics() -> anyhow::Result<()> {
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
from typing_extensions import reveal_type
reveal_type('str'.lower()) # [revealed-type]
"#,
)?;
@@ -732,6 +756,7 @@ fn github_diagnostics() -> anyhow::Result<()> {
----- stdout -----
::warning title=ty (unresolved-reference),file=<temp_dir>/test.py,line=2,col=7,endLine=2,endColumn=8::test.py:2:7: unresolved-reference: Name `x` used when not defined
::error title=ty (non-subscriptable),file=<temp_dir>/test.py,line=3,col=7,endLine=3,endColumn=11::test.py:3:7: non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
::notice title=ty (revealed-type),file=<temp_dir>/test.py,line=5,col=13,endLine=5,endColumn=26::test.py:5:13: revealed-type: Revealed type: `LiteralString`
----- stderr -----
");

View File

@@ -10,12 +10,13 @@ use ruff_db::system::{
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard, file_time_now,
};
use ruff_python_ast::PythonVersion;
use ty_module_resolver::{Module, ModuleName, resolve_module_confident};
use ty_project::metadata::options::{EnvironmentOptions, Options, ProjectOptionsOverrides};
use ty_project::metadata::pyproject::{PyProject, Tool};
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher};
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module_confident};
use ty_python_semantic::PythonPlatform;
struct TestCase {
db: ProjectDatabase,
@@ -1019,7 +1020,7 @@ fn search_path() -> anyhow::Result<()> {
let site_packages = case.root_path().join("site_packages");
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new("a").unwrap()),
resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()),
None
);
@@ -1192,7 +1193,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
// Unset the custom typeshed directory.
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new("os").unwrap()),
resolve_module_confident(case.db(), &ModuleName::new_static("os").unwrap()),
None
);
@@ -1207,7 +1208,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
case.apply_changes(changes, None);
assert!(resolve_module_confident(case.db(), &ModuleName::new("os").unwrap()).is_some());
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("os").unwrap()).is_some());
Ok(())
}
@@ -1874,11 +1875,11 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
let mut case = setup([("lib.py", "class Foo: ...")])?;
assert!(
resolve_module_confident(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
resolve_module_confident(case.db(), &ModuleName::new_static("lib").unwrap()).is_some(),
"Expected `lib` module to exist."
);
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new("Lib").unwrap()),
resolve_module_confident(case.db(), &ModuleName::new_static("Lib").unwrap()),
None,
"Expected `Lib` module not to exist"
);
@@ -1911,13 +1912,13 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
// Resolving `lib` should now fail but `Lib` should now succeed
assert_eq!(
resolve_module_confident(case.db(), &ModuleName::new("lib").unwrap()),
resolve_module_confident(case.db(), &ModuleName::new_static("lib").unwrap()),
None,
"Expected `lib` module to no longer exist."
);
assert!(
resolve_module_confident(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
resolve_module_confident(case.db(), &ModuleName::new_static("Lib").unwrap()).is_some(),
"Expected `Lib` module to exist"
);

View File

@@ -12,7 +12,6 @@ license.workspace = true
[dependencies]
ruff_db = { workspace = true }
ruff_python_ast = { workspace = true }
ty_python_semantic = { workspace = true }
ordermap = { workspace = true }

View File

@@ -8,7 +8,6 @@ use std::{collections::HashMap, hash::BuildHasher};
use ordermap::OrderMap;
use ruff_db::system::SystemPathBuf;
use ruff_python_ast::PythonVersion;
use ty_python_semantic::PythonPlatform;
/// Combine two values, preferring the values in `self`.
///
@@ -145,7 +144,6 @@ macro_rules! impl_noop_combine {
}
impl_noop_combine!(SystemPathBuf);
impl_noop_combine!(PythonPlatform);
impl_noop_combine!(PythonVersion);
// std types

View File

@@ -15,8 +15,8 @@ ruff_db = { workspace = true, features = ["os"] }
ruff_text_size = { workspace = true }
ty_ide = { workspace = true }
ty_module_resolver = { workspace = true }
ty_project = { workspace = true }
ty_python_semantic = { workspace = true }
anyhow = { workspace = true }
bstr = { workspace = true }

View File

@@ -15,11 +15,11 @@ use regex::bytes::Regex;
use ruff_db::files::system_path_to_file;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use ty_ide::Completion;
use ty_module_resolver::ModuleName;
use ty_project::metadata::Options;
use ty_project::metadata::options::EnvironmentOptions;
use ty_project::metadata::value::RelativePathBuf;
use ty_project::{ProjectDatabase, ProjectMetadata};
use ty_python_semantic::ModuleName;
#[derive(Debug, clap::Parser)]
#[command(

View File

@@ -22,6 +22,7 @@ ruff_python_importer = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ty_module_resolver = { workspace = true }
ty_python_semantic = { workspace = true }
ty_project = { workspace = true, features = ["testing"] }
ty_vendored = { workspace = true }

View File

@@ -1,6 +1,6 @@
use ruff_db::files::File;
use ty_module_resolver::{Module, ModuleName, all_modules, resolve_real_shadowable_module};
use ty_project::Db;
use ty_python_semantic::{Module, ModuleName, all_modules, resolve_real_shadowable_module};
use crate::{
SymbolKind,
@@ -21,7 +21,7 @@ pub fn all_symbols<'db>(
return Vec::new();
}
let typing_extensions = ModuleName::new("typing_extensions").unwrap();
let typing_extensions = ModuleName::new_static("typing_extensions").unwrap();
let is_typing_extensions_available = importing_from.is_stub(db)
|| resolve_real_shadowable_module(db, importing_from, &typing_extensions).is_some();

View File

@@ -11,9 +11,10 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_codegen::Stylist;
use ruff_text_size::{Ranged, TextRange, TextSize};
use rustc_hash::FxHashSet;
use ty_module_resolver::{KnownModule, ModuleName};
use ty_python_semantic::types::UnionType;
use ty_python_semantic::{
Completion as SemanticCompletion, KnownModule, ModuleName, NameKind, SemanticModel,
Completion as SemanticCompletion, NameKind, SemanticModel,
types::{CycleDetector, KnownClass, Type},
};
@@ -2106,7 +2107,7 @@ mod tests {
use insta::assert_snapshot;
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_parser::{Mode, ParseOptions};
use ty_python_semantic::ModuleName;
use ty_module_resolver::ModuleName;
use crate::completion::{Completion, completion};
use crate::tests::{CursorTest, CursorTestBuilder};
@@ -6685,6 +6686,19 @@ def func():
.not_contains("False");
}
#[test]
fn decorator_without_class_or_function() {
completion_test_builder(
"\
from dataclasses import dataclass
@dataclass(froz<CURSOR>
",
)
.build()
.contains("frozen");
}
#[test]
fn statement_keywords_in_if_body() {
completion_test_builder(

View File

@@ -29,10 +29,11 @@ use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal
use ruff_python_codegen::Stylist;
use ruff_python_importer::Insertion;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_module_resolver::ModuleName;
use ty_project::Db;
use ty_python_semantic::semantic_index::definition::DefinitionKind;
use ty_python_semantic::types::Type;
use ty_python_semantic::{MemberDefinition, ModuleName, SemanticModel};
use ty_python_semantic::{MemberDefinition, SemanticModel};
pub(crate) struct Importer<'a> {
/// The ty Salsa database.
@@ -880,10 +881,10 @@ mod tests {
use ruff_python_codegen::Stylist;
use ruff_python_trivia::textwrap::dedent;
use ruff_text_size::TextSize;
use ty_module_resolver::SearchPathSettings;
use ty_project::ProjectMetadata;
use ty_python_semantic::{
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
SemanticModel,
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SemanticModel,
};
use super::*;

View File

@@ -6614,13 +6614,6 @@ mod tests {
3 | y[: type[T@f]] = x
| ^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def f[T](x: type[T]):
y: type[T@f] = x
"#);
}

View File

@@ -17,9 +17,6 @@
//! TODO: Need to properly handle Annotated expressions. All type arguments other
//! than the first should be treated as value expressions, not as type expressions.
//!
//! TODO: An identifier that resolves to a parameter when used within a function
//! should be classified as a parameter, selfParameter, or clsParameter token.
//!
//! TODO: Properties (or perhaps more generally, descriptor objects?) should be
//! classified as property tokens rather than just variables.
//!
@@ -230,6 +227,11 @@ impl<'db> SemanticTokenVisitor<'db> {
modifiers: SemanticTokenModifier,
) {
let range = ranged.range();
if range.is_empty() {
return;
}
// Only emit tokens that intersect with the range filter, if one is specified
if let Some(range_filter) = self.range_filter {
// Only include ranges that have a non-empty overlap. Adjacent ranges
@@ -707,15 +709,15 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
}
ast::Stmt::Import(import) => {
for alias in &import.names {
// Create separate tokens for each part of a dotted module name
self.add_dotted_name_tokens(&alias.name, SemanticTokenType::Namespace);
if let Some(asname) = &alias.asname {
self.add_token(
asname.range(),
SemanticTokenType::Namespace,
SemanticTokenModifier::empty(),
);
} else {
// Create separate tokens for each part of a dotted module name
self.add_dotted_name_tokens(&alias.name, SemanticTokenType::Namespace);
}
}
}
@@ -1131,7 +1133,7 @@ mod tests {
use ty_project::ProjectMetadata;
#[test]
fn test_semantic_tokens_basic() {
fn semantic_tokens_basic() {
let test = SemanticTokenTest::new("def foo(): pass");
let tokens = test.highlight_file();
@@ -1142,7 +1144,7 @@ mod tests {
}
#[test]
fn test_semantic_tokens_class() {
fn semantic_tokens_class() {
let test = SemanticTokenTest::new("class MyClass: pass");
let tokens = test.highlight_file();
@@ -1153,7 +1155,7 @@ mod tests {
}
#[test]
fn test_semantic_tokens_class_args() {
fn semantic_tokens_class_args() {
// This used to cause a panic because of an incorrect
// insertion-order when visiting arguments inside
// class definitions.
@@ -1169,7 +1171,7 @@ mod tests {
}
#[test]
fn test_semantic_tokens_variables() {
fn semantic_tokens_variables() {
let test = SemanticTokenTest::new(
"
x = 42
@@ -1188,7 +1190,7 @@ y = 'hello'
}
#[test]
fn test_semantic_tokens_walrus() {
fn semantic_tokens_walrus() {
let test = SemanticTokenTest::new(
"
if x := 42:
@@ -1207,7 +1209,7 @@ if x := 42:
}
#[test]
fn test_semantic_tokens_self_parameter() {
fn semantic_tokens_self_parameter() {
let test = SemanticTokenTest::new(
"
class MyClass:
@@ -1238,7 +1240,7 @@ class MyClass:
}
#[test]
fn test_semantic_tokens_cls_parameter() {
fn semantic_tokens_cls_parameter() {
let test = SemanticTokenTest::new(
"
class MyClass:
@@ -1261,7 +1263,7 @@ class MyClass:
}
#[test]
fn test_semantic_tokens_staticmethod_parameter() {
fn semantic_tokens_staticmethod_parameter() {
let test = SemanticTokenTest::new(
"
class MyClass:
@@ -1282,7 +1284,7 @@ class MyClass:
}
#[test]
fn test_semantic_tokens_custom_self_cls_names() {
fn semantic_tokens_custom_self_cls_names() {
let test = SemanticTokenTest::new(
"
class MyClass:
@@ -1317,7 +1319,7 @@ class MyClass:
}
#[test]
fn test_semantic_tokens_modifiers() {
fn semantic_tokens_modifiers() {
let test = SemanticTokenTest::new(
"
class MyClass:
@@ -1338,7 +1340,7 @@ class MyClass:
}
#[test]
fn test_semantic_classification_vs_heuristic() {
fn semantic_classification_vs_heuristic() {
let test = SemanticTokenTest::new(
"
import sys
@@ -1372,7 +1374,7 @@ z = sys.version
}
#[test]
fn test_builtin_constants() {
fn builtin_constants() {
let test = SemanticTokenTest::new(
"
x = True
@@ -1394,7 +1396,7 @@ z = None
}
#[test]
fn test_builtin_constants_in_expressions() {
fn builtin_constants_in_expressions() {
let test = SemanticTokenTest::new(
"
def check(value):
@@ -1422,7 +1424,7 @@ result = check(None)
}
#[test]
fn test_builtin_types() {
fn builtin_types() {
let test = SemanticTokenTest::new(
r#"
type U = str | int
@@ -1467,7 +1469,7 @@ result = check(None)
}
#[test]
fn test_semantic_tokens_range() {
fn semantic_tokens_range() {
let test = SemanticTokenTest::new(
"
def function1():
@@ -1532,7 +1534,7 @@ def function2():
/// When a token starts right at where the requested range ends,
/// don't include it in the semantic tokens.
#[test]
fn test_semantic_tokens_range_excludes_boundary_tokens() {
fn semantic_tokens_range_excludes_boundary_tokens() {
let test = SemanticTokenTest::new(
"
x = 1
@@ -1555,7 +1557,7 @@ z = 3
}
#[test]
fn test_dotted_module_names() {
fn dotted_module_names() {
let test = SemanticTokenTest::new(
"
import os.path
@@ -1582,7 +1584,7 @@ from collections.abc import Mapping
}
#[test]
fn test_module_type_classification() {
fn module_type_classification() {
let test = SemanticTokenTest::new(
"
import os
@@ -1610,7 +1612,7 @@ y = sys
}
#[test]
fn test_import_classification() {
fn import_classification() {
let test = SemanticTokenTest::new(
"
from os import path
@@ -1641,7 +1643,7 @@ from mymodule import CONSTANT, my_function, MyClass
}
#[test]
fn test_str_annotation() {
fn str_annotation() {
let test = SemanticTokenTest::new(
r#"
x: int = 1
@@ -1685,7 +1687,7 @@ w5: "float
}
#[test]
fn test_attribute_classification() {
fn attribute_classification() {
let test = SemanticTokenTest::new(
"
import os
@@ -1759,7 +1761,7 @@ u = List.__name__ # __name__ should be variable
}
#[test]
fn test_attribute_fallback_classification() {
fn attribute_fallback_classification() {
let test = SemanticTokenTest::new(
"
class MyClass:
@@ -1790,7 +1792,7 @@ y = obj.unknown_attr # Should fall back to variable
}
#[test]
fn test_constant_name_detection() {
fn constant_name_detection() {
let test = SemanticTokenTest::new(
"
class MyClass:
@@ -1837,7 +1839,7 @@ w = obj.A # Should not have readonly modifier (length == 1)
}
#[test]
fn test_type_annotations() {
fn type_annotations() {
let test = SemanticTokenTest::new(
r#"
from typing import List, Optional
@@ -2325,7 +2327,7 @@ class MyClass:
}
#[test]
fn test_debug_int_classification() {
fn debug_int_classification() {
let test = SemanticTokenTest::new(
"
x: int = 42
@@ -2342,7 +2344,7 @@ x: int = 42
}
#[test]
fn test_debug_user_defined_type_classification() {
fn debug_user_defined_type_classification() {
let test = SemanticTokenTest::new(
"
class MyClass:
@@ -2363,7 +2365,7 @@ x: MyClass = MyClass()
}
#[test]
fn test_type_annotation_vs_variable_classification() {
fn type_annotation_vs_variable_classification() {
let test = SemanticTokenTest::new(
"
from typing import List, Optional
@@ -2413,7 +2415,7 @@ def test_function(param: int, other: MyClass) -> Optional[List[str]]:
}
#[test]
fn test_protocol_types_in_annotations() {
fn protocol_types_in_annotations() {
let test = SemanticTokenTest::new(
"
from typing import Protocol
@@ -2444,7 +2446,7 @@ def test_function(param: MyProtocol) -> None:
}
#[test]
fn test_protocol_type_annotation_vs_value_context() {
fn protocol_type_annotation_vs_value_context() {
let test = SemanticTokenTest::new(
"
from typing import Protocol
@@ -2530,7 +2532,7 @@ def test_function(param: my_type_alias): ...
}
#[test]
fn test_type_parameters_pep695() {
fn type_parameters_pep695() {
let test = SemanticTokenTest::new(
"
# Test Python 3.12 PEP 695 type parameter syntax
@@ -2654,7 +2656,7 @@ class BoundedContainer[T: int, U = str]:
}
#[test]
fn test_type_parameters_usage_in_function_body() {
fn type_parameters_usage_in_function_body() {
let test = SemanticTokenTest::new(
"
def generic_function[T](value: T) -> T:
@@ -2683,7 +2685,7 @@ def generic_function[T](value: T) -> T:
}
#[test]
fn test_decorator_classification() {
fn decorator_classification() {
let test = SemanticTokenTest::new(
r#"
@staticmethod
@@ -2713,7 +2715,7 @@ class MyClass:
}
#[test]
fn test_constant_variations() {
fn constant_variations() {
let test = SemanticTokenTest::new(
r#"
A = 1
@@ -2756,7 +2758,7 @@ A_1 = 1
}
#[test]
fn test_implicitly_concatenated_strings() {
fn implicitly_concatenated_strings() {
let test = SemanticTokenTest::new(
r#"x = "hello" "world"
y = ("multi"
@@ -2783,7 +2785,7 @@ z = 'single' "mixed" 'quotes'"#,
}
#[test]
fn test_bytes_literals() {
fn bytes_literals() {
let test = SemanticTokenTest::new(
r#"x = b"hello" b"world"
y = (b"multi"
@@ -2810,7 +2812,7 @@ z = b'single' b"mixed" b'quotes'"#,
}
#[test]
fn test_mixed_string_and_bytes_literals() {
fn mixed_string_and_bytes_literals() {
let test = SemanticTokenTest::new(
r#"# Test mixed string and bytes literals
string_concat = "hello" "world"
@@ -2846,7 +2848,7 @@ regular_bytes = b"just bytes""#,
}
#[test]
fn test_fstring_with_mixed_literals() {
fn fstring_with_mixed_literals() {
let test = SemanticTokenTest::new(
r#"
# Test f-strings with various literal types
@@ -2898,7 +2900,7 @@ complex_fstring = f"User: {name.upper()}, Count: {len(data)}, Hex: {value:x}"
}
#[test]
fn test_nonlocal_and_global_statements() {
fn nonlocal_and_global_statements() {
let test = SemanticTokenTest::new(
r#"
x = "global_value"
@@ -2960,7 +2962,7 @@ def outer():
}
#[test]
fn test_nonlocal_global_edge_cases() {
fn nonlocal_global_edge_cases() {
let test = SemanticTokenTest::new(
r#"
# Single variable statements
@@ -3000,7 +3002,7 @@ def test():
}
#[test]
fn test_pattern_matching() {
fn pattern_matching() {
let test = SemanticTokenTest::new(
r#"
def process_data(data):
@@ -3056,7 +3058,7 @@ def process_data(data):
}
#[test]
fn test_exception_handlers() {
fn exception_handlers() {
let test = SemanticTokenTest::new(
r#"
try:
@@ -3095,7 +3097,7 @@ finally:
}
#[test]
fn test_self_attribute_expression() {
fn self_attribute_expression() {
let test = SemanticTokenTest::new(
r#"
from typing import Self
@@ -3135,7 +3137,7 @@ class C:
}
#[test]
fn test_augmented_assignment() {
fn augmented_assignment() {
let test = SemanticTokenTest::new(
r#"
x = 0
@@ -3154,7 +3156,7 @@ x += 1
}
#[test]
fn test_type_alias() {
fn type_alias() {
let test = SemanticTokenTest::new("type MyList[T] = list[T]");
let tokens = test.highlight_file();
@@ -3168,7 +3170,7 @@ x += 1
}
#[test]
fn test_for_stmt() {
fn for_stmt() {
let test = SemanticTokenTest::new(
r#"
for item in []:
@@ -3190,7 +3192,7 @@ else:
}
#[test]
fn test_with_stmt() {
fn with_stmt() {
let test = SemanticTokenTest::new(
r#"
with open("file.txt") as f:
@@ -3210,7 +3212,7 @@ with open("file.txt") as f:
}
#[test]
fn test_comprehensions() {
fn comprehensions() {
let test = SemanticTokenTest::new(
r#"
list_comp = [x for x in range(10) if x % 2 == 0]
@@ -3255,7 +3257,7 @@ generator = (x for x in range(10))
/// Regression test for <https://github.com/astral-sh/ty/issues/1406>
#[test]
fn test_invalid_kwargs() {
fn invalid_kwargs() {
let test = SemanticTokenTest::new(
r#"
def foo(self, **key, value=10):
@@ -3274,6 +3276,24 @@ def foo(self, **key, value=10):
"#);
}
#[test]
fn import_as() {
let test = SemanticTokenTest::new(
r#"
import pathlib as path
from pathlib import Path
"#,
);
let tokens = test.highlight_file();
assert_snapshot!(test.to_snapshot(&tokens), @r#"
"pathlib" @ 8..15: Namespace
"path" @ 19..23: Namespace
"pathlib" @ 29..36: Namespace
"Path" @ 44..48: Class
"#);
}
pub(super) struct SemanticTokenTest {
pub(super) db: ty_project::TestDb,
file: File,

View File

@@ -14,8 +14,8 @@ use ruff_python_ast::name::{Name, UnqualifiedName};
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::{FxHashMap, FxHashSet};
use ty_module_resolver::{ModuleName, resolve_module};
use ty_project::Db;
use ty_python_semantic::{ModuleName, resolve_module};
use crate::completion::CompletionKind;

View File

@@ -0,0 +1,38 @@
[package]
name = "ty_module_resolver"
version = "0.0.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
[dependencies]
ruff_db = { workspace = true }
ruff_memory_usage = { workspace = true }
ruff_python_ast = { workspace = true, features = ["salsa"] }
ruff_python_stdlib = { workspace = true }
camino = { workspace = true }
compact_str = { workspace = true }
get-size2 = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing", "os"] }
ty_vendored = { workspace = true }
anyhow = { workspace = true }
insta = { workspace = true, features = ["filters"] }
tempfile = { workspace = true }
[lints]
workspace = true

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,124 @@
use ruff_db::Db as SourceDb;
use crate::resolve::SearchPaths;
#[salsa::db]
pub trait Db: SourceDb {
/// Returns the search paths for module resolution.
fn search_paths(&self) -> &SearchPaths;
}
#[cfg(test)]
pub(crate) mod tests {
use std::sync::{Arc, Mutex};
use ruff_db::Db as SourceDb;
use ruff_db::files::Files;
use ruff_db::system::{DbWithTestSystem, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
use ruff_python_ast::PythonVersion;
use super::Db;
use crate::resolve::SearchPaths;
type Events = Arc<Mutex<Vec<salsa::Event>>>;
#[salsa::db]
#[derive(Clone)]
pub(crate) struct TestDb {
storage: salsa::Storage<Self>,
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
search_paths: Arc<SearchPaths>,
python_version: PythonVersion,
events: Events,
}
impl TestDb {
pub(crate) fn new() -> Self {
let events = Events::default();
Self {
storage: salsa::Storage::new(Some(Box::new({
let events = events.clone();
move |event| {
tracing::trace!("event: {event:?}");
let mut events = events.lock().unwrap();
events.push(event);
}
}))),
system: TestSystem::default(),
vendored: ty_vendored::file_system().clone(),
files: Files::default(),
search_paths: Arc::new(SearchPaths::empty(ty_vendored::file_system())),
python_version: PythonVersion::default(),
events,
}
}
pub(crate) fn with_search_paths(mut self, search_paths: SearchPaths) -> Self {
self.set_search_paths(search_paths);
self
}
pub(crate) fn with_python_version(mut self, python_version: PythonVersion) -> Self {
self.python_version = python_version;
self
}
pub(crate) fn set_search_paths(&mut self, search_paths: SearchPaths) {
search_paths.try_register_static_roots(self);
self.search_paths = Arc::new(search_paths);
}
/// Takes the salsa events.
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
let mut events = self.events.lock().unwrap();
std::mem::take(&mut *events)
}
/// Clears the salsa events.
pub(crate) fn clear_salsa_events(&mut self) {
self.take_salsa_events();
}
}
impl DbWithTestSystem for TestDb {
fn test_system(&self) -> &TestSystem {
&self.system
}
fn test_system_mut(&mut self) -> &mut TestSystem {
&mut self.system
}
}
#[salsa::db]
impl SourceDb for TestDb {
fn vendored(&self) -> &VendoredFileSystem {
&self.vendored
}
fn system(&self) -> &dyn ruff_db::system::System {
&self.system
}
fn files(&self) -> &Files {
&self.files
}
fn python_version(&self) -> PythonVersion {
self.python_version
}
}
#[salsa::db]
impl Db for TestDb {
fn search_paths(&self) -> &SearchPaths {
&self.search_paths
}
}
#[salsa::db]
impl salsa::Database for TestDb {}
}

View File

@@ -1,24 +1,31 @@
use std::iter::FusedIterator;
pub use list::{all_modules, list_modules};
pub use module::KnownModule;
pub use module::Module;
pub use path::{SearchPath, SearchPathValidationError};
pub use resolver::SearchPaths;
pub(crate) use resolver::file_to_module;
pub use resolver::{
resolve_module, resolve_module_confident, resolve_real_module, resolve_real_module_confident,
resolve_real_shadowable_module,
};
use ruff_db::system::SystemPath;
use crate::Db;
pub(crate) use resolver::{ModuleResolveMode, SearchPathIterator, search_paths};
pub use db::Db;
pub use module::KnownModule;
pub use module::Module;
pub use module_name::{ModuleName, ModuleNameResolutionError};
pub use path::{SearchPath, SearchPathError};
pub use resolve::{
SearchPaths, file_to_module, resolve_module, resolve_module_confident, resolve_real_module,
resolve_real_module_confident, resolve_real_shadowable_module,
};
pub use settings::{MisconfigurationMode, SearchPathSettings, SearchPathSettingsError};
pub use typeshed::{
PyVersionRange, TypeshedVersions, TypeshedVersionsParseError, vendored_typeshed_versions,
};
pub use list::{all_modules, list_modules};
pub use resolve::{ModuleResolveMode, SearchPathIterator, search_paths};
mod db;
mod list;
mod module;
mod module_name;
mod path;
mod resolver;
mod resolve;
mod settings;
mod typeshed;
#[cfg(test)]

View File

@@ -3,12 +3,10 @@ use std::collections::btree_map::{BTreeMap, Entry};
use ruff_python_ast::PythonVersion;
use crate::db::Db;
use crate::module::{Module, ModuleKind};
use crate::module_name::ModuleName;
use crate::program::Program;
use super::module::{Module, ModuleKind};
use super::path::{ModulePath, SearchPath, SystemOrVendoredPathRef};
use super::resolver::{ModuleResolveMode, ResolverContext, resolve_file_module, search_paths};
use crate::path::{ModulePath, SearchPath, SystemOrVendoredPathRef};
use crate::resolve::{ModuleResolveMode, ResolverContext, resolve_file_module, search_paths};
/// List all available modules, including all sub-modules, sorted in lexicographic order.
pub fn all_modules(db: &dyn Db) -> Vec<Module<'_>> {
@@ -100,7 +98,6 @@ fn list_modules_in<'db>(
/// in the same directory).
struct Lister<'db> {
db: &'db dyn Db,
program: Program,
search_path: &'db SearchPath,
modules: BTreeMap<&'db ModuleName, Module<'db>>,
}
@@ -109,10 +106,8 @@ impl<'db> Lister<'db> {
/// Create new state that can accumulate modules from a list
/// of file paths.
fn new(db: &'db dyn Db, search_path: &'db SearchPath) -> Lister<'db> {
let program = Program::get(db);
Lister {
db,
program,
search_path,
modules: BTreeMap::new(),
}
@@ -314,7 +309,7 @@ impl<'db> Lister<'db> {
/// Returns the Python version we want to perform module resolution
/// with.
fn python_version(&self) -> PythonVersion {
self.program.python_version(self.db)
self.db.python_version()
}
/// Constructs a resolver context for use with some APIs that require it.
@@ -382,20 +377,17 @@ mod tests {
use camino::{Utf8Component, Utf8Path};
use ruff_db::Db as _;
use ruff_db::files::{File, FilePath, FileRootKind};
use ruff_db::system::{
DbWithTestSystem, DbWithWritableSystem, OsSystem, SystemPath, SystemPathBuf,
};
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem, SystemPath, SystemPathBuf};
use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::PythonVersion;
use crate::db::{Db, tests::TestDb};
use crate::module_resolver::module::Module;
use crate::module_resolver::resolver::{
use crate::module::Module;
use crate::resolve::{
ModuleResolveMode, ModuleResolveModeIngredient, dynamic_resolution_paths,
};
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use crate::program::{Program, ProgramSettings, SearchPathSettings};
use crate::{PythonPlatform, PythonVersionSource, PythonVersionWithSource};
use crate::settings::SearchPathSettings;
use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use super::list_modules;
@@ -940,7 +932,7 @@ mod tests {
fn symlink() -> anyhow::Result<()> {
use anyhow::Context;
let mut db = TestDb::new();
let mut db = TestDb::new().with_python_version(PythonVersion::PY38);
let temp_dir = tempfile::TempDir::with_prefix("PREFIX-SENTINEL")?;
let root = temp_dir
@@ -948,7 +940,7 @@ mod tests {
.canonicalize()
.context("Failed to canonicalize temp dir")?;
let root = SystemPath::from_std_path(&root).unwrap();
db.use_system(OsSystem::new(root));
db.use_system(ruff_db::system::OsSystem::new(root));
let src = root.join("src");
let site_packages = root.join("site-packages");
@@ -965,26 +957,21 @@ mod tests {
std::fs::write(foo.as_std_path(), "")?;
std::os::unix::fs::symlink(foo.as_std_path(), bar.as_std_path())?;
db.files().try_add_root(&db, &src, FileRootKind::Project);
let settings = SearchPathSettings {
src_roots: vec![src.clone()],
custom_typeshed: Some(custom_typeshed),
site_packages_paths: vec![site_packages],
..SearchPathSettings::empty()
};
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource {
version: PythonVersion::PY38,
source: PythonVersionSource::default(),
},
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
custom_typeshed: Some(custom_typeshed),
site_packages_paths: vec![site_packages],
..SearchPathSettings::new(vec![src])
}
db.set_search_paths(
settings
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings"),
},
);
db.files().try_add_root(&db, &src, FileRootKind::Project);
// From the original test in the "resolve this module"
// implementation, this test seems to symlink a Python module
// and assert that they are treated as two distinct modules.
@@ -1479,18 +1466,15 @@ not_a_directory
db.files()
.try_add_root(&db, SystemPath::new("/src"), FileRootKind::Project);
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
site_packages_paths: vec![venv_site_packages],
..SearchPathSettings::new(vec![src.to_path_buf()])
}
let settings = SearchPathSettings {
site_packages_paths: vec![venv_site_packages],
..SearchPathSettings::new(vec![src.to_path_buf()])
};
db.set_search_paths(
settings
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings"),
},
);
insta::assert_debug_snapshot!(
@@ -1533,18 +1517,15 @@ not_a_directory
db.files()
.try_add_root(&db, SystemPath::new("/src"), FileRootKind::Project);
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
site_packages_paths: vec![venv_site_packages, system_site_packages],
..SearchPathSettings::new(vec![SystemPathBuf::from("/src")])
}
let settings = SearchPathSettings {
site_packages_paths: vec![venv_site_packages, system_site_packages],
..SearchPathSettings::new(vec![SystemPathBuf::from("/src")])
};
db.set_search_paths(
settings
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings"),
},
);
// The editable installs discovered from the `.pth` file in the
@@ -1585,7 +1566,7 @@ not_a_directory
#[test]
#[cfg(unix)]
fn case_sensitive_resolution_with_symlinked_directory() -> anyhow::Result<()> {
use anyhow::Context;
use anyhow::Context as _;
let temp_dir = tempfile::TempDir::with_prefix("PREFIX-SENTINEL")?;
let root = SystemPathBuf::from_path_buf(
@@ -1602,7 +1583,7 @@ not_a_directory
let a_package_target = root.join("a-package");
let a_src = src.join("a");
db.use_system(OsSystem::new(&root));
db.use_system(ruff_db::system::OsSystem::new(&root));
db.write_file(
a_package_target.join("__init__.py"),
@@ -1621,16 +1602,11 @@ not_a_directory
db.files().try_add_root(&db, &root, FileRootKind::Project);
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![src])
.to_search_paths(db.system(), db.vendored())
.expect("valid search path settings"),
},
);
let settings = SearchPathSettings::new(vec![src]);
let search_paths = settings
.to_search_paths(db.system(), db.vendored())
.expect("valid search path settings");
db.set_search_paths(search_paths);
insta::with_settings!({
// Temporary directory often have random chars in them, so
@@ -1662,18 +1638,14 @@ not_a_directory
db.files()
.try_add_root(&db, &project_directory, FileRootKind::Project);
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
site_packages_paths: vec![site_packages],
..SearchPathSettings::new(vec![project_directory])
}
let settings = SearchPathSettings {
site_packages_paths: vec![site_packages],
..SearchPathSettings::new(vec![project_directory])
};
db.set_search_paths(
settings
.to_search_paths(db.system(), db.vendored())
.unwrap(),
},
);
insta::assert_debug_snapshot!(
@@ -1820,16 +1792,11 @@ not_a_directory
db.files()
.try_add_root(&db, SystemPath::new("/"), FileRootKind::Project);
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![project_directory])
.to_search_paths(db.system(), db.vendored())
.unwrap(),
},
);
let settings = SearchPathSettings::new(vec![project_directory]);
let search_paths = settings
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings");
db.set_search_paths(search_paths);
insta::assert_debug_snapshot!(
list_snapshot_filter(&db, |m| m.name(&db).as_str() == "foo"),

View File

@@ -7,10 +7,9 @@ use ruff_db::vendored::VendoredPath;
use salsa::Database;
use salsa::plumbing::AsId;
use super::path::SearchPath;
use crate::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::path::SystemOrVendoredPathRef;
use crate::path::{SearchPath, SystemOrVendoredPathRef};
/// Representation of a Python module.
#[derive(Clone, Copy, Eq, Hash, PartialEq, salsa::Supertype, salsa::Update)]
@@ -330,16 +329,14 @@ pub enum KnownModule {
TyExtensions,
#[strum(serialize = "importlib")]
ImportLib,
#[cfg(test)]
#[strum(serialize = "unittest.mock")]
UnittestMock,
#[cfg(test)]
Uuid,
Warnings,
}
impl KnownModule {
pub(crate) const fn as_str(self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Self::Builtins => "builtins",
Self::Enum => "enum",
@@ -360,23 +357,18 @@ impl KnownModule {
Self::TyExtensions => "ty_extensions",
Self::ImportLib => "importlib",
Self::Warnings => "warnings",
#[cfg(test)]
Self::UnittestMock => "unittest.mock",
#[cfg(test)]
Self::Uuid => "uuid",
Self::Templatelib => "string.templatelib",
}
}
pub(crate) fn name(self) -> ModuleName {
pub fn name(self) -> ModuleName {
ModuleName::new_static(self.as_str())
.unwrap_or_else(|| panic!("{self} should be a valid module name!"))
}
pub(crate) fn try_from_search_path_and_name(
search_path: &SearchPath,
name: &ModuleName,
) -> Option<Self> {
fn try_from_search_path_and_name(search_path: &SearchPath, name: &ModuleName) -> Option<Self> {
if search_path.is_standard_library() {
Self::from_str(name.as_str()).ok()
} else {
@@ -384,23 +376,23 @@ impl KnownModule {
}
}
pub(crate) const fn is_builtins(self) -> bool {
pub const fn is_builtins(self) -> bool {
matches!(self, Self::Builtins)
}
pub(crate) const fn is_typing(self) -> bool {
pub const fn is_typing(self) -> bool {
matches!(self, Self::Typing)
}
pub(crate) const fn is_ty_extensions(self) -> bool {
pub const fn is_ty_extensions(self) -> bool {
matches!(self, Self::TyExtensions)
}
pub(crate) const fn is_inspect(self) -> bool {
pub const fn is_inspect(self) -> bool {
matches!(self, Self::Inspect)
}
pub(crate) const fn is_importlib(self) -> bool {
pub const fn is_importlib(self) -> bool {
matches!(self, Self::ImportLib)
}
}

View File

@@ -8,7 +8,8 @@ use ruff_db::files::File;
use ruff_python_ast as ast;
use ruff_python_stdlib::identifiers::is_identifier;
use crate::{db::Db, module_resolver::file_to_module};
use crate::db::Db;
use crate::resolve::file_to_module;
/// A module name, e.g. `foo.bar`.
///
@@ -47,7 +48,7 @@ impl ModuleName {
/// ## Examples
///
/// ```
/// use ty_python_semantic::ModuleName;
/// use ty_module_resolver::ModuleName;
///
/// assert_eq!(ModuleName::new_static("foo.bar").as_deref(), Some("foo.bar"));
/// assert_eq!(ModuleName::new_static(""), None);
@@ -73,7 +74,7 @@ impl ModuleName {
/// # Examples
///
/// ```
/// use ty_python_semantic::ModuleName;
/// use ty_module_resolver::ModuleName;
///
/// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().components().collect::<Vec<_>>(), vec!["foo", "bar", "baz"]);
/// ```
@@ -87,7 +88,7 @@ impl ModuleName {
/// # Examples
///
/// ```
/// use ty_python_semantic::ModuleName;
/// use ty_module_resolver::ModuleName;
///
/// assert_eq!(ModuleName::new_static("foo.bar").unwrap().parent(), Some(ModuleName::new_static("foo").unwrap()));
/// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().parent(), Some(ModuleName::new_static("foo.bar").unwrap()));
@@ -106,7 +107,7 @@ impl ModuleName {
/// # Examples
///
/// ```
/// use ty_python_semantic::ModuleName;
/// use ty_module_resolver::ModuleName;
///
/// assert!(ModuleName::new_static("foo.bar").unwrap().starts_with(&ModuleName::new_static("foo").unwrap()));
///
@@ -142,7 +143,7 @@ impl ModuleName {
/// module name:
///
/// ```
/// use ty_python_semantic::ModuleName;
/// use ty_module_resolver::ModuleName;
///
/// let this = ModuleName::new_static("importlib.resources").unwrap();
/// let parent = ModuleName::new_static("importlib").unwrap();
@@ -156,7 +157,7 @@ impl ModuleName {
/// This shows some cases where it isn't a parent:
///
/// ```
/// use ty_python_semantic::ModuleName;
/// use ty_module_resolver::ModuleName;
///
/// let this = ModuleName::new_static("importliblib.resources").unwrap();
/// let parent = ModuleName::new_static("importlib").unwrap();
@@ -199,7 +200,7 @@ impl ModuleName {
/// # Examples
///
/// ```
/// use ty_python_semantic::ModuleName;
/// use ty_module_resolver::ModuleName;
///
/// assert_eq!(&*ModuleName::from_components(["a"]).unwrap(), "a");
/// assert_eq!(&*ModuleName::from_components(["a", "b"]).unwrap(), "a.b");
@@ -240,7 +241,7 @@ impl ModuleName {
/// # Examples
///
/// ```
/// use ty_python_semantic::ModuleName;
/// use ty_module_resolver::ModuleName;
///
/// let mut module_name = ModuleName::new_static("foo").unwrap();
/// module_name.extend(&ModuleName::new_static("bar").unwrap());
@@ -258,7 +259,7 @@ impl ModuleName {
/// # Examples
///
/// ```
/// use ty_python_semantic::ModuleName;
/// use ty_module_resolver::ModuleName;
///
/// assert_eq!(
/// ModuleName::new_static("foo.bar.baz").unwrap().ancestors().collect::<Vec<_>>(),
@@ -314,7 +315,7 @@ impl ModuleName {
/// Computes the absolute module name for the package this file belongs to.
///
/// i.e. this resolves `.`
pub(crate) fn package_for_file(
pub fn package_for_file(
db: &dyn Db,
importing_file: File,
) -> Result<Self, ModuleNameResolutionError> {

View File

@@ -8,11 +8,10 @@ use ruff_db::files::{File, FileError, FilePath, system_path_to_file, vendored_pa
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_db::vendored::{VendoredPath, VendoredPathBuf};
use super::typeshed::{TypeshedVersionsParseError, TypeshedVersionsQueryResult, typeshed_versions};
use crate::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::resolver::{PyTyped, ResolverContext};
use crate::site_packages::SitePackagesDiscoveryError;
use crate::resolve::{PyTyped, ResolverContext};
use crate::typeshed::{TypeshedVersionsQueryResult, typeshed_versions};
/// A path that points to a Python module.
///
@@ -397,13 +396,8 @@ fn query_stdlib_version(
typeshed_versions(*db).query_module(&module_name, *python_version)
}
/// Enumeration describing the various ways in which validation of a search path might fail.
///
/// If validation fails for a search path derived from the user settings,
/// a message must be displayed to the user,
/// as type checking cannot be done reliably in these circumstances.
#[derive(Debug, thiserror::Error)]
pub enum SearchPathValidationError {
pub enum SearchPathError {
/// The path provided by the user was not a directory
#[error("{0} does not point to a directory")]
NotADirectory(SystemPathBuf),
@@ -413,41 +407,9 @@ pub enum SearchPathValidationError {
/// (This is only relevant for stdlib search paths.)
#[error("The directory at {0} has no `stdlib/` subdirectory")]
NoStdlibSubdirectory(SystemPathBuf),
/// The typeshed path provided by the user is a directory,
/// but `stdlib/VERSIONS` could not be read.
/// (This is only relevant for stdlib search paths.)
#[error("Failed to read the custom typeshed versions file '{path}'")]
FailedToReadVersionsFile {
path: SystemPathBuf,
#[source]
error: std::io::Error,
},
/// The path provided by the user is a directory,
/// and a `stdlib/VERSIONS` file exists, but it fails to parse.
/// (This is only relevant for stdlib search paths.)
#[error(transparent)]
VersionsParseError(TypeshedVersionsParseError),
/// Failed to discover the site-packages for the configured virtual environment.
#[error("Failed to discover the site-packages directory")]
SitePackagesDiscovery(#[source] SitePackagesDiscoveryError),
}
impl From<TypeshedVersionsParseError> for SearchPathValidationError {
fn from(value: TypeshedVersionsParseError) -> Self {
Self::VersionsParseError(value)
}
}
impl From<SitePackagesDiscoveryError> for SearchPathValidationError {
fn from(value: SitePackagesDiscoveryError) -> Self {
Self::SitePackagesDiscovery(value)
}
}
type SearchPathResult<T> = Result<T, SearchPathValidationError>;
type SearchPathResult<T> = Result<T, SearchPathError>;
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
enum SearchPathInner {
@@ -495,7 +457,7 @@ impl SearchPath {
if system.is_directory(&root) {
Ok(root)
} else {
Err(SearchPathValidationError::NotADirectory(root))
Err(SearchPathError::NotADirectory(root))
}
}
@@ -519,17 +481,15 @@ impl SearchPath {
typeshed: &SystemPath,
) -> SearchPathResult<Self> {
if !system.is_directory(typeshed) {
return Err(SearchPathValidationError::NotADirectory(
typeshed.to_path_buf(),
));
return Err(SearchPathError::NotADirectory(typeshed.to_path_buf()));
}
let stdlib =
Self::directory_path(system, typeshed.join("stdlib")).map_err(|err| match err {
SearchPathValidationError::NotADirectory(_) => {
SearchPathValidationError::NoStdlibSubdirectory(typeshed.to_path_buf())
SearchPathError::NotADirectory(_) => {
SearchPathError::NoStdlibSubdirectory(typeshed.to_path_buf())
}
err => err,
SearchPathError::NoStdlibSubdirectory(_) => err,
})?;
Ok(Self(Arc::new(SearchPathInner::StandardLibraryCustom(
@@ -585,7 +545,7 @@ impl SearchPath {
/// Does this search path point to the standard library?
#[must_use]
pub(crate) fn is_standard_library(&self) -> bool {
pub fn is_standard_library(&self) -> bool {
matches!(
&*self.0,
SearchPathInner::StandardLibraryCustom(_)
@@ -680,7 +640,7 @@ impl SearchPath {
}
#[must_use]
pub(crate) fn as_system_path(&self) -> Option<&SystemPath> {
pub fn as_system_path(&self) -> Option<&SystemPath> {
self.as_path().as_system_path()
}
@@ -709,7 +669,7 @@ impl SearchPath {
/// Returns a string suitable for describing what kind of search path this is
/// in user-facing diagnostics.
#[must_use]
pub(crate) fn describe_kind(&self) -> &'static str {
pub fn describe_kind(&self) -> &'static str {
match *self.0 {
SearchPathInner::Extra(_) => {
"extra search path specified on the CLI or in your config file"
@@ -869,8 +829,8 @@ mod tests {
use ruff_python_ast::PythonVersion;
use crate::db::tests::TestDb;
use crate::module_resolver::resolver::ModuleResolveMode;
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use crate::resolve::ModuleResolveMode;
use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use super::*;

View File

@@ -48,13 +48,11 @@ use ruff_python_ast::{
};
use crate::db::Db;
use crate::module::{Module, ModuleKind};
use crate::module_name::ModuleName;
use crate::module_resolver::typeshed::{TypeshedVersions, vendored_typeshed_versions};
use crate::program::MisconfigurationMode;
use crate::{Program, SearchPathSettings};
use super::module::{Module, ModuleKind};
use super::path::{ModulePath, SearchPath, SearchPathValidationError, SystemOrVendoredPathRef};
use crate::path::{ModulePath, SearchPath, SystemOrVendoredPathRef};
use crate::typeshed::{TypeshedVersions, vendored_typeshed_versions};
use crate::{MisconfigurationMode, SearchPathSettings, SearchPathSettingsError};
/// Resolves a module name to a module.
pub fn resolve_module<'db>(
@@ -137,7 +135,7 @@ pub fn resolve_real_shadowable_module<'db>(
/// Which files should be visible when doing a module query
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, get_size2::GetSize)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum ModuleResolveMode {
pub enum ModuleResolveMode {
/// Stubs are allowed to appear.
///
/// This is the "normal" mode almost everything uses, as type checkers are in fact supposed
@@ -326,7 +324,7 @@ pub(crate) fn path_to_module<'db>(db: &'db dyn Db, path: &FilePath) -> Option<Mo
/// This intuition is particularly useful for understanding why it's correct that we pass
/// the file itself as `importing_file` to various subroutines.
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module<'_>> {
pub fn file_to_module(db: &dyn Db, file: File) -> Option<Module<'_>> {
let _span = tracing::trace_span!("file_to_module", ?file).entered();
let path = SystemOrVendoredPathRef::try_from_file(db, file)?;
@@ -392,8 +390,8 @@ fn file_to_module_impl<'db, 'a>(
None
}
pub(crate) fn search_paths(db: &dyn Db, resolve_mode: ModuleResolveMode) -> SearchPathIterator<'_> {
Program::get(db).search_paths(db).iter(db, resolve_mode)
pub fn search_paths(db: &dyn Db, resolve_mode: ModuleResolveMode) -> SearchPathIterator<'_> {
db.search_paths().iter(db, resolve_mode)
}
/// Get the search-paths for desperate resolution of absolute imports in this file.
@@ -554,11 +552,11 @@ impl SearchPaths {
/// This method also implements the typing spec's [module resolution order].
///
/// [module resolution order]: https://typing.python.org/en/latest/spec/distributing.html#import-resolution-ordering
pub(crate) fn from_settings(
pub fn from_settings(
settings: &SearchPathSettings,
system: &dyn System,
vendored: &VendoredFileSystem,
) -> Result<Self, SearchPathValidationError> {
) -> Result<Self, SearchPathSettingsError> {
fn canonicalize(path: &SystemPath, system: &dyn System) -> SystemPathBuf {
system
.canonicalize_path(path)
@@ -586,7 +584,7 @@ impl SearchPaths {
if *misconfiguration_mode == MisconfigurationMode::UseDefault {
tracing::debug!("Skipping invalid extra search-path: {err}");
} else {
return Err(err);
return Err(err.into());
}
}
}
@@ -600,7 +598,7 @@ impl SearchPaths {
if *misconfiguration_mode == MisconfigurationMode::UseDefault {
tracing::debug!("Skipping invalid first-party search-path: {err}");
} else {
return Err(err);
return Err(err.into());
}
}
}
@@ -614,12 +612,10 @@ impl SearchPaths {
let results = system
.read_to_string(&versions_path)
.map_err(
|error| SearchPathValidationError::FailedToReadVersionsFile {
path: versions_path,
error,
},
)
.map_err(|error| SearchPathSettingsError::FailedToReadVersionsFile {
path: versions_path,
error,
})
.and_then(|versions_content| Ok(versions_content.parse()?))
.and_then(|parsed| Ok((parsed, SearchPath::custom_stdlib(system, &typeshed)?)));
@@ -653,7 +649,7 @@ impl SearchPaths {
tracing::debug!("Skipping invalid real-stdlib search-path: {err}");
None
} else {
return Err(err);
return Err(err.into());
}
}
}
@@ -669,9 +665,9 @@ impl SearchPaths {
Ok(path) => site_packages.push(path),
Err(err) => {
if settings.misconfiguration_mode == MisconfigurationMode::UseDefault {
tracing::debug!("Skipping invalid real-stdlib search-path: {err}");
tracing::debug!("Skipping invalid site-packages search-path: {err}");
} else {
return Err(err);
return Err(err.into());
}
}
}
@@ -709,13 +705,11 @@ impl SearchPaths {
// preserve this behaviour to avoid getting into the weeds of corner cases.)
let stdlib_path_is_shadowed = stdlib_path
.as_system_path()
.map(|path| seen_paths.contains(path))
.unwrap_or(false);
.is_some_and(|path| seen_paths.contains(path));
let real_stdlib_path_is_shadowed = real_stdlib_path
.as_ref()
.and_then(SearchPath::as_system_path)
.map(|path| seen_paths.contains(path))
.unwrap_or(false);
.is_some_and(|path| seen_paths.contains(path));
let stdlib_path = if stdlib_path_is_shadowed {
None
@@ -737,8 +731,21 @@ impl SearchPaths {
})
}
/// Returns a new `SearchPaths` with no search paths configured.
///
/// This is primarily useful for testing.
pub fn empty(vendored: &VendoredFileSystem) -> Self {
Self {
static_paths: vec![],
stdlib_path: Some(SearchPath::vendored_stdlib()),
real_stdlib_path: None,
site_packages: vec![],
typeshed_versions: vendored_typeshed_versions(vendored),
}
}
/// Registers the file roots for all non-dynamically discovered search paths that aren't first-party.
pub(crate) fn try_register_static_roots(&self, db: &dyn Db) {
pub fn try_register_static_roots(&self, db: &dyn Db) {
let files = db.files();
for path in self
.static_paths
@@ -779,13 +786,13 @@ impl SearchPaths {
}
}
pub(crate) fn custom_stdlib(&self) -> Option<&SystemPath> {
pub fn custom_stdlib(&self) -> Option<&SystemPath> {
self.stdlib_path
.as_ref()
.and_then(SearchPath::as_system_path)
}
pub(crate) fn typeshed_versions(&self) -> &TypeshedVersions {
pub fn typeshed_versions(&self) -> &TypeshedVersions {
&self.typeshed_versions
}
}
@@ -811,7 +818,7 @@ pub(crate) fn dynamic_resolution_paths<'db>(
site_packages,
typeshed_versions: _,
real_stdlib_path,
} = Program::get(db).search_paths(db);
} = db.search_paths();
let mut dynamic_paths = Vec::new();
@@ -932,7 +939,7 @@ pub(crate) fn dynamic_resolution_paths<'db>(
/// are only calculated lazily.
///
/// [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site
pub(crate) struct SearchPathIterator<'db> {
pub struct SearchPathIterator<'db> {
db: &'db dyn Db,
static_paths: std::slice::Iter<'db, SearchPath>,
stdlib_path: Option<&'db SearchPath>,
@@ -1101,8 +1108,7 @@ fn resolve_name_impl<'a>(
mode: ModuleResolveMode,
search_paths: impl Iterator<Item = &'a SearchPath>,
) -> Option<ResolvedName> {
let program = Program::get(db);
let python_version = program.python_version(db);
let python_version = db.python_version();
let resolver_state = ResolverContext::new(db, python_version, mode);
let is_non_shadowable = mode.is_non_shadowable(python_version.minor, name.as_str());
@@ -1740,10 +1746,9 @@ mod tests {
use ruff_python_ast::PythonVersion;
use crate::db::tests::TestDb;
use crate::module::ModuleKind;
use crate::module_name::ModuleName;
use crate::module_resolver::module::ModuleKind;
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use crate::{ProgramSettings, PythonPlatform, PythonVersionWithSource};
use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use super::*;
@@ -2267,15 +2272,11 @@ mod tests {
#[cfg(target_family = "unix")]
fn symlink() -> anyhow::Result<()> {
use anyhow::Context;
use crate::{
PythonPlatform, PythonVersionSource, PythonVersionWithSource, program::Program,
};
use ruff_db::system::{OsSystem, SystemPath};
use crate::db::tests::TestDb;
let mut db = TestDb::new();
let mut db = TestDb::new().with_python_version(PythonVersion::PY38);
let temp_dir = tempfile::tempdir()?;
let root = temp_dir
@@ -2300,22 +2301,15 @@ mod tests {
std::fs::write(foo.as_std_path(), "")?;
std::os::unix::fs::symlink(foo.as_std_path(), bar.as_std_path())?;
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource {
version: PythonVersion::PY38,
source: PythonVersionSource::default(),
},
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
custom_typeshed: Some(custom_typeshed),
site_packages_paths: vec![site_packages],
..SearchPathSettings::new(vec![src.clone()])
}
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings"),
},
db.set_search_paths(
SearchPathSettings {
src_roots: vec![src.clone()],
custom_typeshed: Some(custom_typeshed),
site_packages_paths: vec![site_packages],
..SearchPathSettings::empty()
}
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings"),
);
let foo_module =
@@ -2848,18 +2842,13 @@ not_a_directory
])
.unwrap();
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
site_packages_paths: vec![venv_site_packages, system_site_packages],
..SearchPathSettings::new(vec![SystemPathBuf::from("/src")])
}
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings"),
},
db.set_search_paths(
SearchPathSettings {
site_packages_paths: vec![venv_site_packages, system_site_packages],
..SearchPathSettings::new(vec![SystemPathBuf::from("/src")])
}
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings"),
);
// The editable installs discovered from the `.pth` file in the first `site-packages` directory
@@ -2924,15 +2913,10 @@ not_a_directory
std::os::unix::fs::symlink(a_package_target.as_std_path(), a_src.as_std_path())
.context("Failed to symlink `src/a` to `a-package`")?;
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![src])
.to_search_paths(db.system(), db.vendored())
.expect("valid search path settings"),
},
db.set_search_paths(
SearchPathSettings::new(vec![src])
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings"),
);
// Now try to resolve the module `A` (note the capital `A` instead of `a`).
@@ -2964,19 +2948,14 @@ not_a_directory
let mut db = TestDb::new();
db.write_file(&installed_foo_module, "").unwrap();
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
site_packages_paths: vec![site_packages.clone()],
..SearchPathSettings::new(vec![project_directory])
}
.to_search_paths(db.system(), db.vendored())
.unwrap(),
},
);
let search_paths = SearchPathSettings {
src_roots: vec![project_directory],
site_packages_paths: vec![site_packages.clone()],
..SearchPathSettings::empty()
}
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings");
db.set_search_paths(search_paths);
let foo_module_file = File::new(&db, FilePath::System(installed_foo_module));
let module = file_to_module(&db, foo_module_file).unwrap();

View File

@@ -0,0 +1,105 @@
//! Search path configuration settings.
use ruff_db::system::{System, SystemPathBuf};
use ruff_db::vendored::VendoredFileSystem;
use crate::path::SearchPathError;
use crate::resolve::SearchPaths;
use crate::typeshed::TypeshedVersionsParseError;
/// How to handle apparent misconfiguration
#[derive(PartialEq, Eq, Debug, Copy, Clone, Default, get_size2::GetSize)]
pub enum MisconfigurationMode {
/// Settings Failure Is Not An Error.
///
/// This is used by the default database, which we are incentivized to make infallible,
/// while still trying to "do our best" to set things up properly where we can.
UseDefault,
/// Settings Failure Is An Error.
#[default]
Fail,
}
/// Configures the search paths for module resolution.
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct SearchPathSettings {
/// List of user-provided paths that should take first priority in the module resolution.
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
/// or pyright's stubPath configuration setting.
pub extra_paths: Vec<SystemPathBuf>,
/// The root of the project, used for finding first-party modules.
pub src_roots: Vec<SystemPathBuf>,
/// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types.
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
/// bundled as a zip file in the binary
pub custom_typeshed: Option<SystemPathBuf>,
/// List of site packages paths to use.
pub site_packages_paths: Vec<SystemPathBuf>,
/// Option path to the real stdlib on the system, and not some instance of typeshed.
///
/// We should ideally only ever use this for things like goto-definition,
/// where typeshed isn't the right answer.
pub real_stdlib_path: Option<SystemPathBuf>,
/// How to handle apparent misconfiguration
pub misconfiguration_mode: MisconfigurationMode,
}
impl SearchPathSettings {
pub fn new(src_roots: Vec<SystemPathBuf>) -> Self {
Self {
src_roots,
..SearchPathSettings::empty()
}
}
pub fn empty() -> Self {
SearchPathSettings {
src_roots: vec![],
extra_paths: vec![],
custom_typeshed: None,
site_packages_paths: vec![],
real_stdlib_path: None,
misconfiguration_mode: MisconfigurationMode::Fail,
}
}
pub fn to_search_paths(
&self,
system: &dyn System,
vendored: &VendoredFileSystem,
) -> Result<SearchPaths, SearchPathSettingsError> {
SearchPaths::from_settings(self, system, vendored)
}
}
/// Enumeration describing the various ways in which validation of the search paths options might fail.
///
/// If validation fails for a search path derived from the user settings,
/// a message must be displayed to the user,
/// as type checking cannot be done reliably in these circumstances.
#[derive(Debug, thiserror::Error)]
pub enum SearchPathSettingsError {
#[error(transparent)]
InvalidSearchPath(#[from] SearchPathError),
/// The typeshed path provided by the user is a directory,
/// but `stdlib/VERSIONS` could not be read.
/// (This is only relevant for stdlib search paths.)
#[error("Failed to read the custom typeshed versions file '{path}'")]
FailedToReadVersionsFile {
path: SystemPathBuf,
#[source]
error: std::io::Error,
},
/// The path provided by the user is a directory,
/// and a `stdlib/VERSIONS` file exists, but it fails to parse.
/// (This is only relevant for stdlib search paths.)
#[error(transparent)]
VersionsParseError(#[from] TypeshedVersionsParseError),
}

View File

@@ -1,4 +1,4 @@
use ruff_db::Db;
use ruff_db::Db as _;
use ruff_db::files::FileRootKind;
use ruff_db::system::{
DbWithTestSystem as _, DbWithWritableSystem as _, SystemPath, SystemPathBuf,
@@ -7,8 +7,7 @@ use ruff_db::vendored::VendoredPathBuf;
use ruff_python_ast::PythonVersion;
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::{ProgramSettings, PythonPlatform, PythonVersionSource, PythonVersionWithSource};
use crate::settings::SearchPathSettings;
/// A test case for the module resolver.
///
@@ -20,7 +19,7 @@ pub(crate) struct TestCase<T> {
pub(crate) stdlib: T,
// Most test cases only ever need a single `site-packages` directory,
// so this is a single directory instead of a `Vec` of directories,
// like it is in `ruff_db::Program`.
// like it is in `SearchPaths`.
pub(crate) site_packages: SystemPathBuf,
pub(crate) python_version: PythonVersion,
}
@@ -105,7 +104,6 @@ pub(crate) struct UnspecifiedTypeshed;
pub(crate) struct TestCaseBuilder<T> {
typeshed_option: T,
python_version: PythonVersion,
python_platform: PythonPlatform,
first_party_files: Vec<FileSpec>,
site_packages_files: Vec<FileSpec>,
// Additional file roots (beyond site_packages, src and stdlib)
@@ -166,7 +164,6 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
Self {
typeshed_option: UnspecifiedTypeshed,
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
first_party_files: vec![],
site_packages_files: vec![],
roots: vec![],
@@ -178,7 +175,6 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
let TestCaseBuilder {
typeshed_option: _,
python_version,
python_platform,
first_party_files,
site_packages_files,
roots,
@@ -186,7 +182,6 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
TestCaseBuilder {
typeshed_option: VendoredTypeshed,
python_version,
python_platform,
first_party_files,
site_packages_files,
roots,
@@ -201,7 +196,6 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
let TestCaseBuilder {
typeshed_option: _,
python_version,
python_platform,
first_party_files,
site_packages_files,
roots,
@@ -210,7 +204,6 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
TestCaseBuilder {
typeshed_option: typeshed,
python_version,
python_platform,
first_party_files,
site_packages_files,
roots,
@@ -241,18 +234,29 @@ impl TestCaseBuilder<MockedTypeshed> {
let TestCaseBuilder {
typeshed_option,
python_version,
python_platform,
first_party_files,
site_packages_files,
roots,
} = self;
let mut db = TestDb::new();
let mut db = TestDb::new().with_python_version(python_version);
let site_packages =
Self::write_mock_directory(&mut db, "/site-packages", site_packages_files);
let src = Self::write_mock_directory(&mut db, "/src", first_party_files);
let typeshed = Self::build_typeshed_mock(&mut db, &typeshed_option);
let stdlib = typeshed.join("stdlib");
let search_paths = SearchPathSettings {
src_roots: vec![src.clone()],
custom_typeshed: Some(typeshed),
site_packages_paths: vec![site_packages.clone()],
..SearchPathSettings::empty()
}
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings");
db = db.with_search_paths(search_paths);
// This root is needed for correct Salsa tracking.
// Namely, a `SearchPath` is treated as an input, and
@@ -262,36 +266,19 @@ impl TestCaseBuilder<MockedTypeshed> {
// here, they won't get added.
//
// Roots for other search paths are added as part of
// search path initialization in `Program::from_settings`,
// search path initialization in `SearchPaths::from_settings`,
// and any remaining are added below.
db.files()
.try_add_root(&db, SystemPath::new("/src"), FileRootKind::Project);
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource {
version: python_version,
source: PythonVersionSource::default(),
},
python_platform,
search_paths: SearchPathSettings {
custom_typeshed: Some(typeshed.clone()),
site_packages_paths: vec![site_packages.clone()],
..SearchPathSettings::new(vec![src.clone()])
}
.to_search_paths(db.system(), db.vendored())
.expect("valid search path settings"),
},
);
let stdlib = typeshed.join("stdlib");
db.files()
.try_add_root(&db, &stdlib, FileRootKind::LibrarySearchPath);
for root in &roots {
db.files()
.try_add_root(&db, root, FileRootKind::LibrarySearchPath);
}
TestCase {
db,
src,
@@ -324,42 +311,34 @@ impl TestCaseBuilder<VendoredTypeshed> {
let TestCaseBuilder {
typeshed_option: VendoredTypeshed,
python_version,
python_platform,
first_party_files,
site_packages_files,
roots,
} = self;
let mut db = TestDb::new();
let mut db = TestDb::new().with_python_version(python_version);
let site_packages =
Self::write_mock_directory(&mut db, "/site-packages", site_packages_files);
let src = Self::write_mock_directory(&mut db, "/src", first_party_files);
let search_paths = SearchPathSettings {
src_roots: vec![src.clone()],
site_packages_paths: vec![site_packages.clone()],
..SearchPathSettings::empty()
}
.to_search_paths(db.system(), db.vendored())
.expect("Valid search path settings");
db = db.with_search_paths(search_paths);
db.files()
.try_add_root(&db, SystemPath::new("/src"), FileRootKind::Project);
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource {
version: python_version,
source: PythonVersionSource::default(),
},
python_platform,
search_paths: SearchPathSettings {
site_packages_paths: vec![site_packages.clone()],
..SearchPathSettings::new(vec![src.clone()])
}
.to_search_paths(db.system(), db.vendored())
.expect("valid search path settings"),
},
);
for root in &roots {
db.files()
.try_add_root(&db, root, FileRootKind::LibrarySearchPath);
}
TestCase {
db,
src,

View File

@@ -8,13 +8,10 @@ use ruff_db::vendored::VendoredFileSystem;
use ruff_python_ast::{PythonVersion, PythonVersionDeserializationError};
use rustc_hash::FxHashMap;
use crate::Program;
use crate::db::Db;
use crate::module_name::ModuleName;
pub(in crate::module_resolver) fn vendored_typeshed_versions(
vendored: &VendoredFileSystem,
) -> TypeshedVersions {
pub fn vendored_typeshed_versions(vendored: &VendoredFileSystem) -> TypeshedVersions {
TypeshedVersions::from_str(
&vendored
.read_to_string("stdlib/VERSIONS")
@@ -24,7 +21,7 @@ pub(in crate::module_resolver) fn vendored_typeshed_versions(
}
pub(crate) fn typeshed_versions(db: &dyn Db) -> &TypeshedVersions {
Program::get(db).search_paths(db).typeshed_versions()
db.search_paths().typeshed_versions()
}
#[derive(Debug, PartialEq, Eq, Clone)]
@@ -61,7 +58,7 @@ impl std::error::Error for TypeshedVersionsParseError {
}
#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)]
pub(crate) enum TypeshedVersionsParseErrorKind {
pub enum TypeshedVersionsParseErrorKind {
#[error("File has too many lines ({0}); maximum allowed is {max_allowed}", max_allowed = NonZeroU16::MAX)]
TooManyLines(NonZeroUsize),
#[error("Expected every non-comment line to have exactly one colon")]
@@ -75,16 +72,16 @@ pub(crate) enum TypeshedVersionsParseErrorKind {
}
#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)]
pub(crate) struct TypeshedVersions(FxHashMap<ModuleName, PyVersionRange>);
pub struct TypeshedVersions(FxHashMap<ModuleName, PyVersionRange>);
impl TypeshedVersions {
#[must_use]
pub(crate) fn exact(&self, module_name: &ModuleName) -> Option<&PyVersionRange> {
pub fn exact(&self, module_name: &ModuleName) -> Option<&PyVersionRange> {
self.0.get(module_name)
}
#[must_use]
pub(in crate::module_resolver) fn query_module(
pub(crate) fn query_module(
&self,
module: &ModuleName,
python_version: PythonVersion,
@@ -231,14 +228,14 @@ impl fmt::Display for TypeshedVersions {
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
pub(crate) enum PyVersionRange {
pub enum PyVersionRange {
AvailableFrom(RangeFrom<PythonVersion>),
AvailableWithin(RangeInclusive<PythonVersion>),
}
impl PyVersionRange {
#[must_use]
pub(crate) fn contains(&self, version: PythonVersion) -> bool {
pub fn contains(&self, version: PythonVersion) -> bool {
match self {
Self::AvailableFrom(inner) => inner.contains(&version),
Self::AvailableWithin(inner) => inner.contains(&version),
@@ -246,7 +243,7 @@ impl PyVersionRange {
}
/// Display the version range in a way that is suitable for rendering in user-facing diagnostics.
pub(crate) fn diagnostic_display(&self) -> impl std::fmt::Display {
pub fn diagnostic_display(&self) -> impl std::fmt::Display {
struct DiagnosticDisplay<'a>(&'a PyVersionRange);
impl fmt::Display for DiagnosticDisplay<'_> {

View File

@@ -21,6 +21,7 @@ ruff_python_ast = { workspace = true, features = ["serde"] }
ruff_python_formatter = { workspace = true, optional = true }
ruff_text_size = { workspace = true }
ty_combine = { workspace = true }
ty_module_resolver = { workspace = true }
ty_python_semantic = { workspace = true, features = ["serde"] }
ty_static = { workspace = true }
ty_vendored = { workspace = true }

View File

@@ -14,8 +14,9 @@ use ruff_db::files::{File, Files};
use ruff_db::system::System;
use ruff_db::vendored::VendoredFileSystem;
use salsa::{Database, Event, Setter};
use ty_module_resolver::SearchPaths;
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{Db as SemanticDb, Program};
use ty_python_semantic::{AnalysisSettings, Db as SemanticDb, Program};
mod changes;
@@ -446,6 +447,13 @@ impl SalsaMemoryDump {
}
}
#[salsa::db]
impl ty_module_resolver::Db for ProjectDatabase {
fn search_paths(&self) -> &SearchPaths {
Program::get(self).search_paths(self)
}
}
#[salsa::db]
impl SemanticDb for ProjectDatabase {
fn should_check_file(&self, file: File) -> bool {
@@ -462,6 +470,10 @@ impl SemanticDb for ProjectDatabase {
ty_python_semantic::default_lint_registry()
}
fn analysis_settings(&self) -> &AnalysisSettings {
self.project().settings(self).analysis()
}
fn verbose(&self) -> bool {
self.project().verbose(self)
}
@@ -523,9 +535,10 @@ pub(crate) mod tests {
use ruff_db::files::{FileRootKind, Files};
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
use ty_module_resolver::SearchPathSettings;
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
AnalysisSettings, Program, ProgramSettings, PythonPlatform, PythonVersionWithSource,
};
use crate::db::Db;
@@ -627,6 +640,13 @@ pub(crate) mod tests {
}
}
#[salsa::db]
impl ty_module_resolver::Db for TestDb {
fn search_paths(&self) -> &ty_module_resolver::SearchPaths {
Program::get(self).search_paths(self)
}
}
#[salsa::db]
impl ty_python_semantic::Db for TestDb {
fn should_check_file(&self, file: ruff_db::files::File) -> bool {
@@ -641,6 +661,10 @@ pub(crate) mod tests {
ty_python_semantic::default_lint_registry()
}
fn analysis_settings(&self) -> &AnalysisSettings {
self.project().settings(self).analysis()
}
fn verbose(&self) -> bool {
false
}

View File

@@ -245,7 +245,9 @@ impl ProjectDatabase {
if result.project_changed {
let new_project_metadata = match config_file_override {
Some(config_file) => ProjectMetadata::from_config_file(config_file, self.system()),
Some(config_file) => {
ProjectMetadata::from_config_file(config_file, &project_root, self.system())
}
None => ProjectMetadata::discover(&project_root, self.system()),
};
match new_project_metadata {

View File

@@ -56,6 +56,7 @@ impl ProjectMetadata {
pub fn from_config_file(
path: SystemPathBuf,
root: &SystemPath,
system: &dyn System,
) -> Result<Self, ProjectMetadataError> {
tracing::debug!("Using overridden configuration file at '{path}'");
@@ -70,8 +71,8 @@ impl ProjectMetadata {
let options = config_file.into_options();
Ok(Self {
name: Name::new(system.current_directory().file_name().unwrap_or("root")),
root: system.current_directory().to_path_buf(),
name: Name::new(root.file_name().unwrap_or("root")),
root: root.to_path_buf(),
options,
extra_configuration_paths: vec![path],
misconfiguration_mode: MisconfigurationMode::Fail,

View File

@@ -28,11 +28,12 @@ use std::ops::Deref;
use std::sync::Arc;
use thiserror::Error;
use ty_combine::Combine;
use ty_module_resolver::{SearchPathSettings, SearchPathSettingsError, SearchPaths};
use ty_python_semantic::lint::{Level, LintSource, RuleSelection};
use ty_python_semantic::{
MisconfigurationMode, ProgramSettings, PythonEnvironment, PythonPlatform,
PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource, SearchPathSettings,
SearchPathValidationError, SearchPaths, SitePackagesPaths, SysPrefixPathOrigin,
AnalysisSettings, MisconfigurationMode, ProgramSettings, PythonEnvironment, PythonPlatform,
PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource, SitePackagesPaths,
SysPrefixPathOrigin,
};
use ty_static::EnvVars;
@@ -86,6 +87,10 @@ pub struct Options {
#[option_group]
pub terminal: Option<TerminalOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
#[option_group]
pub analysis: Option<AnalysisOptions>,
/// Override configurations for specific file patterns.
///
/// Each override specifies include/exclude patterns and rule configurations
@@ -258,7 +263,7 @@ impl Options {
system: &dyn System,
vendored: &VendoredFileSystem,
misconfiguration_mode: MisconfigurationMode,
) -> Result<SearchPaths, SearchPathValidationError> {
) -> Result<SearchPaths, SearchPathSettingsError> {
let environment = self.environment.or_default();
let src = self.src.or_default();
@@ -434,6 +439,8 @@ impl Options {
color: colored::control::SHOULD_COLORIZE.should_colorize(),
})?;
let analysis = self.analysis.or_default().to_settings();
let overrides = self
.to_overrides_settings(db, project_root, &mut diagnostics)
.map_err(|err| ToSettingsError {
@@ -446,6 +453,7 @@ impl Options {
rules: Arc::new(rules),
terminal,
src,
analysis,
overrides,
};
@@ -875,10 +883,7 @@ impl Rules {
let lint_source = match source {
ValueSource::File(_) => LintSource::File,
ValueSource::Cli => LintSource::Cli,
ValueSource::Editor => {
unreachable!("Can't configure rules from the user's editor")
}
ValueSource::Editor => LintSource::Editor,
};
if let Ok(severity) = Severity::try_from(**level) {
selection.enable(lint, severity, lint_source);
@@ -1014,7 +1019,12 @@ fn build_include_filter(
SubDiagnosticSeverity::Info,
"The pattern was specified on the CLI",
)),
ValueSource::Editor => unreachable!("Can't configure includes from the user's editor"),
ValueSource::Editor => {
diagnostic.sub(SubDiagnostic::new(
SubDiagnosticSeverity::Info,
"The pattern was specified in the editor settings.",
))
}
}
})?;
}
@@ -1097,9 +1107,10 @@ fn build_exclude_filter(
SubDiagnosticSeverity::Info,
"The pattern was specified on the CLI",
)),
ValueSource::Editor => unreachable!(
"Can't configure excludes from the user's editor"
)
ValueSource::Editor => diagnostic.sub(SubDiagnostic::new(
SubDiagnosticSeverity::Info,
"The pattern was specified in the editor settings",
))
}
})?;
}
@@ -1250,6 +1261,55 @@ pub struct TerminalOptions {
pub error_on_warning: Option<bool>,
}
#[derive(
Debug,
Default,
Clone,
Eq,
PartialEq,
Combine,
Serialize,
Deserialize,
OptionsMetadata,
get_size2::GetSize,
)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AnalysisOptions {
/// Whether ty should respect `type: ignore` comments.
///
/// When set to `false`, `type: ignore` comments are treated like any other normal
/// comment and can't be used to suppress ty errors (you have to use `ty: ignore` instead).
///
/// Setting this option can be useful when using ty alongside other type checkers or when
/// you prefer using `ty: ignore` over `type: ignore`.
///
/// Defaults to `true`.
#[option(
default = r#"true"#,
value_type = "bool",
example = r#"
# Disable support for `type: ignore` comments
respect-type-ignore-comments = false
"#
)]
respect_type_ignore_comments: Option<bool>,
}
impl AnalysisOptions {
fn to_settings(&self) -> AnalysisSettings {
let AnalysisSettings {
respect_type_ignore_comments: respect_type_ignore_default,
} = AnalysisSettings::default();
AnalysisSettings {
respect_type_ignore_comments: self
.respect_type_ignore_comments
.unwrap_or(respect_type_ignore_default),
}
}
}
/// Configuration override that applies to specific files based on glob patterns.
///
/// An override allows you to apply different rule configurations to specific

View File

@@ -2,6 +2,7 @@ use std::sync::Arc;
use ruff_db::files::File;
use ty_combine::Combine;
use ty_python_semantic::AnalysisSettings;
use ty_python_semantic::lint::RuleSelection;
use crate::metadata::options::{InnerOverrideOptions, OutputFormat};
@@ -25,6 +26,7 @@ pub struct Settings {
pub(super) rules: Arc<RuleSelection>,
pub(super) terminal: TerminalSettings,
pub(super) src: SrcSettings,
pub(super) analysis: AnalysisSettings,
/// Settings for configuration overrides that apply to specific file patterns.
///
@@ -54,6 +56,10 @@ impl Settings {
pub fn overrides(&self) -> &[Override] {
&self.overrides
}
pub fn analysis(&self) -> &AnalysisSettings {
&self.analysis
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, get_size2::GetSize)]

View File

@@ -5,7 +5,7 @@ use tracing::info;
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_db::system::{SystemPath, SystemPathBuf};
use ty_python_semantic::system_module_search_paths;
use ty_module_resolver::system_module_search_paths;
use crate::db::{Db, ProjectDatabase};
use crate::watch::Watcher;

View File

@@ -24,6 +24,8 @@ ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_python_literal = { workspace = true }
ruff_python_trivia = { workspace = true }
ty_module_resolver = { workspace = true }
ty_combine = { workspace = true }
ty_static = { workspace = true }
anyhow = { workspace = true }
@@ -56,7 +58,6 @@ strsim = "0.11.1"
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing", "os"] }
ruff_python_parser = { workspace = true }
ty_python_semantic = { workspace = true, features = ["testing"] }
ty_static = { workspace = true }
ty_test = { workspace = true }
ty_vendored = { workspace = true }
@@ -67,7 +68,6 @@ glob = { workspace = true }
indoc = { workspace = true }
insta = { workspace = true }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
quickcheck = { version = "1.0.3", default-features = false }
quickcheck_macros = { version = "1.0.0" }

View File

@@ -0,0 +1,10 @@
# regression test for https://github.com/astral-sh/ty/issues/2085
class Foo:
def __init__(self, x: int):
self.left = x
self.right = x
def method(self):
self.left, self.right = self.right, self.left
if self.right:
self.right = self.right

View File

@@ -0,0 +1,8 @@
# Regression test for https://github.com/astral-sh/ty/issues/1848
T = tuple[int, 'U']
class C(set['U']):
pass
type U = T | C

View File

@@ -266,3 +266,12 @@ def _(
) -> (int, str): # error: [invalid-type-form]
return x
```
### Special-cased diagnostic for `callable` used in a type expression
```py
# error: [invalid-type-form]
# error: [invalid-type-form]
def decorator(fn: callable) -> callable:
return fn
```

View File

@@ -193,8 +193,7 @@ class B:
reveal_type(B().name_does_not_matter()) # revealed: B
reveal_type(B().positional_only(1)) # revealed: B
reveal_type(B().keyword_only(x=1)) # revealed: B
# TODO: This should deally be `B`
reveal_type(B().decorated_method()) # revealed: Self@decorated_method
reveal_type(B().decorated_method()) # revealed: B
reveal_type(B().a_property) # revealed: B

View File

@@ -2775,6 +2775,23 @@ reveal_type(foo.bar) # revealed: Unknown
reveal_type(baz.bar) # revealed: Unknown
```
## Diagnostic for function attribute accessed on `Callable` type
<!-- snapshot-diagnostics -->
```toml
[environment]
python-version = "3.14"
```
```py
from typing import Callable
def f(x: Callable):
x.__name__ # error: [unresolved-attribute]
x.__annotate__ # error: [unresolved-attribute]
```
## References
Some of the tests in the *Class and instance variables* section draw inspiration from

View File

@@ -588,6 +588,31 @@ reveal_type(C.f2(1)) # revealed: str
reveal_type(C().f2(1)) # revealed: str
```
When a `@staticmethod` is decorated with `@contextmanager`, accessing it from an instance should not
bind `self`:
```py
from contextlib import contextmanager
from collections.abc import Iterator
class D:
@staticmethod
@contextmanager
def ctx(num: int) -> Iterator[int]:
yield num
def use_ctx(self) -> None:
# Accessing via self should not bind self
with self.ctx(10) as x:
reveal_type(x) # revealed: int
# Accessing via class works
reveal_type(D.ctx(5)) # revealed: _GeneratorContextManager[int, None, None]
# Accessing via instance should also work (no self-binding)
reveal_type(D().ctx(5)) # revealed: _GeneratorContextManager[int, None, None]
```
### `__new__`
`__new__` is an implicit `@staticmethod`; accessing it on an instance does not bind the `cls`

View File

@@ -71,3 +71,38 @@ e = a.__replace__(x="wrong") # error: [invalid-argument-type]
# TODO: this should ideally also be emit an error
e = replace(a, x="wrong")
```
### NamedTuples
NamedTuples also support the `__replace__` protocol:
```py
from typing import NamedTuple
from copy import replace
class Point(NamedTuple):
x: int
y: int
reveal_type(Point.__replace__) # revealed: (self: Self, *, x: int = ..., y: int = ...) -> Self
```
The `__replace__` method can either be called directly or through the `replace` function:
```py
a = Point(1, 2)
b = a.__replace__(x=3, y=4)
reveal_type(b) # revealed: Point
b = replace(a, x=3, y=4)
# TODO: this should be `Point`, once we support specialization of generic protocols
reveal_type(b) # revealed: Unknown
```
Invalid calls to `__replace__` will raise an error:
```py
# error: [unknown-argument] "Argument `z` does not match any known parameter"
a.__replace__(z=42)
```

View File

@@ -171,6 +171,8 @@ reveal_type(c.x) # revealed: int
## Delete items
### Basic item deletion
Deleting an item also invalidates the narrowing by the assignment, but accessing the item itself is
still valid.
@@ -189,3 +191,93 @@ def f(l: list[int]):
del l[0]
reveal_type(l[0]) # revealed: int
```
### `__delitem__` without `__getitem__`
A class or protocol that only defines `__delitem__` (without `__getitem__`) should still support
item deletion. The `__delitem__` method is independent of `__getitem__`.
```py
from typing import Protocol, TypeVar
KT = TypeVar("KT")
class CanDelItem(Protocol[KT]):
def __delitem__(self, k: KT, /) -> None: ...
def f(x: CanDelItem[int], k: int):
# This should be valid - the object has __delitem__
del x[k]
class OnlyDelItem:
def __delitem__(self, key: int) -> None:
pass
d = OnlyDelItem()
del d[0] # OK
# error: [non-subscriptable] "Cannot subscript object of type `OnlyDelItem` with no `__getitem__` method"
d[0]
```
### `__getitem__` without `__delitem__`
A class that only defines `__getitem__` (without `__delitem__`) should not support item deletion.
```py
class OnlyGetItem:
def __getitem__(self, key: int) -> str:
return "value"
g = OnlyGetItem()
reveal_type(g[0]) # revealed: str
# error: [non-subscriptable] "Cannot delete subscript on object of type `OnlyGetItem` with no `__delitem__` method"
del g[0]
```
### TypedDict deletion
Deleting a required key from a TypedDict is a type error because it would make the object no longer
a valid instance of that TypedDict type. However, deleting `NotRequired` keys (or keys in
`total=False` TypedDicts) is allowed.
<!-- snapshot-diagnostics -->
```py
from typing_extensions import TypedDict, NotRequired
class Movie(TypedDict):
name: str
year: int
class PartialMovie(TypedDict, total=False):
name: str
year: int
class MixedMovie(TypedDict):
name: str
year: NotRequired[int]
m: Movie = {"name": "Blade Runner", "year": 1982}
p: PartialMovie = {"name": "Test"}
mixed: MixedMovie = {"name": "Test"}
# Required keys cannot be deleted.
# error: [invalid-argument-type]
del m["name"]
# In a partial TypedDict (`total=False`), all keys can be deleted.
del p["name"]
# `NotRequired` keys can always be deleted.
del mixed["year"]
# But required keys in mixed `TypedDict` still cannot be deleted.
# error: [invalid-argument-type]
del mixed["name"]
# And keys that don't exist cannot be deleted.
# error: [invalid-argument-type]
del mixed["non_existent"]
```

View File

@@ -118,9 +118,13 @@ class Child(Parent):
class OtherChild(Parent): ...
class Grandchild(OtherChild):
# TODO: The Liskov violation here maybe shouldn't be emitted? Whether called on the
# type or on an instance, it will behave the same from the caller's perspective. The only
# difference is whether the method body gets access to `self`, which is not a
# concern of Liskov.
@staticmethod
# TODO: we should emit a Liskov violation here too
# error: [override-of-final-method]
# error: [invalid-method-override]
def foo(): ...
@property
# TODO: we should emit a Liskov violation here too
@@ -267,6 +271,7 @@ class ChildOfGood(Good):
def f(self, x: str) -> str: ...
@overload
def f(self, x: int) -> int: ...
# error: [override-of-final-method]
def f(self, x: int | str) -> int | str:
return x
@@ -277,6 +282,7 @@ class Bad:
def f(self, x: str) -> str: ...
@overload
def f(self, x: int) -> int: ...
# error: [invalid-overload]
def f(self, x: int | str) -> int | str:
return x
@@ -286,6 +292,7 @@ class Bad:
def g(self, x: str) -> str: ...
@overload
def g(self, x: int) -> int: ...
# error: [invalid-overload]
def g(self, x: int | str) -> int | str:
return x
@@ -295,6 +302,7 @@ class Bad:
@overload
@final
def h(self, x: int) -> int: ...
# error: [invalid-overload]
def h(self, x: int | str) -> int | str:
return x
@@ -304,6 +312,7 @@ class Bad:
@final
@overload
def i(self, x: int) -> int: ...
# error: [invalid-overload]
def i(self, x: int | str) -> int | str:
return x
@@ -478,7 +487,8 @@ class B(A):
#
# TODO: we should emit a Liskov violation here too
# error: [override-of-final-method]
method4 = 42; unrelated = 56 # fmt: skip
method4 = 42
unrelated = 56 # fmt: skip
# Possible overrides of possibly `@final` methods...
class C(A):
@@ -542,6 +552,7 @@ class Child(Parent):
else:
# Fine because this doesn't override any reachable definitions
def foooo(self) -> None: ...
# There are `@final` definitions being overridden here,
# but the definitions that override them are unreachable
def spam(self) -> None: ...

View File

@@ -80,6 +80,7 @@ class CanIndex(Protocol[T]):
def __getitem__(self, index: int, /) -> T: ...
class ExplicitlyImplements(CanIndex[T]): ...
class SubProtocol(CanIndex[T], Protocol): ...
def takes_in_list(x: list[T]) -> list[T]:
return x
@@ -103,6 +104,18 @@ def deep_explicit(x: ExplicitlyImplements[str]) -> None:
def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: set[str]
def deep_subprotocol(x: SubProtocol[str]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: str
def deeper_subprotocol(x: SubProtocol[set[str]]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: set[str]
def itself(x: CanIndex[str]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: str
def deep_itself(x: CanIndex[set[str]]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: set[str]
def takes_in_type(x: type[T]) -> type[T]:
return x

View File

@@ -144,7 +144,7 @@ class Dict[T](TypedDict):
type DictInt = Dict[int]
# error: [non-subscriptable] "Cannot subscript non-generic type alias: `Dict` is already specialized"
# error: [non-subscriptable] "Cannot subscript non-generic type alias: `Dict[int]` is already specialized"
def _(x: DictInt[int]):
reveal_type(x) # revealed: Unknown

View File

@@ -75,6 +75,7 @@ class CanIndex(Protocol[S]):
def __getitem__(self, index: int, /) -> S: ...
class ExplicitlyImplements[T](CanIndex[T]): ...
class SubProtocol[T](CanIndex[T], Protocol): ...
def takes_in_list[T](x: list[T]) -> list[T]:
return x
@@ -98,6 +99,18 @@ def deep_explicit(x: ExplicitlyImplements[str]) -> None:
def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: set[str]
def deep_subprotocol(x: SubProtocol[str]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: str
def deeper_subprotocol(x: SubProtocol[set[str]]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: set[str]
def itself(x: CanIndex[str]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: str
def deep_itself(x: CanIndex[set[str]]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: set[str]
def takes_in_type[T](x: type[T]) -> type[T]:
return x

View File

@@ -1179,6 +1179,34 @@ def _(
reveal_type(subclass_of_p) # revealed: type[P]
```
Using `type[]` with a union type alias distributes the `type[]` over the union elements:
```py
from typing import Union
class C: ...
class D: ...
UnionAlias1 = C | D
UnionAlias2 = Union[C, D]
SubclassOfUnionAlias1 = type[UnionAlias1]
SubclassOfUnionAlias2 = type[UnionAlias2]
reveal_type(SubclassOfUnionAlias1) # revealed: <special-form 'type[C | D]'>
reveal_type(SubclassOfUnionAlias2) # revealed: <special-form 'type[C | D]'>
def _(
subclass_of_union_alias1: SubclassOfUnionAlias1,
subclass_of_union_alias2: SubclassOfUnionAlias2,
):
reveal_type(subclass_of_union_alias1) # revealed: type[C] | type[D]
reveal_type(subclass_of_union_alias1()) # revealed: C | D
reveal_type(subclass_of_union_alias2) # revealed: type[C] | type[D]
reveal_type(subclass_of_union_alias2()) # revealed: C | D
```
Invalid uses result in diagnostics:
```py

View File

@@ -76,3 +76,63 @@ def reveal_type(obj, /): ...
```py
reveal_type(foo) # revealed: Unknown
```
## Builtins imported from custom project-level stubs
The project can add or replace builtins with the `__builtins__.pyi` stub. They will take precedence
over the typeshed ones.
```py
reveal_type(foo) # revealed: int
reveal_type(bar) # revealed: str
reveal_type(quux(1)) # revealed: int
b = baz # error: [unresolved-reference]
reveal_type(ord(100)) # revealed: bool
a = ord("a") # error: [invalid-argument-type]
bar = int(123)
reveal_type(bar) # revealed: int
```
`__builtins__.pyi`:
```pyi
foo: int = ...
bar: str = ...
def quux(value: int) -> int: ...
unused: str = ...
def ord(x: int) -> bool: ...
```
Builtins stubs are searched relative to the project root, not the file using them.
`under/some/folder.py`:
```py
reveal_type(foo) # revealed: int
reveal_type(bar) # revealed: str
```
## Assigning custom builtins
```py
import builtins
builtins.foo = 123
builtins.bar = 456 # error: [unresolved-attribute]
builtins.baz = 789 # error: [invalid-assignment]
builtins.chr = lambda x: str(x) # error: [invalid-assignment]
builtins.chr = 10
```
`__builtins__.pyi`:
```pyi
foo: int
baz: str
chr: int
```

View File

@@ -24,6 +24,16 @@ string = "hello"
reveal_type(f"{string!r}") # revealed: str
```
## Debug Specifier
The `=` specifier causes the expression text and value to be included in the output:
```py
# f"{1=}" evaluates to "1=1", but we fall back to `str` for now
reveal_type(f"{1=}") # revealed: str
reveal_type(f"value: {42=}") # revealed: str
```
## Format Specifiers
```py

View File

@@ -266,10 +266,10 @@ class Person(NamedTuple):
age: int | None = None
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
reveal_type(Person._fields) # revealed: tuple[str, ...]
reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]]
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Person
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace
reveal_type(Person._replace) # revealed: (self: Self, *, name: str = ..., age: int | None = ...) -> Self
reveal_type(Person._make(("Alice", 42))) # revealed: Person
@@ -277,6 +277,10 @@ person = Person("Alice", 42)
reveal_type(person._asdict()) # revealed: dict[str, Any]
reveal_type(person._replace(name="Bob")) # revealed: Person
# Invalid keyword arguments are detected:
# error: [unknown-argument] "Argument `invalid` does not match any known parameter"
person._replace(invalid=42)
```
When accessing them on child classes of generic `NamedTuple`s, the return type is specialized
@@ -343,7 +347,7 @@ satisfy:
def expects_named_tuple(x: typing.NamedTuple):
reveal_type(x) # revealed: tuple[object, ...] & NamedTupleLike
reveal_type(x._make) # revealed: bound method type[NamedTupleLike]._make(iterable: Iterable[Any]) -> NamedTupleLike
reveal_type(x._replace) # revealed: bound method NamedTupleLike._replace(**kwargs) -> NamedTupleLike
reveal_type(x._replace) # revealed: bound method NamedTupleLike._replace(...) -> NamedTupleLike
# revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], (value: tuple[_T@__add__, ...], /) -> tuple[object, ...]]
reveal_type(x.__add__)
reveal_type(x.__iter__) # revealed: bound method tuple[object, ...].__iter__() -> Iterator[object]
@@ -355,8 +359,9 @@ def _(y: type[typing.NamedTuple]):
def _(z: typing.NamedTuple[int]): ...
```
Any instance of a `NamedTuple` class can therefore be passed for a function parameter that is
annotated with `NamedTuple`:
NamedTuples are assignable to `NamedTupleLike`. The `NamedTupleLike._replace` method is typed with
`(*args, **kwargs)`, which type checkers treat as equivalent to `...` (per the typing spec), making
all NamedTuple implementations automatically compatible:
```py
from typing import NamedTuple, Protocol, Iterable, Any
@@ -368,12 +373,15 @@ class Point(NamedTuple):
reveal_type(Point._make) # revealed: bound method <class 'Point'>._make(iterable: Iterable[Any]) -> Point
reveal_type(Point._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Point._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace
reveal_type(Point._replace) # revealed: (self: Self, *, x: int = ..., y: int = ...) -> Self
# Point is assignable to NamedTuple.
static_assert(is_assignable_to(Point, NamedTuple))
expects_named_tuple(Point(x=42, y=56)) # fine
# NamedTuple instances can be passed to functions expecting NamedTupleLike.
expects_named_tuple(Point(x=42, y=56))
# But plain tuples are not NamedTupleLike (they don't have _make, _asdict, _replace, etc.).
# error: [invalid-argument-type] "Argument to function `expects_named_tuple` is incorrect: Expected `tuple[object, ...] & NamedTupleLike`, found `tuple[Literal[1], Literal[2]]`"
expects_named_tuple((1, 2))
```

View File

@@ -0,0 +1,95 @@
# Narrowing for `callable()`
## Basic narrowing
The `callable()` builtin returns `TypeIs[Callable[..., object]]`, which narrows the type to the
intersection with `Top[Callable[..., object]]`. The `Top[...]` wrapper indicates this is a fully
static type representing the top materialization of a gradual callable.
Since all callable types are subtypes of `Top[Callable[..., object]]`, intersections with `Top[...]`
simplify to just the original callable type.
```py
from typing import Any, Callable
def f(x: Callable[..., Any] | None):
if callable(x):
# The intersection simplifies because `(...) -> Any` is a subtype of
# `Top[(...) -> object]` - all callables are subtypes of the top materialization.
reveal_type(x) # revealed: (...) -> Any
else:
# Since `(...) -> Any` is a subtype of `Top[(...) -> object]`, the intersection
# with the negation is empty (Never), leaving just None.
reveal_type(x) # revealed: None
```
## Narrowing with other callable types
```py
from typing import Any, Callable
def g(x: Callable[[int], str] | None):
if callable(x):
# All callables are subtypes of `Top[(...) -> object]`, so the intersection simplifies.
reveal_type(x) # revealed: (int, /) -> str
else:
reveal_type(x) # revealed: None
def h(x: Callable[..., int] | None):
if callable(x):
reveal_type(x) # revealed: (...) -> int
else:
reveal_type(x) # revealed: None
```
## Narrowing from object
```py
from typing import Callable
def f(x: object):
if callable(x):
reveal_type(x) # revealed: Top[(...) -> object]
else:
reveal_type(x) # revealed: ~Top[(...) -> object]
```
## Calling narrowed callables
The narrowed type `Top[Callable[..., object]]` represents the set of all possible callable types
(including, e.g., functions that take no arguments and functions that require arguments). While such
objects *are* callable (they pass `callable()`), no specific set of arguments can be guaranteed to
be valid.
```py
import typing as t
def call_with_args(y: object, a: int, b: str) -> object:
if isinstance(y, t.Callable):
# error: [call-top-callable]
return y(a, b)
return None
```
## Assignability of narrowed callables
A narrowed callable `Top[Callable[..., object]]` should be assignable to `Callable[..., Any]`. This
is important for decorators and other patterns where we need to pass the narrowed callable to
functions expecting gradual callables.
```py
from typing import Any, Callable, TypeVar
from ty_extensions import static_assert, Top, is_assignable_to
static_assert(is_assignable_to(Top[Callable[..., bool]], Callable[..., int]))
F = TypeVar("F", bound=Callable[..., Any])
def wrap(f: F) -> F:
return f
def f(x: object):
if callable(x):
# x has type `Top[(...) -> object]`, which should be assignable to `Callable[..., Any]`
wrap(x)
```

View File

@@ -197,15 +197,86 @@ def _(t1: tuple[int | None, int | None], t2: tuple[int, int] | tuple[None, None]
n = 0
if t1[n] is not None:
# Non-literal subscript narrowing are currently not supported, as well as mypy, pyright
# Narrowing the individual element type with a non-literal subscript is not supported
reveal_type(t1[0]) # revealed: int | None
reveal_type(t1[n]) # revealed: int | None
reveal_type(t1[1]) # revealed: int | None
# However, we can still discriminate between tuples in a union using a variable index:
if t2[n] is not None:
reveal_type(t2) # revealed: tuple[int, int]
if t2[0] is not None:
reveal_type(t2) # revealed: tuple[int, int]
reveal_type(t2[0]) # revealed: int
# TODO: should be int
reveal_type(t2[1]) # revealed: int | None
reveal_type(t2[1]) # revealed: int
else:
reveal_type(t2) # revealed: tuple[None, None]
reveal_type(t2[0]) # revealed: None
reveal_type(t2[1]) # revealed: None
if t2[0] is None:
reveal_type(t2) # revealed: tuple[None, None]
else:
reveal_type(t2) # revealed: tuple[int, int]
def _(t3: tuple[int, str] | tuple[None, None] | tuple[bool, bytes]):
# Narrow to tuples where first element is not None
if t3[0] is not None:
reveal_type(t3) # revealed: tuple[int, str] | tuple[bool, bytes]
# Narrow to tuples where first element is None
if t3[0] is None:
reveal_type(t3) # revealed: tuple[None, None]
def _(t4: tuple[bool, int] | tuple[bool, str]):
# Both tuples have bool at index 0, which is not disjoint from True,
# so neither gets filtered out when checking `is True`
if t4[0] is True:
reveal_type(t4) # revealed: tuple[bool, int] | tuple[bool, str]
def _(t5: tuple[int, None] | tuple[None, int]):
# Narrow on second element (index 1)
if t5[1] is not None:
reveal_type(t5) # revealed: tuple[None, int]
else:
reveal_type(t5) # revealed: tuple[int, None]
# Negative index
if t5[-1] is None:
reveal_type(t5) # revealed: tuple[int, None]
def _(t6: tuple[int, ...] | tuple[None, None]):
# Variadic tuple at index 0 has element type `int` (not a union),
# so `tuple[None, None]` gets filtered out
if t6[0] is not None:
reveal_type(t6) # revealed: tuple[int, ...]
def _(t6b: tuple[int, ...] | tuple[None, ...]):
# Both variadic: `int` is disjoint from None, `None` is not disjoint from None
if t6b[0] is not None:
reveal_type(t6b) # revealed: tuple[int, ...]
else:
reveal_type(t6b) # revealed: tuple[None, ...]
def _(t7: tuple[int, int] | tuple[None, None]):
# Index out of range for both tuples - no narrowing, but errors are emitted
# error: [index-out-of-bounds] "Index 5 is out of bounds for tuple `tuple[int, int]` with length 2"
# error: [index-out-of-bounds] "Index 5 is out of bounds for tuple `tuple[None, None]` with length 2"
if t7[5] is not None:
reveal_type(t7) # revealed: tuple[int, int] | tuple[None, None]
def _(t8: tuple[int, int, int] | tuple[None, None]):
# Index in range for first tuple but out of range for second
# error: [index-out-of-bounds] "Index 2 is out of bounds for tuple `tuple[None, None]` with length 2"
if t8[2] is not None:
reveal_type(t8) # revealed: tuple[int, int, int] | tuple[None, None]
def _(t9: tuple[int | None, str] | tuple[str, int]):
# When the element type is a union (like `int | None`), we can't filter
# out the tuple.
if t9[0] is not None:
reveal_type(t9) # revealed: tuple[int | None, str] | tuple[str, int]
```
### String subscript

View File

@@ -238,3 +238,20 @@ def _(s: LiteralString | None, t: LiteralString | Any):
# TODO could be `Literal["foo"] | Any`
reveal_type(t) # revealed: LiteralString | Any
```
## Narrowing with tuple types
We assume that tuple subclasses don't override `tuple.__eq__`, which only returns True for other
tuples. So they are excluded from the narrowed type when comparing to non-tuple values.
```py
from typing import Literal
def _(x: Literal["a", "b"] | tuple[int, int]):
if x == "a":
# tuple type is excluded because it's disjoint from the string literal
reveal_type(x) # revealed: Literal["a"]
else:
# tuple type remains in the else branch
reveal_type(x) # revealed: Literal["b"] | tuple[int, int]
```

View File

@@ -191,3 +191,20 @@ def test(x: Status | int):
else:
reveal_type(x) # revealed: Literal[Status.REJECTED] | int
```
## Union with tuple and `Literal`
We assume that tuple subclasses don't override `tuple.__eq__`, which only returns True for other
tuples. So they are excluded from the narrowed type when disjoint from the RHS values.
```py
from typing import Literal
def test(x: Literal["none", "auto", "required"] | tuple[list[str], Literal["auto", "required"]]):
if x in ("auto", "required"):
# tuple type is excluded because it's disjoint from the string literals
reveal_type(x) # revealed: Literal["auto", "required"]
else:
# tuple type remains in the else branch
reveal_type(x) # revealed: Literal["none"] | tuple[list[str], Literal["auto", "required"]]
```

View File

@@ -213,8 +213,7 @@ def f(x: dict[str, int] | list[str], y: object):
reveal_type(x) # revealed: list[str]
if isinstance(y, t.Callable):
# TODO: a better top-materialization for `Callable`s (https://github.com/astral-sh/ty/issues/1426)
reveal_type(y) # revealed: () -> object
reveal_type(y) # revealed: Top[(...) -> object]
```
## Class types

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