Compare commits

...

671 Commits

Author SHA1 Message Date
Brent Westbrook
87f0feb21a Bump 0.12.0 (#18724)
- [x] Updated changelog
- [x] Updated breaking changes
2025-06-17 11:05:59 -04:00
Alex Waygood
685eac10e5 Revert "[ty] Offer "Did you mean...?" suggestions for unresolved from imports and unresolved attributes (#18705)" (#18721) 2025-06-17 15:48:09 +01:00
Dylan
a93992fa30 [flake8-return] Stabilize only add return None at the end when fixing implicit-return (RET503) (#18516)
This involved slightly more code changes than usual for a stabilization
- so maybe worth double-checking the logic!

I did verify by hand that the new stable behavior on the test fixture
matches the old preview behavior, even after the internal refactor.
2025-06-17 09:58:12 -04:00
Brent Westbrook
50f84808bc [pyupgrade] Stabilize non-pep695-generic-function (UP047) (#18524)
## Summary
- Stabilizes UP047 (non-pep695-generic-function) rule by changing it
from Preview to Stable

## Test plan
-  Rule is already tested in main test function, no migration needed
-  `make check` passes
-  `make test` passes

## Rule Documentation
- [Test
file](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/rules/pyupgrade/mod.rs#L111)
- [Rule
documentation](https://docs.astral.sh/ruff/rules/non-pep695-generic-function/)
2025-06-17 09:58:12 -04:00
Brent Westbrook
6754e94abc [pyupgrade] Stabilize non-pep695-generic-class (UP046) (#18519)
## Summary
- Stabilizes UP046 (non-pep695-generic-class) rule by changing it from
Preview to Stable

## Test plan
-  Rule is already tested in main test function, no migration needed
-  `make check` passes
-  `make test` passes

## Rule Documentation
- [Test
file](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/rules/pyupgrade/mod.rs#L109-L110)
- [Rule
documentation](https://docs.astral.sh/ruff/rules/non-pep695-generic-class/)
2025-06-17 09:58:12 -04:00
Brent Westbrook
33c8c7569d [pandas-vet] Deprecate pandas-df-variable-name (PD901) (#18618)
Summary
--

Deprecates PD901 as part of #7710. I don't feel particularly strongly
about this one, though I have certainly used `df` as a dataframe name in
the past, just going through the open issues in the 0.12 milestone.

Test Plan
--

N/a
2025-06-17 09:58:12 -04:00
Brent Westbrook
34dc8e0531 [flake8-bandit] Remove suspicious-xmle-tree-usage (S320) (#18617)
Summary
--

Closes #13707. The rule was deprecated in 0.10 (#16680) and slated for
removal in either this or the next release.

Test Plan
--

N/a
2025-06-17 09:58:12 -04:00
Dylan
b01195b166 Stabilize dataclass-enum (RUF049) (#18570)
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-17 09:58:12 -04:00
Dylan
ce176b1acf Stabilize unnecessary-dict-index-lookup (PLR1733) (#18571)
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-17 09:58:12 -04:00
Micha Reiser
7072cf69b4 Remove rust-toolchain.toml from sdist (#17925)
Closes https://github.com/astral-sh/ruff/issues/17909
2025-06-17 09:58:12 -04:00
Brent Westbrook
72c8dc006f Stabilize starmap-zip (RUF058) (#18525)
## Summary
- Stabilizes RUF058 (starmap-zip) rule by changing it from Preview to
Stable
- Migrates test cases from preview_rules to main rules function 
- Updates snapshots accordingly and removes old preview snapshots

## Test plan
-  Migrated tests from preview to main test function
-  `make check` passes
-  `make test` passes  
-  `make citest` passes (no leftover snapshots)

## Rule Documentation
- [Test
file](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/rules/ruff/mod.rs#L103-L104)
- [Rule documentation](https://docs.astral.sh/ruff/rules/starmap-zip/)
2025-06-17 09:58:12 -04:00
Brent Westbrook
ad9ae4e2b6 [flake8-logging] Stabilize exc-info-outside-except-handler (LOG014) (#18517)
## Summary
- Stabilizes LOG014 (exc-info-outside-except-handler) rule by changing
it from Preview to Stable

## Test plan
-  Rule is already tested in main test function, no migration needed
-  `make check` passes
-  `make test` passes

## Rule Documentation
- [Test
file](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/rules/flake8_logging/mod.rs#L22-L23)
- [Rule
documentation](https://docs.astral.sh/ruff/rules/exc-info-outside-except-handler/)
2025-06-17 09:58:12 -04:00
Dylan
de4fc5b171 [pyupgrade] Stabilize non-pep604-annotation-optional (UP045) and preview behavior for non-pep604-annotation-union (UP007) (#18505) 2025-06-17 09:58:12 -04:00
Dylan
56f2aaaebc Stabilize pytest-warns-too-broad (PT030) (#18568) 2025-06-17 09:58:12 -04:00
Dylan
ebd2a27559 Stabilize for-loop-writes (FURB122) (#18565) 2025-06-17 09:58:12 -04:00
Dylan
1278e3442a Stabilize pytest-warns-with-multiple-statements (PT031) (#18569) 2025-06-17 09:58:12 -04:00
Dylan
7efbf469dd Stabilize pytest-parameter-with-default-argument (PT028) (#18566) 2025-06-17 09:58:12 -04:00
Dylan
2a1fed9327 Stabilize nan-comparison (PLW0177) (#18559) 2025-06-17 09:58:12 -04:00
Dylan
7de8a0b429 Stabilize check-and-remove-from-set (FURB132) (#18560) 2025-06-17 09:58:12 -04:00
Dylan
0a1c6cb70b Stabilize unnecessary-round (RUF057) (#18563) 2025-06-17 09:58:12 -04:00
Dylan
2dafc5a8bd Stabilize eq-without-hash (PLW1641) (#18561) 2025-06-17 09:58:12 -04:00
Dylan
72a4c3ed83 Stabilize int-on-sliced-str (FURB166) (#18558) 2025-06-17 09:58:12 -04:00
Dylan
e559e21e93 [pylint] Stabilize import-outside-top-level (PLC0415) (#18554) 2025-06-17 09:58:12 -04:00
Dylan
c948be495a [ruff] Stabilize invalid-formatter-suppression-comment (RUF028) (#18555) 2025-06-17 09:58:12 -04:00
Dylan
cd245d292e Stabilize verbose-decimal-constructor (FURB157) (#18556) 2025-06-17 09:58:12 -04:00
Brent Westbrook
620b84443b [pyupgrade] Stabilize private-type-parameter (UP049) (#18515)
## Summary
Stabilizes the UP049 rule (private-type-parameter) by moving it from
Preview to Stable.

UP049 detects and fixes the use of private type parameters (those with
leading underscores) in PEP 695 generic classes and functions.

## Test plan
- Verified that UP049 tests pass:
`crates/ruff_linter/src/rules/pyupgrade/mod.rs`
- Ran full test suite with `make test` 
- Confirmed that no test migration was needed as UP049 was already in
the main `rules` test function

## Rule documentation
https://docs.astral.sh/ruff/rules/private-type-parameter/
2025-06-17 09:58:12 -04:00
Dylan
1f70ceba0c [flake8-boolean-trap] Stabilize lint bool suprtypes in boolean-type-hint-positional-argument (FBT001) (#18520)
Feel free to complain about the rephrasing in the docs!
2025-06-17 09:58:12 -04:00
Dylan
00e9de8db9 [flake8-bandit] Stabilize more trusted inputs in subprocess-without-shell-equals-true (S603) (#18521) 2025-06-17 09:58:12 -04:00
Dylan
7211660f8b [flake8-pyi] Stabilize autofix for future-annotations-in-stub (PYI044) (#18518) 2025-06-17 09:58:12 -04:00
Dylan
c1610e2eaf [semantic errors] Stabilize semantic errors (#18523) 2025-06-17 09:58:12 -04:00
Dylan
bf53bc4256 [syntax errors] Stabilize version-specific unsupported syntax errors (#18522) 2025-06-17 09:58:12 -04:00
Dylan
9f2ae1f568 [ruff] Stabilize checking for file-level directives in unused-noqa (RUF100) (#18497)
Note that the preview behavior was not documented (shame on us!) so the
documentation was not modified.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-17 09:58:12 -04:00
Dylan
5cf2c40d13 [flake8-simplify] Stabilize further simplification to binary expressions in autofix for if-else-block-instead-of-if-exp (SIM108) (#18506) 2025-06-17 09:58:12 -04:00
Brent Westbrook
02b5376a3c [refurb] Stabilize fromisoformat-replace-z (FURB162) (#18510)
This PR stabilizes the FURB162 rule by moving it from preview to stable
status for the 0.12.0 release.

## Summary
- **Rule**: FURB162 (`fromisoformat-replace-z`)
- **Purpose**: Detects unnecessary timezone replacement operations when
calling `datetime.fromisoformat()`
- **Change**: Move from `RuleGroup::Preview` to `RuleGroup::Stable` in
`codes.rs`

## Verification Links
- **Tests**:
[refurb/mod.rs](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/rules/refurb/mod.rs#L54)
- Confirms FURB162 has only standard tests, no preview-specific test
cases
- **Documentation**:
https://docs.astral.sh/ruff/rules/fromisoformat-replace-z/ - Current
documentation shows preview status that will be automatically updated
2025-06-17 09:58:12 -04:00
Brent Westbrook
18a134ae1f [ruff] Stabilize class-with-mixed-type-vars (RUF053) (#18512)
This PR stabilizes the RUF053 rule by moving it from preview to stable
status for the 0.12.0 release.

## Summary
- **Rule**: RUF053 (`class-with-mixed-type-vars`)
- **Purpose**: Detects classes that have both PEP 695 type parameter
lists while also inheriting from `typing.Generic`
- **Change**: Move from `RuleGroup::Preview` to `RuleGroup::Stable` in
`codes.rs` and migrate preview tests to stable tests

## Verification Links
- **Tests**:
[ruff/mod.rs](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/rules/ruff/mod.rs#L98)
- Shows RUF053 moved from preview_rules to main rules test function
- **Documentation**:
https://docs.astral.sh/ruff/rules/class-with-mixed-type-vars/ - Current
documentation shows preview status that will be automatically updated
2025-06-17 09:58:12 -04:00
Dylan
c063940d52 [ruff] Stabilize checking in presence of slices for collection-literal-concatenation (RUF005) (#18500) 2025-06-17 09:58:12 -04:00
Dylan
8aea383f29 [refurb] Stabilize fix safety for readlines-in-for (FURB129) (#18496)
Note that the preview behavior was not documented (shame on us!) so the
documentation was not modified.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-17 09:58:12 -04:00
Alex Waygood
913f136d33 [ty] Offer "Did you mean...?" suggestions for unresolved from imports and unresolved attributes (#18705)
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-17 11:10:34 +01:00
Dhruv Manilawala
c7e020df6b [ty] Filter overloads based on Any / Unknown (#18607)
## Summary

Closes: astral-sh/ty#552

This PR adds support for step 5 of the overload call evaluation
algorithm which specifies:

> For all arguments, determine whether all possible materializations of
the argument’s type are
> assignable to the corresponding parameter type for each of the
remaining overloads. If so,
> eliminate all of the subsequent remaining overloads.

The algorithm works in two parts:

1. Find out the participating parameter indexes. These are the
parameters that aren't gradual equivalent to one or more parameter types
at the same index in other overloads.
2. Loop over each overload and check whether that would be the _final_
overload for the argument types i.e., the remaining overloads will never
be matched against these argument types

For step 1, the participating parameter indexes are computed by just
comparing whether all the parameter types at the corresponding index for
all the overloads are **gradual equivalent**.

The step 2 of the algorithm used is described in [this
comment](https://github.com/astral-sh/ty/issues/552#issuecomment-2969165421).

## Test Plan

Update the overload call tests.
2025-06-17 15:35:09 +05:30
Alex Waygood
1d458d4314 [ty] Fix panics when pulling types for various special forms that have the wrong number of parameters (#18642) 2025-06-17 10:40:50 +01:00
Shunsuke Shibayama
342b2665db [ty] basic narrowing on attribute and subscript expressions (#17643)
## Summary

This PR closes astral-sh/ty#164.

This PR introduces a basic type narrowing mechanism for
attribute/subscript expressions.
Member accesses, int literal subscripts, string literal subscripts are
supported (same as mypy and pyright).

## Test Plan

New test cases are added to `mdtest/narrow/complex_target.md`.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-06-17 11:07:46 +02:00
Dhruv Manilawala
390918e790 [ty] Add python.ty.disableLanguageServices config (#18230)
## Summary

PR adding support for it in the VS Code extension:
https://github.com/astral-sh/ty-vscode/pull/36

This PR adds support for `python.ty.disableLanguageServices` to the ty
language server by accepting this as server setting.

This has the same issue as https://github.com/astral-sh/ty/issues/282 in
that it only works when configured globally. Fixing that requires
support for multiple workspaces in the server itself.

I also went ahead and did a similar refactor as the Ruff server to use
"Options" and "Settings" to keep the code consistent although the
combine functionality doesn't exists yet because workspace settings
isn't supported in the ty server.

## Test Plan

Refer to https://github.com/astral-sh/ty-vscode/pull/36 for the test
demo.
2025-06-17 13:50:45 +05:30
David Peter
a1c69ca460 [ty] Enable ecosystem check for 'pywin32' (#18716)
## Summary

Follow-up to #18621
2025-06-17 09:52:26 +02:00
David Peter
3a77768f79 [ty] Reachability constraints (#18621)
## Summary



* Completely removes the concept of visibility constraints. Reachability
constraints are now used to model the static visibility of bindings and
declarations. Reachability constraints are *much* easier to reason about
/ work with, since they are applied at the beginning of a branch, and
not applied retroactively. Removing the duplication between visibility
and reachability constraints also leads to major code simplifications
[^1]. For an overview of how the new constraint system works, see the
updated doc comment in `reachability_constraints.rs`.
* Fixes a [control-flow modeling bug
(panic)](https://github.com/astral-sh/ty/issues/365) involving `break`
statements in loops
* Fixes a [bug where](https://github.com/astral-sh/ty/issues/624) where
`elif` branches would have wrong reachability constraints
* Fixes a [bug where](https://github.com/astral-sh/ty/issues/648) code
after infinite loops would not be considered unreachble
* Fixes a panic on the `pywin32` ecosystem project, which we should be
able to move to `good.txt` once this has been merged.
* Removes some false positives in unreachable code because we infer
`Never` more often, due to the fact that reachability constraints now
apply retroactively to *all* active bindings, not just to bindings
inside a branch.
* As one example, this removes the `division-by-zero` diagnostic from
https://github.com/astral-sh/ty/issues/443 because we now infer `Never`
for the divisor.
* Supersedes and includes similar test changes as
https://github.com/astral-sh/ruff/pull/18392


closes https://github.com/astral-sh/ty/issues/365
closes https://github.com/astral-sh/ty/issues/624
closes https://github.com/astral-sh/ty/issues/642
closes https://github.com/astral-sh/ty/issues/648

## Benchmarks

Benchmarks on black, pandas, and sympy showed that this is neither a
performance improvement, nor a regression.

## Test Plan

Regression tests for:
- [x] https://github.com/astral-sh/ty/issues/365
- [x] https://github.com/astral-sh/ty/issues/624
- [x] https://github.com/astral-sh/ty/issues/642
- [x] https://github.com/astral-sh/ty/issues/648

[^1]: I'm afraid this is something that @carljm advocated for since the
beginning, and I'm not sure anymore why we have never seriously tried
this before. So I suggest we do *not* attempt to do a historical deep
dive to find out exactly why this ever became so complicated, and just
enjoy the fact that we eventually arrived here.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-06-17 09:24:28 +02:00
Micha Reiser
c22f809049 Hug closing } when f-string expression has a format specifier (#18704) 2025-06-17 07:39:42 +02:00
Alex Waygood
2b731d19b9 [ty] Fix panic when attempting to provide autocompletions for an instance of a class that assigns attributes to self[0] (#18707) 2025-06-16 21:58:05 +00:00
Victor Hugo Gomes
cff5adf324 [pyupgrade] Suppress UP008 diagnostic if super symbol is not builtin (#18688)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Fixes #18684
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

Add regression test
<!-- How was it tested? -->
2025-06-16 15:09:31 -04:00
Nikolas Hearp
7880a20794 [pylint] Fix PLW0128 to check assignment targets in square brackets and after asterisks (#18665)
## Summary

This fixes PLW0128 to check for redeclared assignments in square
brackets and after asterisks.

Fixes #18660
2025-06-16 15:02:30 -04:00
chiri
83b0cde2fc [refurb] Make the fix for FURB163 unsafe for log2, log10, *args, and deleted comments (#18645)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary
/closes #18639
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
update snapshots
<!-- How was it tested? -->

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-16 18:13:47 +00:00
Felix Scherz
373a3bfcd6 [ty] allow T: Never as subtype of Never (#18687) 2025-06-16 17:46:17 +00:00
Alex Waygood
5e57e4680f [ty] Use more parallelism when running corpus tests (#18711) 2025-06-16 17:38:55 +00:00
Abhijeet Prasad Bodas
2b15f1d240 [ty] Support dataclasses.KW_ONLY (#18677) 2025-06-16 17:27:55 +00:00
Denys Kyslytsyn
c3aa965546 [ruff] Check for non-context-manager use of pytest.raises, pytest.warns, and pytest.deprecated_call (RUF061) (#17368)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

This PR aims to close #16605.

## Summary

This PR introduces a new rule (`RUF061`) that detects non-contextmanager
usage of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call`.
This pattern is discouraged and [was proposed in
flake8-pytest-style](https://github.com/m-burst/flake8-pytest-style/pull/332),
but the corresponding PR has been open for over a month without
activity.

Additionally, this PR provides an unsafe fix for simple cases where the
non-contextmanager form can be transformed into the context manager
form. Examples of supported patterns are listed in `RUF061_raises.py`,
`RUF061_warns.py`, and `RUF061_deprecated_call.py` test files.

The more complex case from the original issue (involving two separate
statements):
```python
excinfo = pytest.raises(ValueError, int, "hello")
assert excinfo.match("^invalid literal")
```
is getting fixed like this:
```python
with pytest.raises(ValueError) as excinfo:
    int("hello")
assert excinfo.match("^invalid literal")
```
Putting match in the raises call requires multi-statement
transformation, which I am not sure how to implement.

## Test Plan

<!-- How was it tested? -->

New test files were added to cover various usages of the
non-contextmanager form of pytest.raises, warns, and deprecated_call.
2025-06-16 13:03:54 -04:00
Dylan
c5b58187da Add syntax error when conversion flag does not immediately follow exclamation mark (#18706)
Closes #18671

Note that while this has, I believe, always been invalid syntax, it was
reported as a different syntax error until Python 3.12:

Python 3.11:

```pycon
>>> x = 1
>>> f"{x! s}"
  File "<stdin>", line 1
    f"{x! s}"
             ^
SyntaxError: f-string: invalid conversion character: expected 's', 'r', or 'a'
```

Python 3.12:

```pycon
>>> x = 1
>>> f"{x! s}"
  File "<stdin>", line 1
    f"{x! s}"
        ^^^
SyntaxError: f-string: conversion type must come right after the exclamanation mark
```
2025-06-16 11:44:42 -05:00
Juriah
a842899862 [flake8-pyi] Fix custom-typevar-for-self with string annotations (PYI019) (#18311)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary
<!-- What's the purpose of the change? What does it do, and why? -->

Solves #18257 

## Test Plan

<!-- How was it tested? -->
Snapshots updated with some cases (negative, positive, mixed
annotations).
2025-06-16 10:47:17 -04:00
Brent Westbrook
ee3152dace Drop confusing second * from glob pattern example (#18709)
Summary
--

As @AlexWaygood noted on the 0.12 release blog post draft, the existing
example is a bit confusing. Either `**/*.py` or just `*.py`, as I went
with here, makes more sense, although the old version (`scripts/**.py`)
also worked when I tested it. However, this probably shouldn't be relied
upon since the [globset](https://docs.rs/globset/latest/globset/#syntax)
docs say:

> Using ** anywhere else is illegal

where "anywhere else" comes after the listing of the three valid
positions:
1. At the start of a pattern (`**/`)
2. At the end of a pattern (`/**`)
3. Or directly between two slashes (`/**/`)

I think the current version is luckily treated the same as a single `*`,
and the default globbing settings allow it to match subdirectories such
that the new example pattern will apply to the whole `scripts` tree in a
project like this:

```
.
├── README.md
├── pyproject.toml
├── scripts
│   ├── matching.py
│   └── sub
│       └── nested.py
└── src
    └── main.py
```

Test Plan
--

Local testing of the new pattern, but the specifics of the pattern
aren't as important as having a more intuitive-looking/correct example.
2025-06-16 10:41:43 -04:00
Andrew Gallant
869d7bf9a8 [ty] Stabilize completions (#18650)
Specifically, this PR reverts "Make completions an opt-in LSP feature
(#17921)",
corresponding to commit 51e2effd2d.

In practice, this means you don't need to opt into completions working
by enabling experimental features. i.e., I was able to remove this from
my LSP configuration:

```
"experimental": {
    "completions": {
        "enable": true
    }
},
```

There's still a lot of work left to do to make completions awesome, but
I think it's in a state where it would be useful to get real user
feedback. It's also meaningfully using ty to provide completions that
use type information.

Ref astral-sh/ty#86
2025-06-16 07:44:08 -04:00
David Peter
2f3bd24900 [ty] Correctly label typeshed-sync PRs (#18702)
## Summary

Ref:
https://github.com/astral-sh/ruff/pull/18679#issuecomment-2973593785

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-16 07:47:52 +00:00
renovate[bot]
d715c1fef8 Update Rust crate memchr to v2.7.5 (#18696)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:10:43 +02:00
renovate[bot]
cb2ae8d9ac Update dependency react-resizable-panels to v3.0.3 (#18691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:10:19 +02:00
renovate[bot]
5383bcc497 Update Rust crate clap to v4.5.40 (#18692)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:09:33 +02:00
renovate[bot]
9b927265f9 Update Rust crate libcst to v1.8.2 (#18695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:09:18 +02:00
renovate[bot]
b38115ba95 Update Rust crate jiff to v0.2.15 (#18693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:08:57 +02:00
renovate[bot]
32a0d4bb21 Update Rust crate libc to v0.2.173 (#18694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:08:34 +02:00
renovate[bot]
ccae65630a Update Rust crate syn to v2.0.103 (#18698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:06:48 +02:00
renovate[bot]
4cdf128748 Update Rust crate toml to v0.8.23 (#18699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:06:34 +02:00
renovate[bot]
0c18a5a737 Update Rust crate pyproject-toml to v0.13.5 (#18697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:02:16 +02:00
renovate[bot]
37b2de90f8 Update to unicode 16 (#18700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:01:54 +02:00
Micha Reiser
3a430fa6da [ty] Allow overriding rules for specific files (#18648) 2025-06-15 14:27:39 +01:00
github-actions[bot]
782363b736 Sync vendored typeshed stubs (#18679) 2025-06-15 10:20:33 +01:00
Micha Reiser
8237d4670c Fix \r and \r\n handling in t- and f-string debug texts (#18673) 2025-06-15 06:53:06 +01:00
Ibraheem Ahmed
5e02d839d5 [ty] Avoid accessing class literal with incorrect AST (#18670) 2025-06-14 06:02:53 +01:00
Brent Westbrook
e4423044f8 [ruff] Validate arguments before offering a fix (RUF056) (#18631)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/18628 by avoiding a fix
if there are "unknown" arguments, including any keyword arguments and
more than the expected 2 positional arguments.

I'm a bit on the fence here because it also seems reasonable to avoid a
diagnostic at all. Especially in the final test case I added (`not
my_dict.get(default=False)`), the hint suggesting to remove
`default=False` seems pretty misleading. At the same time, I guess the
diagnostic at least calls attention to the call site, which could help
to fix the missing argument bug too.

As I commented on the issue, I double-checked that keyword arguments are
invalid as far back as Python 3.8, even though the positional-only
marker was only added to the
[docs](https://docs.python.org/3.11/library/stdtypes.html#dict.get) in
3.12 (link is to 3.11, showing its absence).

## Test Plan

New tests derived from the bug report

## Stabilization

This was planned to be stabilized in 0.12, and the bug is less severe
than some others, but if there's nobody opposed, I will plan **not to
stabilize** this one for now.
2025-06-13 23:07:02 +00:00
InSync
6d56ee803e [ty] Add partial support for TypeIs (#18589)
## Summary

Part of [#117](https://github.com/astral-sh/ty/issues/117).

`TypeIs[]` is a special form that allows users to define their own
narrowing functions. Despite the syntax, `TypeIs` is not a generic and,
on its own, it is meaningless as a type.
[Officially](https://typing.python.org/en/latest/spec/narrowing.html#typeis),
a function annotated as returning a `TypeIs[T]` is a <i>type narrowing
function</i>, where `T` is called the <i>`TypeIs` return type</i>.

A `TypeIs[T]` may or may not be bound to a symbol. Only bound types have
narrowing effect:

```python
def f(v: object = object()) -> TypeIs[int]: ...

a: str = returns_str()

if reveal_type(f()):   # Unbound: TypeIs[int]
	reveal_type(a)     # str

if reveal_type(f(a)):  # Bound:   TypeIs[a, int]
	reveal_type(a)     # str & int
```

Delayed usages of a bound type has no effect, however:

```python
b = f(a)

if b:
	reveal_type(a)     # str
```

A `TypeIs[T]` type:

* Is fully static when `T` is fully static.
* Is a singleton/single-valued when it is bound.
* Has exactly two runtime inhabitants when it is unbound: `True` and
`False`.
  In other words, an unbound type have ambiguous truthiness.
It is possible to infer more precise truthiness for bound types;
however, that is not part of this change.

`TypeIs[T]` is a subtype of or otherwise assignable to `bool`. `TypeIs`
is invariant with respect to the `TypeIs` return type: `TypeIs[int]` is
neither a subtype nor a supertype of `TypeIs[bool]`. When ty sees a
function marked as returning `TypeIs[T]`, its `return`s will be checked
against `bool` instead. ty will also report such functions if they don't
accept a positional argument. Addtionally, a type narrowing function
call with no positional arguments (e.g., `f()` in the example above)
will be considered invalid.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-06-13 15:27:45 -07:00
David Peter
89d915a1e3 [ty] Delay computation of 'unbound' visibility for implicit instance attributes (#18669)
## Summary

Consider the following example, which leads to a excessively large
runtime on `main`. The reason for this is the following. When inferring
types for `self.a`, we look up the `a` attribute on `C`. While looking
for implicit instance attributes, we go through every method and check
for `self.a = …` assignments. There are no such assignments here, but we
always have an implicit `self.a = <unbound>` binding at the beginning
over every method. This binding accumulates a complex visibility
constraint in `C.f`, due to the `isinstance` checks. While evaluating
that constraint, we need to infer the type of `self.b`. There's no
binding for `self.b` either, but there's also an implicit `self.b =
<unbound>` binding with the same complex visibility constraint
(involving `self.b` recursively). This leads to a combinatorial
explosion:

```py
class C:
    def f(self: "C"):
        if isinstance(self.a, str):
            return

        if isinstance(self.b, str):
            return
        if isinstance(self.b, str):
            return
        if isinstance(self.b, str):
            return
        # repeat 20 times
```
(note that the `self` parameter here is annotated explicitly because we
currently still infer `Unknown` for `self` otherwise)

The fix proposed here is rather simple: when there are no `self.name =
…` attribute assignments in a given method, we skip evaluating the
visibility constraint of the implicit `self.name = <unbound>` binding.
This should also generally help with performance, because that's a very
common case.

This is *not* a fix for cases where there *are* actual bindings in the
method. When we add `self.a = 1; self.b = 1` to that example above, we
still see that combinatorial explosion of runtime. I still think it's
worth to make this optimization, as it fixes the problems with `pandas`
and `sqlalchemy` reported by users. I will open a ticket to track that
separately.

closes https://github.com/astral-sh/ty/issues/627
closes https://github.com/astral-sh/ty/issues/641

## Test Plan

* Made sure that `ty` finishes quickly on the MREs in
https://github.com/astral-sh/ty/issues/627
* Made sure that `ty` finishes quickly on `pandas`
* Made sure that `ty` finishes quickly on `sqlalchemy`
2025-06-13 12:50:57 -07:00
Dylan
1889a5e6eb [syntax-errors] Raise unsupported syntax error for template strings prior to Python 3.14 (#18664)
Closes #18662

One question is whether we would like the range to exclude the quotes?
2025-06-13 14:04:37 -05:00
𝕂
793ff9bdbc Fix false positive in for mutations in return statements (B909) (#18408)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Fixes false positive in B909 (`loop-iterator-mutation`) where mutations
inside return/break statements were incorrectly flagged as violations.
The fix adds tracking for when mutations occur within return/break
statements and excludes them from violation detection, as they don't
cause the iteration issues B909 is designed to prevent.



## Test Plan

- Added test cases covering the reported false positive scenarios to
`B909.py`
  - Verified existing B909 tests continue to pass (no regressions)
  - Ran `cargo test -p ruff_linter --lib flake8_bugbear` successfully

Fixes #18399
2025-06-13 10:39:55 -04:00
Ibraheem Ahmed
c9dff5c7d5 [ty] AST garbage collection (#18482)
## Summary

Garbage collect ASTs once we are done checking a given file. Queries
with a cross-file dependency on the AST will reparse the file on demand.
This reduces ty's peak memory usage by ~20-30%.

The primary change of this PR is adding a `node_index` field to every
AST node, that is assigned by the parser. `ParsedModule` can use this to
create a flat index of AST nodes any time the file is parsed (or
reparsed). This allows `AstNodeRef` to simply index into the current
instance of the `ParsedModule`, instead of storing a pointer directly.

The indices are somewhat hackily (using an atomic integer) assigned by
the `parsed_module` query instead of by the parser directly. Assigning
the indices in source-order in the (recursive) parser turns out to be
difficult, and collecting the nodes during semantic indexing is
impossible as `SemanticIndex` does not hold onto a specific
`ParsedModuleRef`, which the pointers in the flat AST are tied to. This
means that we have to do an extra AST traversal to assign and collect
the nodes into a flat index, but the small performance impact (~3% on
cold runs) seems worth it for the memory savings.

Part of https://github.com/astral-sh/ty/issues/214.
2025-06-13 08:40:11 -04:00
Victor Hugo Gomes
76d9009a6e [pycodestyle] Fix E731 autofix creating a syntax error for expressions spanned across multiple lines (#18479) 2025-06-13 08:44:15 +02:00
Micha Reiser
015222900f Support cancellation requests (#18627) 2025-06-12 22:08:42 +02:00
Micha Reiser
1f27d53fd5 [ty] File inclusion and exclusion (#18498) 2025-06-12 19:07:31 +02:00
Micha Reiser
3c6c017950 Centralize client options validation (#18623) 2025-06-12 18:58:30 +02:00
Shunsuke Shibayama
ef564094a9 [ty] support del statement and deletion of except handler names (#18593)
## Summary

This PR closes https://github.com/astral-sh/ty/issues/238.

Since `DefinitionState::Deleted` was introduced in #18041, support for
the `del` statement (and deletion of except handler names) is
straightforward.

However, it is difficult to determine whether references to attributes
or subscripts are unresolved after they are deleted. This PR only
invalidates narrowing by assignment if the attribute or subscript is
deleted.

## Test Plan

`mdtest/del.md` is added.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-12 07:44:42 -07:00
Brent Westbrook
96171f41c2 [ruff] Handle extra arguments to deque (RUF037) (#18614)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/18612 by:
- Bailing out without a fix in the case of `*args`, which I don't think
we can fix reliably
- Using an `Edit::deletion` from `remove_argument` instead of an
`Edit::range_replacement` in the presence of unrecognized keyword
arguments

I thought we could always switch to the `Edit::deletion` approach
initially, but it caused problems when `maxlen` was passed positionally,
which we didn't have any existing tests for.

The replacement fix can easily delete comments, so I also marked the fix
unsafe in these cases and updated the docs accordingly.

## Test Plan

New test cases derived from the issue.

## Stabilization

These are pretty significant changes, much like those to PYI059 in
https://github.com/astral-sh/ruff/pull/18611 (and based a bit on the
implementation there!), so I think it probably makes sense to
un-stabilize this for the 0.12 release, but I'm open to other thoughts
there.
2025-06-12 09:07:17 -04:00
Andrew Gallant
8123dab05a [ty] Add some "inside string" tests for object.<CURSOR> completions
Ref https://github.com/astral-sh/ruff/pull/18629#pullrequestreview-2919922754
2025-06-12 07:50:49 -04:00
Alex Waygood
324e5cbc19 [ty] Pull types on synthesized Python files created by mdtest (#18539) 2025-06-12 10:32:17 +01:00
renovate[bot]
e6fe2af292 Update Rust crate anstyle to v1.0.11 (#18583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 08:54:38 +02:00
chiri
dbb0d60caa [pyupgrade] Fix super(__class__, self) detection in UP008 (super-call-with-parameters) (#18478) 2025-06-12 08:52:45 +02:00
Dhruv Manilawala
ef4108af2a [ty] Generate the top and bottom materialization of a type (#18594)
## Summary

This is to support https://github.com/astral-sh/ruff/pull/18607.

This PR adds support for generating the top materialization (or upper
bound materialization) and the bottom materialization (or lower bound
materialization) of a type. This is the most general and the most
specific form of the type which is fully static, respectively.
    
More concretely, `T'`, the top materialization of `T`, is the type `T`
with all occurrences
of dynamic type (`Any`, `Unknown`, `@Todo`) replaced as follows:

- In covariant position, it's replaced with `object`
- In contravariant position, it's replaced with `Never`
- In invariant position, it's replaced with an unresolved type variable

(For an invariant position, it should actually be replaced with an
existential type, but this is not currently representable in our type
system, so we use an unresolved type variable for now instead.)

The bottom materialization is implemented in the same way, except we
start out in "contravariant" position.

## Test Plan

Add test cases for various types.
2025-06-12 12:06:16 +05:30
Jia Chen
f74527f4e9 SourceOrderVisitor should visit the Identifier part of the PatternKeyword node (#18635) 2025-06-12 08:20:14 +02:00
Ibraheem Ahmed
65a2c6d4eb Update salsa (#18636) 2025-06-12 07:17:00 +02:00
justin
1a3befe8d6 [ty] Update mypy_primer doc (#18638)
## Summary
Minor documentation update to make `mypy_primer` instructions a bit more
verbose/helpful for running against a local branch

## Test Plan
N/A
2025-06-11 20:50:37 -07:00
Andrew Gallant
7893cf9fe1 [ty] Improve support for object.<CURSOR> completions
This makes it work for a number of additional cases, like nested
attribute access and things like `[].<CURSOR>`.

The basic idea is that instead of selecting a covering node closest to a
leaf that contains the cursor, we walk up the tree as much as we can.
This lets us access the correct `ExprAttribute` node when performing
nested access.
2025-06-11 14:31:38 -04:00
Andrew Gallant
8fdf3fc47f [ty] Add CoveringNode::find_last
This routine lets us climb up the AST tree when we find
a contiguous sequence of nodes that satisfy our predicate.

This will be useful for making things like `a.b.<CURSOR>`
work. That is, we don't want the `ExprAttribute` closest
to a leaf. We also don't always want the `ExprAttribute`
closest to the root. Rather, (I think) we want the
`ExprAttribute` closest to the root that has an unbroken
chain to the `ExprAttribute` closest to the leaf.
2025-06-11 14:31:38 -04:00
Andrew Gallant
65f32edbc7 [ty] Refactor covering node representation
This commit doesn't change any functionality, but instead changes the
representation of `CoveringNode` to make the implementation simpler (as
well as planned future additions). By putting the found node last in the
list of ancestors (now just generically called `nodes`), we reduce the
amount of special case handling we need.

The downside is that the representation now allows invalid states (a
`CoveringNode` with no elements). But I think this is well mitigated by
encapsulation.
2025-06-11 14:31:38 -04:00
Alex Waygood
e84406d8be [ty] Infer the Python version from --python=<system installation> on Unix (#18550) 2025-06-11 14:32:33 +00:00
Victor Hugo Gomes
a863000cbc [flake8-return] Fix RET504 autofix generating a syntax error (#18428) 2025-06-11 13:38:42 +00:00
Micha Reiser
3aae1cd59b Fix incorrect salsa return_ref attribute (#18605) 2025-06-11 09:19:57 +02:00
Micha Reiser
5dcfc9f074 Move corpus tests to ty_python_semantic (#18609) 2025-06-11 08:55:30 +02:00
Robsdedude
0724bee59c [pyupgrade] Don't offer fix for Optional[None] in non-pep604-annotation-optional (UP045) or non-pep604-annotation-union (UP007) (#18545) 2025-06-11 08:19:00 +02:00
Victor Hugo Gomes
2213698a5d [pep8-naming] Suppress fix for N804 and N805 if the recommend name is already used (#18472) 2025-06-11 07:58:55 +02:00
chiri
dc322d23dd [ruff] skip fix for RUF059 if dummy name is already bound (unused-unpacked-variable) (#18509) 2025-06-11 07:58:05 +02:00
Carl Meyer
a2de81cb27 [ty] implement disjointness of Callable vs SpecialForm (#18503)
## Summary

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

## Test Plan

Stable property tests succeed with a million iterations. Added mdtests.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-10 20:25:08 +00:00
Carl Meyer
eb60bd64fd [ty] more simplification of infer_parameterized_legacy_typing_alias (#18526)
Address post-land review on https://github.com/astral-sh/ruff/pull/18489
2025-06-10 13:22:25 -07:00
Brent Westbrook
b21ac567e1 [refurb] Add a note about float literal handling (FURB157) (#18615)
Summary
--

Updates the rule docs to explicitly state how cases like
`Decimal("0.1")` are handled (not affected) because the discussion of
"float casts" referring to values like `nan` and `inf` is otherwise a
bit confusing.

These changes are based on suggestions from @AlexWaygood on Notion, with
a slight adjustment to use 0.1 instead of 0.5 since it causes a more
immediate issue in the REPL:

```pycon
>>> from decimal import Decimal
>>> Decimal(0.5) == Decimal("0.5")
True
>>> Decimal(0.1) == Decimal("0.1")
False
```

Test plan
--

N/a

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-10 16:09:08 -04:00
Brent Westbrook
6cd0669475 [pylint] De-emphasize __hash__ = Parent.__hash__ (PLW1641) (#18613)
Summary
--

This PR updates the docs for PLW1641 to place less emphasis on the
example of inheriting a parent class's `__hash__` implementation by both
reducing the length of the example and warning that it may be unsound in
general, as @AlexWaygood pointed out on Notion.

Test plan
--

Existing tests

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-10 18:21:34 +00:00
Brent Westbrook
6051a118d1 [flake8-pyi] Avoid syntax error in the case of starred and keyword arguments (PYI059) (#18611)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/18602 by:
1. Avoiding a fix when `*args` are present
2. Inserting the `Generic` base class right before the first keyword
argument, if one is present

In an intermediate commit, I also had special handling to avoid a fix in
the `**kwargs` case, but this is treated (roughly) as a normal keyword,
and I believe handling it properly falls out of the other keyword fix.

I also updated the `add_argument` utility function to insert new
arguments right before the keyword argument list instead of at the very
end of the argument list. This changed a couple of snapshots unrelated
to `PYI059`, but there shouldn't be any functional changes to other
rules because all other calls to `add_argument` were adding a keyword
argument anyway.

## Test Plan

Existing PYI059 cases, plus new tests based on the issue

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-10 12:27:06 -04:00
Suneet Tipirneni
161446a47a [ty] Add support for global __debug__ constant (#18540)
## Summary

Closes https://github.com/astral-sh/ty/issues/577. Make global
`__debug__` a `bool` constant.

## Test Plan

Mdtest `global-constants.md` was created to check if resolved type was
`bool`.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-06-10 06:48:59 +00:00
Dylan
caf885c20a [ruff] Preserve parentheses around deque in fix for unnecessary-empty-iterable-within-deque-call (RUF037) (#18598)
Closes #18552
2025-06-09 15:38:39 -05:00
Brent Westbrook
79006dfb52 [refurb] Parenthesize lambda and ternary expressions in iter (FURB122, FURB142) (#18592)
Summary
--

Fixes #18590 by adding parentheses around lambdas and if expressions in
`for` loop iterators for FURB122 and FURB142. I also updated the docs on
the helper function to reflect the part actually being parenthesized and
the new checks.

The `lambda` case actually causes a `TypeError` at runtime, but I think
it's still worth handling to avoid causing a syntax error.

```pycon
>>> s = set()
... for x in (1,) if True else (2,):
...     s.add(-x)
... for x in lambda: 0:
...     s.discard(-x)
...
Traceback (most recent call last):
  File "<python-input-0>", line 4, in <module>
    for x in lambda: 0:
             ^^^^^^^^^
TypeError: 'function' object is not iterable
```

Test Plan
--

New test cases based on the bug report

---------

Co-authored-by: Dylan <dylwil3@gmail.com>
2025-06-09 16:07:34 -04:00
Micha Reiser
b44062b9ae [ty] Fix stale documents on Windows (#18544) 2025-06-09 16:39:11 +02:00
DetachHead
ae2150bfa3 [ty] document how the default value for python-version is determined (#18549)
Co-authored-by: detachhead <detachhead@users.noreply.github.com>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-06-09 13:32:43 +00:00
DetachHead
07cb84426d [ty] document the "all" option for python-platform (#18548)
Co-authored-by: detachhead <detachhead@users.noreply.github.com>
2025-06-09 12:01:24 +00:00
Frazer McLean
b01c95d460 ruff/__main__.py: Remove unnecessary os.fsdecode (#18551) 2025-06-09 10:34:19 +00:00
Alex Waygood
aa3c312f5f [ty] Fix panic when trying to pull types for subscript expressions inside Callable type expressions (#18534) 2025-06-09 11:26:10 +01:00
renovate[bot]
475a02b725 Update pre-commit dependencies (#18581) 2025-06-09 08:08:17 +02:00
renovate[bot]
b4b53183b7 Update actions/checkout digest to 09d2aca (#18576) 2025-06-09 08:08:02 +02:00
renovate[bot]
5fe6fa74a0 Update rui314/setup-mold digest to b395809 (#18577) 2025-06-09 07:06:19 +02:00
renovate[bot]
ea64c01524 Update cargo-bins/cargo-binstall action to v1.12.7 (#18578) 2025-06-09 07:06:02 +02:00
renovate[bot]
3fa5a9ff3b Update dependency pyodide to v0.27.7 (#18579) 2025-06-09 07:05:16 +02:00
renovate[bot]
b5a77df46f Update Rust crate smallvec to v1.15.1 (#18586) 2025-06-09 07:04:29 +02:00
renovate[bot]
8d1d0be648 Update Rust crate hashbrown to v0.15.4 (#18585) 2025-06-09 07:03:58 +02:00
renovate[bot]
1cf7b67e85 Update Rust crate anstream to v0.6.19 (#18582) 2025-06-09 07:03:19 +02:00
renovate[bot]
c18dc41f1a Update Rust crate camino to v1.1.10 (#18584) 2025-06-09 02:28:52 +01:00
renovate[bot]
6cefbb6b38 Update dependency ruff to v0.11.13 (#18580) 2025-06-09 02:23:11 +01:00
Charlie Marsh
0232e422b2 Add CONDA_PREFIX to --python documentation (#18574)
## Summary

Noticed this while working on https://github.com/astral-sh/ty/pull/612.
2025-06-08 20:20:35 -04:00
Charlie Marsh
331821244b Refactor fix in readlines-in-for (#18573)
## Summary

Post-merge feedback from https://github.com/astral-sh/ruff/pull/18542.
2025-06-08 20:10:13 -04:00
Ben Bar-Or
1dc8f8f903 [ty] Add hints to invalid-type-form for common mistakes (#18543)
Co-authored-by: Ben Bar-Or <ben.baror@ridewithvia.com>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-06-09 00:40:05 +01:00
Charlie Marsh
301b9f4135 Add trailing space around readlines (#18542)
Closes https://github.com/astral-sh/ruff/issues/17683.
2025-06-08 12:00:30 -04:00
Micha Reiser
86e5a311f0 [ty] Introduce and use System::env_var for better test isolation (#18538) 2025-06-07 19:56:58 +02:00
Micha Reiser
0c20010bb9 [ty] Split CLI tests into multiple files (#18537) 2025-06-07 16:43:28 +00:00
Alex Waygood
72552f31e4 [ty] Fix panic when pulling types for UnaryOp expressions inside Literal slices (#18536) 2025-06-07 15:26:10 +00:00
Alex Waygood
95497ffaab [ty] Fix panic when trying to pull types for attribute expressions inside Literal type expressions (#18535) 2025-06-07 15:59:12 +01:00
Micha Reiser
b3b900dc1e Treat ty: comments as pragma comments (#18532)
## Summary

Add support for ty's `ty:` pragma comments to ruff's formatter and E501

Fixes https://github.com/astral-sh/ruff/issues/18529

## Test Plan

Added test
2025-06-07 16:02:43 +02:00
Alex Waygood
503427855d [ty] Enable more corpus tests (#18531) 2025-06-07 14:18:25 +01:00
Alex Waygood
6e785867c3 [ty] Unify Type::is_subtype_of() and Type::is_assignable_to() (#18430) 2025-06-06 17:28:55 +00:00
Alex Waygood
1274521f9f [ty] Track the origin of the environment.python setting for better error messages (#18483) 2025-06-06 13:36:41 +01:00
shimies
8d24760643 Fix doc for Neovim setting examples (#18491)
## Summary
This PR fixes an error in the example Neovim configuration on [this
documentation
page](https://docs.astral.sh/ruff/editors/settings/#configuration).
The `configuration` block should be nested under `settings`, consistent
with other properties and as outlined
[here](https://docs.astral.sh/ruff/editors/setup/#neovim).

I encountered this issue when copying the example to configure ruff
integration in my neovim - the config didn’t work until I corrected the
nesting.

## Test Plan
- [x] Confirmed that the corrected configuration works in a real Neovim
+ Ruff setup
- [x] Verified that the updated configuration renders correctly in
MkDocs
<img width="382" alt="image"
src="https://github.com/user-attachments/assets/0722fb35-8ffa-4b10-90ba-c6e8417e40bf"
/>
2025-06-06 15:19:16 +05:30
Carl Meyer
db8db536f8 [ty] clarify requirements for scope_id argument to in_type_expression (#18488) 2025-06-05 22:46:26 -07:00
Carl Meyer
cb8246bc5f [ty] remove unnecessary Either (#18489)
Just a quick review-comment follow-up.
2025-06-05 18:39:22 -07:00
Dylan
5faf72a4d9 Bump 0.11.13 (#18484) 2025-06-05 15:18:38 -05:00
Micha Reiser
28dbc5c51e [ty] Fix completion order in playground (#18480) 2025-06-05 18:55:54 +02:00
Brent Westbrook
ce216c79cc Remove Message::to_rule (#18447)
## Summary

As the title says, this PR removes the `Message::to_rule` method by
replacing related uses of `Rule` with `NoqaCode` (or the rule's name in
the case of the cache). Where it seemed a `Rule` was really needed, we
convert back to the `Rule` by parsing either the rule name (with
`str::parse`) or the `NoqaCode` (with `Rule::from_code`).

I thought this was kind of like cheating and that it might not resolve
this part of Micha's
[comment](https://github.com/astral-sh/ruff/pull/18391#issuecomment-2933764275):

> because we can't add Rule to Diagnostic or **have it anywhere in our
shared rendering logic**

but after looking again, the only remaining `Rule` conversion in
rendering code is for the SARIF output format. The other two non-test
`Rule` conversions are for caching and writing a fix summary, which I
don't think fall into the shared rendering logic. That leaves the SARIF
format as the only real problem, but maybe we can delay that for now.

The motivation here is that we won't be able to store a `Rule` on the
new `Diagnostic` type, but we should be able to store a `NoqaCode`,
likely as a string.

## Test Plan

Existing tests

##
[Benchmarks](https://codspeed.io/astral-sh/ruff/branches/brent%2Fremove-to-rule)

Almost no perf regression, only -1% on
`linter/default-rules[large/dataset.py]`.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-05 12:48:29 -04:00
Victorien
33468cc8cc [pyupgrade] Apply UP035 only on py313+ for get_type_hints() (#18476) 2025-06-05 17:16:29 +01:00
Ibraheem Ahmed
8531f4b3ca [ty] Add infrastructure for AST garbage collection (#18445)
## Summary

https://github.com/astral-sh/ty/issues/214 will require a couple
invasive changes that I would like to get merged even before garbage
collection is fully implemented (to avoid rebasing):
- `ParsedModule` can no longer be dereferenced directly. Instead you
need to load a `ParsedModuleRef` to access the AST, which requires a
reference to the salsa database (as it may require re-parsing the AST if
it was collected).
- `AstNodeRef` can only be dereferenced with the `node` method, which
takes a reference to the `ParsedModuleRef`. This allows us to encode the
fact that ASTs do not live as long as the database and may be collected
as soon a given instance of a `ParsedModuleRef` is dropped. There are a
number of places where we currently merge the `'db` and `'ast`
lifetimes, so this requires giving some types/functions two separate
lifetime parameters.
2025-06-05 11:43:18 -04:00
Andrew Gallant
55100209c7 [ty] IDE: add support for object.<CURSOR> completions (#18468)
This PR adds logic for detecting `Name Dot [Name]` token patterns,
finding the corresponding `ExprAttribute`, getting the type of the
object and returning the members available on that object.

Here's a video demonstrating this working:

https://github.com/user-attachments/assets/42ce78e8-5930-4211-a18a-fa2a0434d0eb

Ref astral-sh/ty#86
2025-06-05 11:15:19 -04:00
chiri
c0bb83b882 [perflint] fix missing parentheses for lambda and ternary conditions (PERF401, PERF403) (#18412)
Closes #18405
2025-06-05 09:57:08 -05:00
Brent Westbrook
74a4e9af3d Combine lint and syntax error handling (#18471)
## Summary

This is a spin-off from
https://github.com/astral-sh/ruff/pull/18447#discussion_r2125844669 to
avoid using `Message::noqa_code` to differentiate between lints and
syntax errors. I went through all of the calls on `main` and on the
branch from #18447, and the instance in `ruff_server` noted in the
linked comment was actually the primary place where this was being done.
Other calls to `noqa_code` are typically some variation of
`message.noqa_code().map_or(String::new, format!(...))`, with the major
exception of the gitlab output format:


a120610b5b/crates/ruff_linter/src/message/gitlab.rs (L93-L105)

which obviously assumes that `None` means syntax error. A simple fix
here would be to use `message.name()` for `check_name` instead of the
noqa code, but I'm not sure how breaking that would be. This could just
be:

```rust
 let description = message.body();
 let description = description.strip_prefix("SyntaxError: ").unwrap_or(description).to_string();
 let check_name = message.name();
```

In that case. This sounds reasonable based on the [Code Quality report
format](https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format)
docs:

> | Name | Type | Description|
> |-----|-----|----|
> |`check_name` | String | A unique name representing the check, or
rule, associated with this violation. |

## Test Plan

Existing tests
2025-06-05 12:50:02 +00:00
Alex Waygood
8485dbb324 [ty] Fix --python argument for Windows, and improve error messages for bad --python arguments (#18457)
## Summary

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

On Windows, system installations have different layouts to virtual
environments. In Windows virtual environments, the Python executable is
found at `<sys.prefix>/Scripts/python.exe`. But in Windows system
installations, the Python executable is found at
`<sys.prefix>/python.exe`. That means that Windows users were able to
point to Python executables inside virtual environments with the
`--python` flag, but they weren't able to point to Python executables
inside system installations.

This PR fixes that issue. It also makes a couple of other changes:
- Nearly all `sys.prefix` resolution is moved inside `site_packages.rs`.
That was the original design of the `site-packages` resolution logic,
but features implemented since the initial implementation have added
some resolution and validation to `resolver.rs` inside the module
resolver. That means that we've ended up with a somewhat confusing code
structure and a situation where several checks are unnecessarily
duplicated between the two modules.
- I noticed that we had quite bad error messages if you e.g. pointed to
a path that didn't exist on disk with `--python` (we just gave a
somewhat impenetrable message saying that we "failed to canonicalize"
the path). I improved the error messages here and added CLI tests for
`--python` and the `environment.python` configuration setting.

## Test Plan

- Existing tests pass
- Added new CLI tests
- I manually checked that virtual-environment discovery still works if
no configuration is given
- Micha did some manual testing to check that pointing `--python` to a
system-installation executable now works on Windows
2025-06-05 08:19:15 +01:00
Shunsuke Shibayama
0858896bc4 [ty] type narrowing by attribute/subscript assignments (#18041)
## Summary

This PR partially solves https://github.com/astral-sh/ty/issues/164
(derived from #17643).

Currently, the definitions we manage are limited to those for simple
name (symbol) targets, but we expand this to track definitions for
attribute and subscript targets as well.

This was originally planned as part of the work in #17643, but the
changes are significant, so I made it a separate PR.
After merging this PR, I will reflect this changes in #17643.

There is still some incomplete work remaining, but the basic features
have been implemented, so I am publishing it as a draft PR.
Here is the TODO list (there may be more to come):
* [x] Complete rewrite and refactoring of documentation (removing
`Symbol` and replacing it with `Place`)
* [x] More thorough testing
* [x] Consolidation of duplicated code (maybe we can consolidate the
handling related to name, attribute, and subscript)

This PR replaces the current `Symbol` API with the `Place` API, which is
a concept that includes attributes and subscripts (the term is borrowed
from Rust).

## Test Plan

`mdtest/narrow/assignment.md` is added.

---------

Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-06-04 17:24:27 -07:00
Alex Waygood
ce8b744f17 [ty] Only calculate information for unresolved-reference subdiagnostic if we know we'll emit the diagnostic (#18465)
## Summary

This optimizes some of the logic added in
https://github.com/astral-sh/ruff/pull/18444. In general, we only
calculate information for subdiagnostics if we know we'll actually emit
the diagnostic. The check to see whether we'll emit the diagnostic is
work we'll definitely have to do whereas the the work to gather
information for a subdiagnostic isn't work we necessarily have to do if
the diagnostic isn't going to be emitted at all.

This PR makes us lazier about gathering the information we need for the
subdiagnostic, and moves all the subdiagnostic logic into one function
rather than having some `unresolved-reference` subdiagnostic logic in
`infer.rs` and some in `diagnostic.rs`.

## Test Plan

`cargo test -p ty_python_semantic`
2025-06-04 20:41:00 +01:00
Alex Waygood
5a8cdab771 [ty] Only consider a type T a subtype of a protocol P if all of P's members are fully bound on T (#18466)
## Summary

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

## Test Plan

mdtests
2025-06-04 19:39:14 +00:00
Alex Waygood
3a8191529c [ty] Exclude members starting with _abc_ from a protocol interface (#18467)
## Summary

As well as excluding a hardcoded set of special attributes, CPython at
runtime also excludes any attributes or declarations starting with
`_abc_` from the set of members that make up a protocol interface. I
missed this in my initial implementation.

This is a bit of a CPython implementation detail, but I do think it's
important that we try to model the runtime as best we can here. The
closer we are to the runtime behaviour, the closer we come to sound
behaviour when narrowing types from `isinstance()` checks against
runtime-checkable protocols (for example)

## Test Plan

Extended an existing mdtest
2025-06-04 20:34:09 +01:00
lipefree
e658778ced [ty] Add subdiagnostic suggestion to unresolved-reference diagnostic when variable exists on self (#18444)
## Summary

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

In the following example:
```py
class Foo:
    x: int

    def method(self):
        y = x
```
The user may intended to use `y = self.x` in `method`. 

This is now added as a subdiagnostic in the following form : 

`info: An attribute with the same name as 'x' is defined, consider using
'self.x'`

## Test Plan

Added mdtest with snapshot diagnostics.
2025-06-04 08:13:50 -07:00
David Peter
f1883d71a4 [ty] IDE: only provide declarations and bindings as completions (#18456)
## Summary

Previously, all symbols where provided as possible completions. In an
example like the following, both `foo` and `f` were suggested as
completions, because `f` itself is a symbol.
```py
foo = 1

f<CURSOR>
```
Similarly, in the following example, `hidden_symbol` was suggested, even
though it is not statically visible:
```py
if 1 + 2 != 3:
    hidden_symbol = 1

hidden_<CURSOR>
```

With the change suggested here, we only use statically visible
declarations and bindings as a source for completions.


## Test Plan

- Updated snapshot tests
- New test for statically hidden definitions
- Added test for star import
2025-06-04 16:11:05 +02:00
David Peter
11db567b0b [ty] ty_ide: Hotfix for expression_scope_id panics (#18455)
## Summary

Implement a hotfix for the playground/LSP crashes related to missing
`expression_scope_id`s.

relates to: https://github.com/astral-sh/ty/issues/572

## Test Plan

* Regression tests from https://github.com/astral-sh/ruff/pull/18441
* Ran the playground locally to check if panics occur / completions
still work.

---------

Co-authored-by: Andrew Gallant <andrew@astral.sh>
2025-06-04 10:39:16 +02:00
David Peter
9f8c3de462 [ty] Improve docs for Class{Literal,Type}::instance_member (#18454)
## Summary

Mostly just refer to `Type::instance_member` which has much more
details.
2025-06-04 09:55:45 +02:00
David Peter
293d4ac388 [ty] Add meta-type tests for legavy TypeVars (#18453)
## Summary

Follow up to the comment by @dcreager
[here](https://github.com/astral-sh/ruff/pull/18439#discussion_r2123802784).
2025-06-04 07:44:44 +00:00
Carl Meyer
9e8a7e9353 update to salsa that doesn't panic silently on cycles (#18450) 2025-06-04 07:40:16 +02:00
Dhruv Manilawala
453e5f5934 [ty] Add tests for empty list/tuple unpacking (#18451)
## Summary

This PR is to address this comment:
https://github.com/astral-sh/ruff/pull/18438#issuecomment-2935344415

## Test Plan

Run mdtest
2025-06-04 02:40:26 +00:00
Dhruv Manilawala
7ea773daf2 [ty] Argument type expansion for overload call evaluation (#18382)
## Summary

Part of astral-sh/ty#104, closes: astral-sh/ty#468

This PR implements the argument type expansion which is step 3 of the
overload call evaluation algorithm.

Specifically, this step needs to be taken if type checking resolves to
no matching overload and there are argument types that can be expanded.

## Test Plan

Add new test cases.

## Ecosystem analysis

This PR removes 174 `no-matching-overload` false positives -- I looked
at a lot of them and they all are false positives.

One thing that I'm not able to understand is that in
2b7e3adf27/sphinx/ext/autodoc/preserve_defaults.py (L179)
the inferred type of `value` is `str | None` by ty and Pyright, which is
correct, but it's only ty that raises `invalid-argument-type` error
while Pyright doesn't. The constructor method of `DefaultValue` has
declared type of `str` which is invalid.

There are few cases of false positives resulting due to the fact that ty
doesn't implement narrowing on attribute expressions.
2025-06-04 02:12:00 +00:00
Alex Waygood
0079cc6817 [ty] Minor cleanup for site-packages discovery logic (#18446) 2025-06-03 18:49:14 +00:00
Matthew Mckee
e8ea40012a [ty] Add generic inference for dataclasses (#18443)
## Summary

An issue seen here https://github.com/astral-sh/ty/issues/500

The `__init__` method of dataclasses had no inherited generic context,
so we could not infer the type of an instance from a constructor call
with generics

## Test Plan

Add tests to classes.md` in generics folder
2025-06-03 09:59:43 -07:00
Abhijeet Prasad Bodas
71d8a5da2a [ty] dataclasses: Allow using dataclasses.dataclass as a function. (#18440)
## Summary

Part of https://github.com/astral-sh/ty/issues/111

Using `dataclass` as a function, instead of as a decorator did not work
as expected prior to this.
Fix that by modifying the dataclass overload's return type.

## Test Plan

New mdtests, fixing the existing TODO.
2025-06-03 09:50:29 -07:00
Douglas Creager
2c3b3d3230 [ty] Create separate FunctionLiteral and FunctionType types (#18360)
This updates our representation of functions to more closely match our
representation of classes.

The new `OverloadLiteral` and `FunctionLiteral` classes represent a
function definition in the AST. If a function is generic, this is
unspecialized. `FunctionType` has been updated to represent a function
type, which is specialized if the function is generic. (These names are
chosen to match `ClassLiteral` and `ClassType` on the class side.)

This PR does not add a separate `Type` variant for `FunctionLiteral`.
Maybe we should? Possibly as a follow-on PR?

Part of https://github.com/astral-sh/ty/issues/462

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-03 10:59:31 -04:00
Dhruv Manilawala
8d98c601d8 [ty] Infer list[T] when unpacking non-tuple type (#18438)
## Summary

Follow-up from #18401, I was looking at whether that would fix the issue
at https://github.com/astral-sh/ty/issues/247#issuecomment-2917656676
and it didn't, which made me realize that the PR only inferred `list[T]`
when the value type was tuple but it could be other types as well.

This PR fixes the actual issue by inferring `list[T]` for the non-tuple
type case.

## Test Plan

Add test cases for starred expression involved with non-tuple type. I
also added a few test cases for list type and list literal.

I also verified that the example in the linked issue comment works:
```py
def _(line: str):
    a, b, *c = line.split(maxsplit=2)
    c.pop()
```
2025-06-03 19:17:47 +05:30
David Peter
0986edf427 [ty] Meta-type of type variables should be type[..] (#18439)
## Summary

Came across this while debugging some ecosystem changes in
https://github.com/astral-sh/ruff/pull/18347. I think the meta-type of a
typevar-annotated variable should be equal to `type`, not `<class
'object'>`.

## Test Plan

New Markdown tests.
2025-06-03 15:22:00 +02:00
chiri
03f1f8e218 [pyupgrade] Make fix unsafe if it deletes comments (UP050) (#18390)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary
/closes #18387
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
update snapshots
<!-- How was it tested? -->
2025-06-03 09:10:15 -04:00
chiri
628bb2cd1d [pyupgrade] Make fix unsafe if it deletes comments (UP004) (#18393)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary
https://github.com/astral-sh/ruff/issues/18387#issuecomment-2923039331
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
update snapshots
<!-- How was it tested? -->
2025-06-03 09:09:33 -04:00
lipefree
f23d2c9b9e [ty] Support using legacy typing aliases for generic classes in type annotations (#18404)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-06-03 12:09:51 +01:00
Micha Reiser
67d94d9ec8 Use ty's completions in playground (#18425) 2025-06-03 10:11:39 +02:00
otakutyrant
d1cb8e2142 Update editor setup docs about Neovim and Vim (#18324)
## Summary

I struggled to make ruff_organize_imports work and then I found out I
missed the key note about conform.nvim before because it was put in the
Vim section wrongly! So I refined them both.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-06-03 07:40:22 +00:00
renovate[bot]
57202c1c77 Update NPM Development dependencies (#18423) 2025-06-03 08:06:56 +02:00
Dhruv Manilawala
2289187b74 Infer list[T] for starred target in unpacking (#18401)
## Summary

Closes: astral-sh/ty#191

## Test Plan

Update existing tests.
2025-06-03 07:25:07 +05:30
Robsdedude
14c42a8ddf [refurb] Mark FURB180 fix unsafe when class has bases (#18149)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Mark `FURB180`'s fix as unsafe if the class already has base classes.
This is because the base classes might validate the other base classes
(like `typing.Protocol` does) or otherwise alter runtime behavior if
more base classes are added.

## Test Plan

The existing snapshot test covers this case already.

## References

Partially addresses https://github.com/astral-sh/ruff/issues/13307 (left
out way to permit certain exceptions)

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-03 00:51:09 +00:00
Denys Kyslytsyn
e677863787 [fastapi] Avoid false positive for class dependencies (FAST003) (#18271)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Closes #17226.

This PR updates the `FAST003` rule to correctly handle [FastAPI class
dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/).
Specifically, if a path parameter is declared in either:

- a `pydantic.BaseModel` used as a dependency, or  
- the `__init__` method of a class used as a dependency,  

then `FAST003` will no longer incorrectly report it as unused.

FastAPI allows a shortcut when using annotated class dependencies -
`Depends` can be called without arguments, e.g.:

```python
class MyParams(BaseModel):
    my_id: int

@router.get("/{my_id}")
def get_id(params: Annotated[MyParams, Depends()]): ...
```
This PR ensures that such usage is properly supported by the linter.

Note: Support for dataclasses is not included in this PR. Let me know if
you’d like it to be added.

## Test Plan

Added relevant test cases to the `FAST003.py` fixture.
2025-06-02 14:34:50 -04:00
lipefree
f379eb6e62 [ty] Treat lambda functions as instances of types.FunctionType (#18431) 2025-06-02 16:46:26 +01:00
Alex Waygood
47698883ae [ty] Fix false positives for legacy ParamSpecs inside Callable type expressions (#18426) 2025-06-02 14:10:00 +01:00
Alex Waygood
e2d96df501 [ty] Improve diagnostics if the user attempts to import a stdlib module that does not exist on their configured Python version (#18403) 2025-06-02 10:52:26 +00:00
renovate[bot]
384e80ec80 Update taiki-e/install-action action to v2.52.4 (#18420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 09:03:32 +02:00
renovate[bot]
b9f3b0e0a6 Update docker/build-push-action action to v6.18.0 (#18422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 09:03:09 +02:00
Micha Reiser
1e6d76c878 [ty] Fix server hang after shutdown request (#18414) 2025-06-02 06:57:51 +00:00
renovate[bot]
844c8626c3 Update Rust crate libcst to v1.8.0 (#18424) 2025-06-02 07:40:18 +02:00
renovate[bot]
1c8d9d707e Update Rust crate clap to v4.5.39 (#18419) 2025-06-02 07:39:27 +02:00
renovate[bot]
4856377478 Update cargo-bins/cargo-binstall action to v1.12.6 (#18416) 2025-06-02 07:38:57 +02:00
renovate[bot]
643c845a47 Update dependency mdformat-mkdocs to v4.3.0 (#18421) 2025-06-02 07:38:36 +02:00
renovate[bot]
9e952cf0e0 Update pre-commit dependencies (#18418) 2025-06-02 07:38:10 +02:00
renovate[bot]
c4015edf48 Update dependency ruff to v0.11.12 (#18417) 2025-06-02 07:37:56 +02:00
Matthew Mckee
97b824db3e [ty] Ensure Literal types are considered assignable to anything their Instance supertypes are assignable to (#18351) 2025-06-01 16:39:56 +01:00
Micha Reiser
220ab88779 [ty] Promote projects to good that now no longer hang (#18370) 2025-06-01 17:25:46 +02:00
github-actions[bot]
7a63ac145a Sync vendored typeshed stubs (#18407)
Co-authored-by: typeshedbot <>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-06-01 15:21:18 +01:00
Micha Reiser
54f597658c [ty] Fix multithreading related hangs and panics (#18238) 2025-06-01 11:07:55 +02:00
Ibraheem Ahmed
aa1fad61e0 Support relative --ty-path in ty-benchmark (#18385)
## Summary

This currently doesn't work because the benchmark changes the working
directory. Also updates the process name to make it easier to compare
two local ty binaries.
2025-05-30 18:19:20 -04:00
Alex Waygood
b390b3cb8e [ty] Update docs for Python version inference (#18397) 2025-05-30 22:45:28 +01:00
Zanie Blue
88866f0048 [ty] Infer the Python version from the environment if feasible (#18057)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-05-30 21:22:51 +00:00
Dylan
9bbf4987e8 Implement template strings (#17851)
This PR implements template strings (t-strings) in the parser and
formatter for Ruff.

Minimal changes necessary to compile were made in other parts of the code (e.g. ty, the linter, etc.). These will be covered properly in follow-up PRs.
2025-05-30 15:00:56 -05:00
Carl Meyer
ad024f9a09 [ty] support callability of bound/constrained typevars (#18389)
## Summary

Allow a typevar to be callable if it is bound to a callable type, or
constrained to callable types.

I spent some time digging into why this support didn't fall out
naturally, and ultimately the reason is that we look up `__call__` on
the meta type (since its a dunder), and our implementation of
`Type::to_meta_type` for `Type::Callable` does not return a type with
`__call__`.

A more general solution here would be to have `Type::to_meta_type` for
`Type::Callable` synthesize a protocol with `__call__` and return an
intersection with that protocol (since for a type to be callable, we
know its meta-type must have `__call__`). That solution could in
principle also replace the special-case handling of `Type::Callable`
itself, here in `Type::bindings`. But that more general approach would
also be slower, and our protocol support isn't quite ready for that yet,
and handling this directly in `Type::bindings` is really not bad.

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

## Test Plan

Added mdtests.
2025-05-30 12:01:51 -07:00
Andrew Gallant
fc549bda94 [ty] Minor tweaks to "list all members" docs and tests (#18388)
Ref https://github.com/astral-sh/ruff/pull/18251#pullrequestreview-2881810681
2025-05-30 13:36:57 -04:00
Alex Waygood
77c8ddf101 [ty] Fix broken property tests for disjointness (#18384) 2025-05-30 16:49:20 +01:00
David Peter
e730f27f80 [ty] List available members for a given type (#18251)
This PR adds initial support for listing all attributes of
an object. It is exposed through a new `all_members`
routine in `ty_extensions`, which is in turn used to test
the functionality.

The purpose of listing all members is for code
completion. That is, given a `object.<CURSOR>`, we
would like to list all available attributes on
`object`.
2025-05-30 11:24:20 -04:00
Wei Lee
d65bd69963 [airflow] Add unsafe fix for module moved cases (AIR312) (#18363)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Follow up on https://github.com/astral-sh/ruff/pull/18093 and apply it
to AIR312

## Test Plan

<!-- How was it tested? -->

The existing test fixtures have been updated
2025-05-30 09:36:20 -04:00
Brent Westbrook
c713e76e4d Add a SourceFile to OldDiagnostic (#18356)
Summary
--

This is the last main difference between the `OldDiagnostic` and
`Message`
types, so attaching a `SourceFile` to `OldDiagnostic` should make
combining the
two types almost trivial.

Initially I updated the remaining rules without access to a `Checker` to
take a
`&SourceFile` directly, but after Micha's suggestion in
https://github.com/astral-sh/ruff/pull/18356#discussion_r2113281552, I
updated all of these calls to take a
`LintContext` instead. This new type is a thin wrapper around a
`RefCell<Vec<OldDiagnostic>>`
and a `SourceFile` and now has the `report_diagnostic` method returning
a `DiagnosticGuard` instead of `Checker`.
This allows the same `Drop`-based implementation to be used in cases
without a `Checker` and also avoids a lot of intermediate allocations of
`Vec<OldDiagnostic>`s.

`Checker` now also contains a `LintContext`, which it defers to for its
`report_diagnostic` methods, which I preserved for convenience.

Test Plan
--

Existing tests
2025-05-30 13:34:38 +00:00
Micha Reiser
8005ebb405 Update salsa past generational id change (#18362) 2025-05-30 15:31:33 +02:00
Wei Lee
0c29e258c6 [airflow] Add unsafe fix for module moved cases (AIR311) (#18366)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Follow up on https://github.com/astral-sh/ruff/pull/18093 and apply it
to AIR311

---

Rules fixed
* `airflow.models.datasets.expand_alias_to_datasets` →
`airflow.models.asset.expand_alias_to_assets`
* `airflow.models.baseoperatorlink.BaseOperatorLink` →
`airflow.sdk.BaseOperatorLink`


## Test Plan

<!-- How was it tested? -->
The existing test fixtures have been updated
2025-05-30 09:27:14 -04:00
Wei Lee
b5b6b657cc [airflow] Add unsafe fix for module moved cases (AIR301) (#18367)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Follow up on https://github.com/astral-sh/ruff/pull/18093 and apply it
to AIR301

## Test Plan

<!-- How was it tested? -->

The existing test fixtures have been updated
2025-05-30 08:46:39 -04:00
Alex Waygood
ad2f667ee4 [ty] Improve tests for site-packages discovery (#18374)
## Summary

- Convert tests demonstrating our resilience to malformed/absent
`version` fields in `pyvenf.cfg` files to mdtests. Also make them more
expansive.
- Convert the regression test I added in
https://github.com/astral-sh/ruff/pull/18157 to an mdtest
- Add comments next to unit tests that cannot be converted to mdtests
(but where it's not obvious why they can't) so I don't have to do this
exercise again 😄
- In `site_packages.rs`, factor out the logic for figuring out where we
expect the system-installation `site-packages` to be. Currently we have
the same logic twice.

## Test Plan

`cargo test -p ty_python_semantic`
2025-05-30 07:32:21 +01:00
Carl Meyer
363f061f09 [ty] _typeshed.Self is not a special form (#18377)
## Summary

This change was based on a mis-reading of a comment in typeshed, and a
wrong assumption about what was causing a test failure in a prior PR.
Reverting it doesn't cause any tests to fail.

## Test Plan

Existing tests.
2025-05-29 17:11:13 -07:00
InSync
9b0dfc505f [ty] Callable types are disjoint from non-callable @final nominal instance types (#18368)
## Summary

Resolves [#513](https://github.com/astral-sh/ty/issues/513).

Callable types are now considered to be disjoint from nominal instance
types where:

* The class is `@final`, and
* Its `__call__` either does not exist or is not assignable to `(...) ->
Unknown`.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-29 23:27:27 +00:00
lipefree
695de4f27f [ty] Add diagnosis for function with no return statement but with return type annotation (#18359)
## Summary

Partially implement https://github.com/astral-sh/ty/issues/538, 
```py
from pathlib import Path

def setup_test_project(registry_name: str, registry_url: str, project_dir: str) -> Path:
    pyproject_file = Path(project_dir) / "pyproject.toml"
    pyproject_file.write_text("...", encoding="utf-8")
```
As no return statement is defined in the function `setup_test_project`
with annotated return type `Path`, we provide the following diagnosis :

- error[invalid-return-type]: Function **always** implicitly returns
`None`, which is not assignable to return type `Path`

with a subdiagnostic : 
- note: Consider changing your return annotation to `-> None` or adding a `return` statement
 
## Test Plan

mdtests with snapshots to capture the subdiagnostic. I have to mention
that existing snapshots were modified since they now fall in this
category.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-29 23:17:18 +00:00
Wei Lee
3445d1322d [airflow] Add unsafe fix module moved cases (AIR302) (#18093)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Add utility functions `generate_import_edit` and
`generate_remove_and_runtime_import_edit` to generate the fix needed for
the airflow rules.

1. `generate_import_edit` is for the cases where the member name has
changed. (e.g., `airflow.datasts.Dataset` to `airflow.sdk.Asset`) It's
just extracted from the original logic
2. `generate_remove_and_runtime_import_edit` is for cases where the
member name has not changed. (e.g.,
`airflow.operators.pig_operator.PigOperator` to
`airflow.providers.apache.pig.hooks.pig.PigCliHook`) This is newly
introduced. As it introduced runtime import, I mark it as an unsafe fix.
Under the hook, it tried to find the original import statement, remove
it, and add a new import fix

---

* rules fix
* `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` →
`airflow.providers.standard.sensors.external_task.ExternalDagLink`

## Test Plan

<!-- How was it tested? -->
The existing test fixtures have been updated
2025-05-29 16:30:40 -04:00
Brent Westbrook
2c3f091e0e Rename ruff_linter::Diagnostic to OldDiagnostic (#18355)
Summary
--

It's a bit late in the refactoring process, but I think there are still
a couple of PRs left before getting rid of this type entirely, so I
thought it would still be worth doing.

This PR is just a quick rename with no other changes.

Test Plan
--

Existing tests
2025-05-29 15:04:31 -04:00
Marcus Näslund
9d3cad95bc [refurb] Add coverage of set and frozenset calls (FURB171) (#18035)
## Summary

Adds coverage of using set(...) in addition to `{...} in
SingleItemMembershipTest.

Fixes #15792
(and replaces the old PR #15793)

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

Updated unit test and snapshot.

Steps to reproduce are in the issue linked above.

<!-- How was it tested? -->
2025-05-29 14:59:49 -04:00
Alex Waygood
7df79cfb70 Add offset method to ruff_python_trivia::Cursor (#18371) 2025-05-29 16:08:15 +01:00
Andrew Gallant
33ed502edb ty_ide: improve completions by using scopes
Previously, completions were based on just returning every identifier
parsed in the current Python file. In this commit, we change it to
identify an expression under the cursor and then return all symbols
available to the scope containing that expression.

This is still returning too much, and also, in some cases, not enough.
Namely, it doesn't really take the specific context into account other
than scope. But this does improve on the status quo. For example:

    def foo(): ...
    def bar():
        def fast(): ...
    def foofoo(): ...

    f<CURSOR>

When asking for completions here, the LSP will no longer include `fast`
as a possible completion in this context.

Ref https://github.com/astral-sh/ty/issues/86
2025-05-29 10:31:30 -04:00
Andrew Gallant
a827b16ebd ruff_python_parser: add Tokens::before method
This is analogous to the existing `Tokens::after` method. Its
implementation is almost identical.

We plan to use this for looking at the tokens immediately before the
cursor when fetching completions.
2025-05-29 10:31:30 -04:00
Alex Waygood
47a2ec002e [ty] Split Type::KnownInstance into two type variants (#18350) 2025-05-29 14:47:55 +01:00
Brent Westbrook
aee3af0f7a Bump 0.11.12 (#18369) 2025-05-29 09:17:12 -04:00
Victor Hugo Gomes
04dc48e17c [refurb] Fix FURB129 autofix generating invalid syntax (#18235)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Fixes #18231

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

Snapshot tests
<!-- How was it tested? -->
2025-05-28 17:01:03 -04:00
vjurczenia
27743efa1b [pylint] Implement missing-maxsplit-arg (PLC0207) (#17454)
## Summary

Implements  `use-maxsplit-arg` (`PLC0207`)

https://pylint.readthedocs.io/en/latest/user_guide/messages/convention/use-maxsplit-arg.html
> Emitted when accessing only the first or last element of str.split().
The first and last element can be accessed by using str.split(sep,
maxsplit=1)[0] or str.rsplit(sep, maxsplit=1)[-1] instead.

This is part of https://github.com/astral-sh/ruff/issues/970

## Test Plan

`cargo test`

Additionally compared Ruff output to Pylint:
```
pylint --disable=all --enable=use-maxsplit-arg crates/ruff_linter/resources/test/fixtures/pylint/missing_maxsplit_arg.py

cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pylint/missing_maxsplit_arg.py --no-cache --select PLC0207
```

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-05-28 20:46:30 +00:00
Matthew Mckee
c60b4d7f30 [ty] Add subtyping between Callable types and class literals with __init__ (#17638)
## Summary

Allow classes with `__init__` to be subtypes of `Callable`

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

## Test Plan

Update is_subtype_of.md

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-28 13:43:07 -07:00
Hans
16621fa19d [flake8-bugbear ] Add fix safety section (B006) (#17652)
## Summary

This PR add the `fix safety` section for rule `B006` in
`mutable_argument_default.rs` for #15584

When applying this rule for fixes, certain changes may alter the
original logical behavior. For example:

before:
```python
def cache(x, storage=[]):
    storage.append(x)
    return storage

print(cache(1))  # [1]
print(cache(2))  # [1, 2]
```

after:
```python
def cache(x, storage=[]):
    storage.append(x)
    return storage

print(cache(1))  # [1]
print(cache(2))  # [2]
```
2025-05-28 16:27:13 -04:00
Victor Hugo Gomes
e23d4ea027 [flake8-bugbear] Ignore __debug__ attribute in B010 (#18357)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Fixes #18353
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
Snapshot tests
2025-05-28 16:24:52 -04:00
Douglas Creager
452f992fbc [ty] Simplify signature types, use them in CallableType (#18344)
There were many fields in `Signature` and friends that really had more
to do with how a signature was being _used_ — how it was looked up,
details about an individual call site, etc. Those fields more properly
belong in `Bindings` and friends.

This is a pure refactoring, and should not affect any tests or ecosystem
projects.

I started on this journey in support of
https://github.com/astral-sh/ty/issues/462. It seemed worth pulling out
as a separate PR.

One major concrete benefit of this refactoring is that we can now use
`CallableSignature` directly in `CallableType`. (We can't use
`CallableSignature` directly in that `Type` variant because signatures
are not currently interned.)
2025-05-28 13:11:45 -04:00
Alex Waygood
a5ebb3f3a2 [ty] Support ephemeral uv virtual environments (#18335) 2025-05-28 14:54:59 +00:00
Brent Westbrook
9925910a29 Add a ViolationMetadata::rule method (#18234)
Summary
--

This PR adds a macro-generated method to retrieve the `Rule` associated
with a given `Violation` struct, which makes it substantially cheaper
than parsing from the rule name. The rule is then converted to a
`NoqaCode` for storage on the `Message` (and eventually on the new
diagnostic type). The `ViolationMetadata::rule_name` method was now
unused, so the `rule` method replaces it.

Several types had to be moved from the `ruff_diagnostics` crate to the
`ruff_linter` crate to make this work, namely the `Violation` traits and
the old `Diagnostic` type, which had a constructor generic over a
`Violation`.

It's actually a fairly small PR, minus the hundreds of import changes.
The main changes are in these files:

-
[crates/ruff_linter/src/message/mod.rs](https://github.com/astral-sh/ruff/pull/18234/files#diff-139754ea310d75f28307008d21c771a190038bd106efe3b9267cc2d6c0fa0921)
-
[crates/ruff_diagnostics/src/lib.rs](https://github.com/astral-sh/ruff/pull/18234/files#diff-8e8ea5c586935bf21ea439f24253fcfd5955d2cb130f5377c2fa7bfee3ea3a81)
-
[crates/ruff_linter/src/diagnostic.rs](https://github.com/astral-sh/ruff/pull/18234/files#diff-1d0c9aad90d8f9446079c5be5f284150d97797158715bd9729e6f1f70246297a)
-
[crates/ruff_linter/src/lib.rs](https://github.com/astral-sh/ruff/pull/18234/files#diff-eb93ef7e78a612f5fa9145412c75cf6b1a5cefba1c2233e4a11a880a1ce1fbcc)

Test Plan
--

Existing tests
2025-05-28 09:27:09 -04:00
Brent Westbrook
a3ee6bb3b5 Return DiagnosticGuard from Checker::report_diagnostic (#18232)
Summary
--

This PR adds a `DiagnosticGuard` type to ruff that is adapted from the
`DiagnosticGuard` and `LintDiagnosticGuard` types from ty. This guard is
returned by `Checker::report_diagnostic` and derefs to a
`ruff_diagnostics::Diagnostic` (`OldDiagnostic`), allowing methods like
`OldDiagnostic::set_fix` to be called on the result. On `Drop` the
`DiagnosticGuard` pushes its contained `OldDiagnostic` to the `Checker`.

The main motivation for this is to make a following PR adding a
`SourceFile` to each diagnostic easier. For every rule where a `Checker`
is available, this will now only require modifying
`Checker::report_diagnostic` rather than all the rules.

In the few cases where we need to create a diagnostic before we know if
we actually want to emit it, there is a `DiagnosticGuard::defuse`
method, which consumes the guard without emitting the diagnostic. I was
able to restructure about half of the rules that naively called this to
avoid calling it, but a handful of rules still need it.

One of the fairly common patterns where `defuse` was needed initially
was something like

```rust
let diagnostic = Diagnostic::new(DiagnosticKind, range);

if !checker.enabled(diagnostic.rule()) {
    return;
}
```

So I also added a `Checker::checked_report_diagnostic` method that
handles this check internally. That helped to avoid some additional
`defuse` calls. The name is a bit repetitive, so I'm definitely open to
suggestions there. I included a warning against using it in the docs
since, as we've seen, the conversion from a diagnostic to a rule is
actually pretty expensive.

Test Plan
--

Existing tests
2025-05-28 07:41:31 -04:00
Viktor Merkurev
b60ba75d09 [flake8_use_pathlib]: Replace os.symlink with Path.symlink_to (PTH211) (#18337)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-28 12:39:05 +02:00
Micha Reiser
66ba1d8775 [ty] Support cancellation and retry in the server (#18273) 2025-05-28 10:59:29 +02:00
David Peter
bbcd7e0196 [ty] Synthetic function-like callables (#18242)
## Summary

We create `Callable` types for synthesized functions like the `__init__`
method of a dataclass. These generated functions are real functions
though, with descriptor-like behavior. That is, they can bind `self`
when accessed on an instance. This was modeled incorrectly so far.

## Test Plan

Updated tests
2025-05-28 10:00:56 +02:00
Dhruv Manilawala
48c425c15b [ty] Support publishing diagnostics in the server (#18309)
## Summary

This PR adds support for [publishing
diagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics)
from the ty language server.

It only adds support for it for text documents and not notebook
documents because the server doesn't have full notebook support yet.

Closes: astral-sh/ty#79

## Test Plan

Testing this out in Helix and Zed since those are the two editors that I
know of that doesn't support pull diagnostics:

### Helix


https://github.com/user-attachments/assets/e193f804-0b32-4f7e-8b83-6f9307e3d2d4



### Zed



https://github.com/user-attachments/assets/93ec7169-ce2b-4521-b009-a82d8afb9eaa
2025-05-28 13:15:11 +05:30
Max Mynter
6d210dd0c7 Add Autofix for ISC003 (#18256)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-28 09:30:51 +02:00
chiri
9ce83c215d [pyupgrade]: new rule UP050 (useless-class-metaclass-type) (#18334)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-28 09:22:44 +02:00
हिमांशु
602dd5c039 [pycodestyle] Make E712 suggestion not assume a context (#18328) 2025-05-28 09:06:39 +02:00
Carl Meyer
3eada01153 put similar dunder-call tests next to each other (#18343)
Follow-up from post-land review on
https://github.com/astral-sh/ruff/pull/18260
2025-05-27 12:16:41 -07:00
Alex Waygood
3e811fc369 [ty] Derive PartialOrd, Ord for KnownInstanceType (#18340) 2025-05-27 19:37:01 +01:00
Alex Waygood
743764d384 [ty] Simplify Type::try_bool() (#18342)
## Summary

I don't think we're ever going to add any `KnownInstanceType` variants
that evaluate to `False` in a boolean context; the
`KnownInstanceType::bool()` method just seems like unnecessary
complexity.

## Test Plan

`cargo test -p ty_python_semantic`
2025-05-27 19:32:17 +01:00
Alex Waygood
e03e05d2b3 [ty] Simplify Type::normalized slightly (#18339) 2025-05-27 18:08:59 +00:00
Alex Waygood
9ec4a178a4 [ty] Move arviz off the list of selected primer projects (#18336) 2025-05-27 17:51:19 +01:00
justin
8d5655a7ba [ty] Add --config-file CLI arg (#18083) 2025-05-27 08:00:38 +02:00
Alex Waygood
6453ac9ea1 [ty] Tell the user why we inferred a certain Python version when reporting version-specific syntax errors (#18295) 2025-05-26 20:44:43 +00:00
Alex Waygood
0a11baf29c [ty] Implement implicit inheritance from Generic[] for PEP-695 generic classes (#18283) 2025-05-26 20:40:16 +01:00
lipefree
1d20cf9570 [ty] Add hint if async context manager is used in non-async with statement (#18299)
# Summary

Adds a subdiagnostic hint in the following scenario where a
synchronous `with` is used with an async context manager:
```py
class Manager:
    async def __aenter__(self): ...
    async def __aexit__(self, *args): ...

# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
# note: Objects of type `Manager` *can* be used as async context managers
# note: Consider using `async with` here
with Manager():
    ...
```

closes https://github.com/astral-sh/ty/issues/508

## Test Plan

New MD snapshot tests

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-05-26 21:34:47 +02:00
Micha Reiser
62ef96f51e [ty] Move respect-ignore-files under src section (#18322) 2025-05-26 18:45:48 +01:00
David Peter
4e68dd96a6 [ty] Infer types for ty_extensions.Intersection[A, B] tuple expressions (#18321)
## Summary

fixes astral-sh/ty#366

## Test Plan

* Added panic corpus regression tests
* I also wrote a hover regression test (see below), but decided not to
include it. The corpus tests are much more "effective" at finding these
types of errors, since they exhaustively check all expressions for
types.

<details>

```rs
#[test]
fn hover_regression_test_366() {
    let test = cursor_test(
        r#"
    from ty_extensions import Intersection

    class A: ...
    class B: ...

    def _(x: Intersection[A,<CURSOR> B]):
        pass
    "#,
    );

    assert_snapshot!(test.hover(), @r"
    A & B
    ---------------------------------------------
    ```text
    A & B
    ```
    ---------------------------------------------
    info[hover]: Hovered content is
     --> main.py:7:31
      |
    5 |         class B: ...
    6 |
    7 |         def _(x: Intersection[A, B]):
      |                               ^^-^
      |                               | |
      |                               | Cursor offset
      |                               source
    8 |             pass
      |
    ");
}
```

</details>
2025-05-26 17:08:52 +02:00
Maddy Guthridge
b25b642371 Improve readability of rule status icons in documentation (#18297)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-26 14:35:20 +00:00
Micha Reiser
175402aa75 [ty] Remove unnecessary lifetimes for Task (#18261) 2025-05-26 12:44:43 +00:00
Micha Reiser
d8216fa328 [ty] Gracefully handle salsa cancellations and panics in background request handlers (#18254) 2025-05-26 13:37:49 +01:00
David Peter
d51f6940fe [ty] Playground: Better default settings (#18316)
## Summary

The playground default settings set the `division-by-zero` rule severity
to `error`. This slightly confusing because `division-by-zero` is now
disabled by default. I am assuming that we have a `rules` section in
there to make it easier for users to customize those settings (in
addition to what the JSON schema gives us).

Here, I'm proposing a different default rule-set (`"undefined-reveal":
"ignore"`) that I would personally find more helpful for the playground,
since we're using it so frequently for MREs that often involve some
`reveal_type` calls.
2025-05-26 14:14:23 +02:00
Micha Reiser
66b082ff71 [ty] Abort process if worker thread panics (#18211) 2025-05-26 13:09:06 +01:00
Micha Reiser
5d93d619f3 Use git-commit as ty playground version instead of 0.0.0 (#18314) 2025-05-26 11:55:11 +00:00
David Peter
e1b662bf5d [ty] Always pass NO_INSTANCE_FALLBACK in try_call_dunder_with_policy (#18315)
## Summary

The previous `try_call_dunder_with_policy` API was a bit of a footgun
since you needed to pass `NO_INSTANCE_FALLBACK` in *addition* to other
policies that you wanted for the member lookup. Implicit calls to dunder
methods never access instance members though, so we can do this
implicitly in `try_call_dunder_with_policy`.

No functional changes.
2025-05-26 13:20:27 +02:00
Felix Scherz
f885cb8a2f [ty] use __getattribute__ to lookup unknown members on a type (#18280)
## Summary

`Type::member_lookup_with_policy` now falls back to calling
`__getattribute__` when a member cannot be found as a second fallback
after `__getattr__`.


closes https://github.com/astral-sh/ty/issues/441

## Test Plan

Added markdown tests.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: David Peter <mail@david-peter.de>
2025-05-26 12:59:45 +02:00
David Peter
4ef2c223c9 [ty] Respect MRO_NO_OBJECT_FALLBACK policy when looking up symbols on type instances (#18312)
## Summary

This should address a problem that came up while working on
https://github.com/astral-sh/ruff/pull/18280. When looking up an
attribute (typically a dunder method) with the `MRO_NO_OBJECT_FALLBACK`
policy, the attribute is first looked up on the meta type. If the meta
type happens to be `type`, we go through the following branch in
`find_name_in_mro_with_policy`:


97ff015c88/crates/ty_python_semantic/src/types.rs (L2565-L2573)

The problem is that we now look up the attribute on `object` *directly*
(instead of just having `object` in the MRO). In this case,
`MRO_NO_OBJECT_FALLBACK` has no effect in `class_member_from_mro`:


c3feb8ce27/crates/ty_python_semantic/src/types/class.rs (L1081-L1082)

So instead, we need to explicitly respect the `MRO_NO_OBJECT_FALLBACK`
policy here by returning `Symbol::Unbound`.

## Test Plan

Added new Markdown tests that explain the ecosystem changes that we
observe.
2025-05-26 12:03:29 +02:00
Vasanth
d078ecff37 [flake8_async] Refactor argument name resolution for async sleep func… (#18262)
Co-authored-by: Vasanth-96 <ramavath.naik@itilite.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-26 09:53:03 +00:00
David Peter
7eca6f96e3 [ty] Fix attribute writes to unions/intersections including modules (#18313)
## Summary

Fix a bug that involved writes to attributes on union/intersection types
that included modules as elements.

This is a prerequisite to avoid some ecosystem false positives in
https://github.com/astral-sh/ruff/pull/18312

## Test Plan

Added regression test
2025-05-26 11:41:03 +02:00
David Sherret
fbaf826a9d Only enable js feature of uuid crate for wasm crates (#18152) 2025-05-26 10:33:51 +01:00
Wei Lee
d8a5b9de17 [airflow] Revise fix title AIR3 (#18215) 2025-05-26 10:31:48 +01:00
otakutyrant
c3feb8ce27 Update editor integrations link in README (#17977)
Co-authored-by: Oscar Gustafsson <oscar.gustafsson@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-26 09:50:09 +01:00
Jo
97ff015c88 [ty] Add tests to src.root if it exists and is not a package (#18286) 2025-05-26 09:08:57 +01:00
renovate[bot]
1f7134f727 Update rui314/setup-mold digest to 67424c1 (#18300) 2025-05-26 07:43:52 +02:00
renovate[bot]
6a0b93170e Update pre-commit dependencies (#18302) 2025-05-26 07:43:31 +02:00
renovate[bot]
cc59ff8aad Update dependency ruff to v0.11.11 (#18301) 2025-05-26 07:41:54 +02:00
renovate[bot]
2b90e7fcd7 Update NPM Development dependencies (#18305) 2025-05-26 07:41:37 +02:00
renovate[bot]
a43f5b2129 Update taiki-e/install-action action to v2.52.1 (#18307) 2025-05-26 07:41:18 +02:00
renovate[bot]
f3fb7429ca Update astral-sh/setup-uv action to v6.1.0 (#18304) 2025-05-26 07:40:51 +02:00
renovate[bot]
83498b95fb Update Rust crate uuid to v1.17.0 (#18306) 2025-05-26 07:40:01 +02:00
renovate[bot]
03d7be3747 Update Rust crate jiff to v0.2.14 (#18303) 2025-05-26 07:38:37 +02:00
Dhruv Manilawala
d95b029862 [ty] Move diagnostics API for the server (#18308)
## Summary

This PR moves the diagnostics API for the language server out from the
request handler module to the diagnostics API module.

This is in preparation to add support for publishing diagnostics.
2025-05-26 04:16:38 +00:00
Charlie Marsh
14c3755445 Fix YTT201 for '!=' comparisons (#18293)
## Summary

Closes #18292.
2025-05-25 13:16:19 -04:00
Jo
83a036960b [ty] Add long help for --config argument (#18285) 2025-05-25 13:09:02 +02:00
chiri
be76fadb05 [pyupgrade] make fix unsafe if it deletes comments (UP010, unnecessary-future-import) (#18291) 2025-05-25 12:44:21 +02:00
Alex Waygood
e293411679 [ty] get_protocol_members returns a frozenset, not a tuple (#18284) 2025-05-23 23:20:34 +00:00
lipefree
53d19f8368 [ty] Resolving Python path using CONDA_PREFIX variable to support Conda and Pixi (#18267) 2025-05-23 20:00:42 +02:00
InSync
a1399656c9 [ty] Fix binary intersection comparison inference logic (#18266)
## Summary

Resolves https://github.com/astral-sh/ty/issues/485.

`infer_binary_intersection_type_comparison()` now checks for all
positive members before concluding that an operation is unsupported for
a given intersection type.

## Test Plan

Markdown tests.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-05-23 12:55:17 +02:00
David Peter
6392dccd24 [ty] Add warning that docs are autogenerated (#18270)
## Summary

This is a practice I followed on previous projects. Should hopefully
further help developers who want to update the documentation.

The big downside is that it's annoying to see this *as a user of the
documentation* if you don't open the Markdown file in the browser. But
I'd argue that those files don't really follow the original Markdown
spirit anyway with all the inline HTML.
2025-05-23 09:58:16 +00:00
David Peter
93ac0934dd [ty] Type compendium (#18263)
## Summary

This is something I wrote a few months ago, and continued to update from
time to time. It was mostly written for my own education. I found a few
bugs while writing it at the time (there are still one or two TODOs in
the test assertions that are probably bugs). Our other tests are fairly
comprehensive, but they are usually structured around a certain
functionality or operation (subtyping, assignability, narrowing). The
idea here was to focus on individual *types and their properties*.

closes #197 (added `JustFloat` and `JustComplex` to `ty_extensions`).
2025-05-23 11:41:31 +02:00
David Peter
aae4482c55 [ty] Replace remaining knot.toml reference (#18269)
## Summary

Fix remaining `knot.toml` reference and replace it with `ty.toml`. This
change was probably still in flight while we renamed things.

## Test Plan

Added a second assertion which ensures that the config file has any
effect.
2025-05-23 10:44:46 +02:00
Alex Waygood
d02c9ada5d [ty] Do not carry the generic context of Protocol or Generic in the ClassBase enum (#17989)
## Summary

It doesn't seem to be necessary for our generics implementation to carry
the `GenericContext` in the `ClassBase` variants. Removing it simplifies
the code, fixes many TODOs about `Generic` or `Protocol` appearing
multiple times in MROs when each should only appear at most once, and
allows us to more accurately detect runtime errors that occur due to
`Generic` or `Protocol` appearing multiple times in a class's bases.

In order to remove the `GenericContext` from the `ClassBase` variant, it
turns out to be necessary to emulate
`typing._GenericAlias.__mro_entries__`, or we end up with a large number
of false-positive `inconsistent-mro` errors. This PR therefore also does
that.

Lastly, this PR fixes the inferred MROs of PEP-695 generic classes,
which implicitly inherit from `Generic` even if they have no explicit
bases.

## Test Plan

mdtests
2025-05-22 21:37:03 -04:00
Dylan
6c0a59ea78 Fix insider docs requirement syntax (#18265)
Attempting to fix the `mkdocs` workflow (maybe `uv` is more forgiving
than `pip` for the syntax in `requirements.txt`?)
2025-05-22 16:21:51 -05:00
Carl Meyer
0b181bc2ad Fix instance vs callable subtyping/assignability (#18260)
## Summary

Fix some issues with subtying/assignability for instances vs callables.
We need to look up dunders on the class, not the instance, and we should
limit our logic here to delegating to the type of `__call__`, so it
doesn't get out of sync with the calls we allow.

Also, we were just entirely missing assignability handling for
`__call__` implemented as anything other than a normal bound method
(though we had it for subtyping.)

A first step towards considering what else we want to change in
https://github.com/astral-sh/ty/issues/491

## Test Plan

mdtests

---------

Co-authored-by: med <medioqrity@gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-05-22 19:47:05 +00:00
Dylan
0397682f1f Bump 0.11.11 (#18259) 2025-05-22 13:09:44 -05:00
InSync
bcefa459f4 [ty] Rename call-possibly-unbound-method to possibly-unbound-implicit-call (#18017) 2025-05-22 15:25:51 +00:00
Brandt Bucher
91b7a570c2 [ty] Implement Python's floor division semantics for Literal ints (#18249)
Division works differently in Python than in Rust. If the result is
negative and there is a remainder, the division rounds down (instead of
towards zero). The remainder needs to be adjusted to compensate so that
`(lhs // rhs) * rhs + (lhs % rhs) == lhs`.

Fixes astral-sh/ty#481.
2025-05-22 10:42:29 -04:00
Micha Reiser
98da200d45 [ty] Fix server panic when calling system_mut (#18252) 2025-05-22 16:10:07 +02:00
Sumana Harihareswara
029085fa72 [ty] Clarify ty check output default in documentation. (#18246)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-22 15:24:58 +02:00
Denys Kyslytsyn
6df10c638e [pylint] Fix docs example that produced different output (PLW0603) (#18216) 2025-05-22 07:55:37 +02:00
Max Mynter
bdf488462a Preserve tuple parentheses in case patterns (#18147) 2025-05-22 07:52:21 +02:00
justin
01eeb2f0d6 [ty] Support frozen dataclasses (#17974)
## Summary
https://github.com/astral-sh/ty/issues/111

This PR adds support for `frozen` dataclasses. It will emit a diagnostic
with a similar message to mypy

Note: This does not include emitting a diagnostic if `__setattr__` or
`__delattr__` are defined on the object as per the
[spec](https://docs.python.org/3/library/dataclasses.html#module-contents)

## Test Plan
mdtest

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-22 00:20:34 -04:00
Alex Waygood
cb04343b3b [ty] Split invalid-base error code into two error codes (#18245) 2025-05-21 18:02:39 -04:00
Alex Waygood
02394b8049 [ty] Improve invalid-type-form diagnostic where a module-literal type is used in a type expression and the module has a member which would be valid in a type expression (#18244) 2025-05-21 15:38:56 -04:00
Alex Waygood
41463396cf [ty] Add a subdiagnostic if invalid-return-type is emitted on a method with an empty body on a non-protocol subclass of a protocol class (#18243) 2025-05-21 17:38:07 +00:00
David Peter
da4be789ef [ty] Ignore ClassVar declarations when resolving instance members (#18241)
## Summary

Make sure that the following definitions all lead to the same outcome
(bug originally noticed by @AlexWaygood)

```py
from typing import ClassVar

class Descriptor:
    def __get__(self, instance, owner) -> int:
        return 42

class C:
    a: ClassVar[Descriptor]
    b: Descriptor = Descriptor()
    c: ClassVar[Descriptor] = Descriptor()

reveal_type(C().a)  # revealed: int  (previously: int | Descriptor)
reveal_type(C().b)  # revealed: int
reveal_type(C().c)  # revealed: int
```

## Test Plan

New Markdown tests
2025-05-21 19:23:35 +02:00
Max Mynter
02fd48132c [ty] Don't warn yield not in function when yield is in function (#18008) 2025-05-21 18:16:25 +02:00
Alex Waygood
d37592175f [ty] Tell the user why we inferred the Python version we inferred (#18082) 2025-05-21 11:06:27 -04:00
Micha Reiser
cb9e66927e Run mypy primer on Cargo.lock changes (#18239) 2025-05-21 13:21:38 +02:00
Micha Reiser
76ab77fe01 [ty] Support import <namespace> and from <namespace> import module (#18137) 2025-05-21 07:28:33 +00:00
Carl Meyer
7b253100f8 switch the playground repo button to ty repo (#18228)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-21 06:35:13 +00:00
Carl Meyer
d098118e37 [ty] disable division-by-zero by default (#18220)
## Summary

I think `division-by-zero` is a low-value diagnostic in general; most
real division-by-zero errors (especially those that are less obvious to
the human eye) will occur on values typed as `int`, in which case we
don't issue the diagnostic anyway. Mypy and pyright do not emit this
diagnostic.

Currently the diagnostic is prone to false positives because a) we do
not silence it in unreachable code, and b) we do not implement narrowing
of literals from inequality checks. We will probably fix (a) regardless,
but (b) is low priority apart from division-by-zero.

I think we have many more important things to do and should not allow
false positives on a low-value diagnostic to be a distraction. Not
opposed to re-enabling this diagnostic in future when we can prioritize
reducing its false positives.

References https://github.com/astral-sh/ty/issues/443

## Test Plan

Existing tests.
2025-05-20 14:47:56 -04:00
Ramil Aleskerov
7917269d9a [ty] Add support for PyPy virtual environments (#18203)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-05-20 14:46:50 -04:00
Alex Waygood
e8d4f6d891 [ty] Ensure that a function-literal type is always equivalent to itself (#18227) 2025-05-20 14:11:03 -04:00
Alex Waygood
60b486abce [ty] Deeply normalize many types (#18222) 2025-05-20 11:41:26 -04:00
Dhruv Manilawala
32403dfb28 [ty] Avoid panicking when there are multiple workspaces (#18151)
## Summary

This PR updates the language server to avoid panicking when there are
multiple workspace folders passed during initialization. The server
currently picks up the first workspace folder and provides a warning and
a log message.

## Test Plan

<img width="1724" alt="Screenshot 2025-05-17 at 11 43 09"
src="https://github.com/user-attachments/assets/1a7ddbc3-198d-4191-a28f-9b69321e8f99"
/>
2025-05-20 20:53:23 +05:30
InSync
76ab3425d3 [ty] Integer indexing into bytes returns int (#18218)
## Summary

Resolves [#461](https://github.com/astral-sh/ty/issues/461).

ty was hardcoded to infer `BytesLiteral` types for integer indexing into
`BytesLiteral`. It will now infer `IntLiteral` types instead.

## Test Plan

Markdown tests.
2025-05-20 16:44:12 +02:00
हिमांशु
90ca0a4c13 add full option name in formatter warning (#18217) 2025-05-20 16:26:47 +02:00
Brent Westbrook
15dbfad265 Remove Checker::report_diagnostics (#18206)
Summary
--

I thought that emitting multiple diagnostics at once would be difficult
to port to a diagnostic construction model closer to ty's
`InferContext::report_lint`, so as a first step toward that, this PR
removes `Checker::report_diagnostics`.

In many cases I was able to do some related refactoring to avoid
allocating a `Vec<Diagnostic>` at all, often by adding a `Checker` field
to a `Visitor` or by passing a `Checker` instead of a `&mut
Vec<Diagnostic>`.

In other cases, I had to fall back on something like

```rust
for diagnostic in diagnostics {
    checker.report_diagnostic(diagnostic);
}
```

which I guess is a bit worse than the `extend` call in
`report_diagnostics`, but hopefully it won't make too much of a
difference.

I'm still not quite sure what to do with the remaining loop cases. The
two main use cases for collecting a sequence of diagnostics before
emitting any of them are:

1. Applying a single `Fix` to a group of diagnostics
2. Avoiding an earlier diagnostic if something goes wrong later

I was hoping we could get away with just a `DiagnosticGuard` that
reported a `Diagnostic` on drop, but I guess we will still need a
`DiagnosticGuardBuilder` that can be collected in these cases and
produce a `DiagnosticGuard` once we know we actually want the
diagnostics.

Test Plan
--

Existing tests
2025-05-20 10:00:06 -04:00
Vasco Schiavo
4f8a005f8f [flake8-simplify] enable fix in preview mode (SIM117) (#18208)
The PR add the `fix safety` section for rule `SIM117` (#15584 ), and
enable a fix in preview mode.
2025-05-20 08:34:50 -05:00
Micha Reiser
3b56c7ca3d Update salsa (#18212) 2025-05-20 09:19:34 +02:00
Micha Reiser
f9ca6eb63e Fix rendering of admonition in docs (#18163) 2025-05-20 08:22:06 +02:00
Adam Aaronson
8729cb208f [ty] Raise invalid-exception-caught even when exception is not captured (#18202)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-19 18:13:34 -04:00
Emily B. Zhang
a2c87c2bc1 [ty] Add note to unresolved-import hinting to users to configure their Python environment (#18207)
Closes https://github.com/astral-sh/ty/issues/453.

## Summary

Add an additional info diagnostic to `unresolved-import` check to hint
to users that they should make sure their Python environment is properly
configured for ty, linking them to the corresponding doc. This
diagnostic is only shown when an import is not relative, e.g., `import
maturin` not `import .maturin`.

## Test Plan

Updated snapshots with new info message and reran tests.
2025-05-19 17:24:25 -04:00
Vasco Schiavo
b302d89da3 [flake8-simplify] add fix safety section (SIM110) (#18114)
The PR add the `fix safety` section for rule `SIM110` (#15584 )

### Unsafe Fix Example

```python
def predicate(item):
    global called
    called += 1
    if called == 1:
    # after first call we change the method
        def new_predicate(_): return False
        globals()['predicate'] = new_predicate
    return True

def foo():
    for item in range(10):
        if predicate(item):
            return True
    return False

def foo_gen():
    return any(predicate(item) for item in range(10))

called = 0
print(foo())      # true – returns immediately on first call

called = 0
print(foo_gen())  # false – second call uses new `predicate`
```

### Note

I notice that
[here](46be305ad2/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs (L60))
we have two rules, `SIM110` & `SIM111`. The second one seems not anymore
active. Should I delete `SIM111`?
2025-05-19 16:38:08 -04:00
Douglas Creager
ce43dbab58 [ty] Promote literals when inferring class specializations from constructors (#18102)
This implements the stopgap approach described in
https://github.com/astral-sh/ty/issues/336#issuecomment-2880532213 for
handling literal types in generic class specializations.

With this approach, we will promote any literal to its instance type,
but _only_ when inferring a generic class specialization from a
constructor call:

```py
class C[T]:
    def __init__(self, x: T) -> None: ...

reveal_type(C("string"))  # revealed: C[str]
```

If you specialize the class explicitly, we still use whatever type you
provide, even if it's a literal:

```py
from typing import Literal

reveal_type(C[Literal[5]](5))  # revealed: C[Literal[5]]
```

And this doesn't apply at all to generic functions:

```py
def f[T](x: T) -> T:
    return x

reveal_type(f(5))  # revealed: Literal[5]
```

---

As part of making this happen, we also generalize the `TypeMapping`
machinery. This provides a way to apply a function to type, returning a
new type. Complicating matters is that for function literals, we have to
apply the mapping lazily, since the function's signature is not created
until (and if) someone calls its `signature` method. That means we have
to stash away the mappings that we want to apply to the signatures
parameter/return annotations once we do create it. This requires some
minor `Cow` shenanigans to continue working for partial specializations.
2025-05-19 15:42:54 -04:00
Felix Scherz
fb589730ef [ty]: Consider a class with a dynamic element in its MRO assignable to any subtype of type (#18205) 2025-05-19 19:30:30 +00:00
Douglas Creager
4fad15805b [ty] Use first matching constructor overload when inferring specializations (#18204)
This is a follow-on to #18155. For the example raised in
https://github.com/astral-sh/ty/issues/370:

```py
import tempfile

with tempfile.TemporaryDirectory() as tmp: ...
```

the new logic would notice that both overloads of `TemporaryDirectory`
match, and combine their specializations, resulting in an inferred type
of `str | bytes`.

This PR updates the logic to match our other handling of other calls,
where we only keep the _first_ matching overload. The result for this
example then becomes `str`, matching the runtime behavior. (We still do
not implement the full [overload resolution
algorithm](https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation)
from the spec.)
2025-05-19 15:12:28 -04:00
David Peter
0ede831a3f [ty] Add hint that PEP 604 union syntax is only available in 3.10+ (#18192)
## Summary

Add a new diagnostic hint if you try to use PEP 604 `X | Y` union syntax
in a non-type-expression before 3.10.

closes https://github.com/astral-sh/ty/issues/437

## Test Plan

New snapshot test
2025-05-19 19:47:31 +02:00
Brent Westbrook
d6009eb942 Unify Message variants (#18051)
## Summary

This PR unifies the ruff `Message` enum variants for syntax errors and
rule violations into a single `Message` struct consisting of a shared
`db::Diagnostic` and some additional, optional fields used for some rule
violations.

This version of `Message` is nearly a drop-in replacement for
`ruff_diagnostics::Diagnostic`, which is the next step I have in mind
for the refactor.

I think this is also a useful checkpoint because we could possibly add
some of these optional fields to the new `Diagnostic` type. I think
we've previously discussed wanting support for `Fix`es, but the other
fields seem less relevant, so we may just need to preserve the `Message`
wrapper for a bit longer.

## Test plan

Existing tests

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-19 13:34:04 -04:00
Wei Lee
236633cd42 [airflow] Update AIR301 and AIR311 with the latest Airflow implementations (#17985)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->



* Remove the following rules
    * name
* `airflow.auth.managers.base_auth_manager.is_authorized_dataset` →
`airflow.api_fastapi.auth.managers.base_auth_manager.is_authorized_asset`
*
`airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_dataset`
→
`airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_asset`
* Update the following rules
    * name
* `airflow.models.baseoperatorlink.BaseOperatorLink` →
`airflow.sdk.BaseOperatorLink`
* `airflow.api_connexion.security.requires_access` → "Use
`airflow.api_fastapi.core_api.security.requires_access_*` instead`"
* `airflow.api_connexion.security.requires_access_dataset`→
`airflow.api_fastapi.core_api.security.requires_access_asset`
* `airflow.notifications.basenotifier.BaseNotifier` →
`airflow.sdk.bases.notifier.BaseNotifier`
        * `airflow.www.auth.has_access`  → None
        * `airflow.www.auth.has_access_dataset` → None
        * `airflow.www.utils.get_sensitive_variables_fields`→ None
        * `airflow.www.utils.should_hide_value_for_key`→ None
    * class attribute
        * `airflow..sensors.weekday.DayOfWeekSensor`
            * `use_task_execution_day` removed
*
`airflow.providers.amazon.aws.auth_manager.aws_auth_manager.AwsAuthManager`
            * `is_authorized_dataset`
* Add the following rules
    * class attribute
* `airflow.auth.managers.base_auth_manager.BaseAuthManager` |
`airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager`
     * name
* `airflow.auth.managers.base_auth_manager.BaseAuthManager` →
`airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager` *
`is_authorized_dataset` → `is_authorized_asset`
* refactor
    * simplify unnecessary match with if else
    * rename Replacement::Name as Replacement::AttrName

## Test Plan

<!-- How was it tested? -->
The test fixtures have been revised and updated.
2025-05-19 13:28:04 -04:00
Wei Lee
99cb89f90f [airflow] Move rules from AIR312 to AIR302 (#17940)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

In the later development of Airflow 3.0, backward compatibility was not
added for some cases. Thus, the following rules are moved back to AIR302

* airflow.hooks.subprocess.SubprocessResult →
airflow.providers.standard.hooks.subprocess.SubprocessResult
* airflow.hooks.subprocess.working_directory →
airflow.providers.standard.hooks.subprocess.working_directory
* airflow.operators.datetime.target_times_as_dates →
airflow.providers.standard.operators.datetime.target_times_as_dates
* airflow.operators.trigger_dagrun.TriggerDagRunLink →
airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunLink
* airflow.sensors.external_task.ExternalTaskSensorLink →
airflow.providers.standard.sensors.external_task.ExternalDagLink (**This
one contains a minor change**)
* airflow.sensors.time_delta.WaitSensor →
airflow.providers.standard.sensors.time_delta.WaitSensor

## Test Plan

<!-- How was it tested? -->
2025-05-19 13:20:21 -04:00
Micha Reiser
ac5df56aa3 [ty] Small LSP cleanups (#18201) 2025-05-19 17:08:59 +00:00
Micha Reiser
6985de4c40 [ty] Show related information in diagnostic (#17359) 2025-05-19 18:52:12 +02:00
Micha Reiser
55a410a885 Default src.root to ['.', '<project_name>'] if the directory exists (#18141) 2025-05-19 18:11:27 +02:00
Douglas Creager
97058e8093 [ty] Infer function call typevars in both directions (#18155)
This primarily comes up with annotated `self` parameters in
constructors:

```py
class C[T]:
    def __init__(self: C[int]): ...
```

Here, we want infer a specialization of `{T = int}` for a call that hits
this overload.

Normally when inferring a specialization of a function call, typevars
appear in the parameter annotations, and not in the argument types. In
this case, this is reversed: we need to verify that the `self` argument
(`C[T]`, as we have not yet completed specialization inference) is
assignable to the parameter type `C[int]`.

To do this, we simply look for a typevar/type in both directions when
performing inference, and apply the inferred specialization to argument
types as well as parameter types before verifying assignability.

As a wrinkle, this exposed that we were not checking
subtyping/assignability for function literals correctly. Our function
literal representation includes an optional specialization that should
be applied to the signature. Before, function literals were considered
subtypes of (assignable to) each other only if they were identical Salsa
objects. Two function literals with different specializations should
still be considered subtypes of (assignable to) each other if those
specializations result in the same function signature (typically because
the function doesn't use the typevars in the specialization).

Closes https://github.com/astral-sh/ty/issues/370
Closes https://github.com/astral-sh/ty/issues/100
Closes https://github.com/astral-sh/ty/issues/258

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-19 11:45:40 -04:00
Douglas Creager
569c94b71b Add rustfmt.toml file (#18197)
My editor runs `rustfmt` on save to format Rust code, not `cargo fmt`.

With our recent bump to the Rust 2024 edition, the formatting that
`rustfmt`/`cargo fmt` applies changed. Unfortunately, `rustfmt` and
`cargo fmt` have different behaviors for determining which edition to
use when formatting: `cargo fmt` looks for the Rust edition in
`Cargo.toml`, whereas `rustfmt` looks for it in `rustfmt.toml`. As a
result, whenever I save, I have to remember to manually run `cargo fmt`
before committing/pushing.

There is an open issue asking for `rustfmt` to also look at `Cargo.toml`
when it's present (https://github.com/rust-lang/rust.vim/issues/368),
but it seems like they "closed" that issue just by bumping the default
edition (six years ago, from 2015 to 2018).

In the meantime, this PR adds a `rustfmt.toml` file with our current
Rust edition so that both invocation have the same behavior. I don't
love that this duplicates information in `Cargo.toml`, but I've added a
reminder comment there to hopefully ensure that we bump the edition in
both places three years from now.
2025-05-19 11:40:58 -04:00
Micha Reiser
59d80aff9f [ty] Update mypy primer (#18196) 2025-05-19 17:35:48 +02:00
David Peter
b913f568c4 [ty] Mark generated files as such in .gitattributes (#18195)
## Summary

See comment here:
https://github.com/astral-sh/ruff/pull/18156#discussion_r2095850586
2025-05-19 16:50:48 +02:00
David Peter
4c889d5251 [ty] Support typing.TypeAliasType (#18156)
## Summary

Support direct uses of `typing.TypeAliasType`, as in:

```py
from typing import TypeAliasType

IntOrStr = TypeAliasType("IntOrStr", int | str)

def f(x: IntOrStr) -> None:
    reveal_type(x)  # revealed: int | str
```

closes https://github.com/astral-sh/ty/issues/392

## Ecosystem

The new false positive here:
```diff
+ error[invalid-type-form] altair/utils/core.py:49:53: The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`
```
comes from the fact that we infer the second argument as a type
expression now. We silence false positives for PEP695 `ParamSpec`s, but
not for `P = ParamSpec("P")` inside `Callable[P, ...]`.

## Test Plan

New Markdown tests
2025-05-19 16:36:49 +02:00
Micha Reiser
220137ca7b Cargo update (#18191) 2025-05-19 09:14:11 +02:00
renovate[bot]
34337fb8ba Update NPM Development dependencies (#18187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:57:45 +02:00
renovate[bot]
38c332fe23 Update Rust crate bincode to v2 (#18188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-19 08:57:09 +02:00
renovate[bot]
9f743d1b9f Update astral-sh/setup-uv action to v6 (#18184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:46:40 +02:00
renovate[bot]
405544cc8f Update dependency react-resizable-panels to v3 (#18185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 06:29:31 +00:00
renovate[bot]
ab96adbcd1 Update dependency ruff to v0.11.10 (#18171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:29:03 +02:00
renovate[bot]
a761b8cfa2 Update pre-commit dependencies (#18172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-19 08:28:43 +02:00
renovate[bot]
8c020cc2e9 Update docker/build-push-action action to v6.17.0 (#18174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:28:31 +02:00
renovate[bot]
c67aa0cce2 Update uraimo/run-on-arch-action action to v3 (#18190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:28:22 +02:00
renovate[bot]
b00e390f3a Update docker/metadata-action action to v5.7.0 (#18175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:28:09 +02:00
renovate[bot]
1f9df0c8f0 Update docker/setup-buildx-action action to v3.10.0 (#18176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:27:55 +02:00
renovate[bot]
9dd9227bca Update taiki-e/install-action action to v2.51.2 (#18183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:27:44 +02:00
renovate[bot]
181a380ee0 Update extractions/setup-just action to v3 (#18186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:26:34 +02:00
renovate[bot]
12f5e99389 Update Rust crate jod-thread to v1 (#18189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:25:46 +02:00
renovate[bot]
d9cd6399e6 Update Rust crate insta to v1.43.1 (#18180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 06:24:40 +00:00
renovate[bot]
c40a801002 Update dependency pyodide to v0.27.6 (#18170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:23:21 +02:00
renovate[bot]
04168cf1ce Update react monorepo to v19.1.0 (#18178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:13:33 +02:00
renovate[bot]
6d0703ae78 Update taiki-e/install-action digest to 941e8a4 (#18168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:10:30 +02:00
renovate[bot]
f7691a79a0 Update peter-evans/find-comment action to v3.1.0 (#18177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:10:14 +02:00
renovate[bot]
6e7340c68b Update cargo-bins/cargo-binstall action to v1.12.5 (#18169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:09:54 +02:00
renovate[bot]
5095248b7e Update Rust crate bitflags to v2.9.1 (#18173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:09:42 +02:00
renovate[bot]
b1e6c6edce Update Rust crate criterion to 0.6.0 (#18179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:09:18 +02:00
renovate[bot]
8ee92c6c77 Update Rust crate tempfile to v3.20.0 (#18182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:07:22 +02:00
renovate[bot]
d6709abd94 Update Rust crate quickcheck_macros to v1.1.0 (#18181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 08:06:32 +02:00
Dragon
660375d429 T201/T203 Improve print/pprint docs (#18130)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-18 18:40:42 +02:00
Alex Waygood
dd04ca7f58 [ty] Add regression test for fixed pyvenv.cfg parsing bug (#18157) 2025-05-17 21:10:15 +00:00
Chandra Kiran G
b86960f18c [ty] Add rule link to server diagnostics (#18128)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-17 17:27:59 +00:00
Carl Meyer
2abcd86c57 Revert "[ty] Better control flow for boolean expressions that are inside if (#18010)" (#18150)
This reverts commit 9910ec700c.

## Summary

This change introduced a serious performance regression. Revert it while
we investigate.

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

## Test Plan

Timing on the snippet in https://github.com/astral-sh/ty/issues/431
again shows times similar to before the regression.
2025-05-17 08:27:32 -04:00
Matthew Mckee
c6e55f673c Remove pyvenv.cfg validation check for lines with multiple = (#18144) 2025-05-17 08:42:39 +02:00
Micha Reiser
3d55a16c91 [ty] Migrate the namespace package module resolver tests to mdtests (#18133)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-05-16 19:56:33 +02:00
Micha Reiser
e21972a79b Fix test scripts CI job (#18140) 2025-05-16 17:49:27 +00:00
Alex Waygood
0adbb3d600 [ty] Fix assignability checks for invariant generics parameterized by gradual types (#18138) 2025-05-16 13:37:07 -04:00
Alex Waygood
28fb802467 [ty] Merge SemanticIndexBuilder impl blocks (#18135)
## Summary

just a minor nit followup to
https://github.com/astral-sh/ruff/pull/18010 -- put all the
non-`Visitor` methods of `SemanticIndexBuilder` in the same impl block
rather than having multiple impl blocks

## Test Plan

`cargo build`
2025-05-16 11:05:02 -04:00
Brent Westbrook
a1d007c37c Use insta settings instead of cfg (#18134)
Summary
--

I noticed these `cfg` directives while working on diagnostics. I think
it makes more sense to apply an `insta` filter in the test instead. I
copied this filter from a CLI test for the same rule.

Test Plan
--

Existing tests, especially Windows CI on this PR
2025-05-16 10:55:33 -04:00
Micha Reiser
1ba56b4bc6 [ty] Fix relative imports in stub packages (#18132) 2025-05-16 15:30:10 +02:00
David Peter
e677cabd69 [ty] Reduce size of the many-tuple-assignments benchmark (#18131)
## Summary

The previous version took several minute to complete on codspeed.
2025-05-16 15:28:23 +02:00
TomerBin
9910ec700c [ty] Better control flow for boolean expressions that are inside if (#18010)
## Summary
With this PR we now detect that x is always defined in `use`:
```py
if flag and (x := number):
    use(x)
```

When outside if, it's still detected as possibly not defined
```py
flag and (x := number)
# error: [possibly-unresolved-reference]
use(x)
```
In order to achieve that, I had to find a way to get access to the
flow-snapshots of the boolean expression when analyzing the flow of the
if statement. I did it by special casing the visitor of boolean
expression to return flow control information, exporting two snapshots -
`maybe_short_circuit` and `no_short_circuit`. When indexing
boolean expression itself we must assume all possible flows, but when
it's inside if statement, we can be smarter than that.

## Test Plan
Fixed existing and added new mdtests.
I went through some of mypy primer results and they look fine

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-16 11:59:21 +00:00
Micha Reiser
9ae698fe30 Switch to Rust 2024 edition (#18129) 2025-05-16 13:25:28 +02:00
David Peter
e67b35743a [ty] NamedTuple 'fallback' attributes (#18127)
## Summary

Add various attributes to `NamedTuple` classes/instances that are
available at runtime.

closes https://github.com/astral-sh/ty/issues/417

## Test Plan

New Markdown tests
2025-05-16 12:56:43 +02:00
David Peter
8644c9da43 [ty] Regression test for relative import in stubs package (#18123)
## Summary

Regression test for https://github.com/astral-sh/ty/issues/408
2025-05-16 12:49:35 +02:00
Micha Reiser
196e4befba Update MSRV to 1.85 and toolchain to 1.87 (#18126) 2025-05-16 09:19:55 +02:00
David Peter
6e39250015 [ty] Allow unions including Any/Unknown as bases (#18094)
## Summary

Alternative fix for https://github.com/astral-sh/ty/issues/312

## Test Plan

New Markdown test
2025-05-16 06:57:26 +02:00
Matthew Mckee
7dc4fefb47 Remove ty property tests (#18124) 2025-05-15 20:57:00 -04:00
Vasco Schiavo
e5435eb106 [flake8-simplify] add fix safety section (SIM210) (#18100)
The PR add the `fix safety` section for rule `SIM210` (#15584 )

It is a little cheating, as the Fix safety section is copy/pasted by
#18086 as the problem is the same.

### Unsafe Fix Example

```python
class Foo():
    def __eq__(self, other):
        return 0

def foo():
    return True if Foo() == 0 else False

def foo_fix():
    return Foo() == 0

print(foo()) # False
print(foo_fix()) # 0
```
2025-05-15 16:26:10 -04:00
Victor Hugo Gomes
f53c580c53 [pylint] Fix PLW1514 not recognizing the encoding positional argument of codecs.open (#18109)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Fixes #18107
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

Snapshot tests
<!-- How was it tested? -->
2025-05-15 16:17:07 -04:00
Wei Lee
2ceba6ae67 [airflow] Add autofixes for AIR302 and AIR312 (#17942)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

`ProviderReplacement::Name` was designed back when we only wanted to do
linting. Now we also want to fix the user code. It would be easier for
us to replace them with better AutoImport struct.

## Test Plan

<!-- How was it tested? -->

The test fixture has been updated as some cases can now be fixed
2025-05-15 16:03:02 -04:00
Felix Scherz
d3a7cb3fe4 [ty] support accessing __builtins__ global (#18118)
## Summary

The PR adds an explicit check for `"__builtins__"` during name lookup,
similar to how `"__file__"` is implemented. The inferred type is
`Any`.

closes https://github.com/astral-sh/ty/issues/393

## Test Plan

Added a markdown test for `__builtins__`.

---------

Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
2025-05-15 22:01:38 +02:00
Andrew Gallant
69393b2e6e [ty] Improve invalid method calls for unmatched overloads (#18122)
This makes an easy tweak to allow our diagnostics for unmatched
overloads to apply to method calls. Previously, they only worked for
function calls.

There is at least one other case worth addressing too, namely, class
literals. e.g., `type()`. We had a diagnostic snapshot test case to
track it.

Closes astral-sh/ty#274
2025-05-15 11:39:14 -04:00
David Peter
c066bf0127 [ty] type[…] is always assignable to type (#18121)
## Summary

Model that `type[C]` is always assignable to `type`, even if `C` is not
fully static.

closes https://github.com/astral-sh/ty/issues/312

## Test Plan

* New Markdown tests
* Property tests
2025-05-15 17:13:47 +02:00
Max Mynter
a5ee1a3bb1 Bump py-fuzzer Dependencies (#18113) 2025-05-15 10:47:37 -04:00
Brent Westbrook
e2c5b83fe1 Inline DiagnosticKind into other diagnostic types (#18074)
## Summary

This PR deletes the `DiagnosticKind` type by inlining its three fields
(`name`, `body`, and `suggestion`) into three other diagnostic types:
`Diagnostic`, `DiagnosticMessage`, and `CacheMessage`.

Instead of deferring to an internal `DiagnosticKind`, both `Diagnostic`
and `DiagnosticMessage` now have their own macro-generated `AsRule`
implementations.

This should make both https://github.com/astral-sh/ruff/pull/18051 and
another follow-up PR changing the type of `name` on `CacheMessage`
easier since its type will be able to change separately from
`Diagnostic` and `DiagnosticMessage`.

## Test Plan

Existing tests
2025-05-15 10:27:21 -04:00
Brent Westbrook
b35bf8ae07 Bump 0.11.10 (#18120) 2025-05-15 09:54:08 -04:00
David Peter
279dac1c0e [ty] Make dataclass instances adhere to DataclassInstance (#18115)
## Summary

Make dataclass instances adhere to the `DataclassInstance` protocol.

fixes astral-sh/ty#400

## Test Plan

New Markdown tests
2025-05-15 14:27:23 +02:00
Micha Reiser
57617031de [ty] Enable optimizations for salsa in debug profile (#18117) 2025-05-15 12:31:46 +02:00
Micha Reiser
28b5a868d3 [ty] Enable 'ansi' feature to fix compile error (#18116) 2025-05-15 11:43:15 +02:00
Micha Reiser
b6b7caa023 [ty] Change layout of extra verbose output and respect --color for verbose output (#18089) 2025-05-15 09:57:59 +02:00
InSync
46be305ad2 [ty] Include synthesized arguments in displayed counts for too-many-positional-arguments (#18098)
## Summary

Resolves [#290](https://github.com/astral-sh/ty/issues/290).

All arguments, synthesized or not, are now accounted for in
`too-many-positional-arguments`'s error message.

For example, consider this example:

```python
class C:
	def foo(self): ...

C().foo(1)  # !!!
```

Previously, ty would say:

> Too many positional arguments to bound method foo: expected 0, got 1

After this change, it will say:

> Too many positional arguments to bound method foo: expected 1, got 2

This is what Python itself does too:

```text
Traceback (most recent call last):
  File "<python-input-0>", line 3, in <module>
    C().foo()
    ~~~~~~~^^
TypeError: C.foo() takes 0 positional arguments but 1 was given
```

## Test Plan

Markdown tests.
2025-05-14 22:51:23 -04:00
Alex Waygood
c3a4992ae9 [ty] Fix normalization of unions containing instances parameterized with unions (#18112) 2025-05-14 22:48:33 -04:00
Alex Waygood
9aa6330bb1 [ty] Fix redundant-cast false positives when casting to Unknown (#18111) 2025-05-14 22:38:53 -04:00
github-actions[bot]
b600ff106a Sync vendored typeshed stubs (#18110)
Close and reopen this PR to trigger CI

---------

Co-authored-by: typeshedbot <>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-14 22:14:52 -04:00
Vasco Schiavo
466021d5e1 [flake8-simplify] add fix safety section (SIM112) (#18099)
The PR add the `fix safety` section for rule `SIM112` (#15584 ).
2025-05-14 17:16:20 -04:00
Simon Sawert
33e14c5963 Update Neovim setup docs (#18108)
## Summary

Nvim 0.11+ uses the builtin `vim.lsp.enable` and `vim.lsp.config` to
enable and configure LSP clients. This adds the new non legacy way of
configuring Nvim with `nvim-lspconfig` according to the upstream
documentation.

Update documentation for Nvim LSP configuration according to
`nvim-lspconfig` and Nvim 0.11+

## Test Plan

Tested locally on macOS with Nvim 0.11.1 and `nvim-lspconfig`
master/[ac1dfbe](ac1dfbe3b6).
2025-05-14 20:54:24 +00:00
David Peter
6800a9f6f3 [ty] Add type-expression syntax link to invalid-type-expression (#18104)
## Summary

Add a link to [this
page](https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions)
when emitting `invalid-type-expression` diagnostics.
2025-05-14 18:56:44 +00:00
Vasco Schiavo
68559fc17d [flake8-simplify] add fix safety section (SIM103) (#18086)
The PR add the `fix safety` section for rule `SIM103` (#15584 )

### Unsafe Fix Example

```python
class Foo:
    def __eq__(self, other):
        return 1
    
def foo():
    if Foo() == 1:
        return True
    return False

def foo_fix():
    return Foo() == 1
    
print(foo()) # True
print(foo_fix()) # 1
```

### Note

I updated the code snippet example, because I thought it was cool to
have a correct example, i.e., that I can paste inside the playground and
it works :-)
2025-05-14 14:24:15 -04:00
David Peter
2a217e80ca [ty] mypy_primer: fix static-frame setup (#18103)
## Summary

Pull in https://github.com/hauntsaninja/mypy_primer/pull/169
2025-05-14 20:23:53 +02:00
Dan Parizher
030a16cb5f [flake8-simplify] Correct behavior for str.split/rsplit with maxsplit=0 (SIM905) (#18075)
Fixes #18069

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

This PR addresses a bug in the `flake8-simplify` rule `SIM905`
(split-static-string) where `str.split(maxsplit=0)` and
`str.rsplit(maxsplit=0)` produced incorrect results for empty strings or
strings starting/ending with whitespace. The fix ensures that the
linting rule's suggested replacements now align with Python's native
behavior for these specific `maxsplit=0` scenarios.

## Test Plan

1. Added new test cases to the existing
`crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py`
fixture to cover the scenarios described in issue #18069.
2.  Ran `cargo test -p ruff_linter`.
3. Verified and accepted the updated snapshots for `SIM905.py` using
`cargo insta review`. The new snapshots confirm the corrected behavior
for `maxsplit=0`.
2025-05-14 14:20:18 -04:00
Alex Waygood
0590b38214 [ty] Fix more generics-related TODOs (#18062) 2025-05-14 12:26:52 -04:00
Usul-Dev
8104b1e83b [ty] fix missing '>' in HTML anchor tags in CLI reference (#18096)
Co-authored-by: Usul <Usul-Dev@users.noreply.github.com>
2025-05-14 15:50:35 +00:00
Dhruv Manilawala
cf70c7863c Remove symlinks from the fuzz directory (#18095)
## Summary

This PR does the following:
1. Remove the symlinks from the `fuzz/` directory
2. Update `init-fuzzer.sh` script to create those symlinks
3. Update `fuzz/.gitignore` to ignore those corpus directories

## Test Plan

Initialize the fuzzer:

```sh
./fuzz/init-fuzzer.sh
```

And, run a fuzz target:

```sh
cargo +nightly fuzz run ruff_parse_simple -- -timeout=1 -only_ascii=1
```
2025-05-14 21:05:52 +05:30
Andrew Gallant
faf54c0181 ty_python_semantic: improve failed overloaded function call
The diagnostic now includes a pointer to the implementation definition
along with each possible overload.

This doesn't include information about *why* each overload failed. But
given the emphasis on concise output (since there can be *many*
unmatched overloads), it's not totally clear how to include that
additional information.

Fixes #274
2025-05-14 11:13:41 -04:00
Andrew Gallant
451c5db7a3 ty_python_semantic: move some routines to FunctionType
These are, after all, specific to function types. The methods on `Type`
are more like conveniences that return something when the type *happens*
to be a function. But defining them on `FunctionType` itself makes it
easy to call them when you have a `FunctionType` instead of a `Type`.
2025-05-14 11:13:41 -04:00
Andrew Gallant
bd5b7f415f ty_python_semantic: rejigger handling of overload error conditions
I found the previous code somewhat harder to read. Namely, a `for`
loop was being used to encode "execute zero or one times, but not
more." Which is sometimes okay, but it seemed clearer to me to use
more explicit case analysis here.

This should have no behavioral changes.
2025-05-14 11:13:41 -04:00
Andrew Gallant
0230cbac2c ty_python_semantic: update "no matching overload" diagnostic test
It looks like support for `@overload` has been added since this test was
created, so we remove the TODO and add a snippet (from #274).
2025-05-14 11:13:41 -04:00
Wei Lee
2e94d37275 [airflow] Get rid of Replacement::Name and replace them with Replacement::AutoImport for enabling auto fixing (AIR301, AIR311) (#17941)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Similiar to https://github.com/astral-sh/ruff/pull/17941.

`Replacement::Name` was designed for linting only. Now, we also want to
fix the user code. It would be easier to replace it with a better
AutoImport struct whenever possible.

On the other hand, `AIR301` and `AIR311` contain attribute changes that
can still use a struct like `Replacement::Name`. To reduce the
confusion, I also updated it as `Replacement::AttrName`

Some of the original `Replacement::Name` has been replaced as
`Replacement::Message` as they're not directly mapping and the message
has now been moved to `help`


## Test Plan

<!-- How was it tested? -->

The test fixtures have been updated
2025-05-14 11:10:15 -04:00
Vasco Schiavo
1e4377c9c6 [ruff] add fix safety section (RUF007) (#17755)
The PR add the `fix safety` section for rule `RUF007` (#15584 )

It seems that the fix was always marked as unsafe #14401

## Unsafety example

This first example is a little extreme. In fact, the class `Foo`
overrides the `__getitem__` method but in a very special, way. The
difference lies in the fact that `zip(letters, letters[1:])` call the
slice `letters[1:]` which is behaving weird in this case, while
`itertools.pairwise(letters)` call just `__getitem__(0), __getitem__(1),
...` and so on.

Note that the diagnostic is emitted: [playground](https://play.ruff.rs)

I don't know if we want to mention this problem, as there is a subtile
bug in the python implementation of `Foo` which make the rule unsafe.

```python
from dataclasses import dataclass
import itertools

@dataclass
class Foo:
    letters: str
    
    def __getitem__(self, index):
        return self.letters[index] + "_foo"


letters = Foo("ABCD")
zip_ = zip(letters, letters[1:])
for a, b in zip_:
    print(a, b) # A_foo B, B_foo C, C_foo D, D_foo _
    
pair = itertools.pairwise(letters)
for a, b in pair:
    print(a, b) # A_foo B_foo, B_foo C_foo, C_foo D_foo
```

This other example is much probable.
here, `itertools.pairwise` was shadowed by a costume function
[(playground)](https://play.ruff.rs)

```python
from dataclasses import dataclass
from itertools import pairwise

def pairwise(a):
    return []
    
letters = "ABCD"
zip_ = zip(letters, letters[1:])
print([(a, b) for a, b in zip_]) # [('A', 'B'), ('B', 'C'), ('C', 'D')]

pair = pairwise(letters)
print(pair) # []
```
2025-05-14 11:07:11 -04:00
Dimitri Papadopoulos Orfanos
1b4f7de840 [pyupgrade] Add resource.error as deprecated alias of OSError (UP024) (#17933)
## Summary

Partially addresses #17935.


[`resource.error`](https://docs.python.org/3/library/resource.html#resource.error)
is a deprecated alias of
[`OSError`](https://docs.python.org/3/library/exceptions.html#OSError).
> _Changed in version 3.3:_ Following [**PEP
3151**](https://peps.python.org/pep-3151/), this class was made an alias
of
[`OSError`](https://docs.python.org/3/library/exceptions.html#OSError).

Add it to the list of `OSError` aliases found by [os-error-alias
(UP024)](https://docs.astral.sh/ruff/rules/os-error-alias/#os-error-alias-up024).

## Test Plan

Sorry, I usually don't program in Rust. Could you at least point me to
the test I would need to modify?
2025-05-14 10:37:25 -04:00
Victor Hugo Gomes
9b52ae8991 [flake8-pytest-style] Don't recommend usefixtures for parametrize values in PT019 (#17650)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary
Fixes #17599.

## Test Plan

Snapshot tests.

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-05-14 10:31:42 -04:00
David Peter
97d7b46936 [ty] Do not look up __init__ on instances (#18092)
## Summary

Dunder methods are never looked up on instances. We do this implicitly
in `try_call_dunder`, but the corresponding flag was missing in the
instance-construction code where we use `member_lookup_with_policy`
directly.

fixes https://github.com/astral-sh/ty/issues/322

## Test Plan

Added regression test.
2025-05-14 15:33:42 +02:00
Luke
1eab59e681 Remove double whitespace (#18090) 2025-05-14 11:20:19 +02:00
Micha Reiser
e7f97a3e4b [ty] Reduce log level of 'symbol .. (via star import) not found' log message (#18087) 2025-05-14 09:20:23 +02:00
Chandra Kiran G
d17557f0ae [ty] Fix Inconsistent casing in diagnostic (#18084) 2025-05-14 08:26:48 +02:00
Dhruv Manilawala
8cbd433a31 [ty] Add cycle handling for unpacking targets (#18078)
## Summary

This PR adds cycle handling for `infer_unpack_types` based on the
analysis in astral-sh/ty#364.

Fixes: astral-sh/ty#364

## Test Plan

Add a cycle handling test for unpacking in `cycle.md`
2025-05-13 21:27:48 +00:00
Alex Waygood
65e48cb439 [ty] Check assignments to implicit global symbols are assignable to the types declared on types.ModuleType (#18077) 2025-05-13 16:37:20 -04:00
David Peter
301d9985d8 [ty] Add benchmark for union of tuples (#18076)
## Summary

Add a micro-benchmark for the code pattern observed in
https://github.com/astral-sh/ty/issues/362.

This currently takes around 1 second on my machine.

## Test Plan

```bash
cargo bench -p ruff_benchmark -- 'ty_micro\[many_tuple' --sample-size 10
```
2025-05-13 22:14:30 +02:00
Micha Reiser
cfbb914100 Use https://ty.dev/rules when linking to the rules table (#18072) 2025-05-13 19:21:06 +02:00
Douglas Creager
fe653de3dd [ty] Infer parameter specializations of explicitly implemented generic protocols (#18054)
Follows on from (and depends on)
https://github.com/astral-sh/ruff/pull/18021.

This updates our function specialization inference to infer type
mappings from parameters that are generic protocols.

For now, this only works when the argument _explicitly_ implements the
protocol by listing it as a base class. (We end up using exactly the
same logic as for generic classes in #18021.) For this to work with
classes that _implicitly_ implement the protocol, we will have to check
the types of the protocol members (which we are not currently doing), so
that we can infer the specialization of the protocol that the class
implements.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-05-13 13:13:00 -04:00
InSync
a9f7521944 [ty] Shorten snapshot names (#18039)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-13 18:43:19 +02:00
Carl Meyer
f8890b70c3 [ty] __file__ is always a string inside a Python module (#18071)
## Summary

Understand that `__file__` is always set and a `str` when looked up as
an implicit global from a Python file we are type checking.

## Test Plan

mdtests
2025-05-13 08:20:43 -07:00
David Peter
142c1bc760 [ty] Recognize submodules in self-referential imports (#18005)
## Summary

Fix the lookup of `submodule`s in cases where the `parent` module has a
self-referential import like `from parent import submodule`. This allows
us to infer proper types for many symbols where we previously inferred
`Never`. This leads to many new false (and true) positives across the
ecosystem because the fact that we previously inferred `Never` shadowed
a lot of problems. For example, we inferred `Never` for `os.path`, which
is why we now see a lot of new diagnostics related to `os.path.abspath`
and similar.

```py
import os

reveal_type(os.path)  # previously: Never, now: <module 'os.path'>
```

closes https://github.com/astral-sh/ty/issues/261
closes https://github.com/astral-sh/ty/issues/307

## Ecosystem analysis

```
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━┓
┃ Diagnostic ID                 ┃ Severity ┃ Removed ┃ Added ┃ Net Change ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━┩
│ call-non-callable             │ error    │       1 │     5 │         +4 │
│ call-possibly-unbound-method  │ warning  │       6 │    26 │        +20 │
│ invalid-argument-type         │ error    │      26 │    94 │        +68 │
│ invalid-assignment            │ error    │      18 │    46 │        +28 │
│ invalid-context-manager       │ error    │       9 │     4 │         -5 │
│ invalid-raise                 │ error    │       1 │     1 │          0 │
│ invalid-return-type           │ error    │       3 │    20 │        +17 │
│ invalid-super-argument        │ error    │       4 │     0 │         -4 │
│ invalid-type-form             │ error    │     573 │     0 │       -573 │
│ missing-argument              │ error    │       2 │    10 │         +8 │
│ no-matching-overload          │ error    │       0 │   715 │       +715 │
│ non-subscriptable             │ error    │       0 │    35 │        +35 │
│ not-iterable                  │ error    │       6 │     7 │         +1 │
│ possibly-unbound-attribute    │ warning  │      14 │    31 │        +17 │
│ possibly-unbound-import       │ warning  │      13 │     0 │        -13 │
│ possibly-unresolved-reference │ warning  │       0 │     8 │         +8 │
│ redundant-cast                │ warning  │       1 │     0 │         -1 │
│ too-many-positional-arguments │ error    │       2 │     0 │         -2 │
│ unknown-argument              │ error    │       2 │     0 │         -2 │
│ unresolved-attribute          │ error    │     583 │   304 │       -279 │
│ unresolved-import             │ error    │       0 │    96 │        +96 │
│ unsupported-operator          │ error    │       0 │    17 │        +17 │
│ unused-ignore-comment         │ warning  │      29 │     2 │        -27 │
├───────────────────────────────┼──────────┼─────────┼───────┼────────────┤
│ TOTAL                         │          │    1293 │  1421 │       +128 │
└───────────────────────────────┴──────────┴─────────┴───────┴────────────┘

Analysis complete. Found 23 unique diagnostic IDs.
Total diagnostics removed: 1293
Total diagnostics added: 1421
Net change: +128
```

* We see a lot of new errors (`no-matching-overload`) related to
`os.path.dirname` and other `os.path` operations because we infer `str |
None` for `__file__`, but many projects use something like
`os.path.dirname(__file__)`.
* We also see many new `unresolved-attribute` errors related to the fact
that we now infer proper module types for some imports (e.g. `import
kornia.augmentation as K`), but we don't allow implicit imports (e.g.
accessing `K.auto.operations` without also importing `K.auto`). See
https://github.com/astral-sh/ty/issues/133.
* Many false positive `invalid-type-form` are removed because we now
infer the correct type for some type expression instead of `Never`,
which is not valid in a type annotation/expression context.

## Test Plan

Added new Markdown tests
2025-05-13 16:59:11 +02:00
Alex Waygood
c0f22928bd [ty] Add a note to the diagnostic if a new builtin is used on an old Python version (#18068)
## Summary

If the user tries to use a new builtin on an old Python version, tell
them what Python version the builtin was added on, what our inferred
Python version is for their project, and what configuration settings
they can tweak to fix the error.

## Test Plan

Snapshots and screenshots:


![image](https://github.com/user-attachments/assets/767d570e-7af1-4e1f-98cf-50e4311db511)
2025-05-13 10:08:04 -04:00
Alex Waygood
5bf5f3682a [ty] Add tests for else branches of hasattr() narrowing (#18067)
## Summary

This addresses @sharkdp's post-merge review in
https://github.com/astral-sh/ruff/pull/18053#discussion_r2086190617

## Test Plan

`cargo test -p ty_python_semantic`
2025-05-13 09:57:53 -04:00
Alex Waygood
5913997c72 [ty] Improve diagnostics for assert_type and assert_never (#18050) 2025-05-13 13:00:20 +00:00
Carl Meyer
00f672a83b [ty] contribution guide (#18061)
First take on a contributing guide for `ty`. Lots of it is copied from
the existing Ruff contribution guide.

I've put this in Ruff repo, since I think a contributing guide belongs
where the code is. I also updated the Ruff contributing guide to link to
the `ty` one.

Once this is merged, we can also add a link from the `CONTRIBUTING.md`
in ty repo (which focuses on making contributions to things that are
actually in the ty repo), to this guide.

I also updated the pull request template to mention that it might be a
ty PR, and mention the `[ty]` PR title prefix.

Feel free to update/modify/merge this PR before I'm awake tomorrow.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
Co-authored-by: David Peter <mail@david-peter.de>
2025-05-13 10:55:01 +02:00
Abhijeet Prasad Bodas
68b0386007 [ty] Implement DataClassInstance protocol for dataclasses. (#18018)
Fixes: https://github.com/astral-sh/ty/issues/92

## Summary

We currently get a `invalid-argument-type` error when using
`dataclass.fields` on a dataclass, because we do not synthesize the
`__dataclass_fields__` member.

This PR fixes this diagnostic.

Note that we do not yet model the `Field` type correctly. After that is
done, we can assign a more precise `tuple[Field, ...]` type to this new
member.

## Test Plan
New mdtest.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-05-13 10:31:26 +02:00
ZGY
0ae07cdd1f [ruff_python_ast] Fix redundant visitation of test expressions in elif clause statements (#18064) 2025-05-13 07:10:23 +00:00
Douglas Creager
0fb94c052e [ty] Infer parameter specializations of generic aliases (#18021)
This updates our function specialization inference to infer type
mappings from parameters that are generic aliases, e.g.:

```py
def f[T](x: list[T]) -> T: ...

reveal_type(f(["a", "b"]))  # revealed: str
```

Though note that we're still inferring the type of list literals as
`list[Unknown]`, so for now we actually need something like the
following in our tests:

```py
def _(x: list[str]):
    reveal_type(f(x))  # revealed: str
```
2025-05-12 22:12:44 -04:00
Alex Waygood
55df9271ba [ty] Understand homogeneous tuple annotations (#17998) 2025-05-12 22:02:25 -04:00
Douglas Creager
f301931159 [ty] Induct into instances and subclasses when finding and applying generics (#18052)
We were not inducting into instance types and subclass-of types when
looking for legacy typevars, nor when apply specializations.

This addresses
https://github.com/astral-sh/ruff/pull/17832#discussion_r2081502056

```py
from __future__ import annotations
from typing import TypeVar, Any, reveal_type

S = TypeVar("S")

class Foo[T]:
    def method(self, other: Foo[S]) -> Foo[T | S]: ...  # type: ignore[invalid-return-type]

def f(x: Foo[Any], y: Foo[Any]):
    reveal_type(x.method(y))  # revealed: `Foo[Any | S]`, but should be `Foo[Any]`
```

We were not detecting that `S` made `method` generic, since we were not
finding it when searching the function signature for legacy typevars.
2025-05-12 21:53:11 -04:00
Alex Waygood
7e9b0df18a [ty] Allow classes to inherit from type[Any] or type[Unknown] (#18060) 2025-05-12 20:30:21 -04:00
Alex Waygood
41fa082414 [ty] Allow a class to inherit from an intersection if the intersection contains a dynamic type and the intersection is not disjoint from type (#18055) 2025-05-12 23:07:11 +00:00
Alex Waygood
c7b6108cb8 [ty] Narrowing for hasattr() (#18053) 2025-05-12 18:58:14 -04:00
Zanie Blue
a97e72fb5e Update reference documentation for --python-version (#18056)
Adding more detail here
2025-05-12 22:31:04 +00:00
Victor Hugo Gomes
0d6fafd0f9 [flake8-bugbear] Ignore B028 if skip_file_prefixes is present (#18047)
## Summary

Fixes #18011
2025-05-12 17:06:51 -05:00
Wei Lee
2eb2d5359b [airflow] Apply try-catch guard to all AIR3 rules (AIR3) (#17887)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

If a try-catch block guards the names, we don't raise warnings. During
this change, I discovered that some of the replacement types were
missed. Thus, I extend the fix to types other than AutoImport as well

## Test Plan

<!-- How was it tested? -->

Test fixtures are added and updated.
2025-05-12 17:13:41 -04:00
Yunchi Pang
f549dfe39d [pylint] add fix safety section (PLW3301) (#17878)
parent: #15584 
issue: #16163

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-05-12 20:51:05 +00:00
Zanie Blue
6b64630635 Update --python to accept paths to executables in virtual environments (#17954)
## Summary

Updates the `--python` flag to accept Python executables in virtual
environments. Notably, we do not query the executable and it _must_ be
in a canonical location in a virtual environment. This is pretty naive,
but solves for the trivial case of `ty check --python .venv/bin/python3`
which will be a common mistake (and `ty check --python $(which python)`)

I explored this while trying to understand Python discovery in ty in
service of https://github.com/astral-sh/ty/issues/272, I'm not attached
to it, but figure it's worth sharing.

As an alternative, we can add more variants to the
`SearchPathValidationError` and just improve the _error_ message, i.e.,
by hinting that this looks like a virtual environment and suggesting the
concrete alternative path they should provide. We'll probably want to do
that for some other cases anyway (e.g., `3.13` as described in the
linked issue)

This functionality is also briefly mentioned in
https://github.com/astral-sh/ty/issues/193

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

## Test Plan

e.g.,

```
uv run ty check --python .venv/bin/python3
```

needs test coverage still
2025-05-12 15:39:04 -05:00
Yunchi Pang
d545b5bfd2 [pylint] add fix safety section (PLE4703) (#17824)
This PR adds a fix safety section in comment for rule PLE4703.

parent: #15584 
impl was introduced at #970 (couldn't find newer PRs sorry!)
2025-05-12 16:27:54 -04:00
Marcus Näslund
b2d9f59937 [ruff] Implement a recursive check for RUF060 (#17976)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
The existing implementation of RUF060 (InEmptyCollection) is not
recursive, meaning that although set([]) results in an empty collection,
the existing code fails it because set is taking an argument.

The updated implementation allows set and frozenset to take empty
collection as positional argument (which results in empty
set/frozenset).

## Test Plan

Added test cases for recursive cases + updated snapshot (see RUF060.py).

---------

Co-authored-by: Marcus Näslund <marcus.naslund@kognity.com>
2025-05-12 16:17:13 -04:00
Victor Hugo Gomes
d7ef01401c [flake8-use-pathlib] PTH* suppress diagnostic for all os.* functions that have the dir_fd parameter (#17968)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Fixes #17776.

This PR also handles all other `PTH*` rules that don't support file
descriptors.

## Test Plan

<!-- How was it tested? -->

Update existing tests.
2025-05-12 16:11:56 -04:00
Victor Hugo Gomes
c9031ce59f [refurb] Mark autofix as safe only for number literals in FURB116 (#17692)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary
We can only guarantee the safety of the autofix for number literals, all
other cases may change the runtime behaviour of the program or introduce
a syntax error. For the cases reported in the issue that would result in
a syntax error, I disabled the autofix.

Follow-up of #17661. 

Fixes #16472.
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

Snapshot tests.
<!-- How was it tested? -->
2025-05-12 16:08:12 -04:00
Victor Hugo Gomes
138ab91def [flake8-simplify] Fix SIM905 autofix for rsplit creating a reversed list literal (#18045)
## Summary

Fixes #18042
2025-05-12 14:53:08 -05:00
Ibraheem Ahmed
550b8be552 Avoid initializing progress bars early (#18049)
## Summary

Resolves https://github.com/astral-sh/ty/issues/324.
2025-05-12 15:07:55 -04:00
Douglas Creager
bdccb37b4a [ty] Apply function specialization to all overloads (#18020)
Function literals have an optional specialization, which is applied to
the parameter/return type annotations lazily when the function's
signature is requested. We were previously only applying this
specialization to the final overload of an overloaded function.

This manifested most visibly for `list.__add__`, which has an overloaded
definition in the typeshed:


b398b83631/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi (L1069-L1072)

Closes https://github.com/astral-sh/ty/issues/314
2025-05-12 13:48:54 -04:00
Charlie Marsh
3ccc0edfe4 Add comma to panic message (#18048)
## Summary

Consistent with other variants of this, separate the conditional clause.
2025-05-12 11:52:55 -04:00
Victor Hugo Gomes
6b3ff6f5b8 [flake8-pie] Mark autofix for PIE804 as unsafe if the dictionary contains comments (#18046)
## Summary

Fixes #18036
2025-05-12 10:16:59 -05:00
Shunsuke Shibayama
6f8f7506b4 [ty] fix infinite recursion bug in is_disjoint_from (#18043)
## Summary

I found this bug while working on #18041. The following code leads to
infinite recursion.

```python
from ty_extensions import is_disjoint_from, static_assert, TypeOf

class C:
    @property
    def prop(self) -> int:
        return 1

static_assert(not is_disjoint_from(int, TypeOf[C.prop]))
```

The cause is a trivial missing binding in `is_disjoint_from`. This PR
fixes the bug and adds a test case (this is a simple fix and may not
require a new test case?).

## Test Plan

A new test case is added to
`mdtest/type_properties/is_disjoint_from.md`.
2025-05-12 09:44:00 -04:00
Micha Reiser
797eb70904 disable jemalloc on android (#18033) 2025-05-12 14:41:00 +02:00
Micha Reiser
be6ec613db [ty] Fix incorrect type of src.root in documentation (#18040) 2025-05-12 12:28:14 +00:00
Micha Reiser
fcd858e0c8 [ty] Refine message for why a rule is enabled (#18038) 2025-05-12 13:31:42 +02:00
Micha Reiser
d944a1397e [ty] Remove brackets around option names (#18037) 2025-05-12 11:16:03 +00:00
renovate[bot]
d3f3d92df3 Update pre-commit dependencies (#18025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:31:23 +02:00
renovate[bot]
38c00dfad5 Update docker/build-push-action action to v6.16.0 (#18030)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:26:41 +02:00
renovate[bot]
d6280c5aea Update docker/login-action action to v3.4.0 (#18031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:26:22 +02:00
renovate[bot]
a34240a3f0 Update taiki-e/install-action digest to 83254c5 (#18022)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:26:03 +02:00
renovate[bot]
b86c7bbf7c Update cargo-bins/cargo-binstall action to v1.12.4 (#18023)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:25:28 +02:00
renovate[bot]
d7c54ba8c4 Update Rust crate ctrlc to v3.4.7 (#18027)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:25:08 +02:00
renovate[bot]
c38d6e8045 Update Rust crate clap to v4.5.38 (#18026)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:24:40 +02:00
renovate[bot]
2bfd7b1816 Update Rust crate jiff to v0.2.13 (#18029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:24:16 +02:00
renovate[bot]
c1cfb43bf0 Update Rust crate getrandom to v0.3.3 (#18028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:23:49 +02:00
renovate[bot]
99555b775c Update dependency ruff to v0.11.9 (#18024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 22:03:55 -04:00
Yunchi Pang
b398b83631 [pylint] add fix safety section (PLW1514) (#17932)
parent #15584 
fix was made unsafe at #8928
2025-05-11 12:25:07 -05:00
Rogdham
bc7b30364d python_stdlib: update for 3.14 (#18014)
## Summary

Added version 3.14 to the script generating the `known_stdlib.rs` file.

Rebuilt the known stdlibs with latest version (2025.5.10) of [stdlibs
Python lib](https://pypi.org/project/stdlibs/) (which added support for
3.14.0b1).

_Note: Python 3.14 is now in [feature
freeze](https://peps.python.org/pep-0745/) so the modules in stdlib
should be stable._

_See also: #15506_

## Test Plan

The following command has been run. Using for tests the `compression`
module which been introduced with Python 3.14.
```sh
ruff check --no-cache --select I001 --target-version py314 --fix
```

With ruff 0.11.9:
```python
import base64
import datetime

import compression

print(base64, compression, datetime)
```

With this PR:
```python
import base64
import compression
import datetime   

print(base64, compression, datetime)
```
2025-05-11 11:25:54 -05:00
Vasco Schiavo
5792ed15da [ruff] add fix safety section (RUF033) (#17760)
This PR adds the fix safety section for rule `RUF033`
(https://github.com/astral-sh/ruff/issues/15584 ).
2025-05-11 11:15:15 -05:00
Yunchi Pang
8845a13efb [pylint] add fix safety section (PLC0414) (#17802)
This PR adds a fix safety section in comment for rule `PLC0414`.

parent: #15584 
discussion: #6294
2025-05-11 11:01:26 -05:00
Alex Waygood
669855d2b5 [ty] Remove unused variants from various Known* enums (#18015)
## Summary

`KnownClass::Range`, `KnownInstanceType::Any` and `ClassBase::any()` are
no longer used or useful: all our tests pass with them removed.
`KnownModule::Abc` _is_ now used outside of tests, however, so I removed
the `#[allow(dead_code)]` branch above that variant.

## Test Plan

`cargo test -p ty_python_semantic`
2025-05-11 11:18:55 +01:00
Alex Waygood
ff7ebecf89 [ty] Remove generic types from the daily property test run (for now) (#18004) 2025-05-11 09:27:27 +00:00
Zanie Blue
7e8ba2b68e [ty] Remove vestigial pyvenv.cfg creation in mdtest (#18006)
Following #17991, removes some of
https://github.com/astral-sh/ruff/pull/17222 which is no longer strictly
necessary. I don't actually think it's that ugly to have around? no
strong feelings on retaining it or not.
2025-05-10 20:52:49 +00:00
Zanie Blue
0bb8cbdf07 [ty] Do not allow invalid virtual environments from discovered .venv or VIRTUAL_ENV (#18003)
Follow-up to https://github.com/astral-sh/ruff/pull/17991 ensuring we do
not allow detection of system environments when the origin is
`VIRTUAL_ENV` or a discovered `.venv` directory — i.e., those always
require a `pyvenv.cfg` file.
2025-05-10 20:36:12 +00:00
Zanie Blue
2923c55698 [ty] Add test coverage for PythonEnvironment::System variants (#17996)
Adds test coverage for https://github.com/astral-sh/ruff/pull/17991,
which includes some minor refactoring of the virtual environment test
infrastructure.

I tried to minimize stylistic changes, but there are still a few because
I was a little confused by the setup. I could see this evolving more in
the future, as I don't think the existing model can capture all the test
coverage I'm looking for.
2025-05-10 20:28:15 +00:00
Zanie Blue
316e406ca4 [ty] Add basic support for non-virtual Python environments (#17991)
This adds basic support for non-virtual Python environments by accepting
a directory without a `pyvenv.cfg` which allows existing, subsequent
site-packages discovery logic to succeed. We can do better here in the
long-term, by adding more eager validation (for error messages) and
parsing the Python version from the discovered site-packages directory
(which isn't relevant yet, because we don't use the discovered Python
version from virtual environments as the default `--python-version` yet
either).

Related

- https://github.com/astral-sh/ty/issues/265
- https://github.com/astral-sh/ty/issues/193

You can review this commit by commit if it makes you happy.

I tested this manually; I think refactoring the test setup is going to
be a bit more invasive so I'll stack it on top (see
https://github.com/astral-sh/ruff/pull/17996).

```
❯ uv run ty check --python /Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none/ -vv example
2025-05-09 12:06:33.685911 DEBUG Version: 0.0.0-alpha.7 (f9c4c8999 2025-05-08)
2025-05-09 12:06:33.685987 DEBUG Architecture: aarch64, OS: macos, case-sensitive: case-insensitive
2025-05-09 12:06:33.686002 DEBUG Searching for a project in '/Users/zb/workspace/ty'
2025-05-09 12:06:33.686123 DEBUG Resolving requires-python constraint: `>=3.8`
2025-05-09 12:06:33.686129 DEBUG Resolved requires-python constraint to: 3.8
2025-05-09 12:06:33.686142 DEBUG Project without `tool.ty` section: '/Users/zb/workspace/ty'
2025-05-09 12:06:33.686147 DEBUG Searching for a user-level configuration at `/Users/zb/.config/ty/ty.toml`
2025-05-09 12:06:33.686156 INFO Defaulting to python-platform `darwin`
2025-05-09 12:06:33.68636 INFO Python version: Python 3.8, platform: darwin
2025-05-09 12:06:33.686375 DEBUG Adding first-party search path '/Users/zb/workspace/ty'
2025-05-09 12:06:33.68638 DEBUG Using vendored stdlib
2025-05-09 12:06:33.686634 DEBUG Discovering site-packages paths from sys-prefix `/Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none` (`--python` argument')
2025-05-09 12:06:33.686667 DEBUG Attempting to parse virtual environment metadata at '/Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none/pyvenv.cfg'
2025-05-09 12:06:33.686671 DEBUG Searching for site-packages directory in `sys.prefix` path `/Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none`
2025-05-09 12:06:33.686702 DEBUG Resolved site-packages directories for this environment are: ["/Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none/lib/python3.10/site-packages"]
2025-05-09 12:06:33.686706 DEBUG Adding site-packages search path '/Users/zb/.local/share/uv/python/cpython-3.10.17-macos-aarch64-none/lib/python3.10/site-packages'
...

❯ uv run ty check --python /tmp -vv example
2025-05-09 15:36:10.819416 DEBUG Version: 0.0.0-alpha.7 (f9c4c8999 2025-05-08)
2025-05-09 15:36:10.819708 DEBUG Architecture: aarch64, OS: macos, case-sensitive: case-insensitive
2025-05-09 15:36:10.820118 DEBUG Searching for a project in '/Users/zb/workspace/ty'
2025-05-09 15:36:10.821652 DEBUG Resolving requires-python constraint: `>=3.8`
2025-05-09 15:36:10.821667 DEBUG Resolved requires-python constraint to: 3.8
2025-05-09 15:36:10.8217 DEBUG Project without `tool.ty` section: '/Users/zb/workspace/ty'
2025-05-09 15:36:10.821888 DEBUG Searching for a user-level configuration at `/Users/zb/.config/ty/ty.toml`
2025-05-09 15:36:10.822072 INFO Defaulting to python-platform `darwin`
2025-05-09 15:36:10.822439 INFO Python version: Python 3.8, platform: darwin
2025-05-09 15:36:10.822773 DEBUG Adding first-party search path '/Users/zb/workspace/ty'
2025-05-09 15:36:10.822929 DEBUG Using vendored stdlib
2025-05-09 15:36:10.829872 DEBUG Discovering site-packages paths from sys-prefix `/tmp` (`--python` argument')
2025-05-09 15:36:10.829911 DEBUG Attempting to parse virtual environment metadata at '/private/tmp/pyvenv.cfg'
2025-05-09 15:36:10.829917 DEBUG Searching for site-packages directory in `sys.prefix` path `/private/tmp`
ty failed
  Cause: Invalid search path settings
  Cause: Failed to discover the site-packages directory: Failed to search the `lib` directory of the Python installation at `sys.prefix` path `/private/tmp` for `site-packages`
```
2025-05-10 20:17:47 +00:00
Micha Reiser
5ecd560c6f Link to the rules.md in the ty repository (#17979) 2025-05-10 11:40:40 +01:00
Max Mynter
b765dc48e9 Skip S608 for expressionless f-strings (#17999) 2025-05-10 11:37:58 +01:00
David Peter
cd1d906ffa [ty] Silence false positives for PEP-695 ParamSpec annotations (#18001)
## Summary

Suppress false positives for uses of PEP-695 `ParamSpec` in `Callable`
annotations:
```py
from typing_extensions import Callable

def f[**P](c: Callable[P, int]):
    pass
```

addresses a comment here:
https://github.com/astral-sh/ty/issues/157#issuecomment-2859284721

## Test Plan

Adapted Markdown tests
2025-05-10 11:59:25 +02:00
Abhijeet Prasad Bodas
235b74a310 [ty] Add more tests for NamedTuples (#17975)
## Summary

Add more tests and TODOs for `NamedTuple` support, based on the typing
spec: https://typing.python.org/en/latest/spec/namedtuples.html

## Test Plan

This PR adds new tests.
2025-05-10 10:46:08 +02:00
Brent Westbrook
40fd52dde0 Exclude broken symlinks from meson-python ecosystem check (#17993)
Summary
--

This should resolve the formatter ecosystem errors we've been seeing
lately. https://github.com/mesonbuild/meson-python/pull/728 added the
links, which I think are intentionally broken for testing purposes.

Test Plan
--

Ecosystem check on this PR
2025-05-09 16:59:00 -04:00
Carl Meyer
fd1eb3d801 add test for typing_extensions.Self (#17995)
Using `typing_extensions.Self` already worked, but we were lacking a
test for it.
2025-05-09 20:29:13 +00:00
omahs
882a1a702e Fix typos (#17988)
Fix typos

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-05-09 14:57:14 -04:00
Max Mynter
b4a1ebdfe3 [semantic-syntax-tests] IrrefutableCasePattern, SingleStarredAssignment, WriteToDebug, InvalidExpression (#17748)
Re: #17526 

## Summary

Add integration test for semantic syntax for `IrrefutableCasePattern`,
`SingleStarredAssignment`, `WriteToDebug`, and `InvalidExpression`.

## Notes
- Following @ntBre's suggestion, I will keep the test coming in batches
like this over the next few days in separate PRs to keep the review load
per PR manageable while also not spamming too many.

- I did not add a test for `del __debug__` which is one of the examples
in `crates/ruff_python_parser/src/semantic_errors.rs:1051`.
For python version `<= 3.8` there is no error and for `>=3.9` the error
is not `WriteToDebug` but `SyntaxError: cannot delete __debug__ on
Python 3.9 (syntax was removed in 3.9)`.

- The `blacken-docs` bypass is necessary because otherwise the test does
not pass pre-commit checks; but we want to check for this faulty syntax.

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
This is a test.
2025-05-09 14:54:05 -04:00
Brent Westbrook
7a48477c67 [ty] Add a warning about pre-release status to the CLI (#17983)
Summary
--

This was suggested on Discord, I hope this is roughly what we had in
mind. I took the message from the ty README, but I'm more than happy to
update it. Otherwise I just tried to mimic the appearance of the `ruff
analyze graph` warning (although I'm realizing now the whole text is
bold for ruff).

Test Plan
--

New warnings in the CLI tests. I thought this might be undesirable but
it looks like uv did the same thing
(https://github.com/astral-sh/uv/pull/6166).


![image](https://github.com/user-attachments/assets/e5e56a49-02ab-4c5f-9c38-716e4008d6e6)
2025-05-09 13:42:36 -04:00
Andrew Gallant
346e82b572 ty_python_semantic: add union type context to function call type errors
This context gets added only when calling a function through a union
type.
2025-05-09 13:40:51 -04:00
Andrew Gallant
5ea3a52c8a ty_python_semantic: report all union diagnostic
This makes one very simple change: we report all call binding
errors from each union variant.

This does result in duplicate-seeming diagnostics. For example,
when two union variants are invalid for the same reason.
2025-05-09 13:40:51 -04:00
Andrew Gallant
90272ad85a ty_python_semantic: add snapshot tests for existing union function type diagnostics
This is just capturing the status quo so that we can better see the
changes. I took these tests from the (now defunct) PR #17959.
2025-05-09 13:40:51 -04:00
Ibraheem Ahmed
e9da1750a1 Add progress bar for ty check (#17965)
## Summary

Adds a simple progress bar for the `ty check` CLI command. The style is
taken from uv, and like uv the bar is always shown - for smaller
projects it is fast enough that it isn't noticeable. We could
alternatively hide it completely based on some heuristic for the number
of files, or only show it after some amount of time.

I also disabled it when `--watch` is passed, cancelling inflight checks
was leading to zombie progress bars. I think we can fix this by using
[`MultiProgress`](https://docs.rs/indicatif/latest/indicatif/struct.MultiProgress.html)
and managing all the bars globally, but I left that out for now.

Resolves https://github.com/astral-sh/ty/issues/98.
2025-05-09 13:32:27 -04:00
Wei Lee
25e13debc0 [airflow] extend AIR311 rules (#17913)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

* `airflow.models.Connection` → `airflow.sdk.Connection`
* `airflow.models.Variable` → `airflow.sdk.Variable`

## Test Plan

<!-- How was it tested? -->

The test fixtures has been updated (see the first commit for easier
review)
2025-05-09 13:08:37 -04:00
InSync
249a852a6e [ty] Document nearly all lints (#17981) 2025-05-09 18:06:56 +01:00
Andrew Gallant
861ef2504e ty: add more snapshot updates 2025-05-09 12:42:14 -04:00
Andrew Gallant
b71ef8a26e ruff_db: completely rip lint: prefix out
This does a deeper removal of the `lint:` prefix by removing the
`DiagnosticId::as_str` method and replacing it with `as_concise_str`. We
remove the associated error type and simplify the `Display` impl for
`DiagnosticId` as well.

This turned out to catch a `lint:` that was still in the diagnostic
output: the part that says why a lint is enabled.
2025-05-09 12:42:14 -04:00
Andrew Gallant
50c780fc8b ty: switch to use annotate-snippets ID functionality
We just set the ID on the `Message` and it just does what we want in
this case. I think I didn't do this originally because I was trying to
preserve the existing rendering? I'm not sure. I might have just missed
this method.
2025-05-09 12:42:14 -04:00
Andrew Gallant
244ea27d5f ruff_db: a small tweak to remove empty message case
In a subsequent commit, we're going to start using `annotate-snippets`'s
functionality for diagnostic IDs in the rendering. As part of doing
that, I wanted to remove this special casing of an empty message. I did
that independently to see what, if anything, would change. (The changes
look fine to me. They'll be tweaked again in the next commit along with
a bunch of others.)
2025-05-09 12:42:14 -04:00
Andrew Gallant
2c4cbb6e29 ty: get rid of lint: prefix in ID for diagnostic rendering
In #289, we seem to have consensus that this prefix isn't really pulling
its weight.

Ref #289
2025-05-09 12:42:14 -04:00
Alex Waygood
d1bb10a66b [ty] Understand classes that inherit from subscripted Protocol[] as generic (#17832) 2025-05-09 17:39:15 +01:00
Dylan
2370297cde Bump 0.11.9 (#17986) 2025-05-09 10:43:27 -05:00
Alex Waygood
a137cb18d4 [ty] Display "All checks passed!" message in green (#17982) 2025-05-09 14:29:43 +01:00
Alex Waygood
03a4d56624 [ty] Change range of revealed-type diagnostic to be the range of the argument passed in, not the whole call (#17980) 2025-05-09 14:15:39 +01:00
David Peter
642eac452d [ty] Recursive protocols (#17929)
## Summary

Use a self-reference "marker" ~~and fixpoint iteration~~ to solve the
stack overflow problems with recursive protocols. This is not pretty and
somewhat tedious, but seems to work fine. Much better than all my
fixpoint-iteration attempts anyway.

closes https://github.com/astral-sh/ty/issues/93

## Test Plan

New Markdown tests.
2025-05-09 14:54:02 +02:00
Micha Reiser
c1b875799b [ty] CLI reference (#17978) 2025-05-09 14:23:24 +02:00
Micha Reiser
6cd8a49638 [ty] Update salsa (#17964) 2025-05-09 11:54:07 +02:00
Micha Reiser
12ce445ff7 [ty] Document configuration schema (#17950) 2025-05-09 10:47:45 +02:00
justin
f46ed8d410 [ty] Add --config CLI arg (#17697) 2025-05-09 08:38:37 +02:00
Carl Meyer
6c177e2bbe [ty] primer updates (#17903)
## Summary

Update ecosystem project lists in light of
https://github.com/astral-sh/ruff/pull/17758

## Test Plan

CI on this PR.
2025-05-08 20:43:31 -07:00
Carl Meyer
3d2485eb1b [ty] fix more ecosystem/fuzzer panics with fixpoint (#17758)
## Summary

Add cycle handling for `try_metaclass` and `pep695_generic_context`
queries, as well as adjusting the cycle handling for `try_mro` to ensure
that it short-circuits on cycles and won't grow MROs indefinitely.

This reduces the number of failing fuzzer seeds from 68 to 17. The
latter count includes fuzzer seeds 120, 160, and 335, all of which
previously panicked but now either hang or are very slow; I've
temporarily skipped those seeds in the fuzzer until I can dig into that
slowness further.

This also allows us to move some more ecosystem projects from `bad.txt`
to `good.txt`, which I've done in
https://github.com/astral-sh/ruff/pull/17903

## Test Plan

Added mdtests.
2025-05-08 20:36:20 -07:00
Douglas Creager
f78367979e [ty] Remove SliceLiteral type variant (#17958)
@AlexWaygood pointed out that the `SliceLiteral` type variant was
originally created to handle slices before we had generics.
https://github.com/astral-sh/ruff/pull/17927#discussion_r2078115787

Now that we _do_ have generics, we can use a specialization of the
`slice` builtin type for slice literals.

This depends on https://github.com/astral-sh/ruff/pull/17956, since we
need to make sure that all typevar defaults are fully substituted when
specializing `slice`.
2025-05-08 20:16:41 -04:00
Douglas Creager
b705664d49 [ty] Handle typevars that have other typevars as a default (#17956)
It's possible for a typevar to list another typevar as its default
value:

```py
class C[T, U = T]: ...
```

When specializing this class, if a type isn't provided for `U`, we would
previously use the default as-is, leaving an unspecialized `T` typevar
in the specialization. Instead, we want to use what `T` is mapped to as
the type of `U`.

```py
reveal_type(C())  # revealed: C[Unknown, Unknown]
reveal_type(C[int]())  # revealed: C[int, int]
reveal_type(C[int, str]())  # revealed: C[int, str]
```

This is especially important for the `slice` built-in type.
2025-05-08 19:01:27 -04:00
Alex Waygood
f51f1f7153 [ty] Support extending __all__ from an imported module even when the module is not an ExprName node (#17947) 2025-05-08 23:54:19 +01:00
Alex Waygood
9b694ada82 [ty] Report duplicate Protocol or Generic base classes with [duplicate-base], not [inconsistent-mro] (#17971) 2025-05-08 23:41:22 +01:00
Alex Waygood
4d81a41107 [ty] Respect the gradual guarantee when reporting errors in resolving MROs (#17962) 2025-05-08 22:57:39 +01:00
Brent Westbrook
981bd70d39 Convert Message::SyntaxError to use Diagnostic internally (#17784)
## Summary

This PR is a first step toward integration of the new `Diagnostic` type
into ruff. There are two main changes:
- A new `UnifiedFile` enum wrapping `File` for red-knot and a
`SourceFile` for ruff
- ruff's `Message::SyntaxError` variant is now a `Diagnostic` instead of
a `SyntaxErrorMessage`

The second of these changes was mostly just a proof of concept for the
first, and it went pretty smoothly. Converting `DiagnosticMessage`s will
be most of the work in replacing `Message` entirely.

## Test Plan

Existing tests, which show no changes.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-08 12:45:51 -04:00
Alex Waygood
0763331f7f [ty] Support extending __all__ with a literal tuple or set as well as a literal list (#17948) 2025-05-08 17:37:25 +01:00
Alex Waygood
da8540862d [ty] Make unused-ignore-comment disabled by default for now (#17955) 2025-05-08 17:21:34 +01:00
Micha Reiser
6a5533c44c [ty] Change default severity for unbound-reference to error (#17936) 2025-05-08 17:54:46 +02:00
Micha Reiser
d608eae126 [ty] Ignore possibly-unresolved-reference by default (#17934) 2025-05-08 17:44:56 +02:00
Micha Reiser
067a8ac574 [ty] Default to latest supported python version (#17938) 2025-05-08 16:58:35 +02:00
Micha Reiser
5eb215e8e5 [ty] Generate and add rules table (#17953) 2025-05-08 16:55:39 +02:00
Zanie Blue
91aa853b9c Update the schemastore script to match changes in ty (#17952)
See https://github.com/astral-sh/ty/pull/273
2025-05-08 09:31:52 -05:00
Brent Westbrook
57bf7dfbd9 [ty] Implement global handling and load-before-global-declaration syntax error (#17637)
Summary
--

This PR resolves both the typing-related and syntax error TODOs added in
#17563 by tracking a set of `global` bindings for each scope. As
discussed below, we avoid the additional AST traversal from ruff by
collecting `Name`s from `global` statements while building the semantic
index and emit a syntax error if the `Name` is already bound in the
current scope at the point of the `global` statement. This has the
downside of separating the error from the `SemanticSyntaxChecker`, but I
plan to explore using this approach in the `SemanticSyntaxChecker`
itself as a follow-up. It seems like this may be a better approach for
ruff as well.

Test Plan
--

Updated all of the related mdtests to remove the TODOs (and add quotes I
forgot on the messages).

There is one remaining TODO, but it requires `nonlocal` support, which
isn't even incorporated into the `SemanticSyntaxChecker` yet.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-08 10:30:04 -04:00
Alex Waygood
67cd94ed64 [ty] Add missing bitwise-operator branches for boolean and integer arithmetic (#17949) 2025-05-08 14:10:35 +01:00
Wei Lee
aac862822f [airflow] Fix SQLTableCheckOperator typo (AIR302) (#17946) 2025-05-08 14:34:55 +02:00
Micha Reiser
3755ac9fac Update ty metadata (#17943) 2025-05-08 13:24:31 +02:00
David Peter
4f890b2867 [ty] Update salsa (#17937)
## Summary

* Update salsa to pull in https://github.com/salsa-rs/salsa/pull/850.
* Some refactoring of salsa event callbacks in various `Db`'s due to
https://github.com/salsa-rs/salsa/pull/849

closes https://github.com/astral-sh/ty/issues/108

## Test Plan

Ran `cargo run --bin ty -- -vvv` on a test file to make sure that salsa
Events are still logged.
2025-05-08 12:02:53 +02:00
Shaygan Hooshyari
d566636ca5 Support typing.Self in methods (#17689)
## Summary

Fixes: astral-sh/ty#159 

This PR adds support for using `Self` in methods.
When the type of an annotation is `TypingSelf` it is converted to a type
var based on:
https://typing.python.org/en/latest/spec/generics.html#self

I just skipped Protocols because it had more problems and the tests was
not useful.
Also I need to create a follow up PR that implicitly assumes `self`
argument has type `Self`.

In order to infer the type in the `in_type_expression` method I needed
to have scope id and semantic index available. I used the idea from
[this PR](https://github.com/astral-sh/ruff/pull/17589/files) to pass
additional context to this method.
Also I think in all places that `in_type_expression` is called we need
to have this context because `Self` can be there so I didn't split the
method into one version with context and one without.

## Test Plan

Added new tests from spec.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-07 15:58:00 -07:00
Alex Waygood
51cef5a72b [ty] Recognise functions containing yield from expressions as being generator functions (#17930) 2025-05-07 23:29:44 +01:00
Douglas Creager
2cf5cba7ff [ty] Check base classes when determining subtyping etc for generic aliases (#17927)
#17897 added variance handling for legacy typevars — but they were only
being considered when checking generic aliases of the same class:

```py
class A: ...
class B(A): ...

class C[T]: ...

static_assert(is_subtype_of(C[B], C[A]))
```

and not for generic subclasses:

```py
class D[U](C[U]): ...

static_assert(is_subtype_of(D[B], C[A]))
```

Now we check those too!

Closes https://github.com/astral-sh/ty/issues/101
2025-05-07 15:21:11 -04:00
yunchi
ce0800fccf [pylint] add fix safety section (PLC2801) (#17825)
parent: #15584 
fix was introduced at: #9587 
reasoning: #9572
2025-05-07 14:34:34 -04:00
Micha Reiser
d03a7069ad Add instructions on how to upgrade to a newer Rust version (#17928) 2025-05-07 20:11:58 +02:00
Abhijeet Prasad Bodas
f5096f2050 [parser] Flag single unparenthesized generator expr with trailing comma in arguments. (#17893)
Fixes #17867

## Summary

The CPython parser does not allow generator expressions which are the
sole arguments in an argument list to have a trailing comma.
With this change, we start flagging such instances.

## Test Plan

Added new inline tests.
2025-05-07 14:11:35 -04:00
Alex Waygood
895b6161a6 [ty] Ensure that T is disjoint from ~T even when T is a TypeVar (#17922)
Same as https://github.com/astral-sh/ruff/pull/17910 but for
disjointness
2025-05-07 10:59:16 -07:00
Alex Waygood
74fe7982ba [ty] Sort collected diagnostics before snapshotting them in mdtest (#17926) 2025-05-07 18:23:22 +01:00
Micha Reiser
51386b3c7a [ty] Add basic file watching to server (#17912) 2025-05-07 19:03:30 +02:00
Charlie Marsh
51e2effd2d Make completions an opt-in LSP feature (#17921)
## Summary

We now expect the client to send initialization options to opt-in to
experimental (but LSP-standardized) features, like completion support.
Specifically, the client should set `"experimental.completions.enable":
true`.

Closes https://github.com/astral-sh/ty/issues/74.
2025-05-07 16:39:35 +00:00
Dhruv Manilawala
82d31a6014 Add link to ty issue tracker (#17924)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-07 16:33:27 +00:00
Dhruv Manilawala
78054824c0 [ty] Add support for __all__ (#17856)
## Summary

This PR adds support for the `__all__` module variable.

Reference spec:
https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols

This PR adds a new `dunder_all_names` query that returns a set of
`Name`s defined in the `__all__` variable of the given `File`. The query
works by implementing the `StatementVisitor` and collects all the names
by recognizing the supported idioms as mentioned in the spec. Any idiom
that's not recognized are ignored.

The current implementation is minimum to what's required for us to
remove all the false positives that this is causing. Refer to the
"Follow-ups" section below to see what we can do next. I'll a open
separate issue to keep track of them.

Closes: astral-sh/ty#106 
Closes: astral-sh/ty#199

### Follow-ups

* Diagnostics:
* Add warning diagnostics for unrecognized `__all__` idioms, `__all__`
containing non-string element
* Add an error diagnostic for elements that are present in `__all__` but
not defined in the module. This could lead to runtime error
* Maybe we should return `<type>` instead of `Unknown | <type>` for
`module.__all__`. For example:
https://playknot.ruff.rs/2a6fe5d7-4e16-45b1-8ec3-d79f2d4ca894
* Mark a symbol that's mentioned in `__all__` as used otherwise it could
raise (possibly in the future) "unused-name" diagnostic

Supporting diagnostics will require that we update the return type of
the query to be something other than `Option<FxHashSet<Name>>`,
something that behaves like a result and provides a way to check whether
a name exists in `__all__`, loop over elements in `__all__`, loop over
the invalid elements, etc.

## Ecosystem analysis

The following are the maximum amount of diagnostics **removed** in the
ecosystem:

* "Type <module '...'> has no attribute ..."
    * `collections.abc` - 14
    * `numpy` - 35534
    * `numpy.ma` - 296
    * `numpy.char` - 37
    * `numpy.testing` - 175
    * `hashlib` - 311
    * `scipy.fft` - 2
    * `scipy.stats` - 38
* "Module '...' has no member ..."
    * `collections.abc` - 85
    * `numpy` - 508
    * `numpy.testing` - 741
    * `hashlib` - 36
    * `scipy.stats` - 68
    * `scipy.interpolate` - 7
    * `scipy.signal` - 5

The following modules have dynamic `__all__` definition, so `ty` assumes
that `__all__` doesn't exists in that module:
* `scipy.stats`
(95a5d6ea8b/scipy/stats/__init__.py (L665))
* `scipy.interpolate`
(95a5d6ea8b/scipy/interpolate/__init__.py (L221))
* `scipy.signal` (indirectly via
95a5d6ea8b/scipy/signal/_signal_api.py (L30))
* `numpy.testing`
(de784cd6ee/numpy/testing/__init__.py (L16-L18))

~There's this one category of **false positives** that have been added:~
Fixed the false positives by also ignoring `__all__` from a module that
uses unrecognized idioms.

<details><summary>Details about the false postivie:</summary>
<p>

The `scipy.stats` module has dynamic `__all__` and it imports a bunch of
symbols via star imports. Some of those modules have a mix of valid and
invalid `__all__` idioms. For example, in
95a5d6ea8b/scipy/stats/distributions.py (L18-L24),
2 out of 4 `__all__` idioms are invalid but currently `ty` recognizes
two of them and says that the module has a `__all__` with 5 values. This
leads to around **2055** newly added false positives of the form:
```
Type <module 'scipy.stats'> has no attribute ...
```

I think the fix here is to completely ignore `__all__`, not only if
there are invalid elements in it, but also if there are unrecognized
idioms used in the module.

</p>
</details> 

## Test Plan

Add a bunch of test cases using the new `ty_extensions.dunder_all_names`
function to extract a module's `__all__` names.

Update various test cases to remove false positives around `*` imports
and re-export convention.

Add new test cases for named import behavior as `*` imports covers all
of it already (thanks Alex!).
2025-05-07 21:42:42 +05:30
Alex Waygood
c6f4929cdc [ty] fix assigning a typevar to a union with itself (#17910)
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-07 15:50:22 +00:00
Alex Waygood
2ec0d7e072 [ty] Improve UX for [duplicate-base] diagnostics (#17914) 2025-05-07 15:27:37 +00:00
Charlie Marsh
ad658f4d68 Clean up some Ruff references in the ty server (#17920)
## Summary

Anything user-facing, etc.
2025-05-07 10:55:16 -04:00
Abhijeet Prasad Bodas
3dedd70a92 [ty] Detect overloads decorated with @dataclass_transform (#17835)
## Summary

Fixes #17541

Before this change, in the case of overloaded functions,
`@dataclass_transform` was detected only when applied to the
implementation, not the overloads.
However, the spec also allows this decorator to be applied to any of the
overloads as well.
With this PR, we start handling `@dataclass_transform`s applied to
overloads.

## Test Plan

Fixed existing TODOs in the test suite.
2025-05-07 15:51:13 +02:00
David Peter
fab862c8cd [ty] Ecosystem checks: activate running on 'manticore' (#17916)
## Summary

This is sort of an anticlimactic resolution to #17863, but now that we
understand what the root cause for the stack overflows was, I think it's
fine to enable running on this project. See the linked ticket for the
full analysis.

closes #17863

## Test Plan

Ran lots of times locally and never observed a crash at worker thread
stack sizes > 8 MiB.
2025-05-07 06:27:36 -07:00
Douglas Creager
0d9b6a0975 [ty] Handle explicit variance in legacy typevars (#17897)
We now track the variance of each typevar, and obey the `covariant` and
`contravariant` parameters to the legacy `TypeVar` constructor. We still
don't yet infer variance for PEP-695 typevars or for the
`infer_variance` legacy constructor parameter.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-07 08:44:51 -04:00
Micha Reiser
c5e299e796 Update salsa (#17895) 2025-05-07 09:51:15 +02:00
Victor Hugo Gomes
c504001b32 [pyupgrade] Add spaces between tokens as necessary to avoid syntax errors in UP018 autofix (#17648)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-07 09:34:08 +02:00
David Peter
04457f99b6 [ty] Protocols: Fixpoint iteration for fully-static check (#17880)
## Summary

A recursive protocol like the following would previously lead to stack
overflows when attempting to create the union type for the `P | None`
member, because `UnionBuilder` checks if element types are fully static,
and the fully-static check on `P` would in turn list all members and
check whether all of them were fully static, leading to a cycle.

```py
from __future__ import annotations

from typing import Protocol

class P(Protocol):
    parent: P | None
```

Here, we make the fully-static check on protocols a salsa query and add
fixpoint iteration, starting with `true` as the initial value (assume
that the recursive protocol is fully-static). If the recursive protocol
has any non-fully-static members, we still return `false` when
re-executing the query (see newly added tests).

closes #17861

## Test Plan

Added regression test
2025-05-07 08:55:21 +02:00
InSync
a33d0d4bf4 [ty] Support generate-shell-completion (#17879)
## Summary

Resolves #15502.

`ty generate-shell-completion` now works in a similar manner to `ruff
generate-shell-completion`.

## Test Plan

Manually:

<details>

```shell
$ cargo run --package ty generate-shell-completion nushell
module completions {

  # An extremely fast Python type checker.
  export extern ty [
    --help(-h)                # Print help
    --version(-V)             # Print version
  ]
  
  # ...

}

export use completions *
```
</details>
2025-05-06 18:04:57 -07:00
Charlie Marsh
443f62e98d Remove condensed display type enum (#17902)
## Summary

See: https://github.com/astral-sh/ruff/pull/17889#discussion_r2076556002
2025-05-06 18:04:03 -07:00
Charlie Marsh
a2e9a7732a Update class literal display to use <class 'Foo'> style (#17889)
## Summary

Closes https://github.com/astral-sh/ruff/issues/17238.
2025-05-06 20:11:25 -04:00
Micha Reiser
b2de749c32 Add a note to diagnostics why the rule is enabled (#17854) 2025-05-06 20:29:03 +02:00
Douglas Creager
9085f18353 [ty] Propagate specializations to ancestor base classes (#17892)
@AlexWaygood discovered that even though we've been propagating
specializations to _parent_ base classes correctly, we haven't been
passing them on to _grandparent_ base classes:
https://github.com/astral-sh/ruff/pull/17832#issuecomment-2854360969

```py
class Bar[T]:
    x: T

class Baz[T](Bar[T]): ...
class Spam[T](Baz[T]): ...

reveal_type(Spam[int]().x) # revealed: `T`, but should be `int`
```

This PR updates the MRO machinery to apply the current specialization
when starting to iterate the MRO of each base class.
2025-05-06 14:25:21 -04:00
Dylan
8152ba7cb7 [ty] Add minimal docs for a few lints (#17874)
Just the bare minimum to remove a few TODOs - omitted examples, and only
did 9 but I will check back tomorrow and try to knock out a few more!
2025-05-06 10:36:47 -07:00
Aria Desires
3d01d3be3e update the repository for ty (#17891)
This metadata is used by cargo-dist for artifact URLs (in curl-sh
expressions)
2025-05-06 12:03:38 -04:00
Brent Westbrook
4510a236d3 Default to latest supported Python version for version-related syntax errors (#17529)
## Summary

This PR partially addresses #16418 via the following:

- `LinterSettings::unresolved_python_version` is now a `TargetVersion`,
which is a thin wrapper around an `Option<PythonVersion>`
- `Checker::target_version` now calls `TargetVersion::linter_version`
internally, which in turn uses `unwrap_or_default` to preserve the
current default behavior
- Calls to the parser now call `TargetVersion::parser_version`, which
calls `unwrap_or_else(PythonVersion::latest)`
- The `Checker`'s implementation of
`SemanticSyntaxContext::python_version` also uses
`TargetVersion::parser_version` to use `PythonVersion::latest` for
semantic errors

In short, all lint rule behavior should be unchanged, but we default to
the latest Python version for the new syntax errors, which should
minimize confusing version-related syntax errors for users without a
version configured.

## Test Plan

Existing tests, which showed no changes (except for printing default
settings).
2025-05-06 10:19:13 -04:00
Marcus Näslund
76b6d53d8b Add new rule InEmptyCollection (#16480)
## Summary

Introducing a new rule based on discussions in #15732 and #15729 that
checks for unnecessary in with empty collections.

I called it in_empty_collection and gave the rule number RUF060.

Rule is in preview group.
2025-05-06 13:52:07 +00:00
Zanie Blue
f82b72882b Display ty version for ty --version and ty -V (#17888)
e.g.,

```
❯ uv run -q -- ty -V
ty 0.0.0-alpha.4 (08881edba 2025-05-05)
❯ uv run -q -- ty --version
ty 0.0.0-alpha.4 (08881edba 2025-05-05)
```

Previously, this just displayed `ty 0.0.0` because it didn't use our
custom version implementation. We no longer have a short version —
matching the interface in uv. We could add a variant for it, if it seems
important to people. However, I think we found it more confusing than
not over there and didn't get any complaints about the change.

Closes https://github.com/astral-sh/ty/issues/54
2025-05-06 08:06:41 -05:00
Zanie Blue
d07eefc408 Parse dist-workspace.toml for version (#17868)
Extends https://github.com/astral-sh/ruff/pull/17866, using
`dist-workspace.toml` as a source of truth for versions to enable
version retrieval in distributions that are not Git repositories (i.e.,
Python source distributions and source tarballs consumed by Linux
distros).

I retain the Git tag lookup from
https://github.com/astral-sh/ruff/pull/17866 as a fallback — it seems
harmless, but we could drop it to simplify things here.

I confirmed this works from the repository as well as Python source and
binary distributions:

```
❯ uv run --refresh-package ty --reinstall-package ty -q --  ty version
ty 0.0.1-alpha.1+5 (2eadc9e61 2025-05-05)
❯ uv build
...
❯ uvx --from ty@dist/ty-0.0.0a1.tar.gz --no-cache -q -- ty version
ty 0.0.1-alpha.1
❯ uvx --from ty@dist/ty-0.0.0a1-py3-none-macosx_11_0_arm64.whl -q -- ty version
ty 0.0.1-alpha.1
```

Requires https://github.com/astral-sh/ty/pull/36

cc @Gankra and @MichaReiser for review.
2025-05-06 12:18:17 +00:00
Zanie Blue
f7237e3b69 Update ty version to use parent Git repository information (#17866)
Currently, `ty version` pulls its information from the Ruff repository —
but we want this to pull from the repository in the directory _above_
when Ruff is a submodule.

I tested this in the `ty` repository after tagging an arbitrary commit:

```
❯ uv run --refresh-package ty --reinstall-package ty ty version
      Built ty @ file:///Users/zb/workspace/ty
Uninstalled 1 package in 2ms
Installed 1 package in 1ms
ty 0.0.0+3 (34253b1d4 2025-05-05)
```

We also use the last Git tag as the source of truth for the version,
instead of the crate version. However, we'll need a way to set the
version for releases still, as the tag is published _after_ the build.
We can either tag early (without pushing the tag to the remote), or add
another environment variable. (**Note, this approach is changed in a
follow-up. See https://github.com/astral-sh/ruff/pull/17868**)

From this repository, the version will be `unknown`:

```
❯ cargo run -q --bin ty -- version
ty unknown
```

We could add special handling like... `ty unknown (ruff@...)` but I see
that as a secondary goal.

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

The reviewer situation in this repository is unhinged, cc @Gankra and
@MichaReiser for review.
2025-05-06 07:14:30 -05:00
Alex Waygood
2f9992b6ef [ty] Fix duplicate diagnostics for unresolved module when an import from statement imports multiple members (#17886) 2025-05-06 12:37:10 +01:00
Alex Waygood
457ec4dddd Generalize special-casing for enums constructed with the functional syntax (#17885) 2025-05-06 11:02:55 +01:00
Micha Reiser
aa0614509b Update salsa to pull in fixpoint fixes (#17847) 2025-05-06 11:27:51 +02:00
Micha Reiser
9000eb3bfd Update favicon and URL for playground (#17860) 2025-05-06 10:51:45 +02:00
Micha Reiser
7f50b503cf Allowlist play.ty.dev (#17857) 2025-05-06 10:16:17 +02:00
Micha Reiser
24d3fc27fb Fixup wording of fatal error warning (#17881) 2025-05-06 09:44:23 +02:00
Micha Reiser
6f821ac846 Show a warning at the end of the diagnostic list if there are any fatal warnings (#17855) 2025-05-06 07:14:21 +00:00
Charlie Marsh
d410d12bc5 Update code of conduct email address (#17875)
## Summary

For consistency with the `pyproject.toml`, etc.
2025-05-05 21:34:45 -04:00
Alex Waygood
89424cce5f [ty] Do not emit errors if enums or NamedTuples constructed using functional syntax are used in type expressions (#17873)
## Summary

This fixes some false positives that showed up in the primer diff for
https://github.com/astral-sh/ruff/pull/17832

## Test Plan

new mdtests added that fail with false-positive diagnostics on `main`
2025-05-06 00:37:24 +01:00
Shunsuke Shibayama
fd76d70a31 [red-knot] fix narrowing in nested scopes (#17630)
## Summary

This PR fixes #17595.

## Test Plan

New test cases are added to `mdtest/narrow/conditionals/nested.md`.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-05 16:28:42 -07:00
yunchi
a4c8e43c5f [pylint] Add fix safety section (PLR1722) (#17826)
parent: #15584 
fix introduced at: #816
2025-05-05 19:13:04 -04:00
Douglas Creager
ada4c4cb1f [ty] Don't require default typevars when specializing (#17872)
If a typevar is declared as having a default, we shouldn't require a
type to be specified for that typevar when explicitly specializing a
generic class:

```py
class WithDefault[T, U = int]: ...

reveal_type(WithDefault[str]())  # revealed: WithDefault[str, int]
```

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-05-05 18:29:30 -04:00
Alex Waygood
bb6c7cad07 [ty] Fix false-positive [invalid-return-type] diagnostics on generator functions (#17871) 2025-05-05 21:44:59 +00:00
Douglas Creager
47e3aa40b3 [ty] Specialize bound methods and nominal instances (#17865)
Fixes
https://github.com/astral-sh/ruff/pull/17832#issuecomment-2851224968. We
had a comment that we did not need to apply specializations to generic
aliases, or to the bound `self` of a bound method, because they were
already specialized. But they might be specialized with a type variable,
which _does_ need to be specialized, in the case of a "multi-step"
specialization, such as:

```py
class LinkedList[T]: ...

class C[U]:
    def method(self) -> LinkedList[U]:
        return LinkedList[U]()
```

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-05-05 17:17:36 -04:00
David Peter
9a6633da0b [ty] ecosystem: activate running on 'sympy' (#17870)
## Summary

Following #17869, we can now run `ty` on `sympy`.
2025-05-05 21:50:13 +02:00
David Peter
de78da5ee6 [ty] Increase worker-thread stack size (#17869)
## Summary

closes #17472 

This is obviously just a band-aid solution to this problem (in that you
can always make your [pathological
inputs](28994edd82/sympy/polys/numberfields/resolvent_lookup.py)
bigger and it will still crash), but I think this is not an unreasonable
change — even if we add more sophisticated solutions later. I tried
using `stacker` as suggested by @MichaReiser, and it works. But it's
unclear where exactly would be the right place to put it, and even for
the `sympy` problem, we would need to add it both in the semantic index
builder AST traversal and in type inference. Increasing the default
stack size for worker threads, as proposed here, doesn't solve the
underlying problem (that there is a hard limit), but it is more
universal in the sense that it is not specific to large binary-operator
expression chains.

To determine a reasonable stack size, I created files that look like

*right associative*:
```py
from typing import reveal_type
total = (1 + (1 + (1 + (1 + (… + 1)))))
reveal_type(total)
```

*left associative*
```py
from typing import reveal_type
total = 1 + 1 + 1 + 1 + … + 1
reveal_type(total)
```

with a variable amount of operands (`N`). I then chose the stack size
large enough to still be able to handle cases that existing type
checkers can not:

```
right

  N = 20: mypy takes ~ 1min
  N = 350: pyright crashes with a stack overflow (mypy fails with "too many nested parentheses")
  N = 800: ty(main) infers Literal[800] instantly
  N = 1000: ty(main) crashes with "thread '<unknown>' has overflowed its stack"

  N = 7000: ty(this branch) infers Literal[7000] instantly
  N = 8000+: ty(this branch) crashes


left

  N = 300: pyright emits "Maximum parse depth exceeded; break expression into smaller sub-expressions"
           total is inferred as Unknown
  N = 5500: mypy crashes with "INTERNAL ERROR"
  N = 2500: ty(main) infers Literal[2500] instantly
  N = 3000: ty(main) crashes with "thread '<unknown>' has overflowed its stack"

  N = 22000: ty(this branch) infers Literal[22000] instantly
  N = 23000+: ty(this branch) crashes
```

## Test Plan

New regression test.
2025-05-05 21:31:55 +02:00
Carl Meyer
20d64b9c85 [ty] add passing projects to primer (#17834)
Add projects to primer that now pass, with
https://github.com/astral-sh/ruff/pull/17833
2025-05-05 12:21:06 -07:00
Carl Meyer
4850c187ea [ty] add cycle handling for FunctionType::signature query (#17833)
This fixes cycle panics in several ecosystem projects (moved to
`good.txt` in a following PR
https://github.com/astral-sh/ruff/pull/17834 because our mypy-primer job
doesn't handle it well if we move projects to `good.txt` in the same PR
that fixes `ty` to handle them), as well as in the minimal case in the
added mdtest. It also fixes a number of panicking fuzzer seeds. It
doesn't appear to cause any regression in any ecosystem project or any
fuzzer seed.
2025-05-05 12:12:38 -07:00
Vasco Schiavo
3f32446e16 [ruff] add fix safety section (RUF013) (#17759)
The PR add the fix safety section for rule `RUF013`
(https://github.com/astral-sh/ruff/issues/15584 )
The fix was introduced here #4831

The rule as a lot of False Negative (as it is explained in the docs of
the rule).

The main reason because the fix is unsafe is that it could change code
generation tools behaviour, as in the example here:

```python
def generate_api_docs(func):
    hints = get_type_hints(func)
    for param, hint in hints.items():
        if is_optional_type(hint):
            print(f"Parameter '{param}' is optional")
        else:
            print(f"Parameter '{param}' is required")

# Before fix
def create_user(name: str, roles: list[str] = None):
    pass

# After fix
def create_user(name: str, roles: Optional[list[str]] = None):
    pass

# Generated docs would change from "roles is required" to "roles is optional"
```
2025-05-05 13:47:56 -05:00
Aria Desires
784daae497 migrate to dist-workspace.toml, use new workspace.packages config (#17864) 2025-05-05 14:07:46 -04:00
Max Mynter
178c882740 [semantic-syntax-tests] Add test fixtures for AwaitOutsideAsyncFunction (#17785)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->
Re: #17526 
## Summary
Add test fixtures for `AwaitOutsideAsync` and
`AsyncComprehensionOutsideAsyncFunction` errors.

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
This is a test. 

<!-- How was it tested? -->
2025-05-05 14:02:06 -04:00
Max Mynter
101e1a5ddd [semantic-syntax-tests] for for InvalidStarExpression, DuplicateMatchKey, and DuplicateMatchClassAttribute (#17754)
Re: #17526 

## Summary
Add integration tests for Python Semantic Syntax for
`InvalidStarExpression`, `DuplicateMatchKey`, and
`DuplicateMatchClassAttribute`.

## Note
- Red knot integration tests for `DuplicateMatchKey` exist already in
line 89-101.
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
This is a test.
<!-- How was it tested? -->
2025-05-05 17:30:16 +00:00
Dylan
965a4dd731 [isort] Check full module path against project root(s) when categorizing first-party (#16565)
When attempting to determine whether `import foo.bar.baz` is a known
first-party import relative to [user-provided source
paths](https://docs.astral.sh/ruff/settings/#src), when `preview` is
enabled we now check that `SRC/foo/bar/baz` is a directory or
`SRC/foo/bar/baz.py` or `SRC/foo/bar/baz.pyi` exist.

Previously, we just checked the analogous thing for `SRC/foo`, but this
can be misleading in situations with disjoint namespace packages that
share a common base name (e.g. we may be working inside the namespace
package `foo.buzz` and importing `foo.bar` from elsewhere).

Supersedes #12987 
Closes #12984
2025-05-05 11:40:01 -05:00
Victor Hugo Gomes
5e2c818417 [flake8-bandit] Mark tuples of string literals as trusted input in S603 (#17801)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

Fixes #17798
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

Snapshot tests
<!-- How was it tested? -->
2025-05-05 10:50:44 -04:00
David Peter
90c12f4177 [ty] ecosystem: Activate running on 'materialize' (#17862) 2025-05-05 16:34:23 +02:00
Wei Lee
6e9fb9af38 [airflow] Skip attribute check in try catch block (AIR301) (#17790)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Skip attribute check in try catch block (`AIR301`)

## Test Plan

<!-- How was it tested? -->
update
`crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_try.py`
2025-05-05 10:01:05 -04:00
Wei Lee
a507c1b8b3 [airflow] Remove airflow.utils.dag_parsing_context.get_parsing_context (AIR301) (#17852)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
Remove `airflow.utils.dag_parsing_context.get_parsing_context` from
AIR301 as it has been moved to AIR311

## Test Plan

<!-- How was it tested? -->

the test fixture was updated in the previous PR
2025-05-05 09:31:57 -04:00
David Peter
5a91badb8b [ty] Move 'scipy' to list of 'good' projects (#17850)
## Summary

Adds `scipy` as a new project to `good.txt` as a follow-up to #17849.
2025-05-05 13:52:57 +02:00
David Peter
1945bfdb84 [ty] Fix standalone expression type retrieval in presence of cycles (#17849)
## Summary

When entering an `infer_expression_types` cycle from
`TypeInferenceBuilder::infer_standalone_expression`, we might get back a
`TypeInference::cycle_fallback(…)` that doesn't actually contain any new
types, but instead it contains a `cycle_fallback_type` which is set to
`Some(Type::Never)`. When calling `self.extend(…)`, we therefore don't
really pull in a type for the expression we're interested in. This
caused us to panic if we tried to call `self.expression_type(…)` after
`self.extend(…)`.

The proposed fix here is to retrieve that type from the nested
`TypeInferenceBuilder` directly, which will correctly fall back to
`cycle_fallback_type`.

## Details

I minimized the second example from #17792 a bit further and used this
example for debugging:

```py
from __future__ import annotations

class C: ...

def f(arg: C):
    pass

x, _ = f(1)

assert x
```

This is self-referential because when we check the assignment statement
`x, _ = f(1)`, we need to look up the signature of `f`. Since evaluation
of annotations is deferred, we look up the public type of `C` for the
`arg` parameter. The public use of `C` is visibility-constraint by "`x`"
via the `assert` statement. While evaluating this constraint, we need to
look up the type of `x`, which in turn leads us back to the `x, _ =
f(1)` definition.

The reason why this only showed up in the relatively peculiar case with
unpack assignments is the code here:


78b4c3ccf1/crates/ty_python_semantic/src/types/infer.rs (L2709-L2718)

For a non-unpack assignment like `x = f(1)`, we would not try to infer
the right-hand side eagerly. Instead, we would enter a
`infer_definition_types` cycle that handles the situation correctly. For
unpack assignments, however, we try to infer the type of `value`
(`f(1)`) and therefore enter the cycle via `standalone_expression_type
=> infer_expression_type`.

closes #17792 

## Test Plan

* New regression test
* Made sure that we can now run successfully on scipy => see #17850
2025-05-05 13:52:08 +02:00
Dylan
a95c73d5d0 Implement deferred annotations for Python 3.14 (#17658)
This PR updates the semantic model for Python 3.14 by essentially
equating "run using Python 3.14" with "uses `from __future__ import
annotations`".

While this is not technically correct under the hood, it appears to be
correct for the purposes of our semantic model. That is: from the point
of view of deciding when to parse, bind, etc. annotations, these two
contexts behave the same. More generally these contexts behave the same
unless you are performing some kind of introspection like the following:


Without future import:
```pycon
>>> from annotationlib import get_annotations,Format
>>> def foo()->Bar:...
...
>>> get_annotations(foo,format=Format.FORWARDREF)
{'return': ForwardRef('Bar')}
>>> get_annotations(foo,format=Format.STRING)
{'return': 'Bar'}
>>> get_annotations(foo,format=Format.VALUE)
Traceback (most recent call last):
[...]
NameError: name 'Bar' is not defined
>>> get_annotations(foo)
Traceback (most recent call last):
[...]
NameError: name 'Bar' is not defined
```

With future import:
```
>>> from __future__ import annotations
>>> from annotationlib import get_annotations,Format
>>> def foo()->Bar:...
...
>>> get_annotations(foo,format=Format.FORWARDREF)
{'return': 'Bar'}
>>> get_annotations(foo,format=Format.STRING)
{'return': 'Bar'}
>>> get_annotations(foo,format=Format.VALUE)
{'return': 'Bar'}
>>> get_annotations(foo)
{'return': 'Bar'}
```

(Note: the result of the last call to `get_annotations` in these
examples relies on the fact that, as of this writing, the default value
for `format` is `Format.VALUE`).

If one day we support lint rules targeting code that introspects using
the new `annotationlib`, then it is possible we will need to revisit our
approximation.

Closes #15100
2025-05-05 06:40:36 -05:00
David Peter
78b4c3ccf1 [ty] Minor typo in environment variable name (#17848) 2025-05-05 10:25:48 +00:00
renovate[bot]
2485afe640 Update pre-commit dependencies (#17840)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-05 07:36:09 +00:00
renovate[bot]
b8ed729f59 Update taiki-e/install-action digest to 86c23ee (#17838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 07:43:54 +02:00
renovate[bot]
108c470348 Update actions/download-artifact action to v4.3.0 (#17845)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 07:43:33 +02:00
renovate[bot]
87c64c9eab Update actions/setup-python action to v5.6.0 (#17846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 07:43:01 +02:00
renovate[bot]
a10606dda2 Update Rust crate jiff to v0.2.12 (#17843) 2025-05-05 07:34:09 +02:00
renovate[bot]
d1c6dd9ac1 Update Rust crate toml to v0.8.22 (#17844) 2025-05-05 07:32:50 +02:00
renovate[bot]
073b993ab0 Update Rust crate assert_fs to v1.1.3 (#17841) 2025-05-05 07:31:02 +02:00
renovate[bot]
6a36cd6f02 Update Rust crate hashbrown to v0.15.3 (#17842) 2025-05-05 07:30:46 +02:00
renovate[bot]
3b15af6d4f Update dependency ruff to v0.11.8 (#17839) 2025-05-05 07:29:36 +02:00
Micha Reiser
e95130ad80 Introduce TY_MAX_PARALLELISM environment variable (#17830) 2025-05-04 16:27:15 +02:00
Micha Reiser
68e32c103f Ignore PRs labeled with ty for Ruff changelog (#17831) 2025-05-04 14:42:10 +01:00
Brent Westbrook
fe4051b2e6 Fix missing combine call for lint.typing-extensions setting (#17823)
## Summary

Fixes #17821.

## Test Plan

New CLI test. This might be overkill for such a simple fix, but it made
me feel better to add a test.
2025-05-03 18:19:19 -04:00
Micha Reiser
fa628018b2 Use #[expect(lint)] over #[allow(lint)] where possible (#17822) 2025-05-03 21:20:31 +02:00
Eric Botti
8535af8516 [red-knot] Add support for the LSP diagnostic tag (#17657)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-03 20:35:03 +02:00
Micha Reiser
b51c4f82ea Rename Red Knot (#17820) 2025-05-03 19:49:15 +02:00
Alex Waygood
e6a798b962 [red-knot] Recurse into the types of protocol members when normalizing a protocol's interface (#17808)
## Summary

Currently red-knot does not understand `Foo` and `Bar` here as being
equivalent:

```py
from typing import Protocol

class A: ...
class B: ...
class C: ...

class Foo(Protocol):
    x: A | B | C

class Bar(Protocol):
    x: B | A | C
```

Nor does it understand `A | B | Foo` as being equivalent to `Bar | B |
A`. This PR fixes that.

## Test Plan

new mdtest assertions added that fail on `main`
2025-05-03 16:43:37 +01:00
Alex Waygood
52b0470870 [red-knot] Synthesize a __call__ attribute for Callable types (#17809)
## Summary

Currently this assertion fails on `main`, because we do not synthesize a
`__call__` attribute for Callable types:

```py
from typing import Protocol, Callable
from knot_extensions import static_assert, is_assignable_to

class Foo(Protocol):
    def __call__(self, x: int, /) -> str: ...

static_assert(is_assignable_to(Callable[[int], str], Foo))
```

This PR fixes that.

See previous discussion about this in
https://github.com/astral-sh/ruff/pull/16493#discussion_r1985098508 and
https://github.com/astral-sh/ruff/pull/17682#issuecomment-2839527750

## Test Plan

Existing mdtests updated; a couple of new ones added.
2025-05-03 16:43:18 +01:00
Brent Westbrook
c4a08782cc Add regression test for parent noqa (#17783)
Summary
--

Adds a regression test for #2253 after I tried to delete the fix from
#2464.
2025-05-03 11:38:31 -04:00
Alex Waygood
91481a8be7 [red-knot] Minor simplifications to types/display.rs (#17813) 2025-05-03 15:29:05 +01:00
Victor Hugo Gomes
097af060c9 [refurb] Fix false positive for float and complex numbers in FURB116 (#17661) 2025-05-03 15:59:46 +02:00
Alex Waygood
b7d0b3f9e5 [red-knot] Add tests asserting that subclasses of Any are assignable to arbitrary protocol types (#17810) 2025-05-03 12:41:55 +00:00
Alex Waygood
084352f72c [red-knot] Distinguish fully static protocols from non-fully-static protocols (#17795) 2025-05-03 11:12:23 +01:00
Carl Meyer
78d4356301 [red-knot] add tracing of salsa events in mdtests (#17803) 2025-05-03 09:00:11 +02:00
Douglas Creager
96697c98f3 [red-knot] Legacy generic classes (#17721)
This adds support for legacy generic classes, which use a
`typing.Generic` base class, or which inherit from another generic class
that has been specialized with legacy typevars.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-02 20:34:20 -04:00
Micha Reiser
f7cae4ffb5 [red-knot] Don't panic when primary-span is missing while panicking (#17799) 2025-05-02 21:19:03 +01:00
Douglas Creager
675a5af89a [red-knot] Use Vec in CallArguments; reuse self when we can (#17793)
Quick follow-on to #17788. If there is no bound `self` parameter, we can
reuse the existing `CallArgument{,Type}s`, and we can use a straight
`Vec` instead of a `VecDeque`.
2025-05-02 12:00:02 -04:00
David Peter
ea3f4ac059 [red-knot] Refactor: no mutability in call APIs (#17788)
## Summary

Remove mutability in parameter types for a few functions such as
`with_self` and `try_call`. I tried the `Rc`-approach with cheap cloning
[suggest
here](https://github.com/astral-sh/ruff/pull/17733#discussion_r2068722860)
first, but it turns out we need a whole stack of prepended arguments
(there can be [both `self` *and*
`cls`](3cf44e401a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md (L113))),
and we would need the same construct not just for `CallArguments` but
also for `CallArgumentTypes`. At that point we're cloning `VecDeque`s
anyway, so the overhead of cloning the whole `VecDeque` with all
arguments didn't seem to justify the additional code complexity.

## Benchmarks

Benchmarks on tomllib, black, jinja, isort seem neutral.
2025-05-02 13:53:19 +02:00
David Peter
6d2c10cca2 [red-knot] Fix panic for tuple[x[y]] string annotation (#17787)
## Summary

closes #17775

## Test Plan

Added corpus regression test
2025-05-02 12:11:47 +02:00
David Peter
3cf44e401a [red-knot] Implicit instance attributes in generic methods (#17769)
## Summary

Add the ability to detect instance attribute assignments in class
methods that are generic.

This does not address the code duplication mentioned in #16928. I can
open a ticket for this after this has been merged.

closes #16928

## Test Plan

Added regression test.
2025-05-02 08:20:37 +00:00
Micha Reiser
17050e2ec5 doc: Add link to check-typed-exception from S110 and S112 (#17786) 2025-05-02 09:25:58 +02:00
Micha Reiser
a6dc04f96e Fix module name in ASYNC110, 115, and 116 fixes (#17774) 2025-05-01 23:37:09 +02:00
David Peter
e515899141 [red-knot] More informative hover-types for assignments (#17762)
## Summary

closes https://github.com/astral-sh/ruff/issues/17122

## Test Plan

* New hover tests
* Opened the playground locally and saw that new hover-types are shown
as expected.
2025-05-01 20:33:51 +02:00
Abhijeet Prasad Bodas
0c80c56afc [syntax-errors] Use consistent message for bad starred expression usage. (#17772) 2025-05-01 20:18:35 +02:00
Andrew Gallant
b7ce694162 red_knot_server: add auto-completion MVP
This PR does the wiring necessary to respond to completion requests from
LSP clients.

As far as the actual completion results go, they are nearly about the
dumbest and simplest thing we can do: we simply return a de-duplicated
list of all identifiers from the current module.
2025-05-01 12:08:10 -04:00
Brent Westbrook
163d526407 Allow passing a virtual environment to ruff analyze graph (#17743)
Summary
--

Fixes #16598 by adding the `--python` flag to `ruff analyze graph`,
which adds a `PythonPath` to the `SearchPathSettings` for module
resolution. For the [albatross-virtual-workspace] example from the uv
repo, this updates the output from the initial issue:

```shell
> ruff analyze graph packages/albatross
{
  "packages/albatross/check_installed_albatross.py": [
    "packages/albatross/src/albatross/__init__.py"
  ],
  "packages/albatross/src/albatross/__init__.py": []
}
```

To include both the the workspace `bird_feeder` import _and_ the
third-party `tqdm` import in the output:

```shell
> myruff analyze graph packages/albatross --python .venv
{
  "packages/albatross/check_installed_albatross.py": [
    "packages/albatross/src/albatross/__init__.py"
  ],
  "packages/albatross/src/albatross/__init__.py": [
    ".venv/lib/python3.12/site-packages/tqdm/__init__.py",
    "packages/bird-feeder/src/bird_feeder/__init__.py"
  ]
}
```

Note the hash in the uv link! I was temporarily very confused why my
local tests were showing an `iniconfig` import instead of `tqdm` until I
realized that the example has been updated on the uv main branch, which
I had locally.

Test Plan
--

A new integration test with a stripped down venv based on the
`albatross` example.

[albatross-virtual-workspace]:
aa629c4a54/scripts/workspaces/albatross-virtual-workspace
2025-05-01 11:29:52 -04:00
Brent Westbrook
75effb8ed7 Bump 0.11.8 (#17766) 2025-05-01 10:19:58 -04:00
Victor Hugo Gomes
3353d07938 [flake8-use-pathlib] Fix PTH104false positive when rename is passed a file descriptor (#17712)
## Summary
Contains the same changes to the semantic type inference as
https://github.com/astral-sh/ruff/pull/17705.

Fixes #17694
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
Snapshot tests.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-05-01 10:01:17 -04:00
Alex Waygood
41f3f21629 Improve messages outputted by py-fuzzer (#17764) 2025-05-01 12:32:45 +00:00
Hans
76ec64d535 [red-knot] Allow subclasses of Any to be assignable to Callable types (#17717)
## Summary

Fixes #17701.

## Test plan

New Markdown test.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-05-01 10:18:12 +02:00
Micha Reiser
b7e69ecbfc [red-knot] Increase durability of read-only File fields (#17757) 2025-05-01 09:25:48 +02:00
Micha Reiser
9c57862262 [red-knot] Cache source type during semanic index building (#17756)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-05-01 08:51:53 +02:00
Victor Hugo Gomes
67ef370733 [flake8-use-pathlib] Fix PTH116 false positive when stat is passed a file descriptor (#17709)
Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-05-01 08:16:28 +02:00
github-actions[bot]
e17e1e860b Sync vendored typeshed stubs (#17753)
Co-authored-by: typeshedbot <>
2025-05-01 07:57:03 +02:00
David Peter
03d8679adf [red-knot] Preliminary NamedTuple support (#17738)
## Summary

Adds preliminary support for `NamedTuple`s, including:
* No false positives when constructing a `NamedTuple` object
* Correct signature for the synthesized `__new__` method, i.e. proper
checking of constructor calls
* A patched MRO (`NamedTuple` => `tuple`), mainly to make type inference
of named attributes possible, but also to better reflect the runtime
MRO.

All of this works:
```py
from typing import NamedTuple

class Person(NamedTuple):
    id: int
    name: str
    age: int | None = None

alice = Person(1, "Alice", 42)
alice = Person(id=1, name="Alice", age=42)

reveal_type(alice.id)  # revealed: int
reveal_type(alice.name)  # revealed: str
reveal_type(alice.age)  # revealed: int | None

# error: [missing-argument]
Person(3)

# error: [too-many-positional-arguments]
Person(3, "Eve", 99, "extra")

# error: [invalid-argument-type]
Person(id="3", name="Eve")
```

Not included:
* type inference for index-based access.
* support for the functional `MyTuple = NamedTuple("MyTuple", […])`
syntax

## Test Plan

New Markdown tests

## Ecosystem analysis

```
                          Diagnostic Analysis Report                           
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━┓
┃ Diagnostic ID                     ┃ Severity ┃ Removed ┃ Added ┃ Net Change ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━┩
│ lint:call-non-callable            │ error    │       0 │     3 │         +3 │
│ lint:call-possibly-unbound-method │ warning  │       0 │     4 │         +4 │
│ lint:invalid-argument-type        │ error    │       0 │    72 │        +72 │
│ lint:invalid-context-manager      │ error    │       0 │     2 │         +2 │
│ lint:invalid-return-type          │ error    │       0 │     2 │         +2 │
│ lint:missing-argument             │ error    │       0 │    46 │        +46 │
│ lint:no-matching-overload         │ error    │   19121 │     0 │     -19121 │
│ lint:not-iterable                 │ error    │       0 │     6 │         +6 │
│ lint:possibly-unbound-attribute   │ warning  │      13 │    32 │        +19 │
│ lint:redundant-cast               │ warning  │       0 │     1 │         +1 │
│ lint:unresolved-attribute         │ error    │       0 │    10 │        +10 │
│ lint:unsupported-operator         │ error    │       3 │     9 │         +6 │
│ lint:unused-ignore-comment        │ warning  │      15 │     4 │        -11 │
├───────────────────────────────────┼──────────┼─────────┼───────┼────────────┤
│ TOTAL                             │          │   19152 │   191 │     -18961 │
└───────────────────────────────────┴──────────┴─────────┴───────┴────────────┘

Analysis complete. Found 13 unique diagnostic IDs.
Total diagnostics removed: 19152
Total diagnostics added: 191
Net change: -18961
```

I uploaded the ecosystem full diff (ignoring the 19k
`no-matching-overload` diagnostics)
[here](https://shark.fish/diff-namedtuple.html).

* There are some new `missing-argument` false positives which come from
the fact that named tuples are often created using unpacking as in
`MyNamedTuple(*fields)`, which we do not understand yet.
* There are some new `unresolved-attribute` false positives, because
methods like `_replace` are not available.
* Lots of the `invalid-argument-type` diagnostics look like true
positives

---------

Co-authored-by: Douglas Creager <dcreager@dcreager.net>
2025-04-30 22:52:04 +02:00
Victor Hugo Gomes
d33a503686 [red-knot] Add tests for classes that have incompatible __new__ and __init__ methods (#17747)
Closes #17737
2025-04-30 20:40:16 +00:00
renovate[bot]
650cbdd296 Update dependency vite to v6.2.7 (#17746) 2025-04-30 22:12:03 +02:00
Dhruv Manilawala
d2a238dfad [red-knot] Update call binding to return all matching overloads (#17618)
## Summary

This PR updates the existing overload matching methods to return an
iterator of all the matched overloads instead.

This would be useful once the overload call evaluation algorithm is
implemented which should provide an accurate picture of all the matched
overloads. The return type would then be picked from either the only
matched overload or the first overload from the ones that are matched.

In an earlier version of this PR, it tried to check if using an
intersection of return types from the matched overload would help reduce
the false positives but that's not enough. [This
comment](https://github.com/astral-sh/ruff/pull/17618#issuecomment-2842891696)
keep the ecosystem analysis for that change for prosperity.

> [!NOTE]
>
> The best way to review this PR is by hiding the whitespace changes
because there are two instances where a large match expression is
indented to be inside a loop over matching overlods
>
> <img width="1207" alt="Screenshot 2025-04-28 at 15 12 16"
src="https://github.com/user-attachments/assets/e06cbfa4-04fa-435f-84ef-4e5c3c5626d1"
/>

## Test Plan

Make sure existing test cases are unaffected and no ecosystem changes.
2025-05-01 01:33:21 +05:30
Wei Lee
6e765b4527 [airflow] apply Replacement::AutoImport to AIR312 (#17570)
## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

This is not yet fixing anything as the names are not changed, but it
lays down the foundation for fixing.

## Test Plan

<!-- How was it tested? -->

the existing test fixture should already cover this change
2025-04-30 15:53:10 -04:00
Vasco Schiavo
c5e41c278c [ruff] Add fix safety section (RUF028) (#17722)
The PR add the fix safety section for rule `RUF028`
(https://github.com/astral-sh/ruff/issues/15584 )

See also
[here](https://github.com/astral-sh/ruff/issues/15584#issuecomment-2820424485)
for the reason behind the _unsafe_ of the fix.
2025-04-30 15:06:25 -04:00
Abhijeet Prasad Bodas
0eeb02c0c1 [syntax-errors] Detect single starred expression assignment x = *y (#17624)
## Summary

Part of #17412

Starred expressions cannot be used as values in assignment expressions.
Add a new semantic syntax error to catch such instances.
Note that we already have
`ParseErrorType::InvalidStarredExpressionUsage` to catch some starred
expression errors during parsing, but that does not cover top level
assignment expressions.

## Test Plan

- Added new inline tests for the new rule
- Found some examples marked as "valid" in existing tests (`_ = *data`),
which are not really valid (per this new rule) and updated them
- There was an existing inline test - `assign_stmt_invalid_value_expr`
which had instances of `*` expression which would be deemed invalid by
this new rule. Converted these to tuples, so that they do not trigger
this new rule.
2025-04-30 15:04:00 -04:00
Alex Waygood
f31b1c695c py-fuzzer: fix minimization logic when --only-new-bugs is passed (#17739) 2025-04-30 18:48:31 +01:00
Brendan Cooley
5679bf00bc Fix example syntax for pydocstyle ignore_var_parameters option (#17740)
Co-authored-by: Brendan Cooley <brendanc@ladodgers.com>
2025-04-30 18:19:41 +02:00
Micha Reiser
a7c358ab5c [red-knot] Update salsa to prevent panic in custom panic-handler (#17742) 2025-04-30 18:19:07 +02:00
Alex Waygood
b6de01b9a5 [red-knot] Ban direct instantiation of generic protocols as well as non-generic ones (#17741) 2025-04-30 16:01:28 +00:00
David Peter
18bac94226 [red-knot] Lookup of __new__ (#17733)
## Summary

Model the lookup of `__new__` without going through
`Type::try_call_dunder`. The `__new__` method is only looked up on the
constructed type itself, not on the meta-type.

This now removes ~930 false positives across the ecosystem (vs 255 for
https://github.com/astral-sh/ruff/pull/17662). It introduces 30 new
false positives related to the construction of enums via something like
`Color = enum.Enum("Color", ["RED", "GREEN"])`. This is expected,
because we don't handle custom metaclass `__call__` methods. The fact
that we previously didn't emit diagnostics there was a coincidence (we
incorrectly called `EnumMeta.__new__`, and since we don't fully
understand its signature, that happened to work with `str`, `list`
arguments).

closes #17462

## Test Plan

Regression test
2025-04-30 17:27:09 +02:00
Dhruv Manilawala
7568eeb7a5 [red-knot] Check decorator consistency on overloads (#17684)
## Summary

Part of #15383.

As per the spec
(https://typing.python.org/en/latest/spec/overload.html#invalid-overload-definitions):

For `@staticmethod` and `@classmethod`:

> If one overload signature is decorated with `@staticmethod` or
`@classmethod`, all overload signatures must be similarly decorated. The
implementation, if present, must also have a consistent decorator. Type
checkers should report an error if these conditions are not met.

For `@final` and `@override`:

> If a `@final` or `@override` decorator is supplied for a function with
overloads, the decorator should be applied only to the overload
implementation if it is present. If an overload implementation isn’t
present (for example, in a stub file), the `@final` or `@override`
decorator should be applied only to the first overload. Type checkers
should enforce these rules and generate an error when they are violated.
If a `@final` or `@override` decorator follows these rules, a type
checker should treat the decorator as if it is present on all overloads.

## Test Plan

Update existing tests; add snapshots.
2025-04-30 20:34:21 +05:30
Hans
0e85cbdd91 [flake8-use-pathlib] Avoid suggesting Path.iterdir() for os.listdir with file descriptor (PTH208) (#17715)
## Summary

Fixes: #17695

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-04-30 20:08:57 +05:30
Dhruv Manilawala
7825975972 [red-knot] Check overloads without an implementation (#17681)
## Summary

As mentioned in the spec
(https://typing.python.org/en/latest/spec/overload.html#invalid-overload-definitions),
part of #15383:

> The `@overload`-decorated definitions must be followed by an overload
implementation, which does not include an `@overload` decorator. Type
checkers should report an error or warning if an implementation is
missing. Overload definitions within stub files, protocols, and on
abstract methods within abstract base classes are exempt from this
check.

## Test Plan

Remove TODOs from the test; create one diagnostic snapshot.
2025-04-30 19:54:21 +05:30
Max Mynter
f584b66824 Expand Semantic Syntax Coverage (#17725)
Re: #17526 

## Summary
Adds tests to red knot and `linter.rs` for the semantic syntax. 

Specifically add tests for `ReboundComprehensionVariable`,
`DuplicateTypeParameter`, and `MultipleCaseAssignment`.

Refactor the `test_async_comprehension_in_sync_comprehension` →
`test_semantic_error` to be more general for all semantic syntax test
cases.

## Test Plan
This is a test.

## Question
I'm happy to contribute more tests the coming days. 

Should that happen here or should we merge this PR such that the
refactor `test_async_comprehension_in_sync_comprehension` →
`test_semantic_error` is available on main and others can chime in, too?
2025-04-30 10:14:08 -04:00
Dhruv Manilawala
ad1a8da4d1 [red-knot] Check for invalid overload usages (#17609)
## Summary

Part of #15383, this PR adds the core infrastructure to check for
invalid overloads and adds a diagnostic to raise if there are < 2
overloads for a given definition.

### Design notes

The requirements to check the overloads are:
* Requires `FunctionType` which has the `to_overloaded` method
* The `FunctionType` **should** be for the function that is either the
implementation or the last overload if the implementation doesn't exists
* Avoid checking any `FunctionType` that are part of an overload chain
* Consider visibility constraints

This required a couple of iteration to make sure all of the above
requirements are fulfilled.

#### 1. Use a set to deduplicate

The logic would first collect all the `FunctionType` that are part of
the overload chain except for the implementation or the last overload if
the implementation doesn't exists. Then, when iterating over all the
function declarations within the scope, we'd avoid checking these
functions. But, this approach would fail to consider visibility
constraints as certain overloads _can_ be behind a version check. Those
aren't part of the overload chain but those aren't a separate overload
chain either.

<details><summary>Implementation:</summary>
<p>

```rs
fn check_overloaded_functions(&mut self) {
    let function_definitions = || {
        self.types
            .declarations
            .iter()
            .filter_map(|(definition, ty)| {
                // Filter out function literals that result from anything other than a function
                // definition e.g., imports.
                if let DefinitionKind::Function(function) = definition.kind(self.db()) {
                    ty.inner_type()
                        .into_function_literal()
                        .map(|ty| (ty, definition.symbol(self.db()), function.node()))
                } else {
                    None
                }
            })
    };

    // A set of all the functions that are part of an overloaded function definition except for
    // the implementation function and the last overload in case the implementation doesn't
    // exists. This allows us to collect all the function definitions that needs to be skipped
    // when checking for invalid overload usages.
    let mut overloads: HashSet<FunctionType<'db>> = HashSet::default();

    for (function, _) in function_definitions() {
        let Some(overloaded) = function.to_overloaded(self.db()) else {
            continue;
        };
        if overloaded.implementation.is_some() {
            overloads.extend(overloaded.overloads.iter().copied());
        } else if let Some((_, previous_overloads)) = overloaded.overloads.split_last() {
            overloads.extend(previous_overloads.iter().copied());
        }
    }

    for (function, function_node) in function_definitions() {
        let Some(overloaded) = function.to_overloaded(self.db()) else {
            continue;
        };
        if overloads.contains(&function) {
            continue;
        }

        // At this point, the `function` variable is either the implementation function or the
        // last overloaded function if the implementation doesn't exists.

        if overloaded.overloads.len() < 2 {
            if let Some(builder) = self
                .context
                .report_lint(&INVALID_OVERLOAD, &function_node.name)
            {
                let mut diagnostic = builder.into_diagnostic(format_args!(
                    "Function `{}` requires at least two overloads",
                    &function_node.name
                ));
                if let Some(first_overload) = overloaded.overloads.first() {
                    diagnostic.annotate(
                        self.context
                            .secondary(first_overload.focus_range(self.db()))
                            .message(format_args!("Only one overload defined here")),
                    );
                }
            }
        }
    }
 }
```

</p>
</details> 

#### 2. Define a `predecessor` query

The `predecessor` query would return the previous `FunctionType` for the
given `FunctionType` i.e., the current logic would be extracted to be a
query instead. This could then be used to make sure that we're checking
the entire overload chain once. The way this would've been implemented
is to have a `to_overloaded` implementation which would take the root of
the overload chain instead of the leaf. But, this would require updates
to the use-def map to somehow be able to return the _following_
functions for a given definition.

#### 3. Create a successor link

This is what Pyrefly uses, we'd create a forward link between two
functions that are involved in an overload chain. This means that for a
given function, we can get the successor function. This could be used to
find the _leaf_ of the overload chain which can then be used with the
`to_overloaded` method to get the entire overload chain. But, this would
also require updating the use-def map to be able to "see" the
_following_ function.

### Implementation 

This leads us to the final implementation that this PR implements which
is to consider the overloaded functions using:
* Collect all the **function symbols** that are defined **and** called
within the same file. This could potentially be an overloaded function
* Use the public bindings to get the leaf of the overload chain and use
that to get the entire overload chain via `to_overloaded` and perform
the check

This has a limitation that in case a function redefines an overload,
then that overload will not be checked. For example:

```py
from typing import overload

@overload
def f() -> None: ...
@overload
def f(x: int) -> int: ...

# The above overload will not be checked as the below function with the same name
# shadows it

def f(*args: int) -> int: ...
```

## Test Plan

Update existing mdtest and add snapshot diagnostics.
2025-04-30 19:37:42 +05:30
Micha Reiser
0861ecfa55 [red-knot] Use 'full' salsa backtrace output that includes durability and revisions (#17735) 2025-04-30 11:04:06 +00:00
Alex Waygood
d1f359afbb [red-knot] Initial support for protocol types (#17682) 2025-04-30 11:03:10 +00:00
Alex Waygood
b84b58760e [red-knot] Computing a type ordering for two non-normalized types is meaningless (#17734) 2025-04-30 11:58:55 +01:00
Micha Reiser
d94be0e780 [red-knot] Include salsa backtrace in check and mdtest panic messages (#17732)
Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
2025-04-30 10:26:40 +02:00
Alex Waygood
8a6787b39e [red-knot] Fix control flow for assert statements (#17702)
## Summary

@sharkdp and I realised in our 1:1 this morning that our control flow
for `assert` statements isn't quite accurate at the moment. Namely, for
something like this:

```py
def _(x: int | None):
    assert x is None, reveal_type(x)
```

we currently reveal `None` for `x` here, but this is incorrect. In
actual fact, the `msg` expression of an `assert` statement (the
expression after the comma) will only be evaluated if the test (`x is
None`) evaluates to `False`. As such, we should be adding a constraint
of `~None` to `x` in the `msg` expression, which should simplify the
inferred type of `x` to `int` in that context (`(int | None) & ~None` ->
`int`).

## Test Plan

Mdtests added.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-04-30 09:57:49 +02:00
David Peter
4a621c2c12 [red-knot] Fix recording of negative visibility constraints (#17731)
## Summary

We were previously recording wrong reachability constraints for negative
branches. Instead of `[cond] AND (NOT [True])` below, we were recording
`[cond] AND (NOT ([cond] AND [True]))`, i.e. we were negating not just
the last predicate, but the `AND`-ed reachability constraint from last
clause. With this fix, we now record the correct constraints for the
example from #17723:

```py
def _(cond: bool):
    if cond:
        # reachability: [cond]
        if True:
            # reachability: [cond] AND [True]
            pass
        else:
            # reachability: [cond] AND (NOT [True])
            x
```

closes #17723 

## Test Plan

* Regression test.
* Verified the ecosystem changes
2025-04-30 09:32:13 +02:00
Micha Reiser
2bb99df394 [red-knot] Update salsa (#17730) 2025-04-30 08:58:31 +02:00
Dhruv Manilawala
f11d9cb509 [red-knot] Support overloads for callable equivalence (#17698)
## Summary

Part of #15383, this PR adds `is_equivalent_to` support for overloaded
callables.

This is mainly done by delegating it to the subtyping check in that two
types A and B are considered equivalent if A is a subtype of B and B is
a subtype of A.

## Test Plan

Add test cases for overloaded callables in `is_equivalent_to.md`
2025-04-30 02:53:59 +05:30
Alex Waygood
549ab74bd6 [red-knot] Run py-fuzzer in CI to check for new panics (#17719) 2025-04-29 21:19:29 +00:00
Alex Waygood
81fc7d7d3a Upload red-knot binaries in CI on completion of linux tests (#17720) 2025-04-29 22:15:26 +01:00
Victor Hugo Gomes
8c68d30c3a [flake8-use-pathlib] Fix PTH123 false positive when open is passed a file descriptor from a function call (#17705)
## Summary
Includes minor changes to the semantic type inference to help detect the
return type of function call.

Fixes #17691

## Test Plan

Snapshot tests
2025-04-29 16:51:38 -04:00
Alex Waygood
93d6a3567b [red-knot] mdtest.py: Watch for changes in red_knot_vendored and red_knot_test as well as in red_knot_python_semantic (#17718) 2025-04-29 18:27:49 +00:00
Micha Reiser
1d788981cd [red-knot] Capture backtrace in "check-failed" diagnostic (#17641)
Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
2025-04-29 16:58:58 +00:00
Hans
7d46579808 [docs] fix duplicated 'are' in comment for PTH123 rule (#17714) 2025-04-29 17:58:39 +02:00
Alex Waygood
c9a6b1a9d0 [red-knot] Make Type::signatures() exhaustive (#17706) 2025-04-29 15:14:08 +01:00
Hans
9b9d16c3ba [red-knot] colorize concise output diagnostics (#17232) (#17479)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Andrew Gallant <andrew@astral.sh>
2025-04-29 16:07:16 +02:00
David Peter
79f8473e51 [red-knot] Assignability of class literals to Callables (#17704)
## Summary

Subtyping was already modeled, but assignability also needs an explicit
branch. Removes 921 ecosystem false positives.

## Test Plan

New Markdown tests.
2025-04-29 15:04:22 +02:00
Douglas Creager
ca4fdf452d Create TypeVarInstance type for legacy typevars (#16538)
We are currently representing type variables using a `KnownInstance`
variant, which wraps a `TypeVarInstance` that contains the information
about the typevar (name, bounds, constraints, default type). We were
previously only constructing that type for PEP 695 typevars. This PR
constructs that type for legacy typevars as well.

It also detects functions that are generic because they use legacy
typevars in their parameter list. With the existing logic for inferring
specializations of function calls (#17301), that means that we are
correctly detecting that the definition of `reveal_type` in the typeshed
is generic, and inferring the correct specialization of `_T` for each
call site.

This does not yet handle legacy generic classes; that will come in a
follow-on PR.
2025-04-29 09:03:06 -04:00
Dylan
3c460a7b9a Make syntax error for unparenthesized except tuples version specific to before 3.14 (#17660)
What it says on the tin 😄
2025-04-29 07:55:30 -05:00
Alex Waygood
31e6576971 [red-knot] micro-optimise ClassLiteral::is_protocol (#17703) 2025-04-29 12:35:53 +00:00
Micha Reiser
c953e7d143 [red-knot] Improve log message for default python platform (#17700) 2025-04-29 08:26:41 +00:00
Vasco Schiavo
5096824793 [ruff] add fix safety section (RUF017) (#17480)
The PR add the `fix safety` section for rule `RUF017` (#15584 )
2025-04-28 22:07:22 +00:00
Dylan
ae7691b026 Add Python 3.14 to configuration options (#17647)
A small PR that just updates the various settings/configurations to
allow Python 3.14. At the moment selecting that target version will
have no impact compared to Python 3.13 - except that a warning
is emitted if the user does so with `preview` disabled.
2025-04-28 16:29:00 -05:00
Wei Lee
504fa20057 [airflow] Apply auto fixes to cases where the names have changed in Airflow 3 (AIR302) (#17553)
## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Apply auto fixes to cases where the names have changed in Airflow 3 in
AIR302 and split the huge test cases into different test cases based on
proivder

## Test Plan

<!-- How was it tested? -->

the test cases has been split into multiple for easier checking
2025-04-28 16:35:17 -04:00
David Peter
f0868ac0c9 [red-knot] Revert blanket clippy::too_many_arguments allow (#17688)
## Summary

Now that https://github.com/salsa-rs/salsa/issues/808 has been fixed, we
can revert this global change in `Cargo.toml`.
2025-04-28 21:21:53 +02:00
Brent Westbrook
01a31c08f5 Add config option to disable typing_extensions imports (#17611)
Summary
--

This PR resolves https://github.com/astral-sh/ruff/issues/9761 by adding
a linter configuration option to disable
`typing_extensions` imports. As mentioned [here], it would be ideal if
we could
detect whether or not `typing_extensions` is available as a dependency
automatically, but this seems like a much easier fix in the meantime.

The default for the new option, `typing-extensions`, is `true`,
preserving the current behavior. Setting it to `false` will bail out of
the new
`Checker::typing_importer` method, which has been refactored from the 
`Checker::import_from_typing` method in
https://github.com/astral-sh/ruff/pull/17340),
with `None`, which is then handled specially by each rule that calls it.

I considered some alternatives to a config option, such as checking if
`typing_extensions` has been imported or checking for a `TYPE_CHECKING`
block we could use, but I think defaulting to allowing
`typing_extensions` imports and allowing the user to disable this with
an option is both simple to implement and pretty intuitive.

[here]:
https://github.com/astral-sh/ruff/issues/9761#issuecomment-2790492853

Test Plan
--

New linter tests exercising several combinations of Python versions and
the new config option for PYI019. I also added tests for the other
affected rules, but only in the case where the new config option is
enabled. The rules' existing tests also cover the default case.
2025-04-28 14:57:36 -04:00
Andrew Gallant
405878a128 ruff_db: render file paths in diagnostics as relative paths if possible
This is done in what appears to be the same way as Ruff: we get the CWD,
strip the prefix from the path if possible, and use that. If stripping
the prefix fails, then we print the full path as-is.

Fixes #17233
2025-04-28 14:32:34 -04:00
Alex Waygood
80103a179d Bump mypy_primer pin (#17685) 2025-04-28 16:13:07 +00:00
Andrew Gallant
9a8f3cf247 red_knot_python_semantic: improve not-iterable diagnostic
This cleans up one particular TODO by splitting the "because" part of
the `not-iterable` diagnostic out into an info sub-diagnostic.
2025-04-28 11:03:41 -04:00
David Peter
07718f4788 [red-knot] Allow all callables to be assignable to @Todo-signatures (#17680)
## Summary

Removes ~850 diagnostics related to assignability of callable types,
where the callable-being-assigned-to has a "Todo signature", which
should probably accept any left hand side callable/signature.
2025-04-28 16:40:35 +02:00
Dylan
1e8881f9af [refurb] Mark fix as safe for readlines-in-for (FURB129) (#17644)
This PR promotes the fix applicability of [readlines-in-for
(FURB129)](https://docs.astral.sh/ruff/rules/readlines-in-for/#readlines-in-for-furb129)
to always safe.

In the original PR (https://github.com/astral-sh/ruff/pull/9880), the
author marked the rule as unsafe because Ruff's type inference couldn't
quite guarantee that we had an `IOBase` object in hand. Some false
positives were recorded in the test fixture. However, before the PR was
merged, Charlie added the necessary type inference and the false
positives went away.

According to the [Python
documentation](https://docs.python.org/3/library/io.html#io.IOBase), I
believe this fix is safe for any proper implementation of `IOBase`:

>[IOBase](https://docs.python.org/3/library/io.html#io.IOBase) (and its
subclasses) supports the iterator protocol, meaning that an
[IOBase](https://docs.python.org/3/library/io.html#io.IOBase) object can
be iterated over yielding the lines in a stream. Lines are defined
slightly differently depending on whether the stream is a binary stream
(yielding bytes), or a text stream (yielding character strings). See
[readline()](https://docs.python.org/3/library/io.html#io.IOBase.readline)
below.

and then in the [documentation for
`readlines`](https://docs.python.org/3/library/io.html#io.IOBase.readlines):

>Read and return a list of lines from the stream. hint can be specified
to control the number of lines read: no more lines will be read if the
total size (in bytes/characters) of all lines so far exceeds hint. [...]
>Note that it’s already possible to iterate on file objects using for
line in file: ... without calling file.readlines().

I believe that a careful reading of our [versioning
policy](https://docs.astral.sh/ruff/versioning/#version-changes)
requires that this change be deferred to a minor release - but please
correct me if I'm wrong!
2025-04-28 09:39:55 -05:00
Dylan
152a0b6585 Collect preview lint behaviors in separate module (#17646)
This PR collects all behavior gated under preview into a new module
`ruff_linter::preview` that exposes functions like
`is_my_new_feature_enabled` - just as is done in the formatter crate.
2025-04-28 09:12:24 -05:00
3865 changed files with 145443 additions and 47094 deletions

4
.gitattributes vendored
View File

@@ -5,6 +5,9 @@ crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py text eol=crlf
crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py text eol=crlf
crates/ruff_python_formatter/tests/snapshots/format@f-string-carriage-return-newline.py.snap text eol=crlf
crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf
crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf
@@ -21,6 +24,7 @@ crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py text eol=lf
crates/ruff_python_parser/resources/inline linguist-generated=true
ruff.schema.json -diff linguist-generated=true text=auto eol=lf
ty.schema.json -diff linguist-generated=true text=auto eol=lf
crates/ruff_python_ast/src/generated.rs -diff linguist-generated=true text=auto eol=lf
crates/ruff_python_formatter/src/generated.rs -diff linguist-generated=true text=auto eol=lf
*.md.snap linguist-language=Markdown

10
.github/CODEOWNERS vendored
View File

@@ -14,11 +14,11 @@
# flake8-pyi
/crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood
# Script for fuzzing the parser/red-knot etc.
# Script for fuzzing the parser/ty etc.
/python/py-fuzzer/ @AlexWaygood
# red-knot
/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
# ty
/crates/ty* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/scripts/knot_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/red_knot_python_semantic @carljm @AlexWaygood @sharkdp @dcreager
/scripts/ty_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ty_python_semantic @carljm @AlexWaygood @sharkdp @dcreager

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Report an issue with ty
url: https://github.com/astral-sh/ty/issues/new/choose
about: Please report issues for our type checker ty in the ty repository.
- name: Documentation
url: https://docs.astral.sh/ruff
about: Please consult the documentation before creating an issue.

View File

@@ -1,8 +1,9 @@
<!--
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:
Thank you for contributing to Ruff/ty! To help us out with reviewing, please consider the following:
- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include a descriptive title? (Please prefix with `[ty]` for ty pull
requests.)
- Does this pull request include references to any relevant issues?
-->

8
.github/mypy-primer-ty.toml vendored Normal file
View File

@@ -0,0 +1,8 @@
#:schema ../ty.schema.json
# Configuration overrides for the mypy primer run
# Enable off-by-default rules.
[rules]
possibly-unresolved-reference = "warn"
unused-ignore-comment = "warn"
division-by-zero = "warn"

View File

@@ -43,7 +43,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
@@ -72,7 +72,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -114,7 +114,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: arm64
@@ -170,7 +170,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.platform.arch }}
@@ -223,7 +223,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -298,7 +298,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
@@ -310,7 +310,7 @@ jobs:
manylinux: auto
docker-options: ${{ matrix.platform.maturin_docker_options }}
args: --release --locked --out dist
- uses: uraimo/run-on-arch-action@ac33288c3728ca72563c97b8b88dda5a65a84448 # v2
- uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1
if: ${{ matrix.platform.arch != 'ppc64' && matrix.platform.arch != 'ppc64le'}}
name: Test wheel
with:
@@ -363,7 +363,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -429,7 +429,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
@@ -441,7 +441,7 @@ jobs:
manylinux: musllinux_1_2
args: --release --locked --out dist
docker-options: ${{ matrix.platform.maturin_docker_options }}
- uses: uraimo/run-on-arch-action@ac33288c3728ca72563c97b8b88dda5a65a84448 # v2
- uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1
name: Test wheel
with:
arch: ${{ matrix.platform.arch }}

View File

@@ -38,9 +38,9 @@ jobs:
submodules: recursive
persist-credentials: false
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -63,7 +63,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.RUFF_BASE_IMG }}
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
@@ -79,7 +79,7 @@ jobs:
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Build and push by digest
id: build
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: ${{ matrix.platform }}
@@ -113,17 +113,17 @@ jobs:
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.RUFF_BASE_IMG }}
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
@@ -131,7 +131,7 @@ jobs:
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -167,9 +167,9 @@ jobs:
- debian:bookworm-slim,bookworm-slim,debian-slim
- buildpack-deps:bookworm,bookworm,debian
steps:
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -219,7 +219,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
# ghcr.io prefers index level annotations
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
@@ -231,7 +231,7 @@ jobs:
${{ env.TAG_PATTERNS }}
- name: Build and push
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64
@@ -256,17 +256,17 @@ jobs:
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
with:
@@ -276,7 +276,7 @@ jobs:
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@@ -36,8 +36,8 @@ jobs:
code: ${{ steps.check_code.outputs.changed }}
# Flag that is raised when any code that affects the fuzzer is changed
fuzz: ${{ steps.check_fuzzer.outputs.changed }}
# Flag that is set to "true" when code related to red-knot changes.
red_knot: ${{ steps.check_red_knot.outputs.changed }}
# Flag that is set to "true" when code related to ty changes.
ty: ${{ steps.check_ty.outputs.changed }}
# Flag that is set to "true" when code related to the playground changes.
playground: ${{ steps.check_playground.outputs.changed }}
@@ -84,7 +84,7 @@ jobs:
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':Cargo.toml' \
':Cargo.lock' \
':crates/**' \
':!crates/red_knot*/**' \
':!crates/ty*/**' \
':!crates/ruff_python_formatter/**' \
':!crates/ruff_formatter/**' \
':!crates/ruff_dev/**' \
@@ -145,7 +145,7 @@ jobs:
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**' \
':!**/*.md' \
':crates/red_knot_python_semantic/resources/mdtest/**/*.md' \
':crates/ty_python_semantic/resources/mdtest/**/*.md' \
':!docs/**' \
':!assets/**' \
':.github/workflows/ci.yaml' \
@@ -168,15 +168,15 @@ jobs:
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Check if the red-knot code changed
id: check_red_knot
- name: Check if the ty code changed
id: check_ty
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
':Cargo.toml' \
':Cargo.lock' \
':crates/red_knot*/**' \
':crates/ty*/**' \
':crates/ruff_db/**' \
':crates/ruff_annotate_snippets/**' \
':crates/ruff_python_ast/**' \
@@ -221,7 +221,7 @@ jobs:
- name: "Clippy"
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
- name: "Clippy (wasm)"
run: cargo clippy -p ruff_wasm -p red_knot_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings
run: cargo clippy -p ruff_wasm -p ty_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings
cargo-test-linux:
name: "cargo test (linux)"
@@ -237,23 +237,23 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@b3958095189f34b95d402a680b6e96b7f194f7b9 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
with:
tool: cargo-insta
- name: Red-knot mdtests (GitHub annotations)
if: ${{ needs.determine_changes.outputs.red_knot == 'true' }}
- name: ty mdtests (GitHub annotations)
if: ${{ needs.determine_changes.outputs.ty == 'true' }}
env:
NO_COLOR: 1
MDTEST_GITHUB_ANNOTATIONS_FORMAT: 1
# Ignore errors if this step fails; we want to continue to later steps in the workflow anyway.
# This step is just to get nice GitHub annotations on the PR diff in the files-changed tab.
run: cargo test -p red_knot_python_semantic --test mdtest || true
run: cargo test -p ty_python_semantic --test mdtest || true
- name: "Run tests"
shell: bash
env:
@@ -268,7 +268,7 @@ jobs:
# sync, not just public items. Eventually we should do this for all
# crates; for now add crates here as they are warning-clean to prevent
# regression.
- run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p red_knot_test -p ruff_db --document-private-items
- run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db --document-private-items
env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings"
@@ -276,6 +276,10 @@ jobs:
with:
name: ruff
path: target/debug/ruff
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ty
path: target/debug/ty
cargo-test-linux-release:
name: "cargo test (linux, release)"
@@ -291,13 +295,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@b3958095189f34b95d402a680b6e96b7f194f7b9 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
with:
tool: cargo-insta
- name: "Run tests"
@@ -320,7 +324,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
with:
tool: cargo-nextest
- name: "Run tests"
@@ -358,9 +362,9 @@ jobs:
run: |
cd crates/ruff_wasm
wasm-pack test --node
- name: "Test red_knot_wasm"
- name: "Test ty_wasm"
run: |
cd crates/red_knot_wasm
cd crates/ty_wasm
wasm-pack test --node
cargo-build-release:
@@ -376,7 +380,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@b3958095189f34b95d402a680b6e96b7f194f7b9 # v1
- name: "Build"
run: cargo build --release --locked
@@ -401,13 +405,13 @@ jobs:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@b3958095189f34b95d402a680b6e96b7f194f7b9 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
with:
tool: cargo-insta
- name: "Run tests"
@@ -433,7 +437,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
uses: cargo-bins/cargo-binstall@ea65a39d2dcca142c53bddd3a097a674e903f475 # v1.12.7
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -455,8 +459,8 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download Ruff binary to test
id: download-cached-binary
with:
@@ -500,12 +504,10 @@ jobs:
# Verify that adding a plugin or rule produces clean code.
- run: ./scripts/add_rule.py --name DoTheThing --prefix F --code 999 --linter pyflakes
- run: cargo check
- run: cargo fmt --all --check
- run: |
./scripts/add_plugin.py test --url https://pypi.org/project/-test/0.1.0/ --prefix TST
./scripts/add_rule.py --name FirstRule --prefix TST --code 001 --linter test
- run: cargo check
- run: cargo fmt --all --check
ecosystem:
name: "ecosystem"
@@ -521,11 +523,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download comparison Ruff binary
id: ruff-target
with:
@@ -632,6 +634,53 @@ jobs:
name: ecosystem-result
path: ecosystem-result
fuzz-ty:
name: "Fuzz for new ty panics"
runs-on: depot-ubuntu-22.04-16
needs:
- cargo-test-linux
- determine_changes
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.ty == 'true' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download new ty binary
id: ty-new
with:
name: ty
path: target/debug
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download baseline ty binary
with:
name: ty
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- name: Fuzz
env:
FORCE_COLOR: 1
NEW_TY: ${{ steps.ty-new.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x "${PWD}/ty" "${NEW_TY}/ty"
(
uvx \
--python="${PYTHON_VERSION}" \
--from=./python/py-fuzzer \
fuzz \
--test-executable="${NEW_TY}/ty" \
--baseline-executable="${PWD}/ty" \
--only-new-bugs \
--bin=ty \
0-500
)
cargo-shear:
name: "cargo shear"
runs-on: ubuntu-latest
@@ -641,7 +690,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
- uses: cargo-bins/cargo-binstall@ea65a39d2dcca142c53bddd3a097a674e903f475 # v1.12.7
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -654,7 +703,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -681,7 +730,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- name: "Cache pre-commit"
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
@@ -708,7 +761,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
@@ -720,7 +773,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -769,7 +822,7 @@ jobs:
- determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
steps:
- uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2
- uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -779,12 +832,12 @@ jobs:
persist-credentials: false
repository: "astral-sh/ruff-lsp"
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
# installation fails on 3.13 and newer
python-version: "3.12"
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download development ruff binary
id: ruff-target
with:
@@ -857,7 +910,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
with:
tool: cargo-codspeed

View File

@@ -34,11 +34,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@b3958095189f34b95d402a680b6e96b7f194f7b9 # v1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: Build ruff
# A debug build means the script runs slower once it gets started,

View File

@@ -1,72 +0,0 @@
name: Daily property test run
on:
workflow_dispatch:
schedule:
- cron: "0 12 * * *"
pull_request:
paths:
- ".github/workflows/daily_property_tests.yaml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
FORCE_COLOR: 1
jobs:
property_tests:
name: Property tests
runs-on: ubuntu-latest
timeout-minutes: 20
# Don't run the cron job on forks:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: Build Red Knot
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
# mode (1.5 min vs 14 min), so the overall time is shorter with a release build.
run: cargo build --locked --release --package red_knot_python_semantic --tests
- name: Run property tests
shell: bash
run: |
export QUICKCHECK_TESTS=100000
for _ in {1..5}; do
cargo test --locked --release --package red_knot_python_semantic -- --ignored list::property_tests
cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable
done
create-issue-on-failure:
name: Create an issue if the daily property test run surfaced any bugs
runs-on: ubuntu-latest
needs: property_tests
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.property_tests.result == 'failure' }}
permissions:
issues: write
steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.create({
owner: "astral-sh",
repo: "ruff",
title: `Daily property test run failed on ${new Date().toDateString()}`,
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
labels: ["bug", "red-knot", "testing"],
})

View File

@@ -5,12 +5,13 @@ permissions: {}
on:
pull_request:
paths:
- "crates/red_knot*/**"
- "crates/ty*/**"
- "crates/ruff_db"
- "crates/ruff_python_ast"
- "crates/ruff_python_parser"
- ".github/workflows/mypy_primer.yaml"
- ".github/workflows/mypy_primer_comment.yaml"
- "Cargo.lock"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
@@ -36,24 +37,25 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: "ruff"
- name: Install Rust toolchain
run: rustup show
- name: Install mypy_primer
run: |
uv tool install "git+https://github.com/hauntsaninja/mypy_primer@4c22d192a456e27badf85b3ea0f830707375d2b7"
- name: Run mypy_primer
shell: bash
run: |
cd ruff
PRIMER_SELECTOR="$(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt)"
echo "Enabling mypy primer specific configuration overloads (see .github/mypy-primer-ty.toml)"
mkdir -p ~/.config/ty
cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml
PRIMER_SELECTOR="$(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt)"
echo "new commit"
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
@@ -67,9 +69,11 @@ jobs:
echo "Project selector: $PRIMER_SELECTOR"
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx mypy_primer \
uvx \
--from="git+https://github.com/hauntsaninja/mypy_primer@01a7ca325f674433c58e02416a867178d1571128" \
mypy_primer \
--repo ruff \
--type-checker knot \
--type-checker ty \
--old base_commit \
--new "$GITHUB_SHA" \
--project-selector "/($PRIMER_SELECTOR)\$" \

View File

@@ -79,7 +79,7 @@ jobs:
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:

View File

@@ -70,7 +70,7 @@ jobs:
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:

View File

@@ -28,7 +28,7 @@ jobs:
ref: ${{ inputs.ref }}
persist-credentials: true
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: 3.12

View File

@@ -22,8 +22,8 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: wheels-*
path: wheels

View File

@@ -1,5 +1,5 @@
# Publish the Red Knot playground.
name: "[Knot Playground] Release"
# Publish the ty playground.
name: "[ty Playground] Release"
permissions: {}
@@ -7,12 +7,12 @@ on:
push:
branches: [main]
paths:
- "crates/red_knot*/**"
- "crates/ty*/**"
- "crates/ruff_db/**"
- "crates/ruff_python_ast/**"
- "crates/ruff_python_parser/**"
- "playground/**"
- ".github/workflows/publish-knot-playground.yml"
- ".github/workflows/publish-ty-playground.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
@@ -45,8 +45,8 @@ jobs:
- name: "Run TypeScript checks"
run: npm run check
working-directory: playground
- name: "Build Knot playground"
run: npm run build --workspace knot-playground
- name: "Build ty playground"
run: npm run build --workspace ty-playground
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
@@ -55,4 +55,4 @@ jobs:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
# `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production
command: pages deploy playground/knot/dist --project-name=knot-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}
command: pages deploy playground/ty/dist --project-name=ty-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}

View File

@@ -61,7 +61,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
with:
persist-credentials: false
submodules: recursive
@@ -69,7 +69,7 @@ jobs:
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4/cargo-dist-installer.sh | sh"
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.5-prerelease.1/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
@@ -124,7 +124,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
with:
persist-credentials: false
submodules: recursive
@@ -175,7 +175,7 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
with:
persist-credentials: false
submodules: recursive
@@ -251,7 +251,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
with:
persist-credentials: false
submodules: recursive

View File

@@ -39,13 +39,13 @@ jobs:
- name: Sync typeshed
id: sync
run: |
rm -rf ruff/crates/red_knot_vendored/vendor/typeshed
mkdir ruff/crates/red_knot_vendored/vendor/typeshed
cp typeshed/README.md ruff/crates/red_knot_vendored/vendor/typeshed
cp typeshed/LICENSE ruff/crates/red_knot_vendored/vendor/typeshed
cp -r typeshed/stdlib ruff/crates/red_knot_vendored/vendor/typeshed/stdlib
rm -rf ruff/crates/red_knot_vendored/vendor/typeshed/stdlib/@tests
git -C typeshed rev-parse HEAD > ruff/crates/red_knot_vendored/vendor/typeshed/source_commit.txt
rm -rf ruff/crates/ty_vendored/vendor/typeshed
mkdir ruff/crates/ty_vendored/vendor/typeshed
cp typeshed/README.md ruff/crates/ty_vendored/vendor/typeshed
cp typeshed/LICENSE ruff/crates/ty_vendored/vendor/typeshed
cp -r typeshed/stdlib ruff/crates/ty_vendored/vendor/typeshed/stdlib
rm -rf ruff/crates/ty_vendored/vendor/typeshed/stdlib/@tests
git -C typeshed rev-parse HEAD > ruff/crates/ty_vendored/vendor/typeshed/source_commit.txt
- name: Commit the changes
id: commit
if: ${{ steps.sync.outcome == 'success' }}
@@ -60,7 +60,7 @@ jobs:
cd ruff
git push --force origin typeshedbot/sync-typeshed
gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "internal"
gh pr create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty"
create-issue-on-failure:
name: Create an issue if the typeshed sync failed
@@ -79,5 +79,5 @@ jobs:
repo: "ruff",
title: `Automated typeshed sync failed on ${new Date().toDateString()}`,
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
labels: ["bug", "red-knot"],
labels: ["bug", "ty"],
})

View File

@@ -29,3 +29,7 @@ MD024:
#
# Ref: https://github.com/astral-sh/ruff/pull/15011#issuecomment-2544790854
MD046: false
# Link text should be descriptive
# Disallows link text like *here* which is annoying.
MD059: false

View File

@@ -3,8 +3,10 @@ fail_fast: false
exclude: |
(?x)^(
.github/workflows/release.yml|
crates/red_knot_vendored/vendor/.*|
crates/red_knot_project/resources/.*|
crates/ty_vendored/vendor/.*|
crates/ty_project/resources/.*|
crates/ty_python_semantic/resources/corpus/.*|
crates/ty/docs/(configuration|rules|cli).md|
crates/ruff_benchmark/resources/.*|
crates/ruff_linter/resources/.*|
crates/ruff_linter/src/rules/.*/snapshots/.*|
@@ -42,7 +44,7 @@ repos:
)$
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.44.0
rev: v0.45.0
hooks:
- id: markdownlint-fix
exclude: |
@@ -65,7 +67,7 @@ repos:
- black==25.1.0
- repo: https://github.com/crate-ci/typos
rev: v1.31.1
rev: v1.33.1
hooks:
- id: typos
@@ -79,7 +81,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.7
rev: v0.11.13
hooks:
- id: ruff-format
- id: ruff
@@ -97,7 +99,7 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.6.0
rev: v1.9.0
hooks:
- id: zizmor

View File

@@ -1,5 +1,39 @@
# Breaking Changes
## 0.12.0
- **Detection of more syntax errors**
Ruff now detects version-related syntax errors, such as the use of the `match`
statement on Python versions before 3.10, and syntax errors emitted by
CPython's compiler, such as irrefutable `match` patterns before the final
`case` arm.
- **New default Python version handling for syntax errors**
Ruff will default to the _latest_ supported Python version (3.13) when
checking for the version-related syntax errors mentioned above to prevent
false positives in projects without a Python version configured. The default
in all other cases, like applying lint rules, is unchanged and remains at the
minimum supported Python version (3.9).
- **Updated f-string formatting**
Ruff now formats multi-line f-strings with format specifiers to avoid adding a
line break after the format specifier. This addresses a change to the Python
grammar in version 3.13.4 that made such a line break a syntax error.
- **`rust-toolchain.toml` is no longer included in source distributions**
The `rust-toolchain.toml` is used to specify a higher Rust version than Ruff's
minimum supported Rust version (MSRV) for development and building release
artifacts. However, when present in source distributions, it would also cause
downstream package maintainers to pull in the same Rust toolchain, even if
their available toolchain was MSRV-compatible.
- **[`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/)
(`S320`) has been removed**
## 0.11.0
This is a follow-up to release 0.10.0. Because of a mistake in the release process, the `requires-python` inference changes were not included in that release. Ruff 0.11.0 now includes this change as well as the stabilization of the preview behavior for `PGH004`.

View File

@@ -1,5 +1,332 @@
# Changelog
## 0.12.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.12.0) for a migration
guide and overview of the changes!
### Breaking changes
- **Detection of more syntax errors**
Ruff now detects version-related syntax errors, such as the use of the `match`
statement on Python versions before 3.10, and syntax errors emitted by
CPython's compiler, such as irrefutable `match` patterns before the final
`case` arm.
- **New default Python version handling for syntax errors**
Ruff will default to the _latest_ supported Python version (3.13) when
checking for the version-related syntax errors mentioned above to prevent
false positives in projects without a Python version configured. The default
in all other cases, like applying lint rules, is unchanged and remains at the
minimum supported Python version (3.9).
- **Updated f-string formatting**
Ruff now formats multi-line f-strings with format specifiers to avoid adding a
line break after the format specifier. This addresses a change to the Python
grammar in version 3.13.4 that made such a line break a syntax error.
- **`rust-toolchain.toml` is no longer included in source distributions**
The `rust-toolchain.toml` is used to specify a higher Rust version than Ruff's
minimum supported Rust version (MSRV) for development and building release
artifacts. However, when present in source distributions, it would also cause
downstream package maintainers to pull in the same Rust toolchain, even if
their available toolchain was MSRV-compatible.
### Removed Rules
The following rules have been removed:
- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/)
(`S320`)
### Deprecated Rules
The following rules have been deprecated:
- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name/)
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`for-loop-writes`](https://docs.astral.sh/ruff/rules/for-loop-writes) (`FURB122`)
- [`check-and-remove-from-set`](https://docs.astral.sh/ruff/rules/check-and-remove-from-set) (`FURB132`)
- [`verbose-decimal-constructor`](https://docs.astral.sh/ruff/rules/verbose-decimal-constructor) (`FURB157`)
- [`fromisoformat-replace-z`](https://docs.astral.sh/ruff/rules/fromisoformat-replace-z) (`FURB162`)
- [`int-on-sliced-str`](https://docs.astral.sh/ruff/rules/int-on-sliced-str) (`FURB166`)
- [`exc-info-outside-except-handler`](https://docs.astral.sh/ruff/rules/exc-info-outside-except-handler) (`LOG014`)
- [`import-outside-top-level`](https://docs.astral.sh/ruff/rules/import-outside-top-level) (`PLC0415`)
- [`unnecessary-dict-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-dict-index-lookup) (`PLR1733`)
- [`nan-comparison`](https://docs.astral.sh/ruff/rules/nan-comparison) (`PLW0177`)
- [`eq-without-hash`](https://docs.astral.sh/ruff/rules/eq-without-hash) (`PLW1641`)
- [`pytest-parameter-with-default-argument`](https://docs.astral.sh/ruff/rules/pytest-parameter-with-default-argument) (`PT028`)
- [`pytest-warns-too-broad`](https://docs.astral.sh/ruff/rules/pytest-warns-too-broad) (`PT030`)
- [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements) (`PT031`)
- [`invalid-formatter-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-formatter-suppression-comment) (`RUF028`)
- [`dataclass-enum`](https://docs.astral.sh/ruff/rules/dataclass-enum) (`RUF049`)
- [`class-with-mixed-type-vars`](https://docs.astral.sh/ruff/rules/class-with-mixed-type-vars) (`RUF053`)
- [`unnecessary-round`](https://docs.astral.sh/ruff/rules/unnecessary-round) (`RUF057`)
- [`starmap-zip`](https://docs.astral.sh/ruff/rules/starmap-zip) (`RUF058`)
- [`non-pep604-annotation-optional`](https://docs.astral.sh/ruff/rules/non-pep604-annotation-optional) (`UP045`)
- [`non-pep695-generic-class`](https://docs.astral.sh/ruff/rules/non-pep695-generic-class) (`UP046`)
- [`non-pep695-generic-function`](https://docs.astral.sh/ruff/rules/non-pep695-generic-function) (`UP047`)
- [`private-type-parameter`](https://docs.astral.sh/ruff/rules/private-type-parameter) (`UP049`)
The following behaviors have been stabilized:
- [`collection-literal-concatenation`] (`RUF005`) now recognizes slices, in
addition to list literals and variables.
- The fix for [`readlines-in-for`] (`FURB129`) is now marked as always safe.
- [`if-else-block-instead-of-if-exp`] (`SIM108`) will now further simplify
expressions to use `or` instead of an `if` expression, where possible.
- [`unused-noqa`] (`RUF100`) now checks for file-level `noqa` comments as well
as inline comments.
- [`subprocess-without-shell-equals-true`] (`S603`) now accepts literal strings,
as well as lists and tuples of literal strings, as trusted input.
- [`boolean-type-hint-positional-argument`] (`FBT001`) now applies to types that
include `bool`, like `bool | int` or `typing.Optional[bool]`, in addition to
plain `bool` annotations.
- [`non-pep604-annotation-union`] (`UP007`) has now been split into two rules.
`UP007` now applies only to `typing.Union`, while
[`non-pep604-annotation-optional`] (`UP045`) checks for use of
`typing.Optional`. `UP045` has also been stabilized in this release, but you
may need to update existing `include`, `ignore`, or `noqa` settings to
accommodate this change.
### Preview features
- \[`ruff`\] Check for non-context-manager use of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call` (`RUF061`) ([#17368](https://github.com/astral-sh/ruff/pull/17368))
- [syntax-errors] Raise unsupported syntax error for template strings prior to Python 3.14 ([#18664](https://github.com/astral-sh/ruff/pull/18664))
### Bug fixes
- Add syntax error when conversion flag does not immediately follow exclamation mark ([#18706](https://github.com/astral-sh/ruff/pull/18706))
- Add trailing space around `readlines` ([#18542](https://github.com/astral-sh/ruff/pull/18542))
- Fix `\r` and `\r\n` handling in t- and f-string debug texts ([#18673](https://github.com/astral-sh/ruff/pull/18673))
- Hug closing `}` when f-string expression has a format specifier ([#18704](https://github.com/astral-sh/ruff/pull/18704))
- \[`flake8-pyi`\] Avoid syntax error in the case of starred and keyword arguments (`PYI059`) ([#18611](https://github.com/astral-sh/ruff/pull/18611))
- \[`flake8-return`\] Fix `RET504` autofix generating a syntax error ([#18428](https://github.com/astral-sh/ruff/pull/18428))
- \[`pep8-naming`\] Suppress fix for `N804` and `N805` if the recommended name is already used ([#18472](https://github.com/astral-sh/ruff/pull/18472))
- \[`pycodestyle`\] Avoid causing a syntax error in expressions spanning multiple lines (`E731`) ([#18479](https://github.com/astral-sh/ruff/pull/18479))
- \[`pyupgrade`\] Suppress `UP008` if `super` is shadowed ([#18688](https://github.com/astral-sh/ruff/pull/18688))
- \[`refurb`\] Parenthesize lambda and ternary expressions (`FURB122`, `FURB142`) ([#18592](https://github.com/astral-sh/ruff/pull/18592))
- \[`ruff`\] Handle extra arguments to `deque` (`RUF037`) ([#18614](https://github.com/astral-sh/ruff/pull/18614))
- \[`ruff`\] Preserve parentheses around `deque` in fix for `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#18598](https://github.com/astral-sh/ruff/pull/18598))
- \[`ruff`\] Validate arguments before offering a fix (`RUF056`) ([#18631](https://github.com/astral-sh/ruff/pull/18631))
- \[`ruff`\] Skip fix for `RUF059` if dummy name is already bound ([#18509](https://github.com/astral-sh/ruff/pull/18509))
- \[`pylint`\] Fix `PLW0128` to check assignment targets in square brackets and after asterisks ([#18665](https://github.com/astral-sh/ruff/pull/18665))
### Rule changes
- Fix false positive on mutations in `return` statements (`B909`) ([#18408](https://github.com/astral-sh/ruff/pull/18408))
- Treat `ty:` comments as pragma comments ([#18532](https://github.com/astral-sh/ruff/pull/18532))
- \[`flake8-pyi`\] Apply `custom-typevar-for-self` to string annotations (`PYI019`) ([#18311](https://github.com/astral-sh/ruff/pull/18311))
- \[`pyupgrade`\] Don't offer a fix for `Optional[None]` (`UP007`, `UP045)` ([#18545](https://github.com/astral-sh/ruff/pull/18545))
- \[`pyupgrade`\] Fix `super(__class__, self)` detection (`UP008`) ([#18478](https://github.com/astral-sh/ruff/pull/18478))
- \[`refurb`\] Make the fix for `FURB163` unsafe for `log2`, `log10`, `*args`, and deleted comments ([#18645](https://github.com/astral-sh/ruff/pull/18645))
### Server
- Support cancellation requests ([#18627](https://github.com/astral-sh/ruff/pull/18627))
### Documentation
- Drop confusing second `*` from glob pattern example for `per-file-target-version` ([#18709](https://github.com/astral-sh/ruff/pull/18709))
- Update Neovim configuration examples ([#18491](https://github.com/astral-sh/ruff/pull/18491))
- \[`pylint`\] De-emphasize `__hash__ = Parent.__hash__` (`PLW1641`) ([#18613](https://github.com/astral-sh/ruff/pull/18613))
- \[`refurb`\] Add a note about float literal handling (`FURB157`) ([#18615](https://github.com/astral-sh/ruff/pull/18615))
## 0.11.13
### Preview features
- \[`airflow`\] Add unsafe fix for module moved cases (`AIR301`,`AIR311`,`AIR312`,`AIR302`) ([#18367](https://github.com/astral-sh/ruff/pull/18367),[#18366](https://github.com/astral-sh/ruff/pull/18366),[#18363](https://github.com/astral-sh/ruff/pull/18363),[#18093](https://github.com/astral-sh/ruff/pull/18093))
- \[`refurb`\] Add coverage of `set` and `frozenset` calls (`FURB171`) ([#18035](https://github.com/astral-sh/ruff/pull/18035))
- \[`refurb`\] Mark `FURB180` fix unsafe when class has bases ([#18149](https://github.com/astral-sh/ruff/pull/18149))
### Bug fixes
- \[`perflint`\] Fix missing parentheses for lambda and ternary conditions (`PERF401`, `PERF403`) ([#18412](https://github.com/astral-sh/ruff/pull/18412))
- \[`pyupgrade`\] Apply `UP035` only on py313+ for `get_type_hints()` ([#18476](https://github.com/astral-sh/ruff/pull/18476))
- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP004`,`UP050`) ([#18393](https://github.com/astral-sh/ruff/pull/18393), [#18390](https://github.com/astral-sh/ruff/pull/18390))
### Rule changes
- \[`fastapi`\] Avoid false positive for class dependencies (`FAST003`) ([#18271](https://github.com/astral-sh/ruff/pull/18271))
### Documentation
- Update editor setup docs for Neovim and Vim ([#18324](https://github.com/astral-sh/ruff/pull/18324))
### Other changes
- Support Python 3.14 template strings (t-strings) in formatter and parser ([#17851](https://github.com/astral-sh/ruff/pull/17851))
## 0.11.12
### Preview features
- \[`airflow`\] Revise fix titles (`AIR3`) ([#18215](https://github.com/astral-sh/ruff/pull/18215))
- \[`pylint`\] Implement `missing-maxsplit-arg` (`PLC0207`) ([#17454](https://github.com/astral-sh/ruff/pull/17454))
- \[`pyupgrade`\] New rule `UP050` (`useless-class-metaclass-type`) ([#18334](https://github.com/astral-sh/ruff/pull/18334))
- \[`flake8-use-pathlib`\] Replace `os.symlink` with `Path.symlink_to` (`PTH211`) ([#18337](https://github.com/astral-sh/ruff/pull/18337))
### Bug fixes
- \[`flake8-bugbear`\] Ignore `__debug__` attribute in `B010` ([#18357](https://github.com/astral-sh/ruff/pull/18357))
- \[`flake8-async`\] Fix `anyio.sleep` argument name (`ASYNC115`, `ASYNC116`) ([#18262](https://github.com/astral-sh/ruff/pull/18262))
- \[`refurb`\] Fix `FURB129` autofix generating invalid syntax ([#18235](https://github.com/astral-sh/ruff/pull/18235))
### Rule changes
- \[`flake8-implicit-str-concat`\] Add autofix for `ISC003` ([#18256](https://github.com/astral-sh/ruff/pull/18256))
- \[`pycodestyle`\] Improve the diagnostic message for `E712` ([#18328](https://github.com/astral-sh/ruff/pull/18328))
- \[`flake8-2020`\] Fix diagnostic message for `!=` comparisons (`YTT201`) ([#18293](https://github.com/astral-sh/ruff/pull/18293))
- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP010`) ([#18291](https://github.com/astral-sh/ruff/pull/18291))
### Documentation
- Simplify rules table to improve readability ([#18297](https://github.com/astral-sh/ruff/pull/18297))
- Update editor integrations link in README ([#17977](https://github.com/astral-sh/ruff/pull/17977))
- \[`flake8-bugbear`\] Add fix safety section (`B006`) ([#17652](https://github.com/astral-sh/ruff/pull/17652))
## 0.11.11
### Preview features
- \[`airflow`\] Add autofixes for `AIR302` and `AIR312` ([#17942](https://github.com/astral-sh/ruff/pull/17942))
- \[`airflow`\] Move rules from `AIR312` to `AIR302` ([#17940](https://github.com/astral-sh/ruff/pull/17940))
- \[`airflow`\] Update `AIR301` and `AIR311` with the latest Airflow implementations ([#17985](https://github.com/astral-sh/ruff/pull/17985))
- \[`flake8-simplify`\] Enable fix in preview mode (`SIM117`) ([#18208](https://github.com/astral-sh/ruff/pull/18208))
### Bug fixes
- Fix inconsistent formatting of match-case on `[]` and `_` ([#18147](https://github.com/astral-sh/ruff/pull/18147))
- \[`pylint`\] Fix `PLW1514` not recognizing the `encoding` positional argument of `codecs.open` ([#18109](https://github.com/astral-sh/ruff/pull/18109))
### CLI
- Add full option name in formatter warning ([#18217](https://github.com/astral-sh/ruff/pull/18217))
### Documentation
- Fix rendering of admonition in docs ([#18163](https://github.com/astral-sh/ruff/pull/18163))
- \[`flake8-print`\] Improve print/pprint docs for `T201` and `T203` ([#18130](https://github.com/astral-sh/ruff/pull/18130))
- \[`flake8-simplify`\] Add fix safety section (`SIM110`,`SIM210`) ([#18114](https://github.com/astral-sh/ruff/pull/18114),[#18100](https://github.com/astral-sh/ruff/pull/18100))
- \[`pylint`\] Fix docs example that produced different output (`PLW0603`) ([#18216](https://github.com/astral-sh/ruff/pull/18216))
## 0.11.10
### Preview features
- \[`ruff`\] Implement a recursive check for `RUF060` ([#17976](https://github.com/astral-sh/ruff/pull/17976))
- \[`airflow`\] Enable autofixes for `AIR301` and `AIR311` ([#17941](https://github.com/astral-sh/ruff/pull/17941))
- \[`airflow`\] Apply try catch guard to all `AIR3` rules ([#17887](https://github.com/astral-sh/ruff/pull/17887))
- \[`airflow`\] Extend `AIR311` rules ([#17913](https://github.com/astral-sh/ruff/pull/17913))
### Bug fixes
- \[`flake8-bugbear`\] Ignore `B028` if `skip_file_prefixes` is present ([#18047](https://github.com/astral-sh/ruff/pull/18047))
- \[`flake8-pie`\] Mark autofix for `PIE804` as unsafe if the dictionary contains comments ([#18046](https://github.com/astral-sh/ruff/pull/18046))
- \[`flake8-simplify`\] Correct behavior for `str.split`/`rsplit` with `maxsplit=0` (`SIM905`) ([#18075](https://github.com/astral-sh/ruff/pull/18075))
- \[`flake8-simplify`\] Fix `SIM905` autofix for `rsplit` creating a reversed list literal ([#18045](https://github.com/astral-sh/ruff/pull/18045))
- \[`flake8-use-pathlib`\] Suppress diagnostics for all `os.*` functions that have the `dir_fd` parameter (`PTH`) ([#17968](https://github.com/astral-sh/ruff/pull/17968))
- \[`refurb`\] Mark autofix as safe only for number literals (`FURB116`) ([#17692](https://github.com/astral-sh/ruff/pull/17692))
### Rule changes
- \[`flake8-bandit`\] Skip `S608` for expressionless f-strings ([#17999](https://github.com/astral-sh/ruff/pull/17999))
- \[`flake8-pytest-style`\] Don't recommend `usefixtures` for `parametrize` values (`PT019`) ([#17650](https://github.com/astral-sh/ruff/pull/17650))
- \[`pyupgrade`\] Add `resource.error` as deprecated alias of `OSError` (`UP024`) ([#17933](https://github.com/astral-sh/ruff/pull/17933))
### CLI
- Disable jemalloc on Android ([#18033](https://github.com/astral-sh/ruff/pull/18033))
### Documentation
- Update Neovim setup docs ([#18108](https://github.com/astral-sh/ruff/pull/18108))
- \[`flake8-simplify`\] Add fix safety section (`SIM103`) ([#18086](https://github.com/astral-sh/ruff/pull/18086))
- \[`flake8-simplify`\] Add fix safety section (`SIM112`) ([#18099](https://github.com/astral-sh/ruff/pull/18099))
- \[`pylint`\] Add fix safety section (`PLC0414`) ([#17802](https://github.com/astral-sh/ruff/pull/17802))
- \[`pylint`\] Add fix safety section (`PLE4703`) ([#17824](https://github.com/astral-sh/ruff/pull/17824))
- \[`pylint`\] Add fix safety section (`PLW1514`) ([#17932](https://github.com/astral-sh/ruff/pull/17932))
- \[`pylint`\] Add fix safety section (`PLW3301`) ([#17878](https://github.com/astral-sh/ruff/pull/17878))
- \[`ruff`\] Add fix safety section (`RUF007`) ([#17755](https://github.com/astral-sh/ruff/pull/17755))
- \[`ruff`\] Add fix safety section (`RUF033`) ([#17760](https://github.com/astral-sh/ruff/pull/17760))
## 0.11.9
### Preview features
- Default to latest supported Python version for version-related syntax errors ([#17529](https://github.com/astral-sh/ruff/pull/17529))
- Implement deferred annotations for Python 3.14 ([#17658](https://github.com/astral-sh/ruff/pull/17658))
- \[`airflow`\] Fix `SQLTableCheckOperator` typo (`AIR302`) ([#17946](https://github.com/astral-sh/ruff/pull/17946))
- \[`airflow`\] Remove `airflow.utils.dag_parsing_context.get_parsing_context` (`AIR301`) ([#17852](https://github.com/astral-sh/ruff/pull/17852))
- \[`airflow`\] Skip attribute check in try catch block (`AIR301`) ([#17790](https://github.com/astral-sh/ruff/pull/17790))
- \[`flake8-bandit`\] Mark tuples of string literals as trusted input in `S603` ([#17801](https://github.com/astral-sh/ruff/pull/17801))
- \[`isort`\] Check full module path against project root(s) when categorizing first-party imports ([#16565](https://github.com/astral-sh/ruff/pull/16565))
- \[`ruff`\] Add new rule `in-empty-collection` (`RUF060`) ([#16480](https://github.com/astral-sh/ruff/pull/16480))
### Bug fixes
- Fix missing `combine` call for `lint.typing-extensions` setting ([#17823](https://github.com/astral-sh/ruff/pull/17823))
- \[`flake8-async`\] Fix module name in `ASYNC110`, `ASYNC115`, and `ASYNC116` fixes ([#17774](https://github.com/astral-sh/ruff/pull/17774))
- \[`pyupgrade`\] Add spaces between tokens as necessary to avoid syntax errors in `UP018` autofix ([#17648](https://github.com/astral-sh/ruff/pull/17648))
- \[`refurb`\] Fix false positive for float and complex numbers in `FURB116` ([#17661](https://github.com/astral-sh/ruff/pull/17661))
- [parser] Flag single unparenthesized generator expr with trailing comma in arguments. ([#17893](https://github.com/astral-sh/ruff/pull/17893))
### Documentation
- Add instructions on how to upgrade to a newer Rust version ([#17928](https://github.com/astral-sh/ruff/pull/17928))
- Update code of conduct email address ([#17875](https://github.com/astral-sh/ruff/pull/17875))
- Add fix safety sections to `PLC2801`, `PLR1722`, and `RUF013` ([#17825](https://github.com/astral-sh/ruff/pull/17825), [#17826](https://github.com/astral-sh/ruff/pull/17826), [#17759](https://github.com/astral-sh/ruff/pull/17759))
- Add link to `check-typed-exception` from `S110` and `S112` ([#17786](https://github.com/astral-sh/ruff/pull/17786))
### Other changes
- Allow passing a virtual environment to `ruff analyze graph` ([#17743](https://github.com/astral-sh/ruff/pull/17743))
## 0.11.8
### Preview features
- \[`airflow`\] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR302`, `AIR311`) ([#17553](https://github.com/astral-sh/ruff/pull/17553), [#17570](https://github.com/astral-sh/ruff/pull/17570), [#17571](https://github.com/astral-sh/ruff/pull/17571))
- \[`airflow`\] Extend `AIR301` rule ([#17598](https://github.com/astral-sh/ruff/pull/17598))
- \[`airflow`\] Update existing `AIR302` rules with better suggestions ([#17542](https://github.com/astral-sh/ruff/pull/17542))
- \[`refurb`\] Mark fix as safe for `readlines-in-for` (`FURB129`) ([#17644](https://github.com/astral-sh/ruff/pull/17644))
- [syntax-errors] `nonlocal` declaration at module level ([#17559](https://github.com/astral-sh/ruff/pull/17559))
- [syntax-errors] Detect single starred expression assignment `x = *y` ([#17624](https://github.com/astral-sh/ruff/pull/17624))
### Bug fixes
- \[`flake8-pyi`\] Ensure `Literal[None,] | Literal[None,]` is not autofixed to `None | None` (`PYI061`) ([#17659](https://github.com/astral-sh/ruff/pull/17659))
- \[`flake8-use-pathlib`\] Avoid suggesting `Path.iterdir()` for `os.listdir` with file descriptor (`PTH208`) ([#17715](https://github.com/astral-sh/ruff/pull/17715))
- \[`flake8-use-pathlib`\] Fix `PTH104` false positive when `rename` is passed a file descriptor ([#17712](https://github.com/astral-sh/ruff/pull/17712))
- \[`flake8-use-pathlib`\] Fix `PTH116` false positive when `stat` is passed a file descriptor ([#17709](https://github.com/astral-sh/ruff/pull/17709))
- \[`flake8-use-pathlib`\] Fix `PTH123` false positive when `open` is passed a file descriptor from a function call ([#17705](https://github.com/astral-sh/ruff/pull/17705))
- \[`pycodestyle`\] Fix duplicated diagnostic in `E712` ([#17651](https://github.com/astral-sh/ruff/pull/17651))
- \[`pylint`\] Detect `global` declarations in module scope (`PLE0118`) ([#17411](https://github.com/astral-sh/ruff/pull/17411))
- [syntax-errors] Make `async-comprehension-in-sync-comprehension` more specific ([#17460](https://github.com/astral-sh/ruff/pull/17460))
### Configuration
- Add option to disable `typing_extensions` imports ([#17611](https://github.com/astral-sh/ruff/pull/17611))
### Documentation
- Fix example syntax for the `lint.pydocstyle.ignore-var-parameters` option ([#17740](https://github.com/astral-sh/ruff/pull/17740))
- Add fix safety sections (`ASYNC116`, `FLY002`, `D200`, `RUF005`, `RUF017`, `RUF027`, `RUF028`, `RUF057`) ([#17497](https://github.com/astral-sh/ruff/pull/17497), [#17496](https://github.com/astral-sh/ruff/pull/17496), [#17502](https://github.com/astral-sh/ruff/pull/17502), [#17484](https://github.com/astral-sh/ruff/pull/17484), [#17480](https://github.com/astral-sh/ruff/pull/17480), [#17485](https://github.com/astral-sh/ruff/pull/17485), [#17722](https://github.com/astral-sh/ruff/pull/17722), [#17483](https://github.com/astral-sh/ruff/pull/17483))
### Other changes
- Add Python 3.14 to configuration options ([#17647](https://github.com/astral-sh/ruff/pull/17647))
- Make syntax error for unparenthesized except tuples version specific to before 3.14 ([#17660](https://github.com/astral-sh/ruff/pull/17660))
## 0.11.7
### Preview features
@@ -43,7 +370,7 @@
- \[`airflow`\] Add missing `AIR302` attribute check ([#17115](https://github.com/astral-sh/ruff/pull/17115))
- \[`airflow`\] Expand module path check to individual symbols (`AIR302`) ([#17278](https://github.com/astral-sh/ruff/pull/17278))
- \[`airflow`\] Extract `AIR312` from `AIR302` rules (`AIR302`, `AIR312`) ([#17152](https://github.com/astral-sh/ruff/pull/17152))
- \[`airflow`\] Update oudated `AIR301`, `AIR302` rules ([#17123](https://github.com/astral-sh/ruff/pull/17123))
- \[`airflow`\] Update outdated `AIR301`, `AIR302` rules ([#17123](https://github.com/astral-sh/ruff/pull/17123))
- [syntax-errors] Async comprehension in sync comprehension ([#17177](https://github.com/astral-sh/ruff/pull/17177))
- [syntax-errors] Check annotations in annotated assignments ([#17283](https://github.com/astral-sh/ruff/pull/17283))
- [syntax-errors] Extend annotation checks to `await` ([#17282](https://github.com/astral-sh/ruff/pull/17282))
@@ -204,7 +531,7 @@ See also, the "Remapped rules" section which may result in disabled rules.
- **More robust noqa parsing** ([#16483](https://github.com/astral-sh/ruff/pull/16483))
The syntax for both file-level and in-line suppression comments has been unified and made more robust to certain errors. In most cases, this will result in more suppression comments being read by Ruff, but there are a few instances where previously read comments will now log an error to the user instead. Please refer to the documentation on [*Error suppression*](https://docs.astral.sh/ruff/linter/#error-suppression) for the full specification.
The syntax for both file-level and in-line suppression comments has been unified and made more robust to certain errors. In most cases, this will result in more suppression comments being read by Ruff, but there are a few instances where previously read comments will now log an error to the user instead. Please refer to the documentation on [_Error suppression_](https://docs.astral.sh/ruff/linter/#error-suppression) for the full specification.
- **Avoid unnecessary parentheses around with statements with a single context manager and a trailing comment** ([#14005](https://github.com/astral-sh/ruff/pull/14005))
@@ -1126,7 +1453,7 @@ The following fixes have been stabilized:
- Detect items that hash to same value in duplicate sets (`B033`, `PLC0208`) ([#14064](https://github.com/astral-sh/ruff/pull/14064))
- \[`eradicate`\] Better detection of IntelliJ language injection comments (`ERA001`) ([#14094](https://github.com/astral-sh/ruff/pull/14094))
- \[`flake8-pyi`\] Add autofix for `docstring-in-stub` (`PYI021`) ([#14150](https://github.com/astral-sh/ruff/pull/14150))
- \[`flake8-pyi`\] Update `duplicate-literal-member` (`PYI062`) to alawys provide an autofix ([#14188](https://github.com/astral-sh/ruff/pull/14188))
- \[`flake8-pyi`\] Update `duplicate-literal-member` (`PYI062`) to always provide an autofix ([#14188](https://github.com/astral-sh/ruff/pull/14188))
- \[`pyflakes`\] Detect items that hash to same value in duplicate dictionaries (`F601`) ([#14065](https://github.com/astral-sh/ruff/pull/14065))
- \[`ruff`\] Fix false positive for decorators (`RUF028`) ([#14061](https://github.com/astral-sh/ruff/pull/14061))
@@ -1616,7 +1943,7 @@ The following fixes have been stabilized:
## 0.5.6
Ruff 0.5.6 automatically enables linting and formatting of notebooks in *preview mode*.
Ruff 0.5.6 automatically enables linting and formatting of notebooks in _preview mode_.
You can opt-out of this behavior by adding `*.ipynb` to the `extend-exclude` setting.
```toml
@@ -2369,7 +2696,7 @@ To setup `ruff server` with your editor, refer to the [README.md](https://github
### Server
*This section is devoted to updates for our new language server, written in Rust.*
_This section is devoted to updates for our new language server, written in Rust._
- Enable ruff-specific source actions ([#10916](https://github.com/astral-sh/ruff/pull/10916))
- Refreshes diagnostics for open files when file configuration is changed ([#10988](https://github.com/astral-sh/ruff/pull/10988))
@@ -3776,7 +4103,7 @@ Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/).
- \[`refurb`\] Add `single-item-membership-test` (`FURB171`) ([#7815](https://github.com/astral-sh/ruff/pull/7815))
- \[`pylint`\] Add `and-or-ternary` (`R1706`) ([#7811](https://github.com/astral-sh/ruff/pull/7811))
*New rules are added in [preview](https://docs.astral.sh/ruff/preview/).*
_New rules are added in [preview](https://docs.astral.sh/ruff/preview/)._
### Configuration
@@ -3851,3 +4178,12 @@ Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/).
### Playground
- Fix playground `Quick Fix` action ([#7824](https://github.com/astral-sh/ruff/pull/7824))
[`boolean-type-hint-positional-argument`]: https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument
[`collection-literal-concatenation`]: https://docs.astral.sh/ruff/rules/collection-literal-concatenation
[`if-else-block-instead-of-if-exp`]: https://docs.astral.sh/ruff/rules/if-else-block-instead-of-if-exp
[`non-pep604-annotation-optional`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-optional
[`non-pep604-annotation-union`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-union
[`readlines-in-for`]: https://docs.astral.sh/ruff/rules/readlines-in-for
[`subprocess-without-shell-equals-true`]: https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true
[`unused-noqa`]: https://docs.astral.sh/ruff/rules/unused-noqa

View File

@@ -71,8 +71,7 @@ representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<charlie.r.marsh@gmail.com>.
reported to the community leaders responsible for enforcement at <hey@astral.sh>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the

View File

@@ -2,6 +2,11 @@
Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff.
> [!NOTE]
>
> This guide is for Ruff. If you're looking to contribute to ty, please see [the ty contributing
> guide](https://github.com/astral-sh/ruff/blob/main/crates/ty/CONTRIBUTING.md).
## The Basics
Ruff welcomes contributions in the form of pull requests.
@@ -366,6 +371,15 @@ uvx --from ./python/ruff-ecosystem ruff-ecosystem format ruff "./target/debug/ru
See the [ruff-ecosystem package](https://github.com/astral-sh/ruff/tree/main/python/ruff-ecosystem) for more details.
## Upgrading Rust
1. Change the `channel` in `./rust-toolchain.toml` to the new Rust version (`<latest>`)
1. Change the `rust-version` in the `./Cargo.toml` to `<latest> - 2` (e.g. 1.84 if the latest is 1.86)
1. Run `cargo clippy --fix --allow-dirty --allow-staged` to fix new clippy warnings
1. Create and merge the PR
1. Bump the Rust version in Ruff's conda forge recipe. See [this PR](https://github.com/conda-forge/ruff-feedstock/pull/266) for an example.
1. Enjoy the new Rust version!
## Benchmarking and Profiling
We have several ways of benchmarking and profiling Ruff:
@@ -397,7 +411,7 @@ cargo install hyperfine
To benchmark the release build:
```shell
cargo build --release && hyperfine --warmup 10 \
cargo build --release --bin ruff && hyperfine --warmup 10 \
"./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e" \
"./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ -e"
@@ -596,8 +610,7 @@ Then convert the recorded profile
perf script -F +pid > /tmp/test.perf
```
You can now view the converted file with [firefox profiler](https://profiler.firefox.com/), with a
more in-depth guide [here](https://profiler.firefox.com/docs/#/./guide-perf-profiling)
You can now view the converted file with [firefox profiler](https://profiler.firefox.com/). To learn more about Firefox profiler, read the [Firefox profiler profiling-guide](https://profiler.firefox.com/docs/#/./guide-perf-profiling).
An alternative is to convert the perf data to `flamegraph.svg` using
[flamegraph](https://github.com/flamegraph-rs/flamegraph) (`cargo install flamegraph`):

1438
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,9 @@ members = ["crates/*"]
resolver = "2"
[workspace.package]
edition = "2021"
rust-version = "1.84"
# Please update rustfmt.toml when bumping the Rust edition
edition = "2024"
rust-version = "1.85"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
@@ -23,6 +24,7 @@ ruff_index = { path = "crates/ruff_index" }
ruff_linter = { path = "crates/ruff_linter" }
ruff_macros = { path = "crates/ruff_macros" }
ruff_notebook = { path = "crates/ruff_notebook" }
ruff_options_metadata = { path = "crates/ruff_options_metadata" }
ruff_python_ast = { path = "crates/ruff_python_ast" }
ruff_python_codegen = { path = "crates/ruff_python_codegen" }
ruff_python_formatter = { path = "crates/ruff_python_formatter" }
@@ -35,22 +37,24 @@ ruff_python_trivia = { path = "crates/ruff_python_trivia" }
ruff_server = { path = "crates/ruff_server" }
ruff_source_file = { path = "crates/ruff_source_file" }
ruff_text_size = { path = "crates/ruff_text_size" }
red_knot_vendored = { path = "crates/red_knot_vendored" }
ruff_workspace = { path = "crates/ruff_workspace" }
red_knot_ide = { path = "crates/red_knot_ide" }
red_knot_project = { path = "crates/red_knot_project", default-features = false }
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
red_knot_server = { path = "crates/red_knot_server" }
red_knot_test = { path = "crates/red_knot_test" }
ty = { path = "crates/ty" }
ty_ide = { path = "crates/ty_ide" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { path = "crates/ty_python_semantic" }
ty_server = { path = "crates/ty_server" }
ty_test = { path = "crates/ty_test" }
ty_vendored = { path = "crates/ty_vendored" }
aho-corasick = { version = "1.1.3" }
anstream = { version = "0.6.18" }
anstyle = { version = "1.0.10" }
anyhow = { version = "1.0.80" }
arc-swap = { version = "1.7.1" }
assert_fs = { version = "1.1.0" }
argfile = { version = "0.2.0" }
bincode = { version = "1.3.3" }
bincode = { version = "2.0.0" }
bitflags = { version = "2.5.0" }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" }
@@ -64,7 +68,7 @@ console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version = "3.0.1" }
compact_str = "0.9.0"
criterion = { version = "0.5.1", default-features = false }
criterion = { version = "0.6.0", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
dir-test = { version = "0.4.0" }
@@ -83,6 +87,7 @@ hashbrown = { version = "0.15.0", default-features = false, features = [
"equivalent",
"inline-more",
] }
heck = "0.5.0"
ignore = { version = "0.4.22" }
imara-diff = { version = "0.1.5" }
imperative = { version = "1.0.4" }
@@ -96,7 +101,7 @@ is-wsl = { version = "0.4.0" }
itertools = { version = "0.14.0" }
jiff = { version = "0.2.0" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "0.1.2" }
jod-thread = { version = "1.0.0" }
libc = { version = "0.2.153" }
libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" }
@@ -122,9 +127,11 @@ quote = { version = "1.0.23" }
rand = { version = "0.9.0" }
rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
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 = "c75b0161aba55965ab6ad8cc9aaee7dc177967f1" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "09627e450566f894956710a3fd923dc80462ae6d" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -159,8 +166,9 @@ tracing-log = { version = "0.2.0" }
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
"env-filter",
"fmt",
"ansi",
"smallvec"
] }
tracing-tree = { version = "0.4.0" }
tryfn = { version = "0.2.1" }
typed-arena = { version = "2.0.2" }
unic-ucd-category = { version = "0.9" }
@@ -173,7 +181,6 @@ uuid = { version = "1.6.1", features = [
"v4",
"fast-rng",
"macro-diagnostics",
"js",
] }
walkdir = { version = "2.3.2" }
wasm-bindgen = { version = "0.2.92" }
@@ -182,7 +189,7 @@ wild = { version = "2" }
zip = { version = "0.6.6", default-features = false }
[workspace.metadata.cargo-shear]
ignored = ["getrandom"]
ignored = ["getrandom", "ruff_options_metadata", "uuid"]
[workspace.lints.rust]
@@ -210,6 +217,7 @@ similar_names = "allow"
single_match_else = "allow"
too_many_lines = "allow"
needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
unnecessary_debug_formatting = "allow" # too many instances, the display also doesn't quote the path which is often desired in logs where we use them the most often.
# Without the hashes we run into a `rustfmt` bug in some snapshot tests, see #13250
needless_raw_string_hashes = "allow"
# Disallowed restriction lints
@@ -231,10 +239,6 @@ unused_peekable = "warn"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"
# Salsa generates functions with parameters for each field of a `salsa::interned` struct.
# If we don't allow this, we get warnings for structs with too many fields.
too_many_arguments = "allow"
[profile.release]
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times
@@ -258,6 +262,9 @@ opt-level = 3
[profile.dev.package.similar]
opt-level = 3
[profile.dev.package.salsa]
opt-level = 3
# Reduce complexity of a parser function that would trigger a locals limit in a wasm tool.
# https://github.com/bytecodealliance/wasm-tools/blob/b5c3d98e40590512a3b12470ef358d5c7b983b15/crates/wasmparser/src/limits.rs#L29
[profile.dev.package.ruff_python_parser]
@@ -272,75 +279,3 @@ debug = 1
# The profile that 'cargo dist' will build with.
[profile.dist]
inherits = "release"
# Config for 'dist'
[workspace.metadata.dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.28.4"
# Make distability of apps opt-in instead of opt-out
dist = false
# CI backends to support
ci = "github"
# The installers to generate for each app
installers = ["shell", "powershell"]
# The archive format to use for windows builds (defaults .zip)
windows-archive = ".zip"
# The archive format to use for non-windows builds (defaults .tar.xz)
unix-archive = ".tar.gz"
# Target platforms to build apps for (Rust target-triple syntax)
targets = [
"aarch64-apple-darwin",
"aarch64-pc-windows-msvc",
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"arm-unknown-linux-musleabihf",
"armv7-unknown-linux-gnueabihf",
"armv7-unknown-linux-musleabihf",
"i686-pc-windows-msvc",
"i686-unknown-linux-gnu",
"i686-unknown-linux-musl",
"powerpc64-unknown-linux-gnu",
"powerpc64le-unknown-linux-gnu",
"s390x-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
]
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
auto-includes = false
# Whether dist should create a Github Release or use an existing draft
create-release = true
# Which actions to run on pull requests
pr-run-mode = "plan"
# Whether CI should trigger releases with dispatches instead of tag pushes
dispatch-releases = true
# Which phase dist should use to create the GitHub release
github-release = "announce"
# Whether CI should include auto-generated code to build local artifacts
build-local-artifacts = false
# Local artifacts jobs to run in CI
local-artifacts-jobs = ["./build-binaries", "./build-docker"]
# Publish jobs to run in CI
publish-jobs = ["./publish-pypi", "./publish-wasm"]
# Post-announce jobs to run in CI
post-announce-jobs = [
"./notify-dependents",
"./publish-docs",
"./publish-playground",
]
# Custom permissions for GitHub Jobs
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
# Whether to install an updater program
install-updater = false
# Path that installers should place binaries in
install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
[workspace.metadata.dist.github-custom-runners]
global = "depot-ubuntu-latest-4"
[workspace.metadata.dist.github-action-commits]
"actions/checkout" = "85e6279cec87321a52edac9c87bce653a07cf6c2" # v4
"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2
"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0
"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3

View File

@@ -34,8 +34,7 @@ An extremely fast Python linter and code formatter, written in Rust.
- 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [800 built-in rules](https://docs.astral.sh/ruff/rules/), with native re-implementations
of popular Flake8 plugins, like flake8-bugbear
- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/integrations/) for
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://docs.astral.sh/ruff/editors/setup)
- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/editors) for [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://docs.astral.sh/ruff/editors/setup)
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://docs.astral.sh/ruff/configuration/#config-file-discovery)
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
@@ -149,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.11.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.7/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.0/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -183,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.7
rev: v0.12.0
hooks:
# Run the linter.
- id: ruff
@@ -255,7 +254,7 @@ indent-width = 4
target-version = "py39"
[lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F"]
ignore = []

View File

@@ -1,9 +1,13 @@
[files]
# https://github.com/crate-ci/typos/issues/868
extend-exclude = [
"crates/red_knot_vendored/vendor/**/*",
"crates/ty_vendored/vendor/**/*",
"**/resources/**/*",
"**/snapshots/**/*",
# Completion tests tend to have a lot of incomplete
# words naturally. It's annoying to have to make all
# of them actually words. So just ignore typos here.
"crates/ty_ide/src/completion.rs",
]
[default.extend-words]

View File

@@ -1,6 +1,7 @@
doc-valid-idents = [
"..",
"CodeQL",
"CPython",
"FastAPI",
"IPython",
"LangChain",
@@ -14,7 +15,7 @@ doc-valid-idents = [
"SNMPv1",
"SNMPv2",
"SNMPv3",
"PyFlakes"
"PyFlakes",
]
ignore-interior-mutability = [

View File

@@ -1,25 +0,0 @@
# Red Knot
Red Knot is an extremely fast type checker.
Currently, it is a work-in-progress and not ready for user testing.
Red Knot is designed to prioritize good type inference, even in unannotated code,
and aims to avoid false positives.
While Red Knot will produce similar results to mypy and pyright on many codebases,
100% compatibility with these tools is a non-goal.
On some codebases, Red Knot's design decisions lead to different outcomes
than you would get from running one of these more established tools.
## Contributing
Core type checking tests are written as Markdown code blocks.
They can be found in [`red_knot_python_semantic/resources/mdtest`][resources-mdtest].
See [`red_knot_test/README.md`][mdtest-readme] for more information
on the test framework itself.
The list of open issues can be found [here][open-issues].
[mdtest-readme]: ../red_knot_test/README.md
[open-issues]: https://github.com/astral-sh/ruff/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3Ared-knot
[resources-mdtest]: ../red_knot_python_semantic/resources/mdtest

File diff suppressed because it is too large Load Diff

View File

@@ -1,106 +0,0 @@
use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal};
use ruff_python_ast::AnyNodeRef;
use ruff_text_size::{Ranged, TextRange};
use std::fmt;
use std::fmt::Formatter;
/// Returns the node with a minimal range that fully contains `range`.
///
/// If `range` is empty and falls within a parser *synthesized* node generated during error recovery,
/// then the first node with the given range is returned.
///
/// ## Panics
/// Panics if `range` is not contained within `root`.
pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
struct Visitor<'a> {
range: TextRange,
found: bool,
ancestors: Vec<AnyNodeRef<'a>>,
}
impl<'a> SourceOrderVisitor<'a> for Visitor<'a> {
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
// If the node fully contains the range, than it is a possible match but traverse into its children
// to see if there's a node with a narrower range.
if !self.found && node.range().contains_range(self.range) {
self.ancestors.push(node);
TraversalSignal::Traverse
} else {
TraversalSignal::Skip
}
}
fn leave_node(&mut self, node: AnyNodeRef<'a>) {
if !self.found && self.ancestors.last() == Some(&node) {
self.found = true;
}
}
}
assert!(
root.range().contains_range(range),
"Range is not contained within root"
);
let mut visitor = Visitor {
range,
found: false,
ancestors: Vec::new(),
};
root.visit_source_order(&mut visitor);
let minimal = visitor.ancestors.pop().unwrap_or(root);
CoveringNode {
node: minimal,
ancestors: visitor.ancestors,
}
}
/// The node with a minimal range that fully contains the search range.
pub(crate) struct CoveringNode<'a> {
/// The node with a minimal range that fully contains the search range.
node: AnyNodeRef<'a>,
/// The node's ancestor (the spine up to the root).
ancestors: Vec<AnyNodeRef<'a>>,
}
impl<'a> CoveringNode<'a> {
pub(crate) fn node(&self) -> AnyNodeRef<'a> {
self.node
}
/// Returns the node's parent.
pub(crate) fn parent(&self) -> Option<AnyNodeRef<'a>> {
self.ancestors.last().copied()
}
/// Finds the minimal node that fully covers the range and fulfills the given predicate.
pub(crate) fn find(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
if f(self.node) {
return Ok(self);
}
match self.ancestors.iter().rposition(|node| f(*node)) {
Some(index) => {
let node = self.ancestors[index];
self.ancestors.truncate(index);
Ok(Self {
node,
ancestors: self.ancestors,
})
}
None => Err(self),
}
}
}
impl fmt::Debug for CoveringNode<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_tuple("NodeWithAncestors")
.field(&self.node)
.finish()
}
}

View File

@@ -1 +0,0 @@
../../../../ruff_python_parser/resources/invalid/statements/invalid_assignment_targets.py

View File

@@ -1 +0,0 @@
../../../../ruff_python_parser/resources/invalid/expressions/named/invalid_target.py

View File

@@ -1 +0,0 @@
../../../../ruff_python_parser/resources/invalid/statements/invalid_augmented_assignment_target.py

View File

@@ -1 +0,0 @@
../../../../ruff_notebook/resources/test/fixtures/jupyter/unused_variable.ipynb

View File

@@ -1 +0,0 @@
../../../../ruff_python_parser/resources/inline/err/type_param_invalid_bound_expr.py

View File

@@ -1 +0,0 @@
../../../../ruff_python_parser/resources/inline/err/ann_assign_stmt_invalid_target.py

View File

@@ -1,69 +0,0 @@
use std::sync::Arc;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use thiserror::Error;
use crate::metadata::value::ValueSource;
use super::options::{KnotTomlError, Options};
/// A `knot.toml` configuration file with the options it contains.
pub(crate) struct ConfigurationFile {
path: SystemPathBuf,
options: Options,
}
impl ConfigurationFile {
/// Loads the user-level configuration file if it exists.
///
/// Returns `None` if the file does not exist or if the concept of user-level configurations
/// doesn't exist on `system`.
pub(crate) fn user(system: &dyn System) -> Result<Option<Self>, ConfigurationFileError> {
let Some(configuration_directory) = system.user_config_directory() else {
return Ok(None);
};
let knot_toml_path = configuration_directory.join("knot").join("knot.toml");
tracing::debug!(
"Searching for a user-level configuration at `{path}`",
path = &knot_toml_path
);
let Ok(knot_toml_str) = system.read_to_string(&knot_toml_path) else {
return Ok(None);
};
match Options::from_toml_str(
&knot_toml_str,
ValueSource::File(Arc::new(knot_toml_path.clone())),
) {
Ok(options) => Ok(Some(Self {
path: knot_toml_path,
options,
})),
Err(error) => Err(ConfigurationFileError::InvalidKnotToml {
source: Box::new(error),
path: knot_toml_path,
}),
}
}
/// Returns the path to the configuration file.
pub(crate) fn path(&self) -> &SystemPath {
&self.path
}
pub(crate) fn into_options(self) -> Options {
self.options
}
}
#[derive(Debug, Error)]
pub enum ConfigurationFileError {
#[error("{path} is not a valid `knot.toml`: {source}")]
InvalidKnotToml {
source: Box<KnotTomlError>,
path: SystemPathBuf,
},
}

View File

@@ -1,422 +0,0 @@
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
use crate::Db;
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
use red_knot_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings};
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span};
use ruff_db::files::system_path_to_file;
use ruff_db::system::{System, SystemPath};
use ruff_macros::Combine;
use ruff_python_ast::PythonVersion;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use thiserror::Error;
use super::settings::{Settings, TerminalSettings};
/// The options for the project.
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options {
/// Configures the type checking environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<EnvironmentOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub src: Option<SrcOptions>,
/// Configures the enabled lints and their severity.
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Rules>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terminal: Option<TerminalOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub respect_ignore_files: Option<bool>,
}
impl Options {
pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, KnotTomlError> {
let _guard = ValueSourceGuard::new(source, true);
let options = toml::from_str(content)?;
Ok(options)
}
pub fn deserialize_with<'de, D>(source: ValueSource, deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let _guard = ValueSourceGuard::new(source, false);
Self::deserialize(deserializer)
}
pub(crate) fn to_program_settings(
&self,
project_root: &SystemPath,
system: &dyn System,
) -> ProgramSettings {
let python_version = self
.environment
.as_ref()
.and_then(|env| env.python_version.as_deref().copied())
.unwrap_or_default();
let python_platform = self
.environment
.as_ref()
.and_then(|env| env.python_platform.as_deref().cloned())
.unwrap_or_else(|| {
let default = PythonPlatform::default();
tracing::info!(
"Defaulting to default python version for this platform: '{default}'",
);
default
});
ProgramSettings {
python_version,
python_platform,
search_paths: self.to_search_path_settings(project_root, system),
}
}
fn to_search_path_settings(
&self,
project_root: &SystemPath,
system: &dyn System,
) -> SearchPathSettings {
let src_roots = if let Some(src_root) = self.src.as_ref().and_then(|src| src.root.as_ref())
{
vec![src_root.absolute(project_root, system)]
} else {
let src = project_root.join("src");
// Default to `src` and the project root if `src` exists and the root hasn't been specified.
if system.is_directory(&src) {
vec![project_root.to_path_buf(), src]
} else {
vec![project_root.to_path_buf()]
}
};
let (extra_paths, python, typeshed) = self
.environment
.as_ref()
.map(|env| {
(
env.extra_paths.clone(),
env.python.clone(),
env.typeshed.clone(),
)
})
.unwrap_or_default();
SearchPathSettings {
extra_paths: extra_paths
.unwrap_or_default()
.into_iter()
.map(|path| path.absolute(project_root, system))
.collect(),
src_roots,
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
python_path: python
.map(|python_path| {
PythonPath::from_cli_flag(python_path.absolute(project_root, system))
})
.or_else(|| {
std::env::var("VIRTUAL_ENV")
.ok()
.map(PythonPath::from_virtual_env_var)
})
.unwrap_or_else(|| PythonPath::Discover(project_root.to_path_buf())),
}
}
#[must_use]
pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec<OptionDiagnostic>) {
let (rules, diagnostics) = self.to_rule_selection(db);
let mut settings = Settings::new(rules, self.respect_ignore_files);
if let Some(terminal) = self.terminal.as_ref() {
settings.set_terminal(TerminalSettings {
output_format: terminal
.output_format
.as_deref()
.copied()
.unwrap_or_default(),
error_on_warning: terminal.error_on_warning.unwrap_or_default(),
});
}
(settings, diagnostics)
}
#[must_use]
fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
let registry = db.lint_registry();
let mut diagnostics = Vec::new();
// Initialize the selection with the defaults
let mut selection = RuleSelection::from_registry(registry);
let rules = self
.rules
.as_ref()
.into_iter()
.flat_map(|rules| rules.inner.iter());
for (rule_name, level) in rules {
let source = rule_name.source();
match registry.get(rule_name) {
Ok(lint) => {
let lint_source = match source {
ValueSource::File(_) => LintSource::File,
ValueSource::Cli => LintSource::Cli,
};
if let Ok(severity) = Severity::try_from(**level) {
selection.enable(lint, severity, lint_source);
} else {
selection.disable(lint);
}
}
Err(error) => {
// `system_path_to_file` can return `Err` if the file was deleted since the configuration
// was read. This should be rare and it should be okay to default to not showing a configuration
// file in that case.
let file = source
.file()
.and_then(|path| system_path_to_file(db.upcast(), path).ok());
// TODO: Add a note if the value was configured on the CLI
let diagnostic = match error {
GetLintError::Unknown(_) => OptionDiagnostic::new(
DiagnosticId::UnknownRule,
format!("Unknown lint rule `{rule_name}`"),
Severity::Warning,
),
GetLintError::PrefixedWithCategory { suggestion, .. } => {
OptionDiagnostic::new(
DiagnosticId::UnknownRule,
format!(
"Unknown lint rule `{rule_name}`. Did you mean `{suggestion}`?"
),
Severity::Warning,
)
}
GetLintError::Removed(_) => OptionDiagnostic::new(
DiagnosticId::UnknownRule,
format!("Unknown lint rule `{rule_name}`"),
Severity::Warning,
),
};
let span = file.map(Span::from).map(|span| {
if let Some(range) = rule_name.range() {
span.with_range(range)
} else {
span
}
});
diagnostics.push(diagnostic.with_span(span));
}
}
}
(selection, diagnostics)
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct EnvironmentOptions {
/// Specifies the version of Python that will be used to analyze the source code.
/// The version should be specified as a string in the format `M.m` where `M` is the major version
/// and `m` is the minor (e.g. "3.0" or "3.6").
/// If a version is provided, knot will generate errors if the source code makes use of language features
/// that are not supported in that version.
/// It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.
#[serde(skip_serializing_if = "Option::is_none")]
pub python_version: Option<RangedValue<PythonVersion>>,
/// Specifies the target platform that will be used to analyze the source code.
/// If specified, Red Knot will tailor its use of type stub files,
/// which conditionalize type definitions based on the platform.
///
/// If no platform is specified, knot will use the current platform:
/// - `win32` for Windows
/// - `darwin` for macOS
/// - `android` for Android
/// - `ios` for iOS
/// - `linux` for everything else
#[serde(skip_serializing_if = "Option::is_none")]
pub python_platform: Option<RangedValue<PythonPlatform>>,
/// 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.
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_paths: Option<Vec<RelativePathBuf>>,
/// Optional path to a "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
#[serde(skip_serializing_if = "Option::is_none")]
pub typeshed: Option<RelativePathBuf>,
/// Path to the Python installation from which Red Knot resolves type information and third-party dependencies.
///
/// Red Knot will search in the path's `site-packages` directories for type information and
/// third-party imports.
///
/// This option is commonly used to specify the path to a virtual environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub python: Option<RelativePathBuf>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SrcOptions {
/// The root of the project, used for finding first-party modules.
#[serde(skip_serializing_if = "Option::is_none")]
pub root: Option<RelativePathBuf>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Rules {
#[cfg_attr(feature = "schemars", schemars(with = "schema::Rules"))]
inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
}
impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
fn from_iter<T: IntoIterator<Item = (RangedValue<String>, RangedValue<Level>)>>(
iter: T,
) -> Self {
Self {
inner: iter.into_iter().collect(),
}
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TerminalOptions {
/// The format to use for printing diagnostic messages.
///
/// Defaults to `full`.
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<RangedValue<DiagnosticFormat>>,
/// Use exit code 1 if there are any warning-level diagnostics.
///
/// Defaults to `false`.
pub error_on_warning: Option<bool>,
}
#[cfg(feature = "schemars")]
mod schema {
use crate::DEFAULT_LINT_REGISTRY;
use red_knot_python_semantic::lint::Level;
use schemars::gen::SchemaGenerator;
use schemars::schema::{
InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SubschemaValidation,
};
use schemars::JsonSchema;
pub(super) struct Rules;
impl JsonSchema for Rules {
fn schema_name() -> String {
"Rules".to_string()
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let registry = &*DEFAULT_LINT_REGISTRY;
let level_schema = gen.subschema_for::<Level>();
let properties: schemars::Map<String, Schema> = registry
.lints()
.iter()
.map(|lint| {
(
lint.name().to_string(),
Schema::Object(SchemaObject {
metadata: Some(Box::new(Metadata {
title: Some(lint.summary().to_string()),
description: Some(lint.documentation()),
deprecated: lint.status.is_deprecated(),
default: Some(lint.default_level.to_string().into()),
..Metadata::default()
})),
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![level_schema.clone()]),
..Default::default()
})),
..Default::default()
}),
)
})
.collect();
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
properties,
// Allow unknown rules: Red Knot will warn about them.
// It gives a better experience when using an older Red Knot version because
// the schema will not deny rules that have been removed in newer versions.
additional_properties: Some(Box::new(level_schema)),
..ObjectValidation::default()
})),
..Default::default()
})
}
}
}
#[derive(Error, Debug)]
pub enum KnotTomlError {
#[error(transparent)]
TomlSyntax(#[from] toml::de::Error),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct OptionDiagnostic {
id: DiagnosticId,
message: String,
severity: Severity,
span: Option<Span>,
}
impl OptionDiagnostic {
pub fn new(id: DiagnosticId, message: String, severity: Severity) -> Self {
Self {
id,
message,
severity,
span: None,
}
}
#[must_use]
fn with_span(self, span: Option<Span>) -> Self {
OptionDiagnostic { span, ..self }
}
pub(crate) fn to_diagnostic(&self) -> Diagnostic {
if let Some(ref span) = self.span {
let mut diag = Diagnostic::new(self.id, self.severity, "");
diag.annotate(Annotation::primary(span.clone()).message(&self.message));
diag
} else {
Diagnostic::new(self.id, self.severity, &self.message)
}
}
}

View File

@@ -1,62 +0,0 @@
use std::sync::Arc;
use red_knot_python_semantic::lint::RuleSelection;
use ruff_db::diagnostic::DiagnosticFormat;
/// The resolved [`super::Options`] for the project.
///
/// Unlike [`super::Options`], the struct has default values filled in and
/// uses representations that are optimized for reads (instead of preserving the source representation).
/// It's also not required that this structure precisely resembles the TOML schema, although
/// it's encouraged to use a similar structure.
///
/// It's worth considering to adding a salsa query for specific settings to
/// limit the blast radius when only some settings change. For example,
/// changing the terminal settings shouldn't invalidate any core type-checking queries.
/// This can be achieved by adding a salsa query for the type checking specific settings.
///
/// Settings that are part of [`red_knot_python_semantic::ProgramSettings`] are not included here.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Settings {
rules: Arc<RuleSelection>,
terminal: TerminalSettings,
respect_ignore_files: bool,
}
impl Settings {
pub fn new(rules: RuleSelection, respect_ignore_files: Option<bool>) -> Self {
Self {
rules: Arc::new(rules),
terminal: TerminalSettings::default(),
respect_ignore_files: respect_ignore_files.unwrap_or(true),
}
}
pub fn rules(&self) -> &RuleSelection {
&self.rules
}
pub fn respect_ignore_files(&self) -> bool {
self.respect_ignore_files
}
pub fn to_rules(&self) -> Arc<RuleSelection> {
self.rules.clone()
}
pub fn terminal(&self) -> &TerminalSettings {
&self.terminal
}
pub fn set_terminal(&mut self, terminal: TerminalSettings) {
self.terminal = terminal;
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TerminalSettings {
pub output_format: DiagnosticFormat,
pub error_on_warning: bool,
}

View File

@@ -1,292 +0,0 @@
use anyhow::{anyhow, Context};
use red_knot_project::{ProjectDatabase, ProjectMetadata};
use red_knot_python_semantic::{HasType, SemanticModel};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module;
use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem};
use ruff_python_ast::visitor::source_order;
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
use ruff_python_ast::{
self as ast, Alias, Comprehension, Expr, Parameter, ParameterWithDefault, Stmt,
};
fn setup_db(project_root: &SystemPath, system: TestSystem) -> anyhow::Result<ProjectDatabase> {
let project = ProjectMetadata::discover(project_root, &system)?;
ProjectDatabase::new(project, system)
}
fn get_cargo_workspace_root() -> anyhow::Result<SystemPathBuf> {
Ok(SystemPathBuf::from(String::from_utf8(
std::process::Command::new("cargo")
.args(["locate-project", "--workspace", "--message-format", "plain"])
.output()?
.stdout,
)?)
.parent()
.unwrap()
.to_owned())
}
/// Test that all snippets in testcorpus can be checked without panic (except for [`KNOWN_FAILURES`])
#[test]
fn corpus_no_panic() -> anyhow::Result<()> {
let crate_root = String::from(env!("CARGO_MANIFEST_DIR"));
run_corpus_tests(&format!("{crate_root}/resources/test/corpus/**/*.py"))
}
#[test]
fn parser_no_panic() -> anyhow::Result<()> {
let workspace_root = get_cargo_workspace_root()?;
run_corpus_tests(&format!(
"{workspace_root}/crates/ruff_python_parser/resources/**/*.py"
))
}
#[test]
fn linter_af_no_panic() -> anyhow::Result<()> {
let workspace_root = get_cargo_workspace_root()?;
run_corpus_tests(&format!(
"{workspace_root}/crates/ruff_linter/resources/test/fixtures/[a-f]*/**/*.py"
))
}
#[test]
fn linter_gz_no_panic() -> anyhow::Result<()> {
let workspace_root = get_cargo_workspace_root()?;
run_corpus_tests(&format!(
"{workspace_root}/crates/ruff_linter/resources/test/fixtures/[g-z]*/**/*.py"
))
}
#[test]
#[ignore = "Enable running once there are fewer failures"]
fn linter_stubs_no_panic() -> anyhow::Result<()> {
let workspace_root = get_cargo_workspace_root()?;
run_corpus_tests(&format!(
"{workspace_root}/crates/ruff_linter/resources/test/fixtures/**/*.pyi"
))
}
#[test]
#[ignore = "Enable running over typeshed stubs once there are fewer failures"]
fn typeshed_no_panic() -> anyhow::Result<()> {
let workspace_root = get_cargo_workspace_root()?;
run_corpus_tests(&format!(
"{workspace_root}/crates/red_knot_vendored/vendor/typeshed/**/*.pyi"
))
}
#[allow(clippy::print_stdout)]
fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> {
let root = SystemPathBuf::from("/src");
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
memory_fs.create_directory_all(root.as_ref())?;
let mut db = setup_db(&root, system.clone())?;
let workspace_root = get_cargo_workspace_root()?;
let workspace_root = workspace_root.to_string();
let corpus = glob::glob(pattern).context("Failed to compile pattern")?;
for path in corpus {
let path = path.context("Failed to glob path")?;
let path = SystemPathBuf::from_path_buf(path).map_err(|path| {
anyhow!(
"Failed to convert path '{path}' to system path",
path = path.display()
)
})?;
let relative_path = path.strip_prefix(&workspace_root)?;
let (py_expected_to_fail, pyi_expected_to_fail) = KNOWN_FAILURES
.iter()
.find_map(|(path, py_fail, pyi_fail)| {
if *path == relative_path.as_str().replace('\\', "/") {
Some((*py_fail, *pyi_fail))
} else {
None
}
})
.unwrap_or((false, false));
let source = path.as_path();
let source_filename = source.file_name().unwrap();
let code = std::fs::read_to_string(source)?;
let mut check_with_file_name = |path: &SystemPath| {
memory_fs.write_file_all(path, &code).unwrap();
File::sync_path(&mut db, path);
// this test is only asserting that we can pull every expression type without a panic
// (and some non-expressions that clearly define a single type)
let file = system_path_to_file(&db, path).unwrap();
let result = std::panic::catch_unwind(|| pull_types(&db, file));
let expected_to_fail = if path.extension().map(|e| e == "pyi").unwrap_or(false) {
pyi_expected_to_fail
} else {
py_expected_to_fail
};
if let Err(err) = result {
if !expected_to_fail {
println!("Check failed for {relative_path:?}. Consider fixing it or adding it to KNOWN_FAILURES");
std::panic::resume_unwind(err);
}
} else {
assert!(!expected_to_fail, "Expected to panic, but did not. Consider removing this path from KNOWN_FAILURES");
}
memory_fs.remove_file(path).unwrap();
file.sync(&mut db);
};
if source.extension() == Some("pyi") {
println!("checking {relative_path}");
let pyi_dest = root.join(source_filename);
check_with_file_name(&pyi_dest);
} else {
println!("checking {relative_path}");
let py_dest = root.join(source_filename);
check_with_file_name(&py_dest);
let pyi_dest = root.join(format!("{source_filename}i"));
println!("re-checking as stub file: {pyi_dest}");
check_with_file_name(&pyi_dest);
}
}
Ok(())
}
fn pull_types(db: &ProjectDatabase, file: File) {
let mut visitor = PullTypesVisitor::new(db, file);
let ast = parsed_module(db, file);
visitor.visit_body(ast.suite());
}
struct PullTypesVisitor<'db> {
model: SemanticModel<'db>,
}
impl<'db> PullTypesVisitor<'db> {
fn new(db: &'db ProjectDatabase, file: File) -> Self {
Self {
model: SemanticModel::new(db, file),
}
}
fn visit_target(&mut self, target: &Expr) {
match target {
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
for element in elts {
self.visit_target(element);
}
}
_ => self.visit_expr(target),
}
}
}
impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::FunctionDef(function) => {
let _ty = function.inferred_type(&self.model);
}
Stmt::ClassDef(class) => {
let _ty = class.inferred_type(&self.model);
}
Stmt::Assign(assign) => {
for target in &assign.targets {
self.visit_target(target);
}
self.visit_expr(&assign.value);
return;
}
Stmt::For(for_stmt) => {
self.visit_target(&for_stmt.target);
self.visit_expr(&for_stmt.iter);
self.visit_body(&for_stmt.body);
self.visit_body(&for_stmt.orelse);
return;
}
Stmt::With(with_stmt) => {
for item in &with_stmt.items {
if let Some(target) = &item.optional_vars {
self.visit_target(target);
}
self.visit_expr(&item.context_expr);
}
self.visit_body(&with_stmt.body);
return;
}
Stmt::AnnAssign(_)
| Stmt::Return(_)
| Stmt::Delete(_)
| Stmt::AugAssign(_)
| Stmt::TypeAlias(_)
| Stmt::While(_)
| Stmt::If(_)
| Stmt::Match(_)
| Stmt::Raise(_)
| Stmt::Try(_)
| Stmt::Assert(_)
| Stmt::Import(_)
| Stmt::ImportFrom(_)
| Stmt::Global(_)
| Stmt::Nonlocal(_)
| Stmt::Expr(_)
| Stmt::Pass(_)
| Stmt::Break(_)
| Stmt::Continue(_)
| Stmt::IpyEscapeCommand(_) => {}
}
source_order::walk_stmt(self, stmt);
}
fn visit_expr(&mut self, expr: &Expr) {
let _ty = expr.inferred_type(&self.model);
source_order::walk_expr(self, expr);
}
fn visit_comprehension(&mut self, comprehension: &Comprehension) {
self.visit_expr(&comprehension.iter);
self.visit_target(&comprehension.target);
for if_expr in &comprehension.ifs {
self.visit_expr(if_expr);
}
}
fn visit_parameter(&mut self, parameter: &Parameter) {
let _ty = parameter.inferred_type(&self.model);
source_order::walk_parameter(self, parameter);
}
fn visit_parameter_with_default(&mut self, parameter_with_default: &ParameterWithDefault) {
let _ty = parameter_with_default.inferred_type(&self.model);
source_order::walk_parameter_with_default(self, parameter_with_default);
}
fn visit_alias(&mut self, alias: &Alias) {
let _ty = alias.inferred_type(&self.model);
source_order::walk_alias(self, alias);
}
}
/// Whether or not the .py/.pyi version of this file is expected to fail
#[rustfmt::skip]
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[];

View File

@@ -1,102 +0,0 @@
# Any
## Annotation
`typing.Any` is a way to name the Any type.
```py
from typing import Any
x: Any = 1
x = "foo"
def f():
reveal_type(x) # revealed: Any
```
## Aliased to a different name
If you alias `typing.Any` to another name, we still recognize that as a spelling of the Any type.
```py
from typing import Any as RenamedAny
x: RenamedAny = 1
x = "foo"
def f():
reveal_type(x) # revealed: Any
```
## Shadowed class
If you define your own class named `Any`, using that in a type expression refers to your class, and
isn't a spelling of the Any type.
```py
class Any: ...
x: Any
def f():
reveal_type(x) # revealed: Any
# This verifies that we're not accidentally seeing typing.Any, since str is assignable
# to that but not to our locally defined class.
y: Any = "not an Any" # error: [invalid-assignment]
```
## Subclass
The spec allows you to define subclasses of `Any`.
`Subclass` has an unknown superclass, which might be `int`. The assignment to `x` should not be
allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since
`Subclass` might have `int` as a superclass, and is therefore assignable to `int`.
```py
from typing import Any
class Subclass(Any): ...
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
x: Subclass = 1 # error: [invalid-assignment]
y: int = Subclass()
def _(s: Subclass):
reveal_type(s) # revealed: Subclass
```
`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly
be a subclass of `FinalClass`:
```py
from typing import final
@final
class FinalClass: ...
f: FinalClass = Subclass() # error: [invalid-assignment]
```
A use case where this comes up is with mocking libraries, where the mock object should be assignable
to any type:
```py
from unittest.mock import MagicMock
x: int = MagicMock()
```
## Invalid
`Any` cannot be parameterized:
```py
from typing import Any
# error: [invalid-type-form] "Type `typing.Any` expected no type parameter"
def f(x: Any[int]):
reveal_type(x) # revealed: Unknown
```

View File

@@ -1,129 +0,0 @@
# Typing-module aliases to other stdlib classes
The `typing` module has various aliases to other stdlib classes. These are a legacy feature, but
still need to be supported by a type checker.
## Correspondence
All of the following symbols can be mapped one-to-one with the actual type:
```py
import typing
def f(
list_bare: typing.List,
list_parametrized: typing.List[int],
dict_bare: typing.Dict,
dict_parametrized: typing.Dict[int, str],
set_bare: typing.Set,
set_parametrized: typing.Set[int],
frozen_set_bare: typing.FrozenSet,
frozen_set_parametrized: typing.FrozenSet[str],
chain_map_bare: typing.ChainMap,
chain_map_parametrized: typing.ChainMap[int],
counter_bare: typing.Counter,
counter_parametrized: typing.Counter[int],
default_dict_bare: typing.DefaultDict,
default_dict_parametrized: typing.DefaultDict[str, int],
deque_bare: typing.Deque,
deque_parametrized: typing.Deque[str],
ordered_dict_bare: typing.OrderedDict,
ordered_dict_parametrized: typing.OrderedDict[int, str],
):
reveal_type(list_bare) # revealed: list
reveal_type(list_parametrized) # revealed: list
reveal_type(dict_bare) # revealed: dict
reveal_type(dict_parametrized) # revealed: dict
reveal_type(set_bare) # revealed: set
reveal_type(set_parametrized) # revealed: set
reveal_type(frozen_set_bare) # revealed: frozenset
reveal_type(frozen_set_parametrized) # revealed: frozenset
reveal_type(chain_map_bare) # revealed: ChainMap
reveal_type(chain_map_parametrized) # revealed: ChainMap
reveal_type(counter_bare) # revealed: Counter
reveal_type(counter_parametrized) # revealed: Counter
reveal_type(default_dict_bare) # revealed: defaultdict
reveal_type(default_dict_parametrized) # revealed: defaultdict
reveal_type(deque_bare) # revealed: deque
reveal_type(deque_parametrized) # revealed: deque
reveal_type(ordered_dict_bare) # revealed: OrderedDict
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict
```
## Inheritance
The aliases can be inherited from. Some of these are still partially or wholly TODOs.
```py
import typing
####################
### Built-ins
####################
class ListSubclass(typing.List): ...
# TODO: generic protocols
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(ListSubclass.__mro__)
class DictSubclass(typing.Dict): ...
# TODO: generic protocols
# revealed: tuple[Literal[DictSubclass], Literal[dict], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(DictSubclass.__mro__)
class SetSubclass(typing.Set): ...
# TODO: generic protocols
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(SetSubclass.__mro__)
class FrozenSetSubclass(typing.FrozenSet): ...
# TODO: should have `Generic`, should not have `Unknown`
# revealed: tuple[Literal[FrozenSetSubclass], Literal[frozenset], Unknown, Literal[object]]
reveal_type(FrozenSetSubclass.__mro__)
####################
### `collections`
####################
class ChainMapSubclass(typing.ChainMap): ...
# TODO: generic protocols
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(ChainMapSubclass.__mro__)
class CounterSubclass(typing.Counter): ...
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[CounterSubclass], Literal[Counter], @Todo(GenericAlias instance), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(CounterSubclass.__mro__)
class DefaultDictSubclass(typing.DefaultDict): ...
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], @Todo(GenericAlias instance), Literal[object]]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
# TODO: generic protocols
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], @Todo(GenericAlias instance), Literal[object]]
reveal_type(OrderedDictSubclass.__mro__)
```

View File

@@ -1,14 +0,0 @@
# No matching overload diagnostics
<!-- snapshot-diagnostics -->
## Calls to overloaded functions
TODO: Note that we do not yet support the `@overload` decorator to define overloaded functions in
real Python code. We are instead testing a special-cased function where we create an overloaded
signature internally. Update this to an `@overload` function in the Python snippet itself once we
can.
```py
type("Foo", ()) # error: [no-matching-overload]
```

View File

@@ -1,165 +0,0 @@
# Semantic syntax error diagnostics
## `async` comprehensions in synchronous comprehensions
### Python 3.10
<!-- snapshot-diagnostics -->
Before Python 3.11, `async` comprehensions could not be used within outer sync comprehensions, even
within an `async` function ([CPython issue](https://github.com/python/cpython/issues/77527)):
```toml
[environment]
python-version = "3.10"
```
```py
async def elements(n):
yield n
async def f():
# error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)"
return {n: [x async for x in elements(n)] for n in range(3)}
```
If all of the comprehensions are `async`, on the other hand, the code was still valid:
```py
async def test():
return [[x async for x in elements(n)] async for n in range(3)]
```
These are a couple of tricky but valid cases to check that nested scope handling is wired up
correctly in the `SemanticSyntaxContext` trait:
```py
async def f():
[x for x in [1]] and [x async for x in elements(1)]
async def f():
def g():
pass
[x async for x in elements(1)]
```
### Python 3.11
All of these same examples are valid after Python 3.11:
```toml
[environment]
python-version = "3.11"
```
```py
async def elements(n):
yield n
async def f():
return {n: [x async for x in elements(n)] for n in range(3)}
```
## Late `__future__` import
```py
from collections import namedtuple
# error: [invalid-syntax] "__future__ imports must be at the top of the file"
from __future__ import print_function
```
## Invalid annotation
This one might be a bit redundant with the `invalid-type-form` error.
```toml
[environment]
python-version = "3.12"
```
```py
from __future__ import annotations
# error: [invalid-type-form] "Named expressions are not allowed in type expressions"
# error: [invalid-syntax] "named expression cannot be used within a type annotation"
def f() -> (y := 3): ...
```
## Duplicate `match` key
```toml
[environment]
python-version = "3.10"
```
```py
match 2:
# error: [invalid-syntax] "mapping pattern checks duplicate key `"x"`"
case {"x": 1, "x": 2}:
...
```
## `return`, `yield`, `yield from`, and `await` outside function
```py
# error: [invalid-syntax] "`return` statement outside of a function"
return
# error: [invalid-syntax] "`yield` statement outside of a function"
yield
# error: [invalid-syntax] "`yield from` statement outside of a function"
yield from []
# error: [invalid-syntax] "`await` statement outside of a function"
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
def f():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
```
Generators are evaluated lazily, so `await` is allowed, even outside of a function.
```py
async def g():
yield 1
(x async for x in g())
```
## `await` outside async function
This error includes `await`, `async for`, `async with`, and `async` comprehensions.
```python
async def elements(n):
yield n
def _():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
# error: [invalid-syntax] "`async for` outside of an asynchronous function"
async for _ in elements(1):
...
# error: [invalid-syntax] "`async with` outside of an asynchronous function"
async with elements(1) as x:
...
# error: [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.9 (syntax was added in 3.11)"
# error: [invalid-syntax] "asynchronous comprehension outside of an asynchronous function"
[x async for x in elements(1)]
```
## Load before `global` declaration
This should be an error, but it's not yet.
TODO implement `SemanticSyntaxContext::global`
```py
def f():
x = 1
global x
```

View File

@@ -1,66 +0,0 @@
# `except*`
`except*` is only available in Python 3.11 and later:
```toml
[environment]
python-version = "3.11"
```
## `except*` with `BaseException`
```py
try:
help()
except* BaseException as e:
# TODO: should be `BaseExceptionGroup[BaseException]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
```
## `except*` with specific exception
```py
try:
help()
except* OSError as e:
# TODO: more precise would be `ExceptionGroup[OSError]` --Alex
# (needs homogeneous tuples + generics)
reveal_type(e) # revealed: BaseExceptionGroup
```
## `except*` with multiple exceptions
```py
try:
help()
except* (TypeError, AttributeError) as e:
# TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex
# (needs homogeneous tuples + generics)
reveal_type(e) # revealed: BaseExceptionGroup
```
## `except*` with mix of `Exception`s and `BaseException`s
```py
try:
help()
except* (KeyboardInterrupt, AttributeError) as e:
# TODO: more precise would be `BaseExceptionGroup[KeyboardInterrupt | AttributeError]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
```
## Invalid `except*` handlers
```py
try:
help()
except* 3 as e: # error: [invalid-exception-caught]
# TODO: Should be `BaseExceptionGroup[Unknown]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
try:
help()
except* (AttributeError, 42) as e: # error: [invalid-exception-caught]
# TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
```

View File

@@ -1,337 +0,0 @@
# Generic classes
```toml
[environment]
python-version = "3.13"
```
## PEP 695 syntax
TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.
This is a generic class defined using PEP 695 syntax:
```py
class C[T]: ...
```
A class that inherits from a generic class, and fills its type parameters with typevars, is generic:
```py
class D[U](C[U]): ...
```
A class that inherits from a generic class, but fills its type parameters with concrete types, is
_not_ generic:
```py
class E(C[int]): ...
```
A class that inherits from a generic class, and doesn't fill its type parameters at all, implicitly
uses the default value for the typevar. In this case, that default type is `Unknown`, so `F`
inherits from `C[Unknown]` and is not itself generic.
```py
class F(C): ...
```
## Legacy syntax
This is a generic class defined using the legacy syntax:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]): ...
```
A class that inherits from a generic class, and fills its type parameters with typevars, is generic.
```py
class D(C[T]): ...
```
(Examples `E` and `F` from above do not have analogues in the legacy syntax.)
## Specializing generic classes explicitly
The type parameter can be specified explicitly:
```py
class C[T]:
x: T
reveal_type(C[int]()) # revealed: C[int]
```
The specialization must match the generic types:
```py
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: Unknown
```
If the type variable has an upper bound, the specialized type must satisfy that bound:
```py
class Bounded[T: int]: ...
class BoundedByUnion[T: int | str]: ...
class IntSubclass(int): ...
reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `str`"
reveal_type(Bounded[str]()) # revealed: Unknown
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`"
reveal_type(Bounded[int | str]()) # revealed: Unknown
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass]
reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str]
reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str]
```
If the type variable is constrained, the specialized type must satisfy those constraints:
```py
class Constrained[T: (int, str)]: ...
reveal_type(Constrained[int]()) # revealed: Constrained[int]
# TODO: error: [invalid-argument-type]
# TODO: revealed: Constrained[Unknown]
reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass]
reveal_type(Constrained[str]()) # revealed: Constrained[str]
# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int | str`, found `object`"
reveal_type(Constrained[object]()) # revealed: Unknown
```
## Inferring generic class parameters
We can infer the type parameter from a type context:
```py
class C[T]:
x: T
c: C[int] = C()
# TODO: revealed: C[int]
reveal_type(c) # revealed: C[Unknown]
```
The typevars of a fully specialized generic class should no longer be visible:
```py
# TODO: revealed: int
reveal_type(c.x) # revealed: Unknown
```
If the type parameter is not specified explicitly, and there are no constraints that let us infer a
specific type, we infer the typevar's default type:
```py
class D[T = int]: ...
reveal_type(D()) # revealed: D[int]
```
If a typevar does not provide a default, we use `Unknown`:
```py
reveal_type(C()) # revealed: C[Unknown]
```
## Inferring generic class parameters from constructors
If the type of a constructor parameter is a class typevar, we can use that to infer the type
parameter. The types inferred from a type context and from a constructor parameter must be
consistent with each other.
## `__new__` only
```py
class C[T]:
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
## `__init__` only
```py
class C[T]:
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
## Identical `__new__` and `__init__` signatures
```py
class C[T]:
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
## Compatible `__new__` and `__init__` signatures
```py
class C[T]:
def __new__(cls, *args, **kwargs) -> "C[T]":
return object.__new__(cls)
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
class D[T]:
def __new__(cls, x: T) -> "D[T]":
return object.__new__(cls)
def __init__(self, *args, **kwargs) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
wrong_innards: D[int] = D("five")
```
## `__init__` is itself generic
TODO: These do not currently work yet, because we don't correctly model the nested generic contexts.
```py
class C[T]:
def __init__[S](self, x: T, y: S) -> None: ...
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
reveal_type(C(1, True)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)
```
## Generic subclass
When a generic subclass fills its superclass's type parameter with one of its own, the actual types
propagate through:
```py
class Base[T]:
x: T | None = None
class Sub[U](Base[U]): ...
reveal_type(Base[int].x) # revealed: int | None
reveal_type(Sub[int].x) # revealed: int | None
```
## Generic methods
Generic classes can contain methods that are themselves generic. The generic methods can refer to
the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in
scope for the method.
```py
class C[T]:
def method[U](self, u: U) -> U:
return u
# error: [unresolved-reference]
def cannot_use_outside_of_method(self, u: U): ...
# TODO: error
def cannot_shadow_class_typevar[T](self, t: T): ...
c: C[int] = C[int]()
reveal_type(c.method("string")) # revealed: Literal["string"]
```
## Cyclic class definitions
### F-bounded quantification
A class can use itself as the type parameter of one of its superclasses. (This is also known as the
[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].)
#### In a stub file
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
```pyi
class Base[T]: ...
class Sub(Base[Sub]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
#### With string forward references
A similar case can work in a non-stub file, if forward references are stringified:
```py
class Base[T]: ...
class Sub(Base["Sub"]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
#### Without string forward references
In a non-stub file, without stringified forward references, this raises a `NameError`:
```py
class Base[T]: ...
# error: [unresolved-reference]
class Sub(Base[Sub]): ...
```
### Cyclic inheritance as a generic parameter
```pyi
class Derived[T](list[Derived[T]]): ...
```
### Direct cyclic inheritance
Inheritance that would result in a cyclic MRO is detected as an error.
```py
# error: [cyclic-class-definition]
class C[T](C): ...
# error: [cyclic-class-definition]
class D[T](D[int]): ...
```
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification

View File

@@ -1,72 +0,0 @@
# Legacy type variables
The tests in this file focus on how type variables are defined using the legacy notation. Most
_uses_ of type variables are tested in other files in this directory; we do not duplicate every test
for both type variable syntaxes.
Unless otherwise specified, all quotations come from the [Generics] section of the typing spec.
## Type variables
### Defining legacy type variables
> Generics can be parameterized by using a factory available in `typing` called `TypeVar`.
This was the only way to create type variables prior to PEP 695/Python 3.12. It is still available
in newer Python releases.
```py
from typing import TypeVar
T = TypeVar("T")
```
### Directly assigned to a variable
> A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as
> part of a larger expression).
```py
from typing import TypeVar
# TODO: error
TestList = list[TypeVar("W")]
```
### `TypeVar` parameter must match variable name
> The argument to `TypeVar()` must be a string equal to the variable name to which it is assigned.
```py
from typing import TypeVar
# TODO: error
T = TypeVar("Q")
```
### No redefinition
> Type variables must not be redefined.
```py
from typing import TypeVar
T = TypeVar("T")
# TODO: error
T = TypeVar("T")
```
### Cannot have only one constraint
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
> be at least two constraints, if any; specifying a single constraint is disallowed.
```py
from typing import TypeVar
# TODO: error: [invalid-type-variable-constraints]
T = TypeVar("T", int)
```
[generics]: https://typing.python.org/en/latest/spec/generics.html

View File

@@ -1,7 +0,0 @@
# Dictionaries
## Empty dictionary
```py
reveal_type({}) # revealed: dict
```

View File

@@ -1,7 +0,0 @@
# Lists
## Empty list
```py
reveal_type([]) # revealed: list
```

View File

@@ -1,7 +0,0 @@
# Sets
## Basic set
```py
reveal_type({1, 2}) # revealed: set
```

View File

@@ -1,408 +0,0 @@
# Method Resolution Order tests
Tests that assert that we can infer the correct type for a class's `__mro__` attribute.
This attribute is rarely accessed directly at runtime. However, it's extremely important for *us* to
know the precise possible values of a class's Method Resolution Order, or we won't be able to infer
the correct type of attributes accessed from instances.
For documentation on method resolution orders, see:
- <https://docs.python.org/3/glossary.html#term-method-resolution-order>
- <https://docs.python.org/3/howto/mro.html#python-2-3-mro>
## No bases
```py
class C: ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
```
## The special case: `object` itself
```py
reveal_type(object.__mro__) # revealed: tuple[Literal[object]]
```
## Explicit inheritance from `object`
```py
class C(object): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
```
## Explicit inheritance from non-`object` single base
```py
class A: ...
class B(A): ...
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
```
## Linearization of multiple bases
```py
class A: ...
class B: ...
class C(A, B): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
```
## Complex diamond inheritance (1)
This is "ex_2" from <https://docs.python.org/3/howto/mro.html#the-end>
```py
class O: ...
class X(O): ...
class Y(O): ...
class A(X, Y): ...
class B(Y, X): ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
```
## Complex diamond inheritance (2)
This is "ex_5" from <https://docs.python.org/3/howto/mro.html#the-end>
```py
class O: ...
class F(O): ...
class E(O): ...
class D(O): ...
class C(D, F): ...
class B(D, E): ...
class A(B, C): ...
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
reveal_type(C.__mro__)
# revealed: tuple[Literal[B], Literal[D], Literal[E], Literal[O], Literal[object]]
reveal_type(B.__mro__)
# revealed: tuple[Literal[A], Literal[B], Literal[C], Literal[D], Literal[E], Literal[F], Literal[O], Literal[object]]
reveal_type(A.__mro__)
```
## Complex diamond inheritance (3)
This is "ex_6" from <https://docs.python.org/3/howto/mro.html#the-end>
```py
class O: ...
class F(O): ...
class E(O): ...
class D(O): ...
class C(D, F): ...
class B(E, D): ...
class A(B, C): ...
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
reveal_type(C.__mro__)
# revealed: tuple[Literal[B], Literal[E], Literal[D], Literal[O], Literal[object]]
reveal_type(B.__mro__)
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
reveal_type(A.__mro__)
```
## Complex diamond inheritance (4)
This is "ex_9" from <https://docs.python.org/3/howto/mro.html#the-end>
```py
class O: ...
class A(O): ...
class B(O): ...
class C(O): ...
class D(O): ...
class E(O): ...
class K1(A, B, C): ...
class K2(D, B, E): ...
class K3(D, A): ...
class Z(K1, K2, K3): ...
# revealed: tuple[Literal[K1], Literal[A], Literal[B], Literal[C], Literal[O], Literal[object]]
reveal_type(K1.__mro__)
# revealed: tuple[Literal[K2], Literal[D], Literal[B], Literal[E], Literal[O], Literal[object]]
reveal_type(K2.__mro__)
# revealed: tuple[Literal[K3], Literal[D], Literal[A], Literal[O], Literal[object]]
reveal_type(K3.__mro__)
# revealed: tuple[Literal[Z], Literal[K1], Literal[K2], Literal[K3], Literal[D], Literal[A], Literal[B], Literal[C], Literal[E], Literal[O], Literal[object]]
reveal_type(Z.__mro__)
```
## Inheritance from `Unknown`
```py
from does_not_exist import DoesNotExist # error: [unresolved-import]
class A(DoesNotExist): ...
class B: ...
class C: ...
class D(A, B, C): ...
class E(B, C): ...
class F(E, A): ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[A], Unknown, Literal[B], Literal[C], Literal[object]]
reveal_type(E.__mro__) # revealed: tuple[Literal[E], Literal[B], Literal[C], Literal[object]]
reveal_type(F.__mro__) # revealed: tuple[Literal[F], Literal[E], Literal[B], Literal[C], Literal[A], Unknown, Literal[object]]
```
## `__bases__` lists that cause errors at runtime
If the class's `__bases__` cause an exception to be raised at runtime and therefore the class
creation to fail, we infer the class's `__mro__` as being `[<class>, Unknown, object]`:
```py
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[<class 'object'>, <class 'int'>]`"
class Foo(object, int): ...
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
class Bar(Foo): ...
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Unknown, Literal[object]]
# This is the `TypeError` at the bottom of "ex_2"
# in the examples at <https://docs.python.org/3/howto/mro.html#the-end>
class O: ...
class X(O): ...
class Y(O): ...
class A(X, Y): ...
class B(Y, X): ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Z` with bases list `[<class 'A'>, <class 'B'>]`"
class Z(A, B): ...
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
class AA(Z): ...
reveal_type(AA.__mro__) # revealed: tuple[Literal[AA], Literal[Z], Unknown, Literal[object]]
```
## `__bases__` includes a `Union`
We don't support union types in a class's bases; a base must resolve to a single `ClassType`. If we
find a union type in a class's bases, we infer the class's `__mro__` as being
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
```py
def returns_bool() -> bool:
return True
class A: ...
class B: ...
if returns_bool():
x = A
else:
x = B
reveal_type(x) # revealed: Literal[A, B]
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class Foo(x): ...
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
```
## `__bases__` includes multiple `Union`s
```py
def returns_bool() -> bool:
return True
class A: ...
class B: ...
class C: ...
class D: ...
if returns_bool():
x = A
else:
x = B
if returns_bool():
y = C
else:
y = D
reveal_type(x) # revealed: Literal[A, B]
reveal_type(y) # revealed: Literal[C, D]
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 14 [invalid-base] "Invalid class base with type `Literal[C, D]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class Foo(x, y): ...
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
```
## `__bases__` lists that cause errors... now with `Union`s
```py
def returns_bool() -> bool:
return True
class O: ...
class X(O): ...
class Y(O): ...
if returns_bool():
foo = Y
else:
foo = object
# error: 21 [invalid-base] "Invalid class base with type `Literal[Y, object]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class PossibleError(foo, X): ...
reveal_type(PossibleError.__mro__) # revealed: tuple[Literal[PossibleError], Unknown, Literal[object]]
class A(X, Y): ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
if returns_bool():
class B(X, Y): ...
else:
class B(Y, X): ...
# revealed: tuple[Literal[B], Literal[X], Literal[Y], Literal[O], Literal[object]] | tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
reveal_type(B.__mro__)
# error: 12 [invalid-base] "Invalid class base with type `Literal[B, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class Z(A, B): ...
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
```
## `__bases__` lists with duplicate bases
```py
class Foo(str, str): ... # error: 16 [duplicate-base] "Duplicate base class `str`"
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
class Spam: ...
class Eggs: ...
class Ham(
Spam,
Eggs,
Spam, # error: [duplicate-base] "Duplicate base class `Spam`"
Eggs, # error: [duplicate-base] "Duplicate base class `Eggs`"
): ...
reveal_type(Ham.__mro__) # revealed: tuple[Literal[Ham], Unknown, Literal[object]]
class Mushrooms: ...
class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
reveal_type(Omelette.__mro__) # revealed: tuple[Literal[Omelette], Unknown, Literal[object]]
```
## `__bases__` lists with duplicate `Unknown` bases
```py
# error: [unresolved-import]
# error: [unresolved-import]
from does_not_exist import unknown_object_1, unknown_object_2
reveal_type(unknown_object_1) # revealed: Unknown
reveal_type(unknown_object_2) # revealed: Unknown
# We *should* emit an error here to warn the user that we have no idea
# what the MRO of this class should really be.
# However, we don't complain about "duplicate base classes" here,
# even though two classes are both inferred as being `Unknown`.
#
# (TODO: should we revisit this? Does it violate the gradual guarantee?
# Should we just silently infer `[Foo, Unknown, object]` as the MRO here
# without emitting any error at all? Not sure...)
#
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[Unknown, Unknown]`"
class Foo(unknown_object_1, unknown_object_2): ...
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
```
## Unrelated objects inferred as `Any`/`Unknown` do not have special `__mro__` attributes
```py
from does_not_exist import unknown_object # error: [unresolved-import]
reveal_type(unknown_object) # revealed: Unknown
reveal_type(unknown_object.__mro__) # revealed: Unknown
```
## Classes that inherit from themselves
These are invalid, but we need to be able to handle them gracefully without panicking.
```pyi
class Foo(Foo): ... # error: [cyclic-class-definition]
reveal_type(Foo) # revealed: Literal[Foo]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
class Bar: ...
class Baz: ...
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition]
reveal_type(Boz) # revealed: Literal[Boz]
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
```
## Classes with indirect cycles in their MROs
These are similarly unlikely, but we still shouldn't crash:
```pyi
class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-definition]
class Baz(Foo): ... # error: [cyclic-class-definition]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
```
## Classes with cycles in their MROs, and multiple inheritance
```pyi
class Spam: ...
class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-definition]
class Baz(Foo, Spam): ... # error: [cyclic-class-definition]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
```
## Classes with cycles in their MRO, and a sub-graph
```pyi
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
class Foo: ...
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
class Bar(Foo): ...
# Avoid emitting the errors for these. The classes have cyclic superclasses,
# but are not themselves cyclic...
class Baz(Bar, BarCycle): ...
class Spam(Baz): ...
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
reveal_type(Spam.__mro__) # revealed: tuple[Literal[Spam], Unknown, Literal[object]]
```

View File

@@ -1,53 +0,0 @@
# Narrowing with assert statements
## `assert` a value `is None` or `is not None`
```py
def _(x: str | None, y: str | None):
assert x is not None
reveal_type(x) # revealed: str
assert y is None
reveal_type(y) # revealed: None
```
## `assert` a value is truthy or falsy
```py
def _(x: bool, y: bool):
assert x
reveal_type(x) # revealed: Literal[True]
assert not y
reveal_type(y) # revealed: Literal[False]
```
## `assert` with `is` and `==` for literals
```py
from typing import Literal
def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]):
assert x is 2
reveal_type(x) # revealed: Literal[2]
assert y == 2
reveal_type(y) # revealed: Literal[2]
```
## `assert` with `isinstance`
```py
def _(x: int | str):
assert isinstance(x, int)
reveal_type(x) # revealed: int
```
## `assert` a value `in` a tuple
```py
from typing import Literal
def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]):
assert x in (1, 2)
reveal_type(x) # revealed: Literal[1, 2]
assert y not in (1, 2)
reveal_type(y) # revealed: Literal[3]
```

View File

@@ -1,44 +0,0 @@
# Narrowing for nested conditionals
## Multiple negative contributions
```py
def _(x: int):
if x != 1:
if x != 2:
if x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
```
## Multiple negative contributions with simplification
```py
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x != 2:
reveal_type(x) # revealed: Literal[3]
```
## elif-else blocks
```py
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x == 2:
reveal_type(x) # revealed: Literal[2]
elif x == 3:
reveal_type(x) # revealed: Literal[3]
else:
reveal_type(x) # revealed: Never
elif x != 2:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: Never
```

View File

@@ -1,177 +0,0 @@
# `global` references
## Implicit global in function
A name reference to a never-defined symbol in a function is implicitly a global lookup.
```py
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Explicit global in function
```py
x = 1
def f():
global x
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Unassignable type in function
```py
x: int = 1
def f():
y: int = 1
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
y = ""
global x
# TODO: error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
x = ""
```
## Nested intervening scope
A `global` statement causes lookup to skip any bindings in intervening scopes:
```py
x: int = 1
def outer():
x: str = ""
def inner():
global x
# TODO: revealed: int
reveal_type(x) # revealed: str
```
## Narrowing
An assignment following a `global` statement should narrow the type in the local scope after the
assignment.
```py
x: int | None
def f():
global x
x = 1
reveal_type(x) # revealed: Literal[1]
```
## `nonlocal` and `global`
A binding cannot be both `nonlocal` and `global`. This should emit a semantic syntax error. CPython
marks the `nonlocal` line, while `mypy`, `pyright`, and `ruff` (`PLE0115`) mark the `global` line.
```py
x = 1
def f():
x = 1
def g() -> None:
nonlocal x
global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global"
x = None
```
## Global declaration after `global` statement
```py
def f():
global x
# TODO this should also not be an error
y = x # error: [unresolved-reference] "Name `x` used when not defined"
x = 1 # No error.
x = 2
```
## Semantic syntax errors
Using a name prior to its `global` declaration in the same scope is a syntax error.
```py
x = 1
def f():
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
def f():
global x
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
def f():
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
def f():
global x, y
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
def f():
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
def f():
global x
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
global x, y
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
def f():
global x
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
global x, y
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
print(f"{x=}") # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
# still an error in module scope
x = None # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
```

View File

@@ -1,28 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: basic.md - Structures - Unresolvable module import
mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
---
# Python source files
## mdtest_snippet.py
```
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
```
# Diagnostics
```
error: lint:unresolved-import: Cannot resolve import `zqzqzqzqzqzqzq`
--> /src/mdtest_snippet.py:1:8
|
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
| ^^^^^^^^^^^^^^
|
```

View File

@@ -1,51 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: basic.md - Structures - Unresolvable submodule imports
mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
---
# Python source files
## mdtest_snippet.py
```
1 | # Topmost component resolvable, submodule not resolvable:
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
3 |
4 | # Topmost component unresolvable:
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
```
## a/__init__.py
```
```
# Diagnostics
```
error: lint:unresolved-import: Cannot resolve import `a.foo`
--> /src/mdtest_snippet.py:2:8
|
1 | # Topmost component resolvable, submodule not resolvable:
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
| ^^^^^
3 |
4 | # Topmost component unresolvable:
|
```
```
error: lint:unresolved-import: Cannot resolve import `b.foo`
--> /src/mdtest_snippet.py:5:8
|
4 | # Topmost component unresolvable:
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
| ^^^^^
|
```

View File

@@ -1,28 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: no_matching_overload.md - No matching overload diagnostics - Calls to overloaded functions
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md
---
# Python source files
## mdtest_snippet.py
```
1 | type("Foo", ()) # error: [no-matching-overload]
```
# Diagnostics
```
error: lint:no-matching-overload: No overload of class `type` matches arguments
--> /src/mdtest_snippet.py:1:1
|
1 | type("Foo", ()) # error: [no-matching-overload]
| ^^^^^^^^^^^^^^^
|
```

View File

@@ -1,117 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: protocols.md - Protocols - Calls to protocol classes
mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import Protocol, reveal_type
2 |
3 | # error: [call-non-callable]
4 | reveal_type(Protocol()) # revealed: Unknown
5 |
6 | class MyProtocol(Protocol):
7 | x: int
8 |
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
11 | class SubclassOfMyProtocol(MyProtocol): ...
12 |
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
```
# Diagnostics
```
error: lint:call-non-callable: Object of type `typing.Protocol` is not callable
--> /src/mdtest_snippet.py:4:13
|
3 | # error: [call-non-callable]
4 | reveal_type(Protocol()) # revealed: Unknown
| ^^^^^^^^^^
5 |
6 | class MyProtocol(Protocol):
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:4:1
|
3 | # error: [call-non-callable]
4 | reveal_type(Protocol()) # revealed: Unknown
| ^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
5 |
6 | class MyProtocol(Protocol):
|
```
```
error: lint:call-non-callable: Cannot instantiate class `MyProtocol`
--> /src/mdtest_snippet.py:10:13
|
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
| ^^^^^^^^^^^^ This call will raise `TypeError` at runtime
11 | class SubclassOfMyProtocol(MyProtocol): ...
|
info: Protocol classes cannot be instantiated
--> /src/mdtest_snippet.py:6:7
|
4 | reveal_type(Protocol()) # revealed: Unknown
5 |
6 | class MyProtocol(Protocol):
| ^^^^^^^^^^^^^^^^^^^^ `MyProtocol` declared as a protocol here
7 | x: int
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:10:1
|
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `MyProtocol`
11 | class SubclassOfMyProtocol(MyProtocol): ...
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:13:1
|
11 | class SubclassOfMyProtocol(MyProtocol): ...
12 |
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfMyProtocol`
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:15:5
|
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
| ^^^^^^^^^^^^^^^^ `MyProtocol`
|
```

View File

@@ -1,81 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: return_type.md - Function return type - Invalid return type
mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | # error: [invalid-return-type]
2 | def f() -> int:
3 | 1
4 |
5 | def f() -> str:
6 | # error: [invalid-return-type]
7 | return 1
8 |
9 | def f() -> int:
10 | # error: [invalid-return-type]
11 | return
12 |
13 | from typing import TypeVar
14 |
15 | T = TypeVar("T")
16 |
17 | # TODO: `invalid-return-type` error should be emitted
18 | def m(x: T) -> T: ...
```
# Diagnostics
```
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.py:2:12
|
1 | # error: [invalid-return-type]
2 | def f() -> int:
| ^^^
3 | 1
|
```
```
error: lint:invalid-return-type: Return type does not match returned value
--> /src/mdtest_snippet.py:5:12
|
3 | 1
4 |
5 | def f() -> str:
| --- Expected `str` because of return type
6 | # error: [invalid-return-type]
7 | return 1
| ^ Expected `str`, found `Literal[1]`
8 |
9 | def f() -> int:
|
```
```
error: lint:invalid-return-type: Return type does not match returned value
--> /src/mdtest_snippet.py:9:12
|
7 | return 1
8 |
9 | def f() -> int:
| --- Expected `int` because of return type
10 | # error: [invalid-return-type]
11 | return
| ^^^^^^ Expected `int`, found `None`
12 |
13 | from typing import TypeVar
|
```

View File

@@ -1,28 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Right hand side not iterable
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
# Python source files
## mdtest_snippet.py
```
1 | a, b = 1 # error: [not-iterable]
```
# Diagnostics
```
error: lint:not-iterable: Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
--> /src/mdtest_snippet.py:1:8
|
1 | a, b = 1 # error: [not-iterable]
| ^
|
```

View File

@@ -1,191 +0,0 @@
# Suppressing errors with `knot: ignore`
Type check errors can be suppressed by a `knot: ignore` comment on the same line as the violation.
## Simple `knot: ignore`
```py
a = 4 + test # knot: ignore
```
## Suppressing a specific code
```py
a = 4 + test # knot: ignore[unresolved-reference]
```
## Unused suppression
```py
test = 10
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'"
a = test + 3 # knot: ignore[possibly-unresolved-reference]
```
## Unused suppression if the error codes don't match
```py
# error: [unresolved-reference]
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'"
a = test + 3 # knot: ignore[possibly-unresolved-reference]
```
## Suppressed unused comment
```py
# error: [unused-ignore-comment]
a = 10 / 2 # knot: ignore[division-by-zero]
a = 10 / 2 # knot: ignore[division-by-zero, unused-ignore-comment]
a = 10 / 2 # knot: ignore[unused-ignore-comment, division-by-zero]
a = 10 / 2 # knot: ignore[unused-ignore-comment] # type: ignore
a = 10 / 2 # type: ignore # knot: ignore[unused-ignore-comment]
```
## Unused ignore comment
```py
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unused-ignore-comment'"
a = 10 / 0 # knot: ignore[division-by-zero, unused-ignore-comment]
```
## Multiple unused comments
Today, Red Knot emits a diagnostic for every unused code. We might want to group the codes by
comment at some point in the future.
```py
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'division-by-zero'"
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'"
a = 10 / 2 # knot: ignore[division-by-zero, unresolved-reference]
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'invalid-assignment'"
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'"
a = 10 / 0 # knot: ignore[invalid-assignment, division-by-zero, unresolved-reference]
```
## Multiple suppressions
```py
# fmt: off
def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... # knot: ignore[fstring-type-annotation, byte-string-type-annotation]
```
## Can't suppress syntax errors
<!-- blacken-docs:off -->
```py
# error: [invalid-syntax]
# error: [unused-ignore-comment]
def test($): # knot: ignore
pass
```
<!-- blacken-docs:on -->
## Can't suppress `revealed-type` diagnostics
```py
a = 10
# revealed: Literal[10]
# error: [unknown-rule] "Unknown rule `revealed-type`"
reveal_type(a) # knot: ignore[revealed-type]
```
## Extra whitespace in type ignore comments is allowed
```py
a = 10 / 0 # knot : ignore
a = 10 / 0 # knot: ignore [ division-by-zero ]
```
## Whitespace is optional
```py
# fmt: off
a = 10 / 0 #knot:ignore[division-by-zero]
```
## Trailing codes comma
Trailing commas in the codes section are allowed:
```py
a = 10 / 0 # knot: ignore[division-by-zero,]
```
## Invalid characters in codes
```py
# error: [division-by-zero]
# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a alphanumeric character or `-` or `_` as code"
a = 10 / 0 # knot: ignore[*-*]
```
## Trailing whitespace
<!-- blacken-docs:off -->
```py
a = 10 / 0 # knot: ignore[division-by-zero]
# ^^^^^^ trailing whitespace
```
<!-- blacken-docs:on -->
## Missing comma
A missing comma results in an invalid suppression comment. We may want to recover from this in the
future.
```py
# error: [unresolved-reference]
# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a comma separating the rule codes"
a = x / 0 # knot: ignore[division-by-zero unresolved-reference]
```
## Missing closing bracket
```py
# error: [unresolved-reference] "Name `x` used when not defined"
# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a comma separating the rule codes"
a = x / 2 # knot: ignore[unresolved-reference
```
## Empty codes
An empty codes array suppresses no-diagnostics and is always useless
```py
# error: [division-by-zero]
# error: [unused-ignore-comment] "Unused `knot: ignore` without a code"
a = 4 / 0 # knot: ignore[]
```
## File-level suppression comments
File level suppression comments are currently intentionally unsupported because we've yet to decide
if they should use a different syntax that also supports enabling rules or changing the rule's
severity: `knot: possibly-undefined-reference=error`
```py
# error: [unused-ignore-comment]
# knot: ignore[division-by-zero]
a = 4 / 0 # error: [division-by-zero]
```
## Unknown rule
```py
# error: [unknown-rule] "Unknown rule `is-equal-14`"
a = 10 + 4 # knot: ignore[is-equal-14]
```
## Code with `lint:` prefix
```py
# error:[unknown-rule] "Unknown rule `lint:division-by-zero`. Did you mean `division-by-zero`?"
# error: [division-by-zero]
a = 10 / 0 # knot: ignore[lint:division-by-zero]
```

View File

@@ -1,35 +0,0 @@
Expression # cycle panic (signature_)
Tanjun # cycle panic (signature_)
aiohttp # missing expression ID
alerta # missing expression ID
altair # cycle panics (try_metaclass_)
antidote # hangs / slow
artigraph # cycle panics (value_type_)
colour # cycle panics (try_metaclass_)
core # cycle panics (value_type_)
cpython # missing expression ID, access to field whilst being initialized, too many cycle iterations
discord.py # some kind of hang, only when multi-threaded?
freqtrade # cycle panics (try_metaclass_)
hydpy # cycle panics (try_metaclass_)
ibis # cycle panics (try_metaclass_)
manticore # stack overflow
materialize # stack overflow
meson # missing expression ID
mypy # cycle panic (signature_)
pandas # slow
pandas-stubs # cycle panics (try_metaclass_)
pandera # cycle panics (try_metaclass_)
prefect # slow
pytest # cycle panics (signature_), missing expression ID
pywin32 # bad use-def map (binding with definitely-visible unbound)
schemathesis # cycle panics (signature_)
scikit-learn # success, but mypy-primer hangs processing the output
scipy # missing expression ID
spack # success, but mypy-primer hangs processing the output
spark # cycle panics (try_metaclass_)
sphinx # missing expression ID
steam.py # missing expression ID
streamlit # cycle panic (signature_)
sympy # stack overflow
trio # missing expression ID
xarray # cycle panics (try_metaclass_)

View File

@@ -1,202 +0,0 @@
use std::hash::Hash;
use std::ops::Deref;
use ruff_db::parsed::ParsedModule;
/// Ref-counted owned reference to an AST node.
///
/// The type holds an owned reference to the node's ref-counted [`ParsedModule`].
/// Holding on to the node's [`ParsedModule`] guarantees that the reference to the
/// node must still be valid.
///
/// Holding on to any [`AstNodeRef`] prevents the [`ParsedModule`] from being released.
///
/// ## Equality
/// Two `AstNodeRef` are considered equal if their pointer addresses are equal.
///
/// ## Usage in salsa tracked structs
/// It's important that [`AstNodeRef`] fields in salsa tracked structs are tracked fields
/// (attributed with `#[tracked`]). It prevents that the tracked struct gets a new ID
/// every time the AST changes, which in turn, invalidates the result of any query
/// that takes said tracked struct as a query argument or returns the tracked struct as part of its result.
///
/// For example, marking the [`AstNodeRef`] as tracked on `Expression`
/// has the effect that salsa will consider the expression as "unchanged" for as long as it:
///
/// * belongs to the same file
/// * belongs to the same scope
/// * has the same kind
/// * was created in the same order
///
/// This means that changes to expressions in other scopes don't invalidate the expression's id, giving
/// us some form of scope-stable identity for expressions. Only queries accessing the node field
/// run on every AST change. All other queries only run when the expression's identity changes.
#[derive(Clone)]
pub struct AstNodeRef<T> {
/// Owned reference to the node's [`ParsedModule`].
///
/// The node's reference is guaranteed to remain valid as long as it's enclosing
/// [`ParsedModule`] is alive.
parsed: ParsedModule,
/// Pointer to the referenced node.
node: std::ptr::NonNull<T>,
}
#[allow(unsafe_code)]
impl<T> AstNodeRef<T> {
/// Creates a new `AstNodeRef` that references `node`. The `parsed` is the [`ParsedModule`] to
/// which the `AstNodeRef` belongs.
///
/// ## Safety
///
/// Dereferencing the `node` can result in undefined behavior if `parsed` isn't the
/// [`ParsedModule`] to which `node` belongs. It's the caller's responsibility to ensure that
/// the invariant `node belongs to parsed` is upheld.
pub(super) unsafe fn new(parsed: ParsedModule, node: &T) -> Self {
Self {
parsed,
node: std::ptr::NonNull::from(node),
}
}
/// Returns a reference to the wrapped node.
pub const fn node(&self) -> &T {
// SAFETY: Holding on to `parsed` ensures that the AST to which `node` belongs is still
// alive and not moved.
unsafe { self.node.as_ref() }
}
}
impl<T> Deref for AstNodeRef<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.node()
}
}
impl<T> std::fmt::Debug for AstNodeRef<T>
where
T: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("AstNodeRef").field(&self.node()).finish()
}
}
impl<T> PartialEq for AstNodeRef<T>
where
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
if self.parsed == other.parsed {
// Comparing the pointer addresses is sufficient to determine equality
// if the parsed are the same.
self.node.eq(&other.node)
} else {
// Otherwise perform a deep comparison.
self.node().eq(other.node())
}
}
}
impl<T> Eq for AstNodeRef<T> where T: Eq {}
impl<T> Hash for AstNodeRef<T>
where
T: Hash,
{
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.node().hash(state);
}
}
#[allow(unsafe_code)]
unsafe impl<T> salsa::Update for AstNodeRef<T> {
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
let old_ref = &mut (*old_pointer);
if old_ref.parsed == new_value.parsed && old_ref.node.eq(&new_value.node) {
false
} else {
*old_ref = new_value;
true
}
}
}
#[allow(unsafe_code)]
unsafe impl<T> Send for AstNodeRef<T> where T: Send {}
#[allow(unsafe_code)]
unsafe impl<T> Sync for AstNodeRef<T> where T: Sync {}
#[cfg(test)]
mod tests {
use crate::ast_node_ref::AstNodeRef;
use ruff_db::parsed::ParsedModule;
use ruff_python_ast::PySourceType;
use ruff_python_parser::parse_unchecked_source;
#[test]
#[allow(unsafe_code)]
fn equality() {
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
let parsed = ParsedModule::new(parsed_raw.clone());
let stmt = &parsed.syntax().body[0];
let node1 = unsafe { AstNodeRef::new(parsed.clone(), stmt) };
let node2 = unsafe { AstNodeRef::new(parsed.clone(), stmt) };
assert_eq!(node1, node2);
// Compare from different trees
let cloned = ParsedModule::new(parsed_raw);
let stmt_cloned = &cloned.syntax().body[0];
let cloned_node = unsafe { AstNodeRef::new(cloned.clone(), stmt_cloned) };
assert_eq!(node1, cloned_node);
let other_raw = parse_unchecked_source("2 + 2", PySourceType::Python);
let other = ParsedModule::new(other_raw);
let other_stmt = &other.syntax().body[0];
let other_node = unsafe { AstNodeRef::new(other.clone(), other_stmt) };
assert_ne!(node1, other_node);
}
#[allow(unsafe_code)]
#[test]
fn inequality() {
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
let parsed = ParsedModule::new(parsed_raw);
let stmt = &parsed.syntax().body[0];
let node = unsafe { AstNodeRef::new(parsed.clone(), stmt) };
let other_raw = parse_unchecked_source("2 + 2", PySourceType::Python);
let other = ParsedModule::new(other_raw);
let other_stmt = &other.syntax().body[0];
let other_node = unsafe { AstNodeRef::new(other.clone(), other_stmt) };
assert_ne!(node, other_node);
}
#[test]
#[allow(unsafe_code)]
fn debug() {
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
let parsed = ParsedModule::new(parsed_raw);
let stmt = &parsed.syntax().body[0];
let stmt_node = unsafe { AstNodeRef::new(parsed.clone(), stmt) };
let debug = format!("{stmt_node:?}");
assert_eq!(debug, format!("AstNodeRef({stmt:?})"));
}
}

View File

@@ -1,21 +0,0 @@
use ruff_python_ast::AnyNodeRef;
/// Compact key for a node for use in a hash map.
///
/// Stores the memory address of the node, because using the range and the kind
/// of the node is not enough to uniquely identify them in ASTs resulting from
/// invalid syntax. For example, parsing the input `for` results in a `StmtFor`
/// AST node where both the `target` and the `iter` field are `ExprName` nodes
/// with the same (empty) range `3..3`.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub(super) struct NodeKey(usize);
impl NodeKey {
pub(super) fn from_node<'a, N>(node: N) -> Self
where
N: Into<AnyNodeRef<'a>>,
{
let node = node.into();
NodeKey(node.as_ptr().as_ptr() as usize)
}
}

View File

@@ -1,166 +0,0 @@
use crate::module_resolver::SearchPaths;
use crate::python_platform::PythonPlatform;
use crate::site_packages::SysPrefixPathOrigin;
use crate::Db;
use anyhow::Context;
use ruff_db::system::{SystemPath, SystemPathBuf};
use ruff_python_ast::PythonVersion;
use salsa::Durability;
use salsa::Setter;
#[salsa::input(singleton)]
pub struct Program {
pub python_version: PythonVersion,
#[return_ref]
pub python_platform: PythonPlatform,
#[return_ref]
pub(crate) search_paths: SearchPaths,
}
impl Program {
pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> anyhow::Result<Self> {
let ProgramSettings {
python_version,
python_platform,
search_paths,
} = settings;
tracing::info!("Python version: Python {python_version}, platform: {python_platform}");
let search_paths = SearchPaths::from_settings(db, &search_paths)
.with_context(|| "Invalid search path settings")?;
Ok(
Program::builder(python_version, python_platform, search_paths)
.durability(Durability::HIGH)
.new(db),
)
}
pub fn update_from_settings(
self,
db: &mut dyn Db,
settings: ProgramSettings,
) -> anyhow::Result<()> {
let ProgramSettings {
python_version,
python_platform,
search_paths,
} = settings;
if &python_platform != self.python_platform(db) {
tracing::debug!("Updating python platform: `{python_platform:?}`");
self.set_python_platform(db).to(python_platform);
}
if python_version != self.python_version(db) {
tracing::debug!("Updating python version: Python {python_version}");
self.set_python_version(db).to(python_version);
}
self.update_search_paths(db, &search_paths)?;
Ok(())
}
pub fn update_search_paths(
self,
db: &mut dyn Db,
search_path_settings: &SearchPathSettings,
) -> anyhow::Result<()> {
let search_paths = SearchPaths::from_settings(db, search_path_settings)?;
if self.search_paths(db) != &search_paths {
tracing::debug!("Update search paths");
self.set_search_paths(db).to(search_paths);
}
Ok(())
}
pub fn custom_stdlib_search_path(self, db: &dyn Db) -> Option<&SystemPath> {
self.search_paths(db).custom_stdlib()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ProgramSettings {
pub python_version: PythonVersion,
pub python_platform: PythonPlatform,
pub search_paths: SearchPathSettings,
}
/// Configures the search paths for module resolution.
#[derive(Eq, PartialEq, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
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>,
/// Path to the Python installation from which Red Knot resolves third party dependencies
/// and their type information.
pub python_path: PythonPath,
}
impl SearchPathSettings {
pub fn new(src_roots: Vec<SystemPathBuf>) -> Self {
Self {
src_roots,
extra_paths: vec![],
custom_typeshed: None,
python_path: PythonPath::KnownSitePackages(vec![]),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PythonPath {
/// A path that represents the value of [`sys.prefix`] at runtime in Python
/// for a given Python executable.
///
/// For the case of a virtual environment, where a
/// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to
/// the virtual environment the Python binary lies inside, i.e. `/.venv`,
/// and `site-packages` will be at `.venv/lib/python3.X/site-packages`.
/// System Python installations generally work the same way: if a system
/// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix`
/// will be `/opt/homebrew`, and `site-packages` will be at
/// `/opt/homebrew/lib/python3.X/site-packages`.
///
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
SysPrefix(SystemPathBuf, SysPrefixPathOrigin),
/// Tries to discover a virtual environment in the given path.
Discover(SystemPathBuf),
/// Resolved site packages paths.
///
/// This variant is mainly intended for testing where we want to skip resolving `site-packages`
/// because it would unnecessarily complicate the test setup.
KnownSitePackages(Vec<SystemPathBuf>),
}
impl PythonPath {
pub fn from_virtual_env_var(path: impl Into<SystemPathBuf>) -> Self {
Self::SysPrefix(path.into(), SysPrefixPathOrigin::VirtualEnvVar)
}
pub fn from_cli_flag(path: SystemPathBuf) -> Self {
Self::SysPrefix(path, SysPrefixPathOrigin::PythonCliFlag)
}
}

View File

@@ -1,576 +0,0 @@
use std::hash::{Hash, Hasher};
use std::ops::Range;
use bitflags::bitflags;
use hashbrown::hash_map::RawEntryMut;
use ruff_db::files::File;
use ruff_db::parsed::ParsedModule;
use ruff_index::{newtype_index, IndexVec};
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use rustc_hash::FxHasher;
use crate::ast_node_ref::AstNodeRef;
use crate::node_key::NodeKey;
use crate::semantic_index::visibility_constraints::ScopedVisibilityConstraintId;
use crate::semantic_index::{semantic_index, SymbolMap};
use crate::Db;
#[derive(Eq, PartialEq, Debug)]
pub struct Symbol {
name: Name,
flags: SymbolFlags,
}
impl Symbol {
fn new(name: Name) -> Self {
Self {
name,
flags: SymbolFlags::empty(),
}
}
fn insert_flags(&mut self, flags: SymbolFlags) {
self.flags.insert(flags);
}
/// The symbol's name.
pub fn name(&self) -> &Name {
&self.name
}
/// Is the symbol used in its containing scope?
pub fn is_used(&self) -> bool {
self.flags.contains(SymbolFlags::IS_USED)
}
/// Is the symbol defined in its containing scope?
pub fn is_bound(&self) -> bool {
self.flags.contains(SymbolFlags::IS_BOUND)
}
/// Is the symbol declared in its containing scope?
pub fn is_declared(&self) -> bool {
self.flags.contains(SymbolFlags::IS_DECLARED)
}
}
bitflags! {
/// Flags that can be queried to obtain information about a symbol in a given scope.
///
/// See the doc-comment at the top of [`super::use_def`] for explanations of what it
/// means for a symbol to be *bound* as opposed to *declared*.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct SymbolFlags: u8 {
const IS_USED = 1 << 0;
const IS_BOUND = 1 << 1;
const IS_DECLARED = 1 << 2;
/// TODO: This flag is not yet set by anything
const MARKED_GLOBAL = 1 << 3;
/// TODO: This flag is not yet set by anything
const MARKED_NONLOCAL = 1 << 4;
}
}
/// ID that uniquely identifies a symbol in a file.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct FileSymbolId {
scope: FileScopeId,
scoped_symbol_id: ScopedSymbolId,
}
impl FileSymbolId {
pub fn scope(self) -> FileScopeId {
self.scope
}
pub(crate) fn scoped_symbol_id(self) -> ScopedSymbolId {
self.scoped_symbol_id
}
}
impl From<FileSymbolId> for ScopedSymbolId {
fn from(val: FileSymbolId) -> Self {
val.scoped_symbol_id()
}
}
/// Symbol ID that uniquely identifies a symbol inside a [`Scope`].
#[newtype_index]
#[derive(salsa::Update)]
pub struct ScopedSymbolId;
/// A cross-module identifier of a scope that can be used as a salsa query parameter.
#[salsa::tracked(debug)]
pub struct ScopeId<'db> {
pub file: File,
pub file_scope_id: FileScopeId,
count: countme::Count<ScopeId<'static>>,
}
impl<'db> ScopeId<'db> {
pub(crate) fn is_function_like(self, db: &'db dyn Db) -> bool {
self.node(db).scope_kind().is_function_like()
}
pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool {
self.node(db).scope_kind().is_type_parameter()
}
pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind {
self.scope(db).node()
}
pub(crate) fn scope(self, db: &dyn Db) -> &Scope {
semantic_index(db, self.file(db)).scope(self.file_scope_id(db))
}
#[cfg(test)]
pub(crate) fn name(self, db: &'db dyn Db) -> &'db str {
match self.node(db) {
NodeWithScopeKind::Module => "<module>",
NodeWithScopeKind::Class(class) | NodeWithScopeKind::ClassTypeParameters(class) => {
class.name.as_str()
}
NodeWithScopeKind::Function(function)
| NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(),
NodeWithScopeKind::TypeAlias(type_alias)
| NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => type_alias
.name
.as_name_expr()
.map(|name| name.id.as_str())
.unwrap_or("<type alias>"),
NodeWithScopeKind::Lambda(_) => "<lambda>",
NodeWithScopeKind::ListComprehension(_) => "<listcomp>",
NodeWithScopeKind::SetComprehension(_) => "<setcomp>",
NodeWithScopeKind::DictComprehension(_) => "<dictcomp>",
NodeWithScopeKind::GeneratorExpression(_) => "<generator>",
}
}
}
/// ID that uniquely identifies a scope inside of a module.
#[newtype_index]
#[derive(salsa::Update)]
pub struct FileScopeId;
impl FileScopeId {
/// Returns the scope id of the module-global scope.
pub fn global() -> Self {
FileScopeId::from_u32(0)
}
pub fn is_global(self) -> bool {
self == FileScopeId::global()
}
pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> {
let index = semantic_index(db, file);
index.scope_ids_by_scope[self]
}
}
#[derive(Debug, salsa::Update)]
pub struct Scope {
parent: Option<FileScopeId>,
node: NodeWithScopeKind,
descendants: Range<FileScopeId>,
reachability: ScopedVisibilityConstraintId,
}
impl Scope {
pub(super) fn new(
parent: Option<FileScopeId>,
node: NodeWithScopeKind,
descendants: Range<FileScopeId>,
reachability: ScopedVisibilityConstraintId,
) -> Self {
Scope {
parent,
node,
descendants,
reachability,
}
}
pub fn parent(&self) -> Option<FileScopeId> {
self.parent
}
pub fn node(&self) -> &NodeWithScopeKind {
&self.node
}
pub fn kind(&self) -> ScopeKind {
self.node().scope_kind()
}
pub fn descendants(&self) -> Range<FileScopeId> {
self.descendants.clone()
}
pub(super) fn extend_descendants(&mut self, children_end: FileScopeId) {
self.descendants = self.descendants.start..children_end;
}
pub(crate) fn is_eager(&self) -> bool {
self.kind().is_eager()
}
pub(crate) fn reachability(&self) -> ScopedVisibilityConstraintId {
self.reachability
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ScopeKind {
Module,
Annotation,
Class,
Function,
Lambda,
Comprehension,
TypeAlias,
}
impl ScopeKind {
pub(crate) fn is_eager(self) -> bool {
match self {
ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => true,
ScopeKind::Annotation
| ScopeKind::Function
| ScopeKind::Lambda
| ScopeKind::TypeAlias => false,
}
}
pub(crate) fn is_function_like(self) -> bool {
// Type parameter scopes behave like function scopes in terms of name resolution; CPython
// symbol table also uses the term "function-like" for these scopes.
matches!(
self,
ScopeKind::Annotation
| ScopeKind::Function
| ScopeKind::Lambda
| ScopeKind::TypeAlias
| ScopeKind::Comprehension
)
}
pub(crate) fn is_class(self) -> bool {
matches!(self, ScopeKind::Class)
}
pub(crate) fn is_type_parameter(self) -> bool {
matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias)
}
}
/// Symbol table for a specific [`Scope`].
#[derive(Default, salsa::Update)]
pub struct SymbolTable {
/// The symbols in this scope.
symbols: IndexVec<ScopedSymbolId, Symbol>,
/// The symbols indexed by name.
symbols_by_name: SymbolMap,
}
impl SymbolTable {
fn shrink_to_fit(&mut self) {
self.symbols.shrink_to_fit();
}
pub(crate) fn symbol(&self, symbol_id: impl Into<ScopedSymbolId>) -> &Symbol {
&self.symbols[symbol_id.into()]
}
#[allow(unused)]
pub(crate) fn symbol_ids(&self) -> impl Iterator<Item = ScopedSymbolId> {
self.symbols.indices()
}
pub fn symbols(&self) -> impl Iterator<Item = &Symbol> {
self.symbols.iter()
}
/// Returns the symbol named `name`.
pub(crate) fn symbol_by_name(&self, name: &str) -> Option<&Symbol> {
let id = self.symbol_id_by_name(name)?;
Some(self.symbol(id))
}
/// Returns the [`ScopedSymbolId`] of the symbol named `name`.
pub(crate) fn symbol_id_by_name(&self, name: &str) -> Option<ScopedSymbolId> {
let (id, ()) = self
.symbols_by_name
.raw_entry()
.from_hash(Self::hash_name(name), |id| {
self.symbol(*id).name().as_str() == name
})?;
Some(*id)
}
fn hash_name(name: &str) -> u64 {
let mut hasher = FxHasher::default();
name.hash(&mut hasher);
hasher.finish()
}
}
impl PartialEq for SymbolTable {
fn eq(&self, other: &Self) -> bool {
// We don't need to compare the symbols_by_name because the name is already captured in `Symbol`.
self.symbols == other.symbols
}
}
impl Eq for SymbolTable {}
impl std::fmt::Debug for SymbolTable {
/// Exclude the `symbols_by_name` field from the debug output.
/// It's very noisy and not useful for debugging.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SymbolTable")
.field(&self.symbols)
.finish_non_exhaustive()
}
}
#[derive(Debug, Default)]
pub(super) struct SymbolTableBuilder {
table: SymbolTable,
}
impl SymbolTableBuilder {
pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) {
let hash = SymbolTable::hash_name(&name);
let entry = self
.table
.symbols_by_name
.raw_entry_mut()
.from_hash(hash, |id| self.table.symbols[*id].name() == &name);
match entry {
RawEntryMut::Occupied(entry) => (*entry.key(), false),
RawEntryMut::Vacant(entry) => {
let symbol = Symbol::new(name);
let id = self.table.symbols.push(symbol);
entry.insert_with_hasher(hash, id, (), |id| {
SymbolTable::hash_name(self.table.symbols[*id].name().as_str())
});
(id, true)
}
}
}
pub(super) fn mark_symbol_bound(&mut self, id: ScopedSymbolId) {
self.table.symbols[id].insert_flags(SymbolFlags::IS_BOUND);
}
pub(super) fn mark_symbol_declared(&mut self, id: ScopedSymbolId) {
self.table.symbols[id].insert_flags(SymbolFlags::IS_DECLARED);
}
pub(super) fn mark_symbol_used(&mut self, id: ScopedSymbolId) {
self.table.symbols[id].insert_flags(SymbolFlags::IS_USED);
}
pub(super) fn symbols(&self) -> impl Iterator<Item = &Symbol> {
self.table.symbols()
}
pub(super) fn symbol_id_by_name(&self, name: &str) -> Option<ScopedSymbolId> {
self.table.symbol_id_by_name(name)
}
pub(super) fn symbol(&self, symbol_id: impl Into<ScopedSymbolId>) -> &Symbol {
self.table.symbol(symbol_id)
}
pub(super) fn finish(mut self) -> SymbolTable {
self.table.shrink_to_fit();
self.table
}
}
/// Reference to a node that introduces a new scope.
#[derive(Copy, Clone, Debug)]
pub(crate) enum NodeWithScopeRef<'a> {
Module,
Class(&'a ast::StmtClassDef),
Function(&'a ast::StmtFunctionDef),
Lambda(&'a ast::ExprLambda),
FunctionTypeParameters(&'a ast::StmtFunctionDef),
ClassTypeParameters(&'a ast::StmtClassDef),
TypeAlias(&'a ast::StmtTypeAlias),
TypeAliasTypeParameters(&'a ast::StmtTypeAlias),
ListComprehension(&'a ast::ExprListComp),
SetComprehension(&'a ast::ExprSetComp),
DictComprehension(&'a ast::ExprDictComp),
GeneratorExpression(&'a ast::ExprGenerator),
}
impl NodeWithScopeRef<'_> {
/// Converts the unowned reference to an owned [`NodeWithScopeKind`].
///
/// # Safety
/// The node wrapped by `self` must be a child of `module`.
#[allow(unsafe_code)]
pub(super) unsafe fn to_kind(self, module: ParsedModule) -> NodeWithScopeKind {
match self {
NodeWithScopeRef::Module => NodeWithScopeKind::Module,
NodeWithScopeRef::Class(class) => {
NodeWithScopeKind::Class(AstNodeRef::new(module, class))
}
NodeWithScopeRef::Function(function) => {
NodeWithScopeKind::Function(AstNodeRef::new(module, function))
}
NodeWithScopeRef::TypeAlias(type_alias) => {
NodeWithScopeKind::TypeAlias(AstNodeRef::new(module, type_alias))
}
NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => {
NodeWithScopeKind::TypeAliasTypeParameters(AstNodeRef::new(module, type_alias))
}
NodeWithScopeRef::Lambda(lambda) => {
NodeWithScopeKind::Lambda(AstNodeRef::new(module, lambda))
}
NodeWithScopeRef::FunctionTypeParameters(function) => {
NodeWithScopeKind::FunctionTypeParameters(AstNodeRef::new(module, function))
}
NodeWithScopeRef::ClassTypeParameters(class) => {
NodeWithScopeKind::ClassTypeParameters(AstNodeRef::new(module, class))
}
NodeWithScopeRef::ListComprehension(comprehension) => {
NodeWithScopeKind::ListComprehension(AstNodeRef::new(module, comprehension))
}
NodeWithScopeRef::SetComprehension(comprehension) => {
NodeWithScopeKind::SetComprehension(AstNodeRef::new(module, comprehension))
}
NodeWithScopeRef::DictComprehension(comprehension) => {
NodeWithScopeKind::DictComprehension(AstNodeRef::new(module, comprehension))
}
NodeWithScopeRef::GeneratorExpression(generator) => {
NodeWithScopeKind::GeneratorExpression(AstNodeRef::new(module, generator))
}
}
}
pub(crate) fn node_key(self) -> NodeWithScopeKey {
match self {
NodeWithScopeRef::Module => NodeWithScopeKey::Module,
NodeWithScopeRef::Class(class) => NodeWithScopeKey::Class(NodeKey::from_node(class)),
NodeWithScopeRef::Function(function) => {
NodeWithScopeKey::Function(NodeKey::from_node(function))
}
NodeWithScopeRef::Lambda(lambda) => {
NodeWithScopeKey::Lambda(NodeKey::from_node(lambda))
}
NodeWithScopeRef::FunctionTypeParameters(function) => {
NodeWithScopeKey::FunctionTypeParameters(NodeKey::from_node(function))
}
NodeWithScopeRef::ClassTypeParameters(class) => {
NodeWithScopeKey::ClassTypeParameters(NodeKey::from_node(class))
}
NodeWithScopeRef::TypeAlias(type_alias) => {
NodeWithScopeKey::TypeAlias(NodeKey::from_node(type_alias))
}
NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => {
NodeWithScopeKey::TypeAliasTypeParameters(NodeKey::from_node(type_alias))
}
NodeWithScopeRef::ListComprehension(comprehension) => {
NodeWithScopeKey::ListComprehension(NodeKey::from_node(comprehension))
}
NodeWithScopeRef::SetComprehension(comprehension) => {
NodeWithScopeKey::SetComprehension(NodeKey::from_node(comprehension))
}
NodeWithScopeRef::DictComprehension(comprehension) => {
NodeWithScopeKey::DictComprehension(NodeKey::from_node(comprehension))
}
NodeWithScopeRef::GeneratorExpression(generator) => {
NodeWithScopeKey::GeneratorExpression(NodeKey::from_node(generator))
}
}
}
}
/// Node that introduces a new scope.
#[derive(Clone, Debug, salsa::Update)]
pub enum NodeWithScopeKind {
Module,
Class(AstNodeRef<ast::StmtClassDef>),
ClassTypeParameters(AstNodeRef<ast::StmtClassDef>),
Function(AstNodeRef<ast::StmtFunctionDef>),
FunctionTypeParameters(AstNodeRef<ast::StmtFunctionDef>),
TypeAliasTypeParameters(AstNodeRef<ast::StmtTypeAlias>),
TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
Lambda(AstNodeRef<ast::ExprLambda>),
ListComprehension(AstNodeRef<ast::ExprListComp>),
SetComprehension(AstNodeRef<ast::ExprSetComp>),
DictComprehension(AstNodeRef<ast::ExprDictComp>),
GeneratorExpression(AstNodeRef<ast::ExprGenerator>),
}
impl NodeWithScopeKind {
pub(crate) const fn scope_kind(&self) -> ScopeKind {
match self {
Self::Module => ScopeKind::Module,
Self::Class(_) => ScopeKind::Class,
Self::Function(_) => ScopeKind::Function,
Self::Lambda(_) => ScopeKind::Lambda,
Self::FunctionTypeParameters(_)
| Self::ClassTypeParameters(_)
| Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation,
Self::TypeAlias(_) => ScopeKind::TypeAlias,
Self::ListComprehension(_)
| Self::SetComprehension(_)
| Self::DictComprehension(_)
| Self::GeneratorExpression(_) => ScopeKind::Comprehension,
}
}
pub fn expect_class(&self) -> &ast::StmtClassDef {
match self {
Self::Class(class) => class.node(),
_ => panic!("expected class"),
}
}
pub fn expect_function(&self) -> &ast::StmtFunctionDef {
self.as_function().expect("expected function")
}
pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias {
match self {
Self::TypeAlias(type_alias) => type_alias.node(),
_ => panic!("expected type alias"),
}
}
pub const fn as_function(&self) -> Option<&ast::StmtFunctionDef> {
match self {
Self::Function(function) => Some(function.node()),
_ => None,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) enum NodeWithScopeKey {
Module,
Class(NodeKey),
ClassTypeParameters(NodeKey),
Function(NodeKey),
FunctionTypeParameters(NodeKey),
TypeAlias(NodeKey),
TypeAliasTypeParameters(NodeKey),
Lambda(NodeKey),
ListComprehension(NodeKey),
SetComprehension(NodeKey),
DictComprehension(NodeKey),
GeneratorExpression(NodeKey),
}

View File

@@ -1,916 +0,0 @@
//! Utilities for finding the `site-packages` directory,
//! into which third-party packages are installed.
//!
//! The routines exposed by this module have different behaviour depending
//! on the platform of the *host machine*, which may be
//! different from the *target platform for type checking*. (A user
//! might be running red-knot on a Windows machine, but might
//! reasonably ask us to type-check code assuming that the code runs
//! on Linux.)
use std::fmt;
use std::fmt::Display;
use std::io;
use std::num::NonZeroUsize;
use std::ops::Deref;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_python_ast::PythonVersion;
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;
/// Abstraction for a Python virtual environment.
///
/// Most of this information is derived from the virtual environment's `pyvenv.cfg` file.
/// The format of this file is not defined anywhere, and exactly which keys are present
/// depends on the tool that was used to create the virtual environment.
#[derive(Debug)]
pub(crate) struct VirtualEnvironment {
venv_path: SysPrefixPath,
base_executable_home_path: PythonHomePath,
include_system_site_packages: bool,
/// The version of the Python executable that was used to create this virtual environment.
///
/// The Python version is encoded under different keys and in different formats
/// by different virtual-environment creation tools,
/// and the key is never read by the standard-library `site.py` module,
/// so it's possible that we might not be able to find this information
/// in an acceptable format under any of the keys we expect.
/// This field will be `None` if so.
version: Option<PythonVersion>,
}
impl VirtualEnvironment {
pub(crate) fn new(
path: impl AsRef<SystemPath>,
origin: SysPrefixPathOrigin,
system: &dyn System,
) -> SitePackagesDiscoveryResult<Self> {
Self::new_impl(path.as_ref(), origin, system)
}
fn new_impl(
path: &SystemPath,
origin: SysPrefixPathOrigin,
system: &dyn System,
) -> SitePackagesDiscoveryResult<Self> {
fn pyvenv_cfg_line_number(index: usize) -> NonZeroUsize {
index.checked_add(1).and_then(NonZeroUsize::new).unwrap()
}
let venv_path = SysPrefixPath::new(path, origin, system)?;
let pyvenv_cfg_path = venv_path.join("pyvenv.cfg");
tracing::debug!("Attempting to parse virtual environment metadata at '{pyvenv_cfg_path}'");
let pyvenv_cfg = system
.read_to_string(&pyvenv_cfg_path)
.map_err(|io_err| SitePackagesDiscoveryError::NoPyvenvCfgFile(origin, io_err))?;
let mut include_system_site_packages = false;
let mut base_executable_home_path = None;
let mut version_info_string = None;
// A `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax!
// The Python standard-library's `site` module parses these files by splitting each line on
// '=' characters, so that's what we should do as well.
//
// See also: https://snarky.ca/how-virtual-environments-work/
for (index, line) in pyvenv_cfg.lines().enumerate() {
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
if key.is_empty() {
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
pyvenv_cfg_path,
PyvenvCfgParseErrorKind::MalformedKeyValuePair {
line_number: pyvenv_cfg_line_number(index),
},
));
}
let value = value.trim();
if value.is_empty() {
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
pyvenv_cfg_path,
PyvenvCfgParseErrorKind::MalformedKeyValuePair {
line_number: pyvenv_cfg_line_number(index),
},
));
}
if value.contains('=') {
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
pyvenv_cfg_path,
PyvenvCfgParseErrorKind::TooManyEquals {
line_number: pyvenv_cfg_line_number(index),
},
));
}
match key {
"include-system-site-packages" => {
include_system_site_packages = value.eq_ignore_ascii_case("true");
}
"home" => base_executable_home_path = Some(value),
// `virtualenv` and `uv` call this key `version_info`,
// but the stdlib venv module calls it `version`
"version" | "version_info" => version_info_string = Some(value),
_ => continue,
}
}
}
// The `home` key is read by the standard library's `site.py` module,
// so if it's missing from the `pyvenv.cfg` file
// (or the provided value is invalid),
// it's reasonable to consider the virtual environment irredeemably broken.
let Some(base_executable_home_path) = base_executable_home_path else {
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
pyvenv_cfg_path,
PyvenvCfgParseErrorKind::NoHomeKey,
));
};
let base_executable_home_path = PythonHomePath::new(base_executable_home_path, system)
.map_err(|io_err| {
SitePackagesDiscoveryError::PyvenvCfgParseError(
pyvenv_cfg_path,
PyvenvCfgParseErrorKind::InvalidHomeValue(io_err),
)
})?;
// but the `version`/`version_info` key is not read by the standard library,
// and is provided under different keys depending on which virtual-environment creation tool
// created the `pyvenv.cfg` file. Lenient parsing is appropriate here:
// the file isn't really *invalid* if it doesn't have this key,
// or if the value doesn't parse according to our expectations.
let version = version_info_string.and_then(|version_string| {
let mut version_info_parts = version_string.split('.');
let (major, minor) = (version_info_parts.next()?, version_info_parts.next()?);
PythonVersion::try_from((major, minor)).ok()
});
let metadata = Self {
venv_path,
base_executable_home_path,
include_system_site_packages,
version,
};
tracing::trace!("Resolved metadata for virtual environment: {metadata:?}");
Ok(metadata)
}
/// Return a list of `site-packages` directories that are available from this virtual environment
///
/// See the documentation for `site_packages_dir_from_sys_prefix` for more details.
pub(crate) fn site_packages_directories(
&self,
system: &dyn System,
) -> SitePackagesDiscoveryResult<Vec<SystemPathBuf>> {
let VirtualEnvironment {
venv_path,
base_executable_home_path,
include_system_site_packages,
version,
} = self;
let mut site_packages_directories = vec![site_packages_directory_from_sys_prefix(
venv_path, *version, system,
)?];
if *include_system_site_packages {
let system_sys_prefix =
SysPrefixPath::from_executable_home_path(base_executable_home_path);
// If we fail to resolve the `sys.prefix` path from the base executable home path,
// or if we fail to resolve the `site-packages` from the `sys.prefix` path,
// we should probably print a warning but *not* abort type checking
if let Some(sys_prefix_path) = system_sys_prefix {
match site_packages_directory_from_sys_prefix(&sys_prefix_path, *version, system) {
Ok(site_packages_directory) => {
site_packages_directories.push(site_packages_directory);
}
Err(error) => tracing::warn!(
"{error}. System site-packages will not be used for module resolution."
),
}
} else {
tracing::warn!(
"Failed to resolve `sys.prefix` of the system Python installation \
from the `home` value in the `pyvenv.cfg` file at `{}`. \
System site-packages will not be used for module resolution.",
venv_path.join("pyvenv.cfg")
);
}
}
tracing::debug!("Resolved site-packages directories for this virtual environment are: {site_packages_directories:?}");
Ok(site_packages_directories)
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum SitePackagesDiscoveryError {
#[error("Invalid {1}: `{0}` could not be canonicalized")]
VenvDirCanonicalizationError(SystemPathBuf, SysPrefixPathOrigin, #[source] io::Error),
#[error("Invalid {1}: `{0}` does not point to a directory on disk")]
VenvDirIsNotADirectory(SystemPathBuf, SysPrefixPathOrigin),
#[error("{0} points to a broken venv with no pyvenv.cfg file")]
NoPyvenvCfgFile(SysPrefixPathOrigin, #[source] io::Error),
#[error("Failed to parse the pyvenv.cfg file at {0} because {1}")]
PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind),
#[error("Failed to search the `lib` directory of the Python installation at {1} for `site-packages`")]
CouldNotReadLibDirectory(#[source] io::Error, SysPrefixPath),
#[error("Could not find the `site-packages` directory for the Python installation at {0}")]
NoSitePackagesDirFound(SysPrefixPath),
}
/// The various ways in which parsing a `pyvenv.cfg` file could fail
#[derive(Debug)]
pub(crate) enum PyvenvCfgParseErrorKind {
TooManyEquals { line_number: NonZeroUsize },
MalformedKeyValuePair { line_number: NonZeroUsize },
NoHomeKey,
InvalidHomeValue(io::Error),
}
impl fmt::Display for PyvenvCfgParseErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooManyEquals { line_number } => {
write!(f, "line {line_number} has too many '=' characters")
}
Self::MalformedKeyValuePair { line_number } => write!(
f,
"line {line_number} has a malformed `<key> = <value>` pair"
),
Self::NoHomeKey => f.write_str("the file does not have a `home` key"),
Self::InvalidHomeValue(io_err) => {
write!(
f,
"the following error was encountered \
when trying to resolve the `home` value to a directory on disk: {io_err}"
)
}
}
}
}
/// Attempt to retrieve the `site-packages` directory
/// associated with a given Python installation.
///
/// The location of the `site-packages` directory can vary according to the
/// Python version that this installation represents. The Python version may
/// or may not be known at this point, which is why the `python_version`
/// parameter is an `Option`.
fn site_packages_directory_from_sys_prefix(
sys_prefix_path: &SysPrefixPath,
python_version: Option<PythonVersion>,
system: &dyn System,
) -> SitePackagesDiscoveryResult<SystemPathBuf> {
tracing::debug!("Searching for site-packages directory in {sys_prefix_path}");
if cfg!(target_os = "windows") {
let site_packages = sys_prefix_path.join(r"Lib\site-packages");
return system
.is_directory(&site_packages)
.then_some(site_packages)
.ok_or(SitePackagesDiscoveryError::NoSitePackagesDirFound(
sys_prefix_path.to_owned(),
));
}
// In the Python standard library's `site.py` module (used for finding `site-packages`
// at runtime), we can find this in [the non-Windows branch]:
//
// ```py
// libdirs = [sys.platlibdir]
// if sys.platlibdir != "lib":
// libdirs.append("lib")
// ```
//
// Pyright therefore searches for both a `lib/python3.X/site-packages` directory
// and a `lib64/python3.X/site-packages` directory on non-MacOS Unix systems,
// since `sys.platlibdir` can sometimes be equal to `"lib64"`.
//
// However, we only care about the `site-packages` directory insofar as it allows
// us to discover Python source code that can be used for inferring type
// information regarding third-party dependencies. That means that we don't need
// to care about any possible `lib64/site-packages` directories, since
// [the `sys`-module documentation] states that `sys.platlibdir` is *only* ever
// used for C extensions, never for pure-Python modules.
//
// [the non-Windows branch]: https://github.com/python/cpython/blob/a8be8fc6c4682089be45a87bd5ee1f686040116c/Lib/site.py#L401-L410
// [the `sys`-module documentation]: https://docs.python.org/3/library/sys.html#sys.platlibdir
// If we were able to figure out what Python version this installation is,
// we should be able to avoid iterating through all items in the `lib/` directory:
if let Some(version) = python_version {
let expected_path = sys_prefix_path.join(format!("lib/python{version}/site-packages"));
if system.is_directory(&expected_path) {
return Ok(expected_path);
}
if version.free_threaded_build_available() {
// Nearly the same as `expected_path`, but with an additional `t` after {version}:
let alternative_path =
sys_prefix_path.join(format!("lib/python{version}t/site-packages"));
if system.is_directory(&alternative_path) {
return Ok(alternative_path);
}
}
}
// Either we couldn't figure out the version before calling this function
// (e.g., from a `pyvenv.cfg` file if this was a venv),
// or we couldn't find a `site-packages` folder at the expected location given
// the parsed version
//
// Note: the `python3.x` part of the `site-packages` path can't be computed from
// the `--python-version` the user has passed, as they might be running Python 3.12 locally
// even if they've requested that we type check their code "as if" they're running 3.8.
for entry_result in system
.read_directory(&sys_prefix_path.join("lib"))
.map_err(|io_err| {
SitePackagesDiscoveryError::CouldNotReadLibDirectory(io_err, sys_prefix_path.to_owned())
})?
{
let Ok(entry) = entry_result else {
continue;
};
if !entry.file_type().is_directory() {
continue;
}
let mut path = entry.into_path();
let name = path
.file_name()
.expect("File name to be non-null because path is guaranteed to be a child of `lib`");
if !name.starts_with("python3.") {
continue;
}
path.push("site-packages");
if system.is_directory(&path) {
return Ok(path);
}
}
Err(SitePackagesDiscoveryError::NoSitePackagesDirFound(
sys_prefix_path.to_owned(),
))
}
/// A path that represents the value of [`sys.prefix`] at runtime in Python
/// for a given Python executable.
///
/// For the case of a virtual environment, where a
/// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to
/// the virtual environment the Python binary lies inside, i.e. `/.venv`,
/// and `site-packages` will be at `.venv/lib/python3.X/site-packages`.
/// System Python installations generally work the same way: if a system
/// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix`
/// will be `/opt/homebrew`, and `site-packages` will be at
/// `/opt/homebrew/lib/python3.X/site-packages`.
///
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
#[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) struct SysPrefixPath {
inner: SystemPathBuf,
origin: SysPrefixPathOrigin,
}
impl SysPrefixPath {
fn new(
unvalidated_path: impl AsRef<SystemPath>,
origin: SysPrefixPathOrigin,
system: &dyn System,
) -> SitePackagesDiscoveryResult<Self> {
Self::new_impl(unvalidated_path.as_ref(), origin, system)
}
fn new_impl(
unvalidated_path: &SystemPath,
origin: SysPrefixPathOrigin,
system: &dyn System,
) -> SitePackagesDiscoveryResult<Self> {
// It's important to resolve symlinks here rather than simply making the path absolute,
// since system Python installations often only put symlinks in the "expected"
// locations for `home` and `site-packages`
let canonicalized = system
.canonicalize_path(unvalidated_path)
.map_err(|io_err| {
SitePackagesDiscoveryError::VenvDirCanonicalizationError(
unvalidated_path.to_path_buf(),
origin,
io_err,
)
})?;
system
.is_directory(&canonicalized)
.then_some(Self {
inner: canonicalized,
origin,
})
.ok_or_else(|| {
SitePackagesDiscoveryError::VenvDirIsNotADirectory(
unvalidated_path.to_path_buf(),
origin,
)
})
}
fn from_executable_home_path(path: &PythonHomePath) -> Option<Self> {
// No need to check whether `path.parent()` is a directory:
// the parent of a canonicalised path that is known to exist
// is guaranteed to be a directory.
if cfg!(target_os = "windows") {
Some(Self {
inner: path.to_path_buf(),
origin: SysPrefixPathOrigin::Derived,
})
} else {
path.parent().map(|path| Self {
inner: path.to_path_buf(),
origin: SysPrefixPathOrigin::Derived,
})
}
}
}
impl Deref for SysPrefixPath {
type Target = SystemPath;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl fmt::Display for SysPrefixPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "`sys.prefix` path `{}`", self.inner)
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SysPrefixPathOrigin {
PythonCliFlag,
VirtualEnvVar,
Derived,
LocalVenv,
}
impl Display for SysPrefixPathOrigin {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::PythonCliFlag => f.write_str("`--python` argument"),
Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"),
Self::Derived => f.write_str("derived `sys.prefix` path"),
Self::LocalVenv => f.write_str("local virtual environment"),
}
}
}
/// The value given by the `home` key in `pyvenv.cfg` files.
///
/// This is equivalent to `{sys_prefix_path}/bin`, and points
/// to a directory in which a Python executable can be found.
/// Confusingly, it is *not* the same as the [`PYTHONHOME`]
/// environment variable that Python provides! However, it's
/// consistent among all mainstream creators of Python virtual
/// environments (the stdlib Python `venv` module, the third-party
/// `virtualenv` library, and `uv`), was specified by
/// [the original PEP adding the `venv` module],
/// and it's one of the few fields that's read by the Python
/// standard library's `site.py` module.
///
/// Although it doesn't appear to be specified anywhere,
/// all existing virtual environment tools always use an absolute path
/// for the `home` value, and the Python standard library also assumes
/// that the `home` value will be an absolute path.
///
/// Other values, such as the path to the Python executable or the
/// base-executable `sys.prefix` value, are either only provided in
/// `pyvenv.cfg` files by some virtual-environment creators,
/// or are included under different keys depending on which
/// virtual-environment creation tool you've used.
///
/// [`PYTHONHOME`]: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME
/// [the original PEP adding the `venv` module]: https://peps.python.org/pep-0405/
#[derive(Debug, PartialEq, Eq)]
struct PythonHomePath(SystemPathBuf);
impl PythonHomePath {
fn new(path: impl AsRef<SystemPath>, system: &dyn System) -> io::Result<Self> {
let path = path.as_ref();
// It's important to resolve symlinks here rather than simply making the path absolute,
// since system Python installations often only put symlinks in the "expected"
// locations for `home` and `site-packages`
let canonicalized = system.canonicalize_path(path)?;
system
.is_directory(&canonicalized)
.then_some(Self(canonicalized))
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "not a directory"))
}
}
impl Deref for PythonHomePath {
type Target = SystemPath;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for PythonHomePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "`home` location `{}`", self.0)
}
}
impl PartialEq<SystemPath> for PythonHomePath {
fn eq(&self, other: &SystemPath) -> bool {
&*self.0 == other
}
}
impl PartialEq<SystemPathBuf> for PythonHomePath {
fn eq(&self, other: &SystemPathBuf) -> bool {
self == &**other
}
}
#[cfg(test)]
mod tests {
use ruff_db::system::TestSystem;
use super::*;
struct VirtualEnvironmentTester {
system: TestSystem,
minor_version: u8,
free_threaded: bool,
system_site_packages: bool,
pyvenv_cfg_version_field: Option<&'static str>,
}
impl VirtualEnvironmentTester {
/// Builds a mock virtual environment, and returns the path to the venv
fn build_mock_venv(&self) -> SystemPathBuf {
let VirtualEnvironmentTester {
system,
minor_version,
system_site_packages,
free_threaded,
pyvenv_cfg_version_field,
} = self;
let memory_fs = system.memory_file_system();
let unix_site_packages = if *free_threaded {
format!("lib/python3.{minor_version}t/site-packages")
} else {
format!("lib/python3.{minor_version}/site-packages")
};
let system_install_sys_prefix =
SystemPathBuf::from(&*format!("/Python3.{minor_version}"));
let (system_home_path, system_exe_path, system_site_packages_path) =
if cfg!(target_os = "windows") {
let system_home_path = system_install_sys_prefix.clone();
let system_exe_path = system_home_path.join("python.exe");
let system_site_packages_path =
system_install_sys_prefix.join(r"Lib\site-packages");
(system_home_path, system_exe_path, system_site_packages_path)
} else {
let system_home_path = system_install_sys_prefix.join("bin");
let system_exe_path = system_home_path.join("python");
let system_site_packages_path =
system_install_sys_prefix.join(&unix_site_packages);
(system_home_path, system_exe_path, system_site_packages_path)
};
memory_fs.write_file_all(system_exe_path, "").unwrap();
memory_fs
.create_directory_all(&system_site_packages_path)
.unwrap();
let venv_sys_prefix = SystemPathBuf::from("/.venv");
let (venv_exe, site_packages_path) = if cfg!(target_os = "windows") {
(
venv_sys_prefix.join(r"Scripts\python.exe"),
venv_sys_prefix.join(r"Lib\site-packages"),
)
} else {
(
venv_sys_prefix.join("bin/python"),
venv_sys_prefix.join(&unix_site_packages),
)
};
memory_fs.write_file_all(&venv_exe, "").unwrap();
memory_fs.create_directory_all(&site_packages_path).unwrap();
let pyvenv_cfg_path = venv_sys_prefix.join("pyvenv.cfg");
let mut pyvenv_cfg_contents = format!("home = {system_home_path}\n");
if let Some(version_field) = pyvenv_cfg_version_field {
pyvenv_cfg_contents.push_str(version_field);
pyvenv_cfg_contents.push('\n');
}
// Deliberately using weird casing here to test that our pyvenv.cfg parsing is case-insensitive:
if *system_site_packages {
pyvenv_cfg_contents.push_str("include-system-site-packages = TRuE\n");
}
memory_fs
.write_file_all(pyvenv_cfg_path, &pyvenv_cfg_contents)
.unwrap();
venv_sys_prefix
}
fn test(self) {
let venv_path = self.build_mock_venv();
let venv = VirtualEnvironment::new(
venv_path.clone(),
SysPrefixPathOrigin::VirtualEnvVar,
&self.system,
)
.unwrap();
assert_eq!(
venv.venv_path,
SysPrefixPath {
inner: self.system.canonicalize_path(&venv_path).unwrap(),
origin: SysPrefixPathOrigin::VirtualEnvVar,
}
);
assert_eq!(venv.include_system_site_packages, self.system_site_packages);
if self.pyvenv_cfg_version_field.is_some() {
assert_eq!(
venv.version,
Some(PythonVersion {
major: 3,
minor: self.minor_version
})
);
} else {
assert_eq!(venv.version, None);
}
let expected_home = if cfg!(target_os = "windows") {
SystemPathBuf::from(&*format!(r"\Python3.{}", self.minor_version))
} else {
SystemPathBuf::from(&*format!("/Python3.{}/bin", self.minor_version))
};
assert_eq!(venv.base_executable_home_path, expected_home);
let site_packages_directories = venv.site_packages_directories(&self.system).unwrap();
let expected_venv_site_packages = if cfg!(target_os = "windows") {
SystemPathBuf::from(r"\.venv\Lib\site-packages")
} else if self.free_threaded {
SystemPathBuf::from(&*format!(
"/.venv/lib/python3.{}t/site-packages",
self.minor_version
))
} else {
SystemPathBuf::from(&*format!(
"/.venv/lib/python3.{}/site-packages",
self.minor_version
))
};
let expected_system_site_packages = if cfg!(target_os = "windows") {
SystemPathBuf::from(&*format!(
r"\Python3.{}\Lib\site-packages",
self.minor_version
))
} else if self.free_threaded {
SystemPathBuf::from(&*format!(
"/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages",
minor_version = self.minor_version
))
} else {
SystemPathBuf::from(&*format!(
"/Python3.{minor_version}/lib/python3.{minor_version}/site-packages",
minor_version = self.minor_version
))
};
if self.system_site_packages {
assert_eq!(
&site_packages_directories,
&[expected_venv_site_packages, expected_system_site_packages]
);
} else {
assert_eq!(&site_packages_directories, &[expected_venv_site_packages]);
}
}
}
#[test]
fn can_find_site_packages_directory_no_version_field_in_pyvenv_cfg() {
let tester = VirtualEnvironmentTester {
system: TestSystem::default(),
minor_version: 12,
free_threaded: false,
system_site_packages: false,
pyvenv_cfg_version_field: None,
};
tester.test();
}
#[test]
fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() {
let tester = VirtualEnvironmentTester {
system: TestSystem::default(),
minor_version: 12,
free_threaded: false,
system_site_packages: false,
pyvenv_cfg_version_field: Some("version = 3.12"),
};
tester.test();
}
#[test]
fn can_find_site_packages_directory_uv_style_version_field_in_pyvenv_cfg() {
let tester = VirtualEnvironmentTester {
system: TestSystem::default(),
minor_version: 12,
free_threaded: false,
system_site_packages: false,
pyvenv_cfg_version_field: Some("version_info = 3.12"),
};
tester.test();
}
#[test]
fn can_find_site_packages_directory_virtualenv_style_version_field_in_pyvenv_cfg() {
let tester = VirtualEnvironmentTester {
system: TestSystem::default(),
minor_version: 12,
free_threaded: false,
system_site_packages: false,
pyvenv_cfg_version_field: Some("version_info = 3.12.0rc2"),
};
tester.test();
}
#[test]
fn can_find_site_packages_directory_freethreaded_build() {
let tester = VirtualEnvironmentTester {
system: TestSystem::default(),
minor_version: 13,
free_threaded: true,
system_site_packages: false,
pyvenv_cfg_version_field: Some("version_info = 3.13"),
};
tester.test();
}
#[test]
fn finds_system_site_packages() {
let tester = VirtualEnvironmentTester {
system: TestSystem::default(),
minor_version: 13,
free_threaded: true,
system_site_packages: true,
pyvenv_cfg_version_field: Some("version_info = 3.13"),
};
tester.test();
}
#[test]
fn reject_venv_that_does_not_exist() {
let system = TestSystem::default();
assert!(matches!(
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system),
Err(SitePackagesDiscoveryError::VenvDirCanonicalizationError(..))
));
}
#[test]
fn reject_venv_that_is_not_a_directory() {
let system = TestSystem::default();
system
.memory_file_system()
.write_file_all("/.venv", "")
.unwrap();
assert!(matches!(
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system),
Err(SitePackagesDiscoveryError::VenvDirIsNotADirectory(..))
));
}
#[test]
fn reject_venv_with_no_pyvenv_cfg_file() {
let system = TestSystem::default();
system
.memory_file_system()
.create_directory_all("/.venv")
.unwrap();
assert!(matches!(
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system),
Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(
SysPrefixPathOrigin::VirtualEnvVar,
_
))
));
}
#[test]
fn parsing_pyvenv_cfg_with_too_many_equals() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
memory_fs
.write_file_all(&pyvenv_cfg_path, "home = bar = /.venv/bin")
.unwrap();
let venv_result =
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
assert!(matches!(
venv_result,
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
path,
PyvenvCfgParseErrorKind::TooManyEquals { line_number }
))
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
));
}
#[test]
fn parsing_pyvenv_cfg_with_key_but_no_value_fails() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
memory_fs
.write_file_all(&pyvenv_cfg_path, "home =")
.unwrap();
let venv_result =
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
assert!(matches!(
venv_result,
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
path,
PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number }
))
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
));
}
#[test]
fn parsing_pyvenv_cfg_with_value_but_no_key_fails() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
memory_fs
.write_file_all(&pyvenv_cfg_path, "= whatever")
.unwrap();
let venv_result =
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
assert!(matches!(
venv_result,
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
path,
PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number }
))
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
));
}
#[test]
fn parsing_pyvenv_cfg_with_no_home_key_fails() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
memory_fs.write_file_all(&pyvenv_cfg_path, "").unwrap();
let venv_result =
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
assert!(matches!(
venv_result,
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
path,
PyvenvCfgParseErrorKind::NoHomeKey
))
if path == pyvenv_cfg_path
));
}
#[test]
fn parsing_pyvenv_cfg_with_invalid_home_key_fails() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
memory_fs
.write_file_all(&pyvenv_cfg_path, "home = foo")
.unwrap();
let venv_result =
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
assert!(matches!(
venv_result,
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
path,
PyvenvCfgParseErrorKind::InvalidHomeValue(_)
))
if path == pyvenv_cfg_path
));
}
}

View File

@@ -1,128 +0,0 @@
use std::collections::VecDeque;
use std::ops::{Deref, DerefMut};
use super::Type;
/// Arguments for a single call, in source order.
#[derive(Clone, Debug, Default)]
pub(crate) struct CallArguments<'a>(VecDeque<Argument<'a>>);
impl<'a> CallArguments<'a> {
/// Invoke a function with an optional extra synthetic argument (for a `self` or `cls`
/// parameter) prepended to the front of this argument list. (If `bound_self` is none, the
/// function is invoked with the unmodified argument list.)
pub(crate) fn with_self<F, R>(&mut self, bound_self: Option<Type<'_>>, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
if bound_self.is_some() {
self.0.push_front(Argument::Synthetic);
}
let result = f(self);
if bound_self.is_some() {
self.0.pop_front();
}
result
}
pub(crate) fn len(&self) -> usize {
self.0.len()
}
pub(crate) fn iter(&self) -> impl Iterator<Item = Argument<'a>> + '_ {
self.0.iter().copied()
}
}
impl<'a> FromIterator<Argument<'a>> for CallArguments<'a> {
fn from_iter<T: IntoIterator<Item = Argument<'a>>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum Argument<'a> {
/// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site.
Synthetic,
/// A positional argument.
Positional,
/// A starred positional argument (e.g. `*args`).
Variadic,
/// A keyword argument (e.g. `a=1`).
Keyword(&'a str),
/// The double-starred keywords argument (e.g. `**kwargs`).
Keywords,
}
/// Arguments for a single call, in source order, along with inferred types for each argument.
pub(crate) struct CallArgumentTypes<'a, 'db> {
arguments: CallArguments<'a>,
types: VecDeque<Type<'db>>,
}
impl<'a, 'db> CallArgumentTypes<'a, 'db> {
/// Create a [`CallArgumentTypes`] with no arguments.
pub(crate) fn none() -> Self {
let arguments = CallArguments::default();
let types = VecDeque::default();
Self { arguments, types }
}
/// Create a [`CallArgumentTypes`] from an iterator over non-variadic positional argument
/// types.
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
let types: VecDeque<_> = positional_tys.into_iter().collect();
let arguments = CallArguments(vec![Argument::Positional; types.len()].into());
Self { arguments, types }
}
/// Create a new [`CallArgumentTypes`] to store the inferred types of the arguments in a
/// [`CallArguments`]. Uses the provided callback to infer each argument type.
pub(crate) fn new<F>(arguments: CallArguments<'a>, mut f: F) -> Self
where
F: FnMut(usize, Argument<'a>) -> Type<'db>,
{
let types = arguments
.iter()
.enumerate()
.map(|(idx, argument)| f(idx, argument))
.collect();
Self { arguments, types }
}
/// Invoke a function with an optional extra synthetic argument (for a `self` or `cls`
/// parameter) prepended to the front of this argument list. (If `bound_self` is none, the
/// function is invoked with the unmodified argument list.)
pub(crate) fn with_self<F, R>(&mut self, bound_self: Option<Type<'db>>, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
if let Some(bound_self) = bound_self {
self.arguments.0.push_front(Argument::Synthetic);
self.types.push_front(bound_self);
}
let result = f(self);
if bound_self.is_some() {
self.arguments.0.pop_front();
self.types.pop_front();
}
result
}
pub(crate) fn iter(&self) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + '_ {
self.arguments.iter().zip(self.types.iter().copied())
}
}
impl<'a> Deref for CallArgumentTypes<'a, '_> {
type Target = CallArguments<'a>;
fn deref(&self) -> &CallArguments<'a> {
&self.arguments
}
}
impl<'a> DerefMut for CallArgumentTypes<'a, '_> {
fn deref_mut(&mut self) -> &mut CallArguments<'a> {
&mut self.arguments
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,263 +0,0 @@
use crate::types::{
todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, MroIterator, Type,
};
use crate::Db;
/// Enumeration of the possible kinds of types we allow in class bases.
///
/// This is much more limited than the [`Type`] enum: all types that would be invalid to have as a
/// class base are transformed into [`ClassBase::unknown()`]
///
/// Note that a non-specialized generic class _cannot_ be a class base. When we see a
/// non-specialized generic class in any type expression (including the list of base classes), we
/// automatically construct the default specialization for that class.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
pub enum ClassBase<'db> {
Dynamic(DynamicType),
Class(ClassType<'db>),
/// Although `Protocol` is not a class in typeshed's stubs, it is at runtime,
/// and can appear in the MRO of a class.
Protocol,
/// Bare `Generic` cannot be subclassed directly in user code,
/// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`,
/// `Protocol[T]`, or bare `Protocol`.
Generic,
}
impl<'db> ClassBase<'db> {
pub(crate) const fn any() -> Self {
Self::Dynamic(DynamicType::Any)
}
pub(crate) const fn unknown() -> Self {
Self::Dynamic(DynamicType::Unknown)
}
pub(crate) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
struct Display<'db> {
base: ClassBase<'db>,
db: &'db dyn Db,
}
impl std::fmt::Display for Display<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.base {
ClassBase::Dynamic(dynamic) => dynamic.fmt(f),
ClassBase::Class(class @ ClassType::NonGeneric(_)) => {
write!(f, "<class '{}'>", class.name(self.db))
}
ClassBase::Class(ClassType::Generic(alias)) => {
write!(f, "<class '{}'>", alias.display(self.db))
}
ClassBase::Protocol => f.write_str("typing.Protocol"),
ClassBase::Generic => f.write_str("typing.Generic"),
}
}
}
Display { base: self, db }
}
/// Return a `ClassBase` representing the class `builtins.object`
pub(super) fn object(db: &'db dyn Db) -> Self {
KnownClass::Object
.to_class_literal(db)
.to_class_type(db)
.map_or(Self::unknown(), Self::Class)
}
/// Attempt to resolve `ty` into a `ClassBase`.
///
/// Return `None` if `ty` is not an acceptable type for a class base.
pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
match ty {
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
Type::ClassLiteral(literal) => Some(if literal.is_known(db, KnownClass::Any) {
Self::Dynamic(DynamicType::Any)
} else {
Self::Class(literal.default_specialization(db))
}),
Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))),
Type::Instance(instance) if instance.class().is_known(db, KnownClass::GenericAlias) => {
Self::try_from_type(db, todo_type!("GenericAlias instance"))
}
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
Type::Intersection(_) => None, // TODO -- probably incorrect?
Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?
Type::PropertyInstance(_) => None,
Type::Never
| Type::BooleanLiteral(_)
| Type::FunctionLiteral(_)
| Type::Callable(..)
| Type::BoundMethod(_)
| Type::MethodWrapper(_)
| Type::WrapperDescriptor(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::BytesLiteral(_)
| Type::IntLiteral(_)
| Type::StringLiteral(_)
| Type::LiteralString
| Type::Tuple(_)
| Type::SliceLiteral(_)
| Type::ModuleLiteral(_)
| Type::SubclassOf(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy => None,
Type::KnownInstance(known_instance) => match known_instance {
KnownInstanceType::TypeVar(_)
| KnownInstanceType::TypeAliasType(_)
| KnownInstanceType::Annotated
| KnownInstanceType::Literal
| KnownInstanceType::LiteralString
| KnownInstanceType::Union
| KnownInstanceType::NoReturn
| KnownInstanceType::Never
| KnownInstanceType::Final
| KnownInstanceType::NotRequired
| KnownInstanceType::TypeGuard
| KnownInstanceType::TypeIs
| KnownInstanceType::TypingSelf
| KnownInstanceType::Unpack
| KnownInstanceType::ClassVar
| KnownInstanceType::Concatenate
| KnownInstanceType::Required
| KnownInstanceType::TypeAlias
| KnownInstanceType::ReadOnly
| KnownInstanceType::Optional
| KnownInstanceType::Not
| KnownInstanceType::Intersection
| KnownInstanceType::TypeOf
| KnownInstanceType::CallableTypeOf
| KnownInstanceType::AlwaysTruthy
| KnownInstanceType::AlwaysFalsy => None,
KnownInstanceType::Unknown => Some(Self::unknown()),
KnownInstanceType::Any => Some(Self::any()),
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
KnownInstanceType::Dict => {
Self::try_from_type(db, KnownClass::Dict.to_class_literal(db))
}
KnownInstanceType::List => {
Self::try_from_type(db, KnownClass::List.to_class_literal(db))
}
KnownInstanceType::Type => {
Self::try_from_type(db, KnownClass::Type.to_class_literal(db))
}
KnownInstanceType::Tuple => {
Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db))
}
KnownInstanceType::Set => {
Self::try_from_type(db, KnownClass::Set.to_class_literal(db))
}
KnownInstanceType::FrozenSet => {
Self::try_from_type(db, KnownClass::FrozenSet.to_class_literal(db))
}
KnownInstanceType::ChainMap => {
Self::try_from_type(db, KnownClass::ChainMap.to_class_literal(db))
}
KnownInstanceType::Counter => {
Self::try_from_type(db, KnownClass::Counter.to_class_literal(db))
}
KnownInstanceType::DefaultDict => {
Self::try_from_type(db, KnownClass::DefaultDict.to_class_literal(db))
}
KnownInstanceType::Deque => {
Self::try_from_type(db, KnownClass::Deque.to_class_literal(db))
}
KnownInstanceType::OrderedDict => {
Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db))
}
KnownInstanceType::TypedDict => Self::try_from_type(db, todo_type!("TypedDict")),
KnownInstanceType::Callable => {
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
}
KnownInstanceType::Protocol => Some(ClassBase::Protocol),
KnownInstanceType::Generic => Some(ClassBase::Generic),
},
}
}
pub(super) fn into_class(self) -> Option<ClassType<'db>> {
match self {
Self::Class(class) => Some(class),
Self::Dynamic(_) | Self::Generic | Self::Protocol => None,
}
}
/// Iterate over the MRO of this base
pub(super) fn mro(self, db: &'db dyn Db) -> impl Iterator<Item = ClassBase<'db>> {
match self {
ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic),
ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => ClassBaseMroIterator::length_3(
db,
self,
ClassBase::Dynamic(DynamicType::SubscriptedGeneric),
),
ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self),
ClassBase::Class(class) => ClassBaseMroIterator::from_class(db, class),
}
}
}
impl<'db> From<ClassType<'db>> for ClassBase<'db> {
fn from(value: ClassType<'db>) -> Self {
ClassBase::Class(value)
}
}
impl<'db> From<ClassBase<'db>> for Type<'db> {
fn from(value: ClassBase<'db>) -> Self {
match value {
ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic),
ClassBase::Class(class) => class.into(),
ClassBase::Protocol => Type::KnownInstance(KnownInstanceType::Protocol),
ClassBase::Generic => Type::KnownInstance(KnownInstanceType::Generic),
}
}
}
impl<'db> From<&ClassBase<'db>> for Type<'db> {
fn from(value: &ClassBase<'db>) -> Self {
Self::from(*value)
}
}
/// An iterator over the MRO of a class base.
enum ClassBaseMroIterator<'db> {
Length2(core::array::IntoIter<ClassBase<'db>, 2>),
Length3(core::array::IntoIter<ClassBase<'db>, 3>),
FromClass(MroIterator<'db>),
}
impl<'db> ClassBaseMroIterator<'db> {
/// Iterate over an MRO of length 2 that consists of `first_element` and then `object`.
fn length_2(db: &'db dyn Db, first_element: ClassBase<'db>) -> Self {
ClassBaseMroIterator::Length2([first_element, ClassBase::object(db)].into_iter())
}
/// Iterate over an MRO of length 3 that consists of `first_element`, then `second_element`, then `object`.
fn length_3(db: &'db dyn Db, element_1: ClassBase<'db>, element_2: ClassBase<'db>) -> Self {
ClassBaseMroIterator::Length3([element_1, element_2, ClassBase::object(db)].into_iter())
}
/// Iterate over the MRO of an arbitrary class. The MRO may be of any length.
fn from_class(db: &'db dyn Db, class: ClassType<'db>) -> Self {
ClassBaseMroIterator::FromClass(class.iter_mro(db))
}
}
impl<'db> Iterator for ClassBaseMroIterator<'db> {
type Item = ClassBase<'db>;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Length2(iter) => iter.next(),
Self::Length3(iter) => iter.next(),
Self::FromClass(iter) => iter.next(),
}
}
}
impl std::iter::FusedIterator for ClassBaseMroIterator<'_> {}

View File

@@ -1,408 +0,0 @@
use ruff_python_ast as ast;
use rustc_hash::FxHashMap;
use crate::semantic_index::SemanticIndex;
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::{
declaration_type, KnownInstanceType, Type, TypeVarBoundOrConstraints, TypeVarInstance,
UnionBuilder, UnionType,
};
use crate::Db;
/// A list of formal type variables for a generic function, class, or type alias.
///
/// TODO: Handle nested generic contexts better, with actual parent links to the lexically
/// containing context.
#[salsa::interned(debug)]
pub struct GenericContext<'db> {
#[return_ref]
pub(crate) variables: Box<[TypeVarInstance<'db>]>,
}
impl<'db> GenericContext<'db> {
pub(crate) fn from_type_params(
db: &'db dyn Db,
index: &'db SemanticIndex<'db>,
type_params_node: &ast::TypeParams,
) -> Self {
let variables: Box<[_]> = type_params_node
.iter()
.filter_map(|type_param| Self::variable_from_type_param(db, index, type_param))
.collect();
Self::new(db, variables)
}
fn variable_from_type_param(
db: &'db dyn Db,
index: &'db SemanticIndex<'db>,
type_param_node: &ast::TypeParam,
) -> Option<TypeVarInstance<'db>> {
match type_param_node {
ast::TypeParam::TypeVar(node) => {
let definition = index.expect_single_definition(node);
let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) =
declaration_type(db, definition).inner_type()
else {
panic!("typevar should be inferred as a TypeVarInstance");
};
Some(typevar)
}
// TODO: Support these!
ast::TypeParam::ParamSpec(_) => None,
ast::TypeParam::TypeVarTuple(_) => None,
}
}
pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> {
let parameters = Parameters::new(
self.variables(db)
.iter()
.map(|typevar| Self::parameter_from_typevar(db, *typevar)),
);
Signature::new(parameters, None)
}
fn parameter_from_typevar(db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Parameter<'db> {
let mut parameter = Parameter::positional_only(Some(typevar.name(db).clone()));
match typevar.bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
// TODO: This should be a type form.
parameter = parameter.with_annotated_type(bound);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
// TODO: This should be a new type variant where only these exact types are
// assignable, and not subclasses of them, nor a union of them.
parameter = parameter
.with_annotated_type(UnionType::from_elements(db, constraints.iter(db)));
}
None => {}
}
parameter
}
pub(crate) fn default_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
let types = self
.variables(db)
.iter()
.map(|typevar| typevar.default_ty(db).unwrap_or(Type::unknown()))
.collect();
self.specialize(db, types)
}
pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
let types = self
.variables(db)
.iter()
.map(|typevar| Type::TypeVar(*typevar))
.collect();
self.specialize(db, types)
}
pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
let types = vec![Type::unknown(); self.variables(db).len()];
self.specialize(db, types.into())
}
pub(crate) fn specialize(
self,
db: &'db dyn Db,
types: Box<[Type<'db>]>,
) -> Specialization<'db> {
Specialization::new(db, self, types)
}
}
/// An assignment of a specific type to each type variable in a generic scope.
///
/// TODO: Handle nested specializations better, with actual parent links to the specialization of
/// the lexically containing context.
#[salsa::interned(debug)]
pub struct Specialization<'db> {
pub(crate) generic_context: GenericContext<'db>,
#[return_ref]
pub(crate) types: Box<[Type<'db>]>,
}
impl<'db> Specialization<'db> {
/// Applies a specialization to this specialization. This is used, for instance, when a generic
/// class inherits from a generic alias:
///
/// ```py
/// class A[T]: ...
/// class B[U](A[U]): ...
/// ```
///
/// `B` is a generic class, whose MRO includes the generic alias `A[U]`, which specializes `A`
/// with the specialization `{T: U}`. If `B` is specialized to `B[int]`, with specialization
/// `{U: int}`, we can apply the second specialization to the first, resulting in `T: int`.
/// That lets us produce the generic alias `A[int]`, which is the corresponding entry in the
/// MRO of `B[int]`.
pub(crate) fn apply_specialization(self, db: &'db dyn Db, other: Specialization<'db>) -> Self {
let types: Box<[_]> = self
.types(db)
.into_iter()
.map(|ty| ty.apply_specialization(db, other))
.collect();
Specialization::new(db, self.generic_context(db), types)
}
/// Combines two specializations of the same generic context. If either specialization maps a
/// typevar to `Type::Unknown`, the other specialization's mapping is used. If both map the
/// typevar to a known type, those types are unioned together.
///
/// Panics if the two specializations are not for the same generic context.
pub(crate) fn combine(self, db: &'db dyn Db, other: Self) -> Self {
let generic_context = self.generic_context(db);
assert!(other.generic_context(db) == generic_context);
// TODO special-casing Unknown to mean "no mapping" is not right here, and can give
// confusing/wrong results in cases where there was a mapping found for a typevar, and it
// was of type Unknown. We should probably add a bitset or similar to Specialization that
// explicitly tells us which typevars are mapped.
let types: Box<[_]> = self
.types(db)
.into_iter()
.zip(other.types(db))
.map(|(self_type, other_type)| match (self_type, other_type) {
(unknown, known) | (known, unknown) if unknown.is_unknown() => *known,
_ => UnionType::from_elements(db, [self_type, other_type]),
})
.collect();
Specialization::new(db, self.generic_context(db), types)
}
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
let types: Box<[_]> = self.types(db).iter().map(|ty| ty.normalized(db)).collect();
Self::new(db, self.generic_context(db), types)
}
/// Returns the type that a typevar is specialized to, or None if the typevar isn't part of
/// this specialization.
pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option<Type<'db>> {
self.generic_context(db)
.variables(db)
.into_iter()
.zip(self.types(db))
.find(|(var, _)| **var == typevar)
.map(|(_, ty)| *ty)
}
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) {
return false;
}
for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
.zip(self.types(db))
.zip(other.types(db))
{
if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) {
return false;
}
// TODO: We currently treat all typevars as invariant. Once we track the actual
// variance of each typevar, these checks should change:
// - covariant: verify that self_type <: other_type
// - contravariant: verify that other_type <: self_type
// - invariant: verify that self_type == other_type
// - bivariant: skip, can't make subtyping false
if !self_type.is_equivalent_to(db, *other_type) {
return false;
}
}
true
}
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) {
return false;
}
for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
.zip(self.types(db))
.zip(other.types(db))
{
if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) {
return false;
}
// TODO: We currently treat all typevars as invariant. Once we track the actual
// variance of each typevar, these checks should change:
// - covariant: verify that self_type == other_type
// - contravariant: verify that other_type == self_type
// - invariant: verify that self_type == other_type
// - bivariant: skip, can't make equivalence false
if !self_type.is_equivalent_to(db, *other_type) {
return false;
}
}
true
}
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) {
return false;
}
for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
.zip(self.types(db))
.zip(other.types(db))
{
if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) {
continue;
}
// TODO: We currently treat all typevars as invariant. Once we track the actual
// variance of each typevar, these checks should change:
// - covariant: verify that self_type <: other_type
// - contravariant: verify that other_type <: self_type
// - invariant: verify that self_type == other_type
// - bivariant: skip, can't make assignability false
if !self_type.is_gradual_equivalent_to(db, *other_type) {
return false;
}
}
true
}
pub(crate) fn is_gradual_equivalent_to(
self,
db: &'db dyn Db,
other: Specialization<'db>,
) -> bool {
let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) {
return false;
}
for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
.zip(self.types(db))
.zip(other.types(db))
{
// TODO: We currently treat all typevars as invariant. Once we track the actual
// variance of each typevar, these checks should change:
// - covariant: verify that self_type == other_type
// - contravariant: verify that other_type == self_type
// - invariant: verify that self_type == other_type
// - bivariant: skip, can't make equivalence false
if !self_type.is_gradual_equivalent_to(db, *other_type) {
return false;
}
}
true
}
}
/// Performs type inference between parameter annotations and argument types, producing a
/// specialization of a generic function.
pub(crate) struct SpecializationBuilder<'db> {
db: &'db dyn Db,
types: FxHashMap<TypeVarInstance<'db>, UnionBuilder<'db>>,
}
impl<'db> SpecializationBuilder<'db> {
pub(crate) fn new(db: &'db dyn Db) -> Self {
Self {
db,
types: FxHashMap::default(),
}
}
pub(crate) fn build(&mut self, generic_context: GenericContext<'db>) -> Specialization<'db> {
let types: Box<[_]> = generic_context
.variables(self.db)
.iter()
.map(|variable| {
self.types
.remove(variable)
.map(UnionBuilder::build)
.unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown()))
})
.collect();
Specialization::new(self.db, generic_context, types)
}
fn add_type_mapping(&mut self, typevar: TypeVarInstance<'db>, ty: Type<'db>) {
let builder = self
.types
.entry(typevar)
.or_insert_with(|| UnionBuilder::new(self.db));
builder.add_in_place(ty);
}
pub(crate) fn infer(&mut self, formal: Type<'db>, actual: Type<'db>) {
// If the actual type is already assignable to the formal type, then return without adding
// any new type mappings. (Note that if the formal type contains any typevars, this check
// will fail, since no non-typevar types are assignable to a typevar.)
//
// In particular, this handles a case like
//
// ```py
// def f[T](t: T | None): ...
//
// f(None)
// ```
//
// without specializing `T` to `None`.
if actual.is_assignable_to(self.db, formal) {
return;
}
match (formal, actual) {
(Type::TypeVar(typevar), _) => self.add_type_mapping(typevar, actual),
(Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => {
let formal_elements = formal_tuple.elements(self.db);
let actual_elements = actual_tuple.elements(self.db);
if formal_elements.len() == actual_elements.len() {
for (formal_element, actual_element) in
formal_elements.iter().zip(actual_elements)
{
self.infer(*formal_element, *actual_element);
}
}
}
(Type::Union(formal), _) => {
// TODO: We haven't implemented a full unification solver yet. If typevars appear
// in multiple union elements, we ideally want to express that _only one_ of them
// needs to match, and that we should infer the smallest type mapping that allows
// that.
//
// For now, we punt on handling multiple typevar elements. Instead, if _precisely
// one_ union element _is_ a typevar (not _contains_ a typevar), then we go ahead
// and add a mapping between that typevar and the actual type. (Note that we've
// already handled above the case where the actual is assignable to a _non-typevar_
// union element.)
let mut typevars = formal.iter(self.db).filter_map(|ty| match ty {
Type::TypeVar(typevar) => Some(*typevar),
_ => None,
});
let typevar = typevars.next();
let additional_typevars = typevars.next();
if let (Some(typevar), None) = (typevar, additional_typevars) {
self.add_type_mapping(typevar, actual);
}
}
(Type::Intersection(formal), _) => {
// The actual type must be assignable to every (positive) element of the
// formal intersection, so we must infer type mappings for each of them. (The
// actual type must also be disjoint from every negative element of the
// intersection, but that doesn't help us infer any type mappings.)
for positive in formal.iter_positive(self.db) {
self.infer(positive, actual);
}
}
// TODO: Add more forms that we can structurally induct into: type[C], callables
_ => {}
}
}
}

View File

@@ -1,94 +0,0 @@
//! Instance types: both nominal and structural.
use super::{ClassType, KnownClass, SubclassOfType, Type};
use crate::Db;
impl<'db> Type<'db> {
pub(crate) const fn instance(class: ClassType<'db>) -> Self {
Self::Instance(InstanceType { class })
}
pub(crate) const fn into_instance(self) -> Option<InstanceType<'db>> {
match self {
Type::Instance(instance_type) => Some(instance_type),
_ => None,
}
}
}
/// A type representing the set of runtime objects which are instances of a certain nominal class.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)]
pub struct InstanceType<'db> {
// Keep this field private, so that the only way of constructing `InstanceType` instances
// is through the `Type::instance` constructor function.
class: ClassType<'db>,
}
impl<'db> InstanceType<'db> {
pub(super) fn class(self) -> ClassType<'db> {
self.class
}
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
// N.B. The subclass relation is fully static
self.class.is_subclass_of(db, other.class)
}
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.class.is_equivalent_to(db, other.class)
}
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
self.class.is_assignable_to(db, other.class)
}
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
if self.class.is_final(db) && !self.class.is_subclass_of(db, other.class) {
return true;
}
if other.class.is_final(db) && !other.class.is_subclass_of(db, self.class) {
return true;
}
// Check to see whether the metaclasses of `self` and `other` are disjoint.
// Avoid this check if the metaclass of either `self` or `other` is `type`,
// however, since we end up with infinite recursion in that case due to the fact
// that `type` is its own metaclass (and we know that `type` cannot be disjoint
// from any metaclass, anyway).
let type_type = KnownClass::Type.to_instance(db);
let self_metaclass = self.class.metaclass_instance_type(db);
if self_metaclass == type_type {
return false;
}
let other_metaclass = other.class.metaclass_instance_type(db);
if other_metaclass == type_type {
return false;
}
self_metaclass.is_disjoint_from(db, other_metaclass)
}
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.class.is_gradual_equivalent_to(db, other.class)
}
pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool {
self.class.known(db).is_some_and(KnownClass::is_singleton)
}
pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool {
self.class
.known(db)
.is_some_and(KnownClass::is_single_valued)
}
pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> {
SubclassOfType::from(db, self.class)
}
}
impl<'db> From<InstanceType<'db>> for Type<'db> {
fn from(value: InstanceType<'db>) -> Self {
Self::Instance(value)
}
}

View File

@@ -1,378 +0,0 @@
//! The `KnownInstance` type.
//!
//! Despite its name, this is quite a different type from [`super::InstanceType`].
//! For the vast majority of instance-types in Python, we cannot say how many possible
//! inhabitants there are or could be of that type at runtime. Each variant of the
//! [`KnownInstanceType`] enum, however, represents a specific runtime symbol
//! that requires heavy special-casing in the type system. Thus any one `KnownInstance`
//! variant can only be inhabited by one or two specific objects at runtime with
//! locations that are known in advance.
use super::{class::KnownClass, ClassType, Truthiness, Type, TypeAliasType, TypeVarInstance};
use crate::db::Db;
use crate::module_resolver::{file_to_module, KnownModule};
use ruff_db::files::File;
/// Enumeration of specific runtime symbols that are special enough
/// that they can each be considered to inhabit a unique type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub enum KnownInstanceType<'db> {
/// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`)
Annotated,
/// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`)
Literal,
/// The symbol `typing.LiteralString` (which can also be found as `typing_extensions.LiteralString`)
LiteralString,
/// The symbol `typing.Optional` (which can also be found as `typing_extensions.Optional`)
Optional,
/// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`)
Union,
/// The symbol `typing.NoReturn` (which can also be found as `typing_extensions.NoReturn`)
NoReturn,
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
Never,
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
/// This is not used since typeshed switched to representing `Any` as a class; now we use
/// `KnownClass::Any` instead. But we still support the old `Any = object()` representation, at
/// least for now. TODO maybe remove?
Any,
/// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`)
Tuple,
/// The symbol `typing.List` (which can also be found as `typing_extensions.List`)
List,
/// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`)
Dict,
/// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`)
Set,
/// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`)
FrozenSet,
/// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`)
ChainMap,
/// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`)
Counter,
/// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`)
DefaultDict,
/// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`)
Deque,
/// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`)
OrderedDict,
/// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`)
Protocol,
/// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`)
Generic,
/// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`)
Type,
/// A single instance of `typing.TypeVar`
TypeVar(TypeVarInstance<'db>),
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
TypeAliasType(TypeAliasType<'db>),
/// The symbol `knot_extensions.Unknown`
Unknown,
/// The symbol `knot_extensions.AlwaysTruthy`
AlwaysTruthy,
/// The symbol `knot_extensions.AlwaysFalsy`
AlwaysFalsy,
/// The symbol `knot_extensions.Not`
Not,
/// The symbol `knot_extensions.Intersection`
Intersection,
/// The symbol `knot_extensions.TypeOf`
TypeOf,
/// The symbol `knot_extensions.CallableTypeOf`
CallableTypeOf,
/// The symbol `typing.Callable`
/// (which can also be found as `typing_extensions.Callable` or as `collections.abc.Callable`)
Callable,
// Various special forms, special aliases and type qualifiers that we don't yet understand
// (all currently inferred as TODO in most contexts):
TypingSelf,
Final,
ClassVar,
Concatenate,
Unpack,
Required,
NotRequired,
TypeAlias,
TypeGuard,
TypedDict,
TypeIs,
ReadOnly,
// TODO: fill this enum out with more special forms, etc.
}
impl<'db> KnownInstanceType<'db> {
/// Evaluate the known instance in boolean context
pub(crate) const fn bool(self) -> Truthiness {
match self {
Self::Annotated
| Self::Literal
| Self::LiteralString
| Self::Optional
| Self::TypeVar(_)
| Self::Union
| Self::NoReturn
| Self::Never
| Self::Any
| Self::Tuple
| Self::Type
| Self::TypingSelf
| Self::Final
| Self::ClassVar
| Self::Callable
| Self::Concatenate
| Self::Unpack
| Self::Required
| Self::NotRequired
| Self::TypeAlias
| Self::TypeGuard
| Self::TypedDict
| Self::TypeIs
| Self::List
| Self::Dict
| Self::DefaultDict
| Self::Set
| Self::FrozenSet
| Self::Counter
| Self::Deque
| Self::ChainMap
| Self::OrderedDict
| Self::Protocol
| Self::Generic
| Self::ReadOnly
| Self::TypeAliasType(_)
| Self::Unknown
| Self::AlwaysTruthy
| Self::AlwaysFalsy
| Self::Not
| Self::Intersection
| Self::TypeOf
| Self::CallableTypeOf => Truthiness::AlwaysTrue,
}
}
/// Return the repr of the symbol at runtime
pub(crate) fn repr(self, db: &'db dyn Db) -> &'db str {
match self {
Self::Annotated => "typing.Annotated",
Self::Literal => "typing.Literal",
Self::LiteralString => "typing.LiteralString",
Self::Optional => "typing.Optional",
Self::Union => "typing.Union",
Self::NoReturn => "typing.NoReturn",
Self::Never => "typing.Never",
Self::Any => "typing.Any",
Self::Tuple => "typing.Tuple",
Self::Type => "typing.Type",
Self::TypingSelf => "typing.Self",
Self::Final => "typing.Final",
Self::ClassVar => "typing.ClassVar",
Self::Callable => "typing.Callable",
Self::Concatenate => "typing.Concatenate",
Self::Unpack => "typing.Unpack",
Self::Required => "typing.Required",
Self::NotRequired => "typing.NotRequired",
Self::TypeAlias => "typing.TypeAlias",
Self::TypeGuard => "typing.TypeGuard",
Self::TypedDict => "typing.TypedDict",
Self::TypeIs => "typing.TypeIs",
Self::List => "typing.List",
Self::Dict => "typing.Dict",
Self::DefaultDict => "typing.DefaultDict",
Self::Set => "typing.Set",
Self::FrozenSet => "typing.FrozenSet",
Self::Counter => "typing.Counter",
Self::Deque => "typing.Deque",
Self::ChainMap => "typing.ChainMap",
Self::OrderedDict => "typing.OrderedDict",
Self::Protocol => "typing.Protocol",
Self::Generic => "typing.Generic",
Self::ReadOnly => "typing.ReadOnly",
Self::TypeVar(typevar) => typevar.name(db),
Self::TypeAliasType(_) => "typing.TypeAliasType",
Self::Unknown => "knot_extensions.Unknown",
Self::AlwaysTruthy => "knot_extensions.AlwaysTruthy",
Self::AlwaysFalsy => "knot_extensions.AlwaysFalsy",
Self::Not => "knot_extensions.Not",
Self::Intersection => "knot_extensions.Intersection",
Self::TypeOf => "knot_extensions.TypeOf",
Self::CallableTypeOf => "knot_extensions.CallableTypeOf",
}
}
/// Return the [`KnownClass`] which this symbol is an instance of
pub(crate) const fn class(self) -> KnownClass {
match self {
Self::Annotated => KnownClass::SpecialForm,
Self::Literal => KnownClass::SpecialForm,
Self::LiteralString => KnownClass::SpecialForm,
Self::Optional => KnownClass::SpecialForm,
Self::Union => KnownClass::SpecialForm,
Self::NoReturn => KnownClass::SpecialForm,
Self::Never => KnownClass::SpecialForm,
Self::Any => KnownClass::Object,
Self::Tuple => KnownClass::SpecialForm,
Self::Type => KnownClass::SpecialForm,
Self::TypingSelf => KnownClass::SpecialForm,
Self::Final => KnownClass::SpecialForm,
Self::ClassVar => KnownClass::SpecialForm,
Self::Callable => KnownClass::SpecialForm,
Self::Concatenate => KnownClass::SpecialForm,
Self::Unpack => KnownClass::SpecialForm,
Self::Required => KnownClass::SpecialForm,
Self::NotRequired => KnownClass::SpecialForm,
Self::TypeAlias => KnownClass::SpecialForm,
Self::TypeGuard => KnownClass::SpecialForm,
Self::TypedDict => KnownClass::SpecialForm,
Self::TypeIs => KnownClass::SpecialForm,
Self::ReadOnly => KnownClass::SpecialForm,
Self::List => KnownClass::StdlibAlias,
Self::Dict => KnownClass::StdlibAlias,
Self::DefaultDict => KnownClass::StdlibAlias,
Self::Set => KnownClass::StdlibAlias,
Self::FrozenSet => KnownClass::StdlibAlias,
Self::Counter => KnownClass::StdlibAlias,
Self::Deque => KnownClass::StdlibAlias,
Self::ChainMap => KnownClass::StdlibAlias,
Self::OrderedDict => KnownClass::StdlibAlias,
Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says
Self::Generic => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
Self::TypeOf => KnownClass::SpecialForm,
Self::Not => KnownClass::SpecialForm,
Self::Intersection => KnownClass::SpecialForm,
Self::CallableTypeOf => KnownClass::SpecialForm,
Self::Unknown => KnownClass::Object,
Self::AlwaysTruthy => KnownClass::Object,
Self::AlwaysFalsy => KnownClass::Object,
}
}
/// Return the instance type which this type is a subtype of.
///
/// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`,
/// so `KnownInstanceType::Literal.instance_fallback(db)`
/// returns `Type::Instance(InstanceType { class: <typing._SpecialForm> })`.
pub(super) fn instance_fallback(self, db: &dyn Db) -> Type {
self.class().to_instance(db)
}
/// Return `true` if this symbol is an instance of `class`.
pub(super) fn is_instance_of(self, db: &'db dyn Db, class: ClassType<'db>) -> bool {
self.class().is_subclass_of(db, class)
}
pub(super) fn try_from_file_and_name(
db: &'db dyn Db,
file: File,
symbol_name: &str,
) -> Option<Self> {
let candidate = match symbol_name {
"Any" => Self::Any,
"ClassVar" => Self::ClassVar,
"Deque" => Self::Deque,
"List" => Self::List,
"Dict" => Self::Dict,
"DefaultDict" => Self::DefaultDict,
"Set" => Self::Set,
"FrozenSet" => Self::FrozenSet,
"Counter" => Self::Counter,
"ChainMap" => Self::ChainMap,
"OrderedDict" => Self::OrderedDict,
"Generic" => Self::Generic,
"Protocol" => Self::Protocol,
"Optional" => Self::Optional,
"Union" => Self::Union,
"NoReturn" => Self::NoReturn,
"Tuple" => Self::Tuple,
"Type" => Self::Type,
"Callable" => Self::Callable,
"Annotated" => Self::Annotated,
"Literal" => Self::Literal,
"Never" => Self::Never,
"Self" => Self::TypingSelf,
"Final" => Self::Final,
"Unpack" => Self::Unpack,
"Required" => Self::Required,
"TypeAlias" => Self::TypeAlias,
"TypeGuard" => Self::TypeGuard,
"TypedDict" => Self::TypedDict,
"TypeIs" => Self::TypeIs,
"ReadOnly" => Self::ReadOnly,
"Concatenate" => Self::Concatenate,
"NotRequired" => Self::NotRequired,
"LiteralString" => Self::LiteralString,
"Unknown" => Self::Unknown,
"AlwaysTruthy" => Self::AlwaysTruthy,
"AlwaysFalsy" => Self::AlwaysFalsy,
"Not" => Self::Not,
"Intersection" => Self::Intersection,
"TypeOf" => Self::TypeOf,
"CallableTypeOf" => Self::CallableTypeOf,
_ => return None,
};
candidate
.check_module(file_to_module(db, file)?.known()?)
.then_some(candidate)
}
/// Return `true` if `module` is a module from which this `KnownInstance` variant can validly originate.
///
/// Most variants can only exist in one module, which is the same as `self.class().canonical_module()`.
/// Some variants could validly be defined in either `typing` or `typing_extensions`, however.
pub(super) fn check_module(self, module: KnownModule) -> bool {
match self {
Self::Any
| Self::ClassVar
| Self::Deque
| Self::List
| Self::Dict
| Self::DefaultDict
| Self::Set
| Self::FrozenSet
| Self::Counter
| Self::ChainMap
| Self::OrderedDict
| Self::Optional
| Self::Union
| Self::NoReturn
| Self::Tuple
| Self::Type
| Self::Generic
| Self::Callable => module.is_typing(),
Self::Annotated
| Self::Protocol
| Self::Literal
| Self::LiteralString
| Self::Never
| Self::TypingSelf
| Self::Final
| Self::Concatenate
| Self::Unpack
| Self::Required
| Self::NotRequired
| Self::TypeAlias
| Self::TypeGuard
| Self::TypedDict
| Self::TypeIs
| Self::ReadOnly
| Self::TypeAliasType(_)
| Self::TypeVar(_) => {
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
}
Self::Unknown
| Self::AlwaysTruthy
| Self::AlwaysFalsy
| Self::Not
| Self::Intersection
| Self::TypeOf
| Self::CallableTypeOf => module.is_knot_extensions(),
}
}
pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> {
self.class().to_class_literal(db)
}
}

View File

@@ -1 +0,0 @@
pub(crate) mod subscript;

View File

@@ -1,40 +0,0 @@
use crate::server::Server;
use anyhow::Context;
pub use document::{DocumentKey, NotebookDocument, PositionEncoding, TextDocument};
pub use session::{ClientSettings, DocumentQuery, DocumentSnapshot, Session};
use std::num::NonZeroUsize;
#[macro_use]
mod message;
mod document;
mod logging;
mod server;
mod session;
mod system;
pub(crate) const SERVER_NAME: &str = "red-knot";
pub(crate) const DIAGNOSTIC_NAME: &str = "Red Knot";
/// A common result type used in most cases where a
/// result type is needed.
pub(crate) type Result<T> = anyhow::Result<T>;
pub(crate) fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub fn run_server() -> anyhow::Result<()> {
let four = NonZeroUsize::new(4).unwrap();
// by default, we set the number of worker threads to `num_cpus`, with a maximum of 4.
let worker_threads = std::thread::available_parallelism()
.unwrap_or(four)
.min(four);
Server::new(worker_threads)
.context("Failed to start server")?
.run()?;
Ok(())
}

View File

@@ -1,46 +0,0 @@
use anyhow::Context;
use lsp_types::notification::Notification;
use std::sync::OnceLock;
use crate::server::ClientSender;
static MESSENGER: OnceLock<ClientSender> = OnceLock::new();
pub(crate) fn init_messenger(client_sender: ClientSender) {
MESSENGER
.set(client_sender)
.expect("messenger should only be initialized once");
}
pub(crate) fn show_message(message: String, message_type: lsp_types::MessageType) {
try_show_message(message, message_type).unwrap();
}
pub(super) fn try_show_message(
message: String,
message_type: lsp_types::MessageType,
) -> crate::Result<()> {
MESSENGER
.get()
.ok_or_else(|| anyhow::anyhow!("messenger not initialized"))?
.send(lsp_server::Message::Notification(
lsp_server::Notification {
method: lsp_types::notification::ShowMessage::METHOD.into(),
params: serde_json::to_value(lsp_types::ShowMessageParams {
typ: message_type,
message,
})?,
},
))
.context("Failed to send message")?;
Ok(())
}
/// Sends an error to the client with a formatted message. The error is sent in a
/// `window/showMessage` notification.
macro_rules! show_err_msg {
($msg:expr$(, $($arg:tt)*)?) => {
crate::message::show_message(::core::format_args!($msg$(, $($arg)*)?).to_string(), lsp_types::MessageType::ERROR)
};
}

View File

@@ -1,233 +0,0 @@
//! Scheduling, I/O, and API endpoints.
use std::num::NonZeroUsize;
// The new PanicInfoHook name requires MSRV >= 1.82
#[allow(deprecated)]
use std::panic::PanicInfo;
use lsp_server::Message;
use lsp_types::{
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability,
InlayHintOptions, InlayHintServerCapabilities, MessageType, ServerCapabilities,
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
TypeDefinitionProviderCapability, Url,
};
use self::connection::{Connection, ConnectionInitializer};
use self::schedule::event_loop_thread;
use crate::session::{AllSettings, ClientSettings, Session};
use crate::PositionEncoding;
mod api;
mod client;
mod connection;
mod schedule;
use crate::message::try_show_message;
pub(crate) use connection::ClientSender;
pub(crate) type Result<T> = std::result::Result<T, api::Error>;
pub(crate) struct Server {
connection: Connection,
client_capabilities: ClientCapabilities,
worker_threads: NonZeroUsize,
session: Session,
}
impl Server {
pub(crate) fn new(worker_threads: NonZeroUsize) -> crate::Result<Self> {
let connection = ConnectionInitializer::stdio();
let (id, init_params) = connection.initialize_start()?;
let client_capabilities = init_params.capabilities;
let position_encoding = Self::find_best_position_encoding(&client_capabilities);
let server_capabilities = Self::server_capabilities(position_encoding);
let connection = connection.initialize_finish(
id,
&server_capabilities,
crate::SERVER_NAME,
crate::version(),
)?;
crate::message::init_messenger(connection.make_sender());
let AllSettings {
global_settings,
mut workspace_settings,
} = AllSettings::from_value(
init_params
.initialization_options
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())),
);
crate::logging::init_logging(
global_settings.tracing.log_level.unwrap_or_default(),
global_settings.tracing.log_file.as_deref(),
);
let mut workspace_for_url = |url: Url| {
let Some(workspace_settings) = workspace_settings.as_mut() else {
return (url, ClientSettings::default());
};
let settings = workspace_settings.remove(&url).unwrap_or_else(|| {
tracing::warn!("No workspace settings found for {}", url);
ClientSettings::default()
});
(url, settings)
};
let workspaces = init_params
.workspace_folders
.filter(|folders| !folders.is_empty())
.map(|folders| folders.into_iter().map(|folder| {
workspace_for_url(folder.uri)
}).collect())
.or_else(|| {
tracing::warn!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace...");
let uri = Url::from_file_path(std::env::current_dir().ok()?).ok()?;
Some(vec![workspace_for_url(uri)])
})
.ok_or_else(|| {
anyhow::anyhow!("Failed to get the current working directory while creating a default workspace.")
})?;
if workspaces.len() > 1 {
// TODO(dhruvmanila): Support multi-root workspaces
anyhow::bail!("Multi-root workspaces are not supported yet");
}
Ok(Self {
connection,
worker_threads,
session: Session::new(
&client_capabilities,
position_encoding,
global_settings,
&workspaces,
)?,
client_capabilities,
})
}
pub(crate) fn run(self) -> crate::Result<()> {
// The new PanicInfoHook name requires MSRV >= 1.82
#[allow(deprecated)]
type PanicHook = Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send>;
struct RestorePanicHook {
hook: Option<PanicHook>,
}
impl Drop for RestorePanicHook {
fn drop(&mut self) {
if let Some(hook) = self.hook.take() {
std::panic::set_hook(hook);
}
}
}
// unregister any previously registered panic hook
// The hook will be restored when this function exits.
let _ = RestorePanicHook {
hook: Some(std::panic::take_hook()),
};
// When we panic, try to notify the client.
std::panic::set_hook(Box::new(move |panic_info| {
use std::io::Write;
let backtrace = std::backtrace::Backtrace::force_capture();
tracing::error!("{panic_info}\n{backtrace}");
// we also need to print to stderr directly for when using `$logTrace` because
// the message won't be sent to the client.
// But don't use `eprintln` because `eprintln` itself may panic if the pipe is broken.
let mut stderr = std::io::stderr().lock();
writeln!(stderr, "{panic_info}\n{backtrace}").ok();
try_show_message(
"The Ruff language server exited with a panic. See the logs for more details."
.to_string(),
MessageType::ERROR,
)
.ok();
}));
event_loop_thread(move || {
Self::event_loop(
&self.connection,
&self.client_capabilities,
self.session,
self.worker_threads,
)?;
self.connection.close()?;
Ok(())
})?
.join()
}
#[allow(clippy::needless_pass_by_value)] // this is because we aren't using `next_request_id` yet.
fn event_loop(
connection: &Connection,
_client_capabilities: &ClientCapabilities,
mut session: Session,
worker_threads: NonZeroUsize,
) -> crate::Result<()> {
let mut scheduler =
schedule::Scheduler::new(&mut session, worker_threads, connection.make_sender());
for msg in connection.incoming() {
if connection.handle_shutdown(&msg)? {
break;
}
let task = match msg {
Message::Request(req) => api::request(req),
Message::Notification(notification) => api::notification(notification),
Message::Response(response) => scheduler.response(response),
};
scheduler.dispatch(task);
}
Ok(())
}
fn find_best_position_encoding(client_capabilities: &ClientCapabilities) -> PositionEncoding {
client_capabilities
.general
.as_ref()
.and_then(|general_capabilities| general_capabilities.position_encodings.as_ref())
.and_then(|encodings| {
encodings
.iter()
.filter_map(|encoding| PositionEncoding::try_from(encoding).ok())
.max() // this selects the highest priority position encoding
})
.unwrap_or_default()
}
fn server_capabilities(position_encoding: PositionEncoding) -> ServerCapabilities {
ServerCapabilities {
position_encoding: Some(position_encoding.into()),
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
identifier: Some(crate::DIAGNOSTIC_NAME.into()),
inter_file_dependencies: true,
..Default::default()
})),
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
open_close: Some(true),
change: Some(TextDocumentSyncKind::INCREMENTAL),
..Default::default()
},
)),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
inlay_hint_provider: Some(lsp_types::OneOf::Right(
InlayHintServerCapabilities::Options(InlayHintOptions::default()),
)),
..Default::default()
}
}
}

View File

@@ -1,269 +0,0 @@
use crate::server::schedule::Task;
use crate::session::Session;
use crate::system::{url_to_any_system_path, AnySystemPath};
use lsp_server as server;
use lsp_types::notification::Notification;
mod diagnostics;
mod notifications;
mod requests;
mod traits;
use notifications as notification;
use requests as request;
use self::traits::{NotificationHandler, RequestHandler};
use super::{client::Responder, schedule::BackgroundSchedule, Result};
pub(super) fn request<'a>(req: server::Request) -> Task<'a> {
let id = req.id.clone();
match req.method.as_str() {
request::DocumentDiagnosticRequestHandler::METHOD => background_request_task::<
request::DocumentDiagnosticRequestHandler,
>(
req, BackgroundSchedule::Worker
),
request::GotoTypeDefinitionRequestHandler::METHOD => background_request_task::<
request::GotoTypeDefinitionRequestHandler,
>(
req, BackgroundSchedule::Worker
),
request::HoverRequestHandler::METHOD => {
background_request_task::<request::HoverRequestHandler>(req, BackgroundSchedule::Worker)
}
request::InlayHintRequestHandler::METHOD => background_request_task::<
request::InlayHintRequestHandler,
>(req, BackgroundSchedule::Worker),
method => {
tracing::warn!("Received request {method} which does not have a handler");
return Task::nothing();
}
}
.unwrap_or_else(|err| {
tracing::error!("Encountered error when routing request with ID {id}: {err}");
show_err_msg!(
"Ruff failed to handle a request from the editor. Check the logs for more details."
);
let result: Result<()> = Err(err);
Task::immediate(id, result)
})
}
pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
match notif.method.as_str() {
notification::DidCloseTextDocumentHandler::METHOD => local_notification_task::<notification::DidCloseTextDocumentHandler>(notif),
notification::DidOpenTextDocumentHandler::METHOD => local_notification_task::<notification::DidOpenTextDocumentHandler>(notif),
notification::DidChangeTextDocumentHandler::METHOD => local_notification_task::<notification::DidChangeTextDocumentHandler>(notif),
notification::DidOpenNotebookHandler::METHOD => {
local_notification_task::<notification::DidOpenNotebookHandler>(notif)
}
notification::DidCloseNotebookHandler::METHOD => {
local_notification_task::<notification::DidCloseNotebookHandler>(notif)
}
lsp_types::notification::SetTrace::METHOD => {
tracing::trace!("Ignoring `setTrace` notification");
return Task::nothing();
},
method => {
tracing::warn!("Received notification {method} which does not have a handler.");
return Task::nothing();
}
}
.unwrap_or_else(|err| {
tracing::error!("Encountered error when routing notification: {err}");
show_err_msg!("Ruff failed to handle a notification from the editor. Check the logs for more details.");
Task::nothing()
})
}
fn _local_request_task<'a, R: traits::SyncRequestHandler>(
req: server::Request,
) -> super::Result<Task<'a>> {
let (id, params) = cast_request::<R>(req)?;
Ok(Task::local(|session, notifier, requester, responder| {
let _span = tracing::trace_span!("request", %id, method = R::METHOD).entered();
let result = R::run(session, notifier, requester, params);
respond::<R>(id, result, &responder);
}))
}
// TODO(micha): Calls to `db` could panic if the db gets mutated while this task is running.
// We should either wrap `R::run_with_snapshot` with a salsa catch cancellation handler or
// use `SemanticModel` instead of passing `db` which uses a Result for all it's methods
// that propagate cancellations.
fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>(
req: server::Request,
schedule: BackgroundSchedule,
) -> super::Result<Task<'a>> {
let (id, params) = cast_request::<R>(req)?;
Ok(Task::background(schedule, move |session: &Session| {
let url = R::document_url(&params).into_owned();
let Ok(path) = url_to_any_system_path(&url) else {
return Box::new(|_, _| {});
};
let db = match path {
AnySystemPath::System(path) => match session.project_db_for_path(path.as_std_path()) {
Some(db) => db.clone(),
None => session.default_project_db().clone(),
},
AnySystemPath::SystemVirtual(_) => session.default_project_db().clone(),
};
let Some(snapshot) = session.take_snapshot(url) else {
return Box::new(|_, _| {});
};
Box::new(move |notifier, responder| {
let _span = tracing::trace_span!("request", %id, method = R::METHOD).entered();
let result = R::run_with_snapshot(snapshot, db, notifier, params);
respond::<R>(id, result, &responder);
})
}))
}
fn local_notification_task<'a, N: traits::SyncNotificationHandler>(
notif: server::Notification,
) -> super::Result<Task<'a>> {
let (id, params) = cast_notification::<N>(notif)?;
Ok(Task::local(move |session, notifier, requester, _| {
let _span = tracing::trace_span!("notification", method = N::METHOD).entered();
if let Err(err) = N::run(session, notifier, requester, params) {
tracing::error!("An error occurred while running {id}: {err}");
show_err_msg!("Ruff encountered a problem. Check the logs for more details.");
}
}))
}
#[allow(dead_code)]
fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationHandler>(
req: server::Notification,
schedule: BackgroundSchedule,
) -> super::Result<Task<'a>> {
let (id, params) = cast_notification::<N>(req)?;
Ok(Task::background(schedule, move |session: &Session| {
// TODO(jane): we should log an error if we can't take a snapshot.
let Some(snapshot) = session.take_snapshot(N::document_url(&params).into_owned()) else {
return Box::new(|_, _| {});
};
Box::new(move |notifier, _| {
let _span = tracing::trace_span!("notification", method = N::METHOD).entered();
if let Err(err) = N::run_with_snapshot(snapshot, notifier, params) {
tracing::error!("An error occurred while running {id}: {err}");
show_err_msg!("Ruff encountered a problem. Check the logs for more details.");
}
})
}))
}
/// Tries to cast a serialized request from the server into
/// a parameter type for a specific request handler.
/// It is *highly* recommended to not override this function in your
/// implementation.
fn cast_request<Req>(
request: server::Request,
) -> super::Result<(
server::RequestId,
<<Req as RequestHandler>::RequestType as lsp_types::request::Request>::Params,
)>
where
Req: traits::RequestHandler,
{
request
.extract(Req::METHOD)
.map_err(|err| match err {
json_err @ server::ExtractError::JsonError { .. } => {
anyhow::anyhow!("JSON parsing failure:\n{json_err}")
}
server::ExtractError::MethodMismatch(_) => {
unreachable!("A method mismatch should not be possible here unless you've used a different handler (`Req`) \
than the one whose method name was matched against earlier.")
}
})
.with_failure_code(server::ErrorCode::InternalError)
}
/// Sends back a response to the server using a [`Responder`].
fn respond<Req>(
id: server::RequestId,
result: crate::server::Result<
<<Req as traits::RequestHandler>::RequestType as lsp_types::request::Request>::Result,
>,
responder: &Responder,
) where
Req: traits::RequestHandler,
{
if let Err(err) = &result {
tracing::error!("An error occurred with request ID {id}: {err}");
show_err_msg!("Ruff encountered a problem. Check the logs for more details.");
}
if let Err(err) = responder.respond(id, result) {
tracing::error!("Failed to send response: {err}");
}
}
/// Tries to cast a serialized request from the server into
/// a parameter type for a specific request handler.
fn cast_notification<N>(
notification: server::Notification,
) -> super::Result<
(
&'static str,
<<N as traits::NotificationHandler>::NotificationType as lsp_types::notification::Notification>::Params,
)> where N: traits::NotificationHandler{
Ok((
N::METHOD,
notification
.extract(N::METHOD)
.map_err(|err| match err {
json_err @ server::ExtractError::JsonError { .. } => {
anyhow::anyhow!("JSON parsing failure:\n{json_err}")
}
server::ExtractError::MethodMismatch(_) => {
unreachable!("A method mismatch should not be possible here unless you've used a different handler (`N`) \
than the one whose method name was matched against earlier.")
}
})
.with_failure_code(server::ErrorCode::InternalError)?,
))
}
pub(crate) struct Error {
pub(crate) code: server::ErrorCode,
pub(crate) error: anyhow::Error,
}
/// A trait to convert result types into the server result type, [`super::Result`].
trait LSPResult<T> {
fn with_failure_code(self, code: server::ErrorCode) -> super::Result<T>;
}
impl<T, E: Into<anyhow::Error>> LSPResult<T> for core::result::Result<T, E> {
fn with_failure_code(self, code: server::ErrorCode) -> super::Result<T> {
self.map_err(|err| Error::new(err.into(), code))
}
}
impl Error {
pub(crate) fn new(err: anyhow::Error, code: server::ErrorCode) -> Self {
Self { code, error: err }
}
}
// Right now, we treat the error code as invisible data that won't
// be printed.
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.error.fmt(f)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.error.fmt(f)
}
}

View File

@@ -1,18 +0,0 @@
use lsp_server::ErrorCode;
use lsp_types::{notification::PublishDiagnostics, PublishDiagnosticsParams, Url};
use crate::server::client::Notifier;
use crate::server::Result;
use super::LSPResult;
pub(super) fn clear_diagnostics(uri: &Url, notifier: &Notifier) -> Result<()> {
notifier
.notify::<PublishDiagnostics>(PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![],
version: None,
})
.with_failure_code(ErrorCode::InternalError)?;
Ok(())
}

View File

@@ -1,106 +0,0 @@
use std::borrow::Cow;
use lsp_types::request::DocumentDiagnosticRequest;
use lsp_types::{
Diagnostic, DiagnosticSeverity, DocumentDiagnosticParams, DocumentDiagnosticReport,
DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, NumberOrString, Range,
RelatedFullDocumentDiagnosticReport, Url,
};
use crate::document::ToRangeExt;
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::server::{client::Notifier, Result};
use crate::session::DocumentSnapshot;
use red_knot_project::{Db, ProjectDatabase};
use ruff_db::diagnostic::Severity;
use ruff_db::source::{line_index, source_text};
pub(crate) struct DocumentDiagnosticRequestHandler;
impl RequestHandler for DocumentDiagnosticRequestHandler {
type RequestType = DocumentDiagnosticRequest;
}
impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
fn document_url(params: &DocumentDiagnosticParams) -> Cow<Url> {
Cow::Borrowed(&params.text_document.uri)
}
fn run_with_snapshot(
snapshot: DocumentSnapshot,
db: ProjectDatabase,
_notifier: Notifier,
_params: DocumentDiagnosticParams,
) -> Result<DocumentDiagnosticReportResult> {
let diagnostics = compute_diagnostics(&snapshot, &db);
Ok(DocumentDiagnosticReportResult::Report(
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: None,
items: diagnostics,
},
}),
))
}
}
fn compute_diagnostics(snapshot: &DocumentSnapshot, db: &ProjectDatabase) -> Vec<Diagnostic> {
let Some(file) = snapshot.file(db) else {
tracing::info!(
"No file found for snapshot for `{}`",
snapshot.query().file_url()
);
return vec![];
};
let diagnostics = match db.check_file(file) {
Ok(diagnostics) => diagnostics,
Err(cancelled) => {
tracing::info!("Diagnostics computation {cancelled}");
return vec![];
}
};
diagnostics
.as_slice()
.iter()
.map(|message| to_lsp_diagnostic(db, message, snapshot.encoding()))
.collect()
}
fn to_lsp_diagnostic(
db: &dyn Db,
diagnostic: &ruff_db::diagnostic::Diagnostic,
encoding: crate::PositionEncoding,
) -> Diagnostic {
let range = if let Some(span) = diagnostic.primary_span() {
let index = line_index(db.upcast(), span.file());
let source = source_text(db.upcast(), span.file());
span.range()
.map(|range| range.to_lsp_range(&source, &index, encoding))
.unwrap_or_default()
} else {
Range::default()
};
let severity = match diagnostic.severity() {
Severity::Info => DiagnosticSeverity::INFORMATION,
Severity::Warning => DiagnosticSeverity::WARNING,
Severity::Error | Severity::Fatal => DiagnosticSeverity::ERROR,
};
Diagnostic {
range,
severity: Some(severity),
tags: None,
code: Some(NumberOrString::String(diagnostic.id().to_string())),
code_description: None,
source: Some("red-knot".into()),
message: diagnostic.concise_message().to_string(),
related_information: None,
data: None,
}
}

View File

@@ -1,169 +0,0 @@
use std::any::TypeId;
use lsp_server::{Notification, RequestId};
use rustc_hash::FxHashMap;
use serde_json::Value;
use super::{schedule::Task, ClientSender};
type ResponseBuilder<'s> = Box<dyn FnOnce(lsp_server::Response) -> Task<'s>>;
pub(crate) struct Client<'s> {
notifier: Notifier,
responder: Responder,
pub(super) requester: Requester<'s>,
}
#[derive(Clone)]
pub(crate) struct Notifier(ClientSender);
#[derive(Clone)]
pub(crate) struct Responder(ClientSender);
pub(crate) struct Requester<'s> {
sender: ClientSender,
next_request_id: i32,
response_handlers: FxHashMap<lsp_server::RequestId, ResponseBuilder<'s>>,
}
impl Client<'_> {
pub(super) fn new(sender: ClientSender) -> Self {
Self {
notifier: Notifier(sender.clone()),
responder: Responder(sender.clone()),
requester: Requester {
sender,
next_request_id: 1,
response_handlers: FxHashMap::default(),
},
}
}
pub(super) fn notifier(&self) -> Notifier {
self.notifier.clone()
}
pub(super) fn responder(&self) -> Responder {
self.responder.clone()
}
}
#[allow(dead_code)] // we'll need to use `Notifier` in the future
impl Notifier {
pub(crate) fn notify<N>(&self, params: N::Params) -> crate::Result<()>
where
N: lsp_types::notification::Notification,
{
let method = N::METHOD.to_string();
let message = lsp_server::Message::Notification(Notification::new(method, params));
self.0.send(message)
}
pub(crate) fn notify_method(&self, method: String) -> crate::Result<()> {
self.0
.send(lsp_server::Message::Notification(Notification::new(
method,
Value::Null,
)))
}
}
impl Responder {
pub(crate) fn respond<R>(
&self,
id: RequestId,
result: crate::server::Result<R>,
) -> crate::Result<()>
where
R: serde::Serialize,
{
self.0.send(
match result {
Ok(res) => lsp_server::Response::new_ok(id, res),
Err(crate::server::api::Error { code, error }) => {
lsp_server::Response::new_err(id, code as i32, format!("{error}"))
}
}
.into(),
)
}
}
impl<'s> Requester<'s> {
/// Sends a request of kind `R` to the client, with associated parameters.
/// The task provided by `response_handler` will be dispatched as soon as the response
/// comes back from the client.
pub(crate) fn request<R>(
&mut self,
params: R::Params,
response_handler: impl Fn(R::Result) -> Task<'s> + 'static,
) -> crate::Result<()>
where
R: lsp_types::request::Request,
{
let serialized_params = serde_json::to_value(params)?;
self.response_handlers.insert(
self.next_request_id.into(),
Box::new(move |response: lsp_server::Response| {
match (response.error, response.result) {
(Some(err), _) => {
tracing::error!(
"Got an error from the client (code {}): {}",
err.code,
err.message
);
Task::nothing()
}
(None, Some(response)) => match serde_json::from_value(response) {
Ok(response) => response_handler(response),
Err(error) => {
tracing::error!("Failed to deserialize response from server: {error}");
Task::nothing()
}
},
(None, None) => {
if TypeId::of::<R::Result>() == TypeId::of::<()>() {
// We can't call `response_handler(())` directly here, but
// since we _know_ the type expected is `()`, we can use
// `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`,
// so this branch works in the general case but we'll only
// hit it if the concrete type is `()`, so the `unwrap()` is safe here.
response_handler(serde_json::from_value(Value::Null).unwrap());
} else {
tracing::error!(
"Server response was invalid: did not contain a result or error"
);
}
Task::nothing()
}
}
}),
);
self.sender
.send(lsp_server::Message::Request(lsp_server::Request {
id: self.next_request_id.into(),
method: R::METHOD.into(),
params: serialized_params,
}))?;
self.next_request_id += 1;
Ok(())
}
pub(crate) fn pop_response_task(&mut self, response: lsp_server::Response) -> Task<'s> {
if let Some(handler) = self.response_handlers.remove(&response.id) {
handler(response)
} else {
tracing::error!(
"Received a response with ID {}, which was not expected",
response.id
);
Task::nothing()
}
}
}

View File

@@ -1,165 +0,0 @@
use lsp_server as lsp;
use lsp_types::{notification::Notification, request::Request};
use std::sync::{Arc, Weak};
type ConnectionSender = crossbeam::channel::Sender<lsp::Message>;
type ConnectionReceiver = crossbeam::channel::Receiver<lsp::Message>;
/// A builder for `Connection` that handles LSP initialization.
pub(crate) struct ConnectionInitializer {
connection: lsp::Connection,
threads: lsp::IoThreads,
}
/// Handles inbound and outbound messages with the client.
pub(crate) struct Connection {
sender: Arc<ConnectionSender>,
receiver: ConnectionReceiver,
threads: lsp::IoThreads,
}
impl ConnectionInitializer {
/// Create a new LSP server connection over stdin/stdout.
pub(super) fn stdio() -> Self {
let (connection, threads) = lsp::Connection::stdio();
Self {
connection,
threads,
}
}
/// Starts the initialization process with the client by listening for an initialization request.
/// Returns a request ID that should be passed into `initialize_finish` later,
/// along with the initialization parameters that were provided.
pub(super) fn initialize_start(
&self,
) -> crate::Result<(lsp::RequestId, lsp_types::InitializeParams)> {
let (id, params) = self.connection.initialize_start()?;
Ok((id, serde_json::from_value(params)?))
}
/// Finishes the initialization process with the client,
/// returning an initialized `Connection`.
pub(super) fn initialize_finish(
self,
id: lsp::RequestId,
server_capabilities: &lsp_types::ServerCapabilities,
name: &str,
version: &str,
) -> crate::Result<Connection> {
self.connection.initialize_finish(
id,
serde_json::json!({
"capabilities": server_capabilities,
"serverInfo": {
"name": name,
"version": version
}
}),
)?;
let Self {
connection: lsp::Connection { sender, receiver },
threads,
} = self;
Ok(Connection {
sender: Arc::new(sender),
receiver,
threads,
})
}
}
impl Connection {
/// Make a new `ClientSender` for sending messages to the client.
pub(super) fn make_sender(&self) -> ClientSender {
ClientSender {
weak_sender: Arc::downgrade(&self.sender),
}
}
/// An iterator over incoming messages from the client.
pub(super) fn incoming(&self) -> crossbeam::channel::Iter<lsp::Message> {
self.receiver.iter()
}
/// Check and respond to any incoming shutdown requests; returns`true` if the server should be shutdown.
pub(super) fn handle_shutdown(&self, message: &lsp::Message) -> crate::Result<bool> {
match message {
lsp::Message::Request(lsp::Request { id, method, .. })
if method == lsp_types::request::Shutdown::METHOD =>
{
self.sender
.send(lsp::Response::new_ok(id.clone(), ()).into())?;
tracing::info!("Shutdown request received. Waiting for an exit notification...");
loop {
match &self
.receiver
.recv_timeout(std::time::Duration::from_secs(30))?
{
lsp::Message::Notification(lsp::Notification { method, .. })
if method == lsp_types::notification::Exit::METHOD =>
{
tracing::info!("Exit notification received. Server shutting down...");
return Ok(true);
}
lsp::Message::Request(lsp::Request { id, method, .. }) => {
tracing::warn!(
"Server received unexpected request {method} ({id}) while waiting for exit notification",
);
self.sender.send(lsp::Message::Response(lsp::Response::new_err(
id.clone(),
lsp::ErrorCode::InvalidRequest as i32,
"Server received unexpected request while waiting for exit notification".to_string(),
)))?;
}
message => {
tracing::warn!(
"Server received unexpected message while waiting for exit notification: {message:?}"
);
}
}
}
}
lsp::Message::Notification(lsp::Notification { method, .. })
if method == lsp_types::notification::Exit::METHOD =>
{
anyhow::bail!("Server received an exit notification before a shutdown request was sent. Exiting...");
}
_ => Ok(false),
}
}
/// Join the I/O threads that underpin this connection.
/// This is guaranteed to be nearly immediate since
/// we close the only active channels to these threads prior
/// to joining them.
pub(super) fn close(self) -> crate::Result<()> {
std::mem::drop(
Arc::into_inner(self.sender)
.expect("the client sender shouldn't have more than one strong reference"),
);
std::mem::drop(self.receiver);
self.threads.join()?;
Ok(())
}
}
/// A weak reference to an underlying sender channel, used for communication with the client.
/// If the `Connection` that created this `ClientSender` is dropped, any `send` calls will throw
/// an error.
#[derive(Clone, Debug)]
pub(crate) struct ClientSender {
weak_sender: Weak<ConnectionSender>,
}
// note: additional wrapper functions for senders may be implemented as needed.
impl ClientSender {
pub(crate) fn send(&self, msg: lsp::Message) -> crate::Result<()> {
let Some(sender) = self.weak_sender.upgrade() else {
anyhow::bail!("The connection with the client has been closed");
};
Ok(sender.send(msg)?)
}
}

View File

@@ -1,111 +0,0 @@
use std::path::PathBuf;
use lsp_types::Url;
use rustc_hash::FxHashMap;
use serde::Deserialize;
/// Maps a workspace URI to its associated client settings. Used during server initialization.
pub(crate) type WorkspaceSettingsMap = FxHashMap<Url, ClientSettings>;
/// This is a direct representation of the settings schema sent by the client.
#[derive(Debug, Deserialize, Default)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub struct ClientSettings {
// These settings are only needed for tracing, and are only read from the global configuration.
// These will not be in the resolved settings.
#[serde(flatten)]
pub(crate) tracing: TracingSettings,
}
/// Settings needed to initialize tracing. These will only be
/// read from the global configuration.
#[derive(Debug, Deserialize, Default)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) struct TracingSettings {
pub(crate) log_level: Option<crate::logging::LogLevel>,
/// Path to the log file - tildes and environment variables are supported.
pub(crate) log_file: Option<PathBuf>,
}
/// This is a direct representation of the workspace settings schema,
/// which inherits the schema of [`ClientSettings`] and adds extra fields
/// to describe the workspace it applies to.
#[derive(Debug, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
struct WorkspaceSettings {
#[serde(flatten)]
settings: ClientSettings,
workspace: Url,
}
/// This is the exact schema for initialization options sent in by the client
/// during initialization.
#[derive(Debug, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(untagged)]
enum InitializationOptions {
#[serde(rename_all = "camelCase")]
HasWorkspaces {
global_settings: ClientSettings,
#[serde(rename = "settings")]
workspace_settings: Vec<WorkspaceSettings>,
},
GlobalOnly {
#[serde(default)]
settings: ClientSettings,
},
}
/// Built from the initialization options provided by the client.
#[derive(Debug)]
pub(crate) struct AllSettings {
pub(crate) global_settings: ClientSettings,
/// If this is `None`, the client only passed in global settings.
pub(crate) workspace_settings: Option<WorkspaceSettingsMap>,
}
impl AllSettings {
/// Initializes the controller from the serialized initialization options.
/// This fails if `options` are not valid initialization options.
pub(crate) fn from_value(options: serde_json::Value) -> Self {
Self::from_init_options(
serde_json::from_value(options)
.map_err(|err| {
tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings...");
show_err_msg!("Ruff received invalid client settings - falling back to default client settings.");
})
.unwrap_or_default(),
)
}
fn from_init_options(options: InitializationOptions) -> Self {
let (global_settings, workspace_settings) = match options {
InitializationOptions::GlobalOnly { settings } => (settings, None),
InitializationOptions::HasWorkspaces {
global_settings,
workspace_settings,
} => (global_settings, Some(workspace_settings)),
};
Self {
global_settings,
workspace_settings: workspace_settings.map(|workspace_settings| {
workspace_settings
.into_iter()
.map(|settings| (settings.workspace, settings.settings))
.collect()
}),
}
}
}
impl Default for InitializationOptions {
fn default() -> Self {
Self::GlobalOnly {
settings: ClientSettings::default(),
}
}
}

View File

@@ -1,3 +0,0 @@
The `knot_extensions.pyi` file in this directory will be symlinked into
the `vendor/typeshed/stdlib` directory every time we sync our `typeshed`
stubs (see `.github/workflows/sync_typeshed.yaml`).

View File

@@ -1,28 +0,0 @@
from typing import Any, LiteralString, _SpecialForm
# Special operations
def static_assert(condition: object, msg: LiteralString | None = None) -> None: ...
# Types
Unknown = object()
AlwaysTruthy = object()
AlwaysFalsy = object()
# Special forms
Not: _SpecialForm
Intersection: _SpecialForm
TypeOf: _SpecialForm
CallableTypeOf: _SpecialForm
# Predicates on types
#
# Ideally, these would be annotated using `TypeForm`, but that has not been
# standardized yet (https://peps.python.org/pep-0747).
def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
def is_subtype_of(type_derived: Any, type_base: Any) -> bool: ...
def is_assignable_to(type_target: Any, type_source: Any) -> bool: ...
def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ...
def is_gradual_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
def is_fully_static(type: Any) -> bool: ...
def is_singleton(type: Any) -> bool: ...
def is_single_valued(type: Any) -> bool: ...

View File

@@ -1 +0,0 @@
f65bdc1acde54fda93c802459280da74518d2eef

View File

@@ -1,3 +0,0 @@
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@@ -1,82 +0,0 @@
import sys
from _typeshed import ReadableBuffer
from collections.abc import Callable
from types import ModuleType
from typing import AnyStr, Protocol, final, overload, type_check_only
from typing_extensions import Self, TypeAlias
_DigestMod: TypeAlias = str | Callable[[], _HashObject] | ModuleType | None
openssl_md_meth_names: frozenset[str]
@type_check_only
class _HashObject(Protocol):
@property
def digest_size(self) -> int: ...
@property
def block_size(self) -> int: ...
@property
def name(self) -> str: ...
def copy(self) -> Self: ...
def digest(self) -> bytes: ...
def hexdigest(self) -> str: ...
def update(self, obj: ReadableBuffer, /) -> None: ...
class HASH:
@property
def digest_size(self) -> int: ...
@property
def block_size(self) -> int: ...
@property
def name(self) -> str: ...
def copy(self) -> Self: ...
def digest(self) -> bytes: ...
def hexdigest(self) -> str: ...
def update(self, obj: ReadableBuffer, /) -> None: ...
if sys.version_info >= (3, 10):
class UnsupportedDigestmodError(ValueError): ...
class HASHXOF(HASH):
def digest(self, length: int) -> bytes: ... # type: ignore[override]
def hexdigest(self, length: int) -> str: ... # type: ignore[override]
@final
class HMAC:
@property
def digest_size(self) -> int: ...
@property
def block_size(self) -> int: ...
@property
def name(self) -> str: ...
def copy(self) -> Self: ...
def digest(self) -> bytes: ...
def hexdigest(self) -> str: ...
def update(self, msg: ReadableBuffer) -> None: ...
@overload
def compare_digest(a: ReadableBuffer, b: ReadableBuffer, /) -> bool: ...
@overload
def compare_digest(a: AnyStr, b: AnyStr, /) -> bool: ...
def get_fips_mode() -> int: ...
def hmac_new(key: bytes | bytearray, msg: ReadableBuffer = b"", digestmod: _DigestMod = None) -> HMAC: ...
def new(name: str, string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_md5(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_sha1(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_sha224(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_sha256(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_sha384(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_sha512(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_sha3_224(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_sha3_256(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_sha3_384(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_sha3_512(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ...
def openssl_shake_128(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASHXOF: ...
def openssl_shake_256(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASHXOF: ...
def hmac_digest(key: bytes | bytearray, msg: ReadableBuffer, digest: str) -> bytes: ...
def pbkdf2_hmac(
hash_name: str, password: ReadableBuffer, salt: ReadableBuffer, iterations: int, dklen: int | None = None
) -> bytes: ...
def scrypt(
password: ReadableBuffer, *, salt: ReadableBuffer, n: int, r: int, p: int, maxmem: int = 0, dklen: int = 64
) -> bytes: ...

View File

@@ -1,11 +0,0 @@
from typing import Any, Final, TypeVar
_T = TypeVar("_T")
__about__: Final[str]
def heapify(heap: list[Any], /) -> None: ...
def heappop(heap: list[_T], /) -> _T: ...
def heappush(heap: list[_T], item: _T, /) -> None: ...
def heappushpop(heap: list[_T], item: _T, /) -> _T: ...
def heapreplace(heap: list[_T], item: _T, /) -> _T: ...

View File

@@ -1,32 +0,0 @@
import sys
from _typeshed import StrOrBytesPath
from collections.abc import Callable, Sequence
from typing import SupportsIndex
if sys.platform != "win32":
def fork_exec(
args: Sequence[StrOrBytesPath] | None,
executable_list: Sequence[bytes],
close_fds: bool,
pass_fds: tuple[int, ...],
cwd: str,
env: Sequence[bytes] | None,
p2cread: int,
p2cwrite: int,
c2pread: int,
c2pwrite: int,
errread: int,
errwrite: int,
errpipe_read: int,
errpipe_write: int,
restore_signals: int,
call_setsid: int,
pgid_to_set: int,
gid: SupportsIndex | None,
extra_groups: list[int] | None,
uid: SupportsIndex | None,
child_umask: int,
preexec_fn: Callable[[], None],
allow_vfork: bool,
/,
) -> int: ...

View File

@@ -1,76 +0,0 @@
import queue
from collections.abc import Callable, Iterable, Mapping, Set as AbstractSet
from threading import Lock, Semaphore, Thread
from types import GenericAlias
from typing import Any, Generic, TypeVar, overload
from typing_extensions import TypeVarTuple, Unpack
from weakref import ref
from ._base import BrokenExecutor, Executor, Future
_Ts = TypeVarTuple("_Ts")
_threads_queues: Mapping[Any, Any]
_shutdown: bool
_global_shutdown_lock: Lock
def _python_exit() -> None: ...
_S = TypeVar("_S")
class _WorkItem(Generic[_S]):
future: Future[_S]
fn: Callable[..., _S]
args: Iterable[Any]
kwargs: Mapping[str, Any]
def __init__(self, future: Future[_S], fn: Callable[..., _S], args: Iterable[Any], kwargs: Mapping[str, Any]) -> None: ...
def run(self) -> None: ...
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
def _worker(
executor_reference: ref[Any],
work_queue: queue.SimpleQueue[Any],
initializer: Callable[[Unpack[_Ts]], object],
initargs: tuple[Unpack[_Ts]],
) -> None: ...
class BrokenThreadPool(BrokenExecutor): ...
class ThreadPoolExecutor(Executor):
_max_workers: int
_idle_semaphore: Semaphore
_threads: AbstractSet[Thread]
_broken: bool
_shutdown: bool
_shutdown_lock: Lock
_thread_name_prefix: str | None
_initializer: Callable[..., None] | None
_initargs: tuple[Any, ...]
_work_queue: queue.SimpleQueue[_WorkItem[Any]]
@overload
def __init__(
self,
max_workers: int | None = None,
thread_name_prefix: str = "",
initializer: Callable[[], object] | None = None,
initargs: tuple[()] = (),
) -> None: ...
@overload
def __init__(
self,
max_workers: int | None = None,
thread_name_prefix: str = "",
*,
initializer: Callable[[Unpack[_Ts]], object],
initargs: tuple[Unpack[_Ts]],
) -> None: ...
@overload
def __init__(
self,
max_workers: int | None,
thread_name_prefix: str,
initializer: Callable[[Unpack[_Ts]], object],
initargs: tuple[Unpack[_Ts]],
) -> None: ...
def _adjust_thread_count(self) -> None: ...
def _initializer_failed(self) -> None: ...

View File

@@ -1,8 +0,0 @@
from typing import TextIO
__all__ = ["getpass", "getuser", "GetPassWarning"]
def getpass(prompt: str = "Password: ", stream: TextIO | None = None) -> str: ...
def getuser() -> str: ...
class GetPassWarning(UserWarning): ...

View File

@@ -1,14 +0,0 @@
import sys
if sys.version_info >= (3, 11):
# These are all actually defined in this file on 3.11+,
# and re-exported from importlib.abc,
# but it's much less code duplication for typeshed if we pretend that they're still defined
# in importlib.abc on 3.11+, and re-exported from this file
from importlib.abc import (
ResourceReader as ResourceReader,
Traversable as Traversable,
TraversableResources as TraversableResources,
)
__all__ = ["ResourceReader", "Traversable", "TraversableResources"]

View File

@@ -1,2 +0,0 @@
def url2pathname(url: str) -> str: ...
def pathname2url(p: str) -> str: ...

View File

@@ -1,10 +0,0 @@
from _typeshed import SupportsRead
from collections.abc import Callable
from typing import Any
__all__ = ("loads", "load", "TOMLDecodeError")
class TOMLDecodeError(ValueError): ...
def load(fp: SupportsRead[bytes], /, *, parse_float: Callable[[str], Any] = ...) -> dict[str, Any]: ...
def loads(s: str, /, *, parse_float: Callable[[str], Any] = ...) -> dict[str, Any]: ...

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.11.7"
version = "0.12.0"
publish = true
authors = { workspace = true }
edition = { workspace = true }
@@ -20,6 +20,7 @@ ruff_graph = { workspace = true, features = ["serde", "clap"] }
ruff_linter = { workspace = true, features = ["clap"] }
ruff_macros = { workspace = true }
ruff_notebook = { workspace = true }
ruff_options_metadata = { workspace = true, features = ["serde"] }
ruff_python_ast = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_python_parser = { workspace = true }
@@ -30,7 +31,7 @@ ruff_workspace = { workspace = true }
anyhow = { workspace = true }
argfile = { workspace = true }
bincode = { workspace = true }
bincode = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
cachedir = { workspace = true }
clap = { workspace = true, features = ["derive", "env", "wrap_help"] }
@@ -83,7 +84,7 @@ dist = true
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
tikv-jemallocator = { workspace = true }
[lints]

View File

@@ -13,7 +13,6 @@ fn main() {
commit_info(&workspace_root);
#[allow(clippy::disallowed_methods)]
let target = std::env::var("TARGET").unwrap();
println!("cargo::rustc-env=RUST_HOST_TARGET={target}");
}

View File

@@ -8,7 +8,7 @@ use std::sync::Arc;
use crate::commands::completions::config::{OptionString, OptionStringParser};
use anyhow::bail;
use clap::builder::{TypedValueParser, ValueParserFactory};
use clap::{command, Parser, Subcommand};
use clap::{Parser, Subcommand, command};
use colored::Colorize;
use itertools::Itertools;
use path_absolutize::path_dedot;
@@ -22,12 +22,12 @@ use ruff_linter::settings::types::{
PythonVersion, UnsafeFixes,
};
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
use ruff_options_metadata::{OptionEntry, OptionsMetadata};
use ruff_python_ast as ast;
use ruff_source_file::{LineIndex, OneIndexed, PositionEncoding};
use ruff_text_size::TextRange;
use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::options::{Options, PycodestyleOptions};
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
use ruff_workspace::resolver::ConfigurationTransformer;
use rustc_hash::FxHashMap;
use toml;
@@ -93,7 +93,7 @@ pub struct Args {
pub(crate) global_options: GlobalConfigArgs,
}
#[allow(clippy::large_enum_variant)]
#[expect(clippy::large_enum_variant)]
#[derive(Debug, clap::Subcommand)]
pub enum Command {
/// Run Ruff on the given files or directories.
@@ -177,11 +177,14 @@ pub struct AnalyzeGraphCommand {
/// The minimum Python version that should be supported.
#[arg(long, value_enum)]
target_version: Option<PythonVersion>,
/// Path to a virtual environment to use for resolving additional dependencies
#[arg(long)]
python: Option<PathBuf>,
}
// The `Parser` derive is for ruff_dev, for ruff `Args` would be sufficient
#[derive(Clone, Debug, clap::Parser)]
#[allow(clippy::struct_excessive_bools)]
#[expect(clippy::struct_excessive_bools)]
pub struct CheckCommand {
/// List of files or directories to check.
#[clap(help = "List of files or directories to check [default: .]")]
@@ -443,7 +446,7 @@ pub struct CheckCommand {
}
#[derive(Clone, Debug, clap::Parser)]
#[allow(clippy::struct_excessive_bools)]
#[expect(clippy::struct_excessive_bools)]
pub struct FormatCommand {
/// List of files or directories to format.
#[clap(help = "List of files or directories to format [default: .]")]
@@ -557,7 +560,7 @@ pub enum HelpFormat {
Json,
}
#[allow(clippy::module_name_repetitions)]
#[expect(clippy::module_name_repetitions)]
#[derive(Debug, Default, Clone, clap::Args)]
pub struct LogLevelArgs {
/// Enable verbose logging.
@@ -796,6 +799,7 @@ impl AnalyzeGraphCommand {
let format_arguments = AnalyzeGraphArgs {
files: self.files,
direction: self.direction,
python: self.python,
};
let cli_overrides = ExplicitConfigOverrides {
@@ -1027,7 +1031,7 @@ Possible choices:
/// CLI settings that are distinct from configuration (commands, lists of files,
/// etc.).
#[allow(clippy::struct_excessive_bools)]
#[expect(clippy::struct_excessive_bools)]
pub struct CheckArguments {
pub add_noqa: bool,
pub diff: bool,
@@ -1046,7 +1050,7 @@ pub struct CheckArguments {
/// CLI settings that are distinct from configuration (commands, lists of files,
/// etc.).
#[allow(clippy::struct_excessive_bools)]
#[expect(clippy::struct_excessive_bools)]
pub struct FormatArguments {
pub check: bool,
pub no_cache: bool,
@@ -1122,10 +1126,10 @@ impl std::fmt::Display for FormatRangeParseError {
write!(
f,
"the start position '{start_invalid}' is greater than the end position '{end_invalid}'.\n {tip} Try switching start and end: '{end}-{start}'",
start_invalid=start.to_string().bold().yellow(),
end_invalid=end.to_string().bold().yellow(),
start=start.to_string().green().bold(),
end=end.to_string().green().bold()
start_invalid = start.to_string().bold().yellow(),
end_invalid = end.to_string().bold().yellow(),
start = start.to_string().green().bold(),
end = end.to_string().green().bold()
)
}
FormatRangeParseError::InvalidStart(inner) => inner.write(f, true),
@@ -1226,30 +1230,36 @@ impl LineColumnParseError {
match self {
LineColumnParseError::ColumnParseError(inner) => {
write!(f, "the {range}s column is not a valid number ({inner})'\n {tip} The format is 'line:column'.")
write!(
f,
"the {range}s column is not a valid number ({inner})'\n {tip} The format is 'line:column'."
)
}
LineColumnParseError::LineParseError(inner) => {
write!(f, "the {range} line is not a valid number ({inner})\n {tip} The format is 'line:column'.")
write!(
f,
"the {range} line is not a valid number ({inner})\n {tip} The format is 'line:column'."
)
}
LineColumnParseError::ZeroColumnIndex { line } => {
write!(
f,
"the {range} column is 0, but it should be 1 or greater.\n {tip} The column numbers start at 1.\n {tip} Try {suggestion} instead.",
suggestion=format!("{line}:1").green().bold()
suggestion = format!("{line}:1").green().bold()
)
}
LineColumnParseError::ZeroLineIndex { column } => {
write!(
f,
"the {range} line is 0, but it should be 1 or greater.\n {tip} The line numbers start at 1.\n {tip} Try {suggestion} instead.",
suggestion=format!("1:{column}").green().bold()
suggestion = format!("1:{column}").green().bold()
)
}
LineColumnParseError::ZeroLineAndColumnIndex => {
write!(
f,
"the {range} line and column are both 0, but they should be 1 or greater.\n {tip} The line and column numbers start at 1.\n {tip} Try {suggestion} instead.",
suggestion="1:1".to_string().green().bold()
suggestion = "1:1".to_string().green().bold()
)
}
}
@@ -1261,12 +1271,12 @@ impl LineColumnParseError {
pub struct AnalyzeGraphArgs {
pub files: Vec<PathBuf>,
pub direction: Direction,
pub python: Option<PathBuf>,
}
/// Configuration overrides provided via dedicated CLI flags:
/// `--line-length`, `--respect-gitignore`, etc.
#[derive(Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
struct ExplicitConfigOverrides {
dummy_variable_rgx: Option<Regex>,
exclude: Option<Vec<FilePattern>>,

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