Compare commits

..

60 Commits

Author SHA1 Message Date
David Peter
36ba9707ca Remove TODO message 2024-11-27 13:19:23 +01:00
David Peter
4aba390838 [red-knot] No panic for illegal self-referential f-string annotations 2024-11-27 10:33:22 +01:00
Lokejoke
82c01aa662 [pylint] Implement len-test (PLC1802) (#14309)
## Summary

This PR implements [`use-implicit-booleaness-not-len` /
`C1802`](https://pylint.pycqa.org/en/latest/user_guide/messages/convention/use-implicit-booleaness-not-len.html)
> For sequences, (strings, lists, tuples), use the fact that empty
sequences are false.

---------

Co-authored-by: xbrtnik1 <524841@mail.muni.cz>
Co-authored-by: xbrtnik1 <xbrtnik1@mail.muni.cz>
2024-11-26 13:30:17 -06:00
Brent Westbrook
9f446faa6c [pyflakes] Avoid false positives in @no_type_check contexts (F821, F722) (#14615) 2024-11-26 19:13:43 +00:00
David Peter
b94d6cf567 [red-knot] Fix panic related to f-strings in annotations (#14613)
## Summary

Fix panics related to expressions without inferred types in invalid
syntax examples like:
```py
x: f"Literal[{1 + 2}]" = 3
```
where the `1 + 2` expression (and its sub-expressions) inside the
annotation did not have an inferred type.

## Test Plan

Added new corpus test.
2024-11-26 16:35:44 +01:00
David Peter
cd0c97211c [red-knot] Update KNOWN_FAILURES (#14612)
## Summary

Remove entry that was prevously fixed in
5a30ec0df6.

## Test Plan

```sh
cargo test -p red_knot_workspace -- --ignored linter_af linter_gz
```
2024-11-26 15:56:42 +01:00
David Peter
0e71c9e3bb [red-knot] Fix unit tests in release mode (#14604)
## Summary

This is about the easiest patch that I can think of. It has a drawback
in that there is no real guarantee this won't happen again. I think this
might be acceptable, given that all of this is a temporary thing.

And we also add a new CI job to prevent regressions like this in the
future.

For the record though, I'm listing alternative approaches I thought of:

- We could get rid of the debug/release distinction and just add `@Todo`
type metadata everywhere. This has possible affects on runtime. The main
reason I didn't follow through with this is that the size of `Type`
increases. We would either have to adapt the `assert_eq_size!` test or
get rid of it. Even if we add messages everywhere and get rid of the
file-and-line-variant in the enum, it's not enough to get back to the
current release-mode size of `Type`.
- We could generally discard `@Todo` meta information when using it in
tests. I think this would be a huge drawback. I like that we can have
the actual messages in the mdtest. And make sure we get the expected
`@Todo` type, not just any `@Todo`. It's also helpful when debugging
tests.

closes #14594

## Test Plan

```rs
cargo nextest run --release
```
2024-11-26 15:40:02 +01:00
Dylan
24c90d6953 [pylint] Do not wrap function calls in parentheses in the fix for unnecessary-dunder-call (PLC2801) (#14601) 2024-11-26 06:47:01 -06:00
Tzu-ping Chung
fbff4dec3a [airflow] Avoid implicit DAG schedule (AIR301) (#14581) 2024-11-26 13:38:18 +01:00
Dhruv Manilawala
f3dac27e9a Fix f-string formatting in assignment statement (#14454)
## Summary

fixes: #13813

This PR fixes a bug in the formatting assignment statement when the
value is an f-string.

This is resolved by using custom best fit layouts if the f-string is (a)
not already a flat f-string (thus, cannot be multiline) and (b) is not a
multiline string (thus, cannot be flattened). So, it is used in cases
like the following:
```py
aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
    expression}moreeeeeeeeeeeeeeeee"
```
Which is (a) `FStringLayout::Multiline` and (b) not a multiline.

There are various other examples in the PR diff along with additional
explanation and context as code comments.

## Test Plan

Add multiple test cases for various scenarios.
2024-11-26 15:07:18 +05:30
Simon Brugman
e4cefd9bf9 Extend test cases for flake8-pyi (#14280) 2024-11-26 09:10:38 +01:00
Lokejoke
9e4ee98109 [ruff] Implement invalid-assert-message-literal-argument (RUF040) (#14488)
## Summary

This PR implements new rule discussed
[here](https://github.com/astral-sh/ruff/discussions/14449).
In short, it searches for assert messages which were unintentionally
used as a expression to be matched against.

## Test Plan

`cargo test` and review of `ruff-ecosystem`
2024-11-25 17:41:07 -06:00
Shaygan Hooshyari
557d583e32 Support typing.NoReturn and typing.Never (#14559)
Fix #14558 
## Summary

- Add `typing.NoReturn` and `typing.Never` to known instances and infer
them as `Type::Never`
- Add `is_assignable_to` cases for `Type::Never`

I skipped emitting diagnostic for when a function is annotated as
`NoReturn` but it actually returns.

## Test Plan

Added tests from

https://github.com/python/typing/blob/main/conformance/tests/specialtypes_never.py
except from generics and checking if the return value of the function
and the annotations match.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2024-11-25 21:37:55 +00:00
cake-monotone
f98eebdbab [red-knot] Fix Leaking Narrowing Constraint in ast::ExprIf (#14590)
## Summary

Closes #14588


```py
x: Literal[42, "hello"] = 42 if bool_instance() else "hello"
reveal_type(x)  # revealed: Literal[42] | Literal["hello"]

_ = ... if isinstance(x, str) else ...

# The `isinstance` test incorrectly narrows the type of `x`.
# As a result, `x` is revealed as Literal["hello"], but it should remain Literal[42, "hello"].
reveal_type(x)  # revealed: Literal["hello"]
```

## Test Plan
mdtest included!

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-11-25 10:36:37 -08:00
Simon Brugman
c606bf014e [flake8-pyi] Improve autofix safety for redundant-none-literal (PYI061) (#14583)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-11-25 17:40:57 +00:00
Simon Brugman
e8fce20736 [ruff] Improve autofix safety for never-union (RUF020) (#14589) 2024-11-25 18:35:07 +01:00
Dhruv Manilawala
5a30ec0df6 Avoid inferring invalid expr types for string annotation (#14447)
## Summary

fixes: #14440

## Test Plan

Add a test case with all the invalid expressions in a string annotation
context.
2024-11-25 21:27:03 +05:30
Alex Waygood
fab1b0d546 fuzz-parser: catch exceptions from pysource-minimize (#14586) 2024-11-25 15:14:01 +00:00
Connor Skees
66abef433b red-knot: adapt fuzz-parser to work with red-knot (#14566)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-11-25 13:12:28 +00:00
Harutaka Kawamura
fa22bd604a Fix pytest.mark.parametrize rules to check calls instead of decorators (#14515)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-11-25 13:55:18 +01:00
Dhruv Manilawala
0c9165fc3a Use Result for failed text document retrieval in LSP requests (#14579)
## Summary

Ref:
https://github.com/astral-sh/ruff-vscode/issues/644#issuecomment-2496588452

## Test Plan

Not sure how to test this as this is mainly to get more context on the
panic that the server is raising.
2024-11-25 15:14:30 +05:30
renovate[bot]
9f6147490b Update NPM Development dependencies (#14577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 08:54:51 +01:00
renovate[bot]
b7571c3e24 Update Rust crate syn to v2.0.89 (#14573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 07:46:06 +00:00
renovate[bot]
d178d115f3 Update dependency mdformat to v0.7.19 (#14576)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 08:40:53 +01:00
renovate[bot]
6501782678 Update Rust crate libcst to v1.5.1 (#14570)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 08:39:22 +01:00
renovate[bot]
bca4341dcc Update Rust crate hashbrown to v0.15.2 (#14569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 08:38:34 +01:00
renovate[bot]
31ede11774 Update Rust crate quick-junit to v0.5.1 (#14572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 08:38:12 +01:00
renovate[bot]
ba9f881687 Update Rust crate proc-macro2 to v1.0.92 (#14571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 08:38:00 +01:00
renovate[bot]
4357a0a3c2 Update Rust crate unicode-ident to v1.0.14 (#14574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 08:36:50 +01:00
renovate[bot]
c18afa93b3 Update Rust crate url to v2.5.4 (#14575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 08:36:28 +01:00
renovate[bot]
8f04202ee4 Update Rust crate dir-test to 0.4.0 (#14578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 08:36:06 +01:00
Dhruv Manilawala
efe54081d6 Remove FormatFStringPart (#14448)
## Summary

This is just a small refactor to remove the `FormatFStringPart` as it's
only used in the case when the f-string is not implicitly concatenated
in which case the only part is going to be `FString`. In implicitly
concatenated f-strings, we use `StringLike` instead.
2024-11-25 10:29:22 +05:30
Alex Waygood
ac23c99744 [ruff] Mark fixes for unsorted-dunder-all and unsorted-dunder-slots as unsafe when there are complex comments in the sequence (RUF022, RUF023) (#14560) 2024-11-24 12:49:29 +00:00
InSync
e5c7d87461 Add @astropy/astropy to ecosystem checks (#14565) 2024-11-24 12:47:11 +01:00
Charlie Marsh
de62e39eba Use truthiness check in auto_attribs detection (#14562) 2024-11-23 22:06:10 -05:00
InSync
d285717da8 [ruff] Handle attrs's auto_attribs correctly (RUF009) (#14520)
## Summary

Resolves #14519.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2024-11-23 21:46:38 -05:00
InSync
545e9deba3 [flake8-builtins] Exempt private built-in modules (A005) (#14505)
## Summary

Resolves #12949.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2024-11-23 21:39:04 -05:00
Harutaka Kawamura
e3d792605f [flake8-bugbear] Fix mutable-contextvar-default (B039) to resolve annotated function calls properly (#14532)
## Summary

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

Fix #14525

## Test Plan

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

New test cases

---------

Signed-off-by: harupy <hkawamura0130@gmail.com>
2024-11-23 21:29:25 -05:00
Harutaka Kawamura
1f303a5eb6 Simplify flake8_pytest_style::rules::fail_call implementation (#14556) 2024-11-23 15:14:28 +01:00
Nikolas Hearp
07d13c6b4a [B028-doc-update] Update documentation for B028 (#14338)
## Summary
Resolves #14289
The documentation for B028 no_explicit_stacklevel is updated to be more
clear.

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2024-11-23 07:45:28 +00:00
Dylan
e1838aac29 Ignore more rules for stub files (#14541)
This PR causes the following rules to ignore stub files, on the grounds
that it is not under the author's control to appease these lints:

- `PLR0904` https://docs.astral.sh/ruff/rules/too-many-public-methods/
- `PLR0913` https://docs.astral.sh/ruff/rules/too-many-arguments/
- `PLR0917`
https://docs.astral.sh/ruff/rules/too-many-positional-arguments/
- `PLW3201` https://docs.astral.sh/ruff/rules/bad-dunder-method-name/
- `SLOT` https://docs.astral.sh/ruff/rules/#flake8-slots-slot
- `FBT` https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt
(except for FBT003 since that involves a function call.)

Progress towards #14535
2024-11-23 07:41:10 +00:00
Carl Meyer
4ba847f250 [red-knot] remove wrong typevar attribute implementations (#14540) 2024-11-22 13:17:16 -08:00
renovate[bot]
13e9fc9362 Update dependency smol-toml to v1.3.1 (#14542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-22 21:16:05 +00:00
Dylan
3fda2d17c7 [ruff] Auto-add r prefix when string has no backslashes for unraw-re-pattern (RUF039) (#14536)
This PR adds a sometimes-available, safe autofix for [unraw-re-pattern
(RUF039)](https://docs.astral.sh/ruff/rules/unraw-re-pattern/#unraw-re-pattern-ruf039),
which prepends an `r` prefix. It is used only when the string in
question has no backslahses (and also does not have a `u` prefix, since
that causes a syntax error.)

Closes #14527

Notes: 
- Test fixture unchanged, but snapshot changed to include fix messages.
- This fix is automatically only available in preview since the rule
itself is in preview
2024-11-22 15:09:53 -06:00
Harutaka Kawamura
931fa06d85 Extend invalid-envvar-default (PLW1508) to detect os.environ.get (#14512) 2024-11-22 19:13:58 +00:00
Micha Reiser
e53ac7985d Enable logging for directory-renamed test (#14533) 2024-11-22 16:41:46 +00:00
David Salvisberg
e25e7044ba [flake8-type-checking] Adds implementation for TC006 (#14511)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-11-22 15:22:59 +01:00
Micha Reiser
b80de52592 Consider quotes inside format-specs when choosing the quotes for an f-string (#14493) 2024-11-22 12:43:53 +00:00
Alex Waygood
2917534279 Fix broken link to PYI063 (#14526) 2024-11-22 12:27:52 +00:00
David Peter
f6b2cd5588 [red-knot] Semantic index: handle invalid breaks (#14522)
## Summary

This fix addresses panics related to invalid syntax like the following
where a `break` statement is used in a nested definition inside a
loop:

```py
while True:

    def b():
        x: int

        break
```

closes #14342

## Test Plan

* New corpus regression tests.
* New unit test to make sure we handle nested while loops correctly.
This test is passing on `main`, but can easily fail if the
`is_inside_loop` state isn't properly saved/restored.
2024-11-22 13:13:55 +01:00
Micha Reiser
302fe76c2b Fix unnecessary space around power op in overlong f-string expressions (#14489) 2024-11-22 13:01:22 +01:00
David Peter
a90e404c3f [red-knot] PEP 695 type aliases (#14357)
## Summary

Add support for (non-generic) type aliases. The main motivation behind
this was to get rid of panics involving expressions in (generic) type
aliases. But it turned out the best way to fix it was to implement
(partial) support for type aliases.

```py
type IntOrStr = int | str

reveal_type(IntOrStr)  # revealed: typing.TypeAliasType
reveal_type(IntOrStr.__name__)  # revealed: Literal["IntOrStr"]

x: IntOrStr = 1

reveal_type(x)  # revealed: Literal[1]

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

## Test Plan

- Updated corpus test allow list to reflect that we don't panic anymore.
- Added Markdown-based test for type aliases (`type_alias.md`)
2024-11-22 08:47:14 +01:00
Micha Reiser
8358ad8d25 Ruff 0.8 release (#14486)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: David Salvisberg <dave@daverball.com>
2024-11-22 08:45:19 +01:00
Alex Waygood
2b8b1ef178 Improve docs for some pycodestyle rules (#14517) 2024-11-21 17:26:06 +00:00
Dylan
2efa3fbb62 [flake8-import-conventions] Syntax check aliases supplied in configuration for unconventional-import-alias (ICN001) (#14477)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-11-21 15:54:49 +00:00
cmp0xff
b9da4305e6 doc(B024): #14455 add annotated but unassgined class variables (#14502)
# Summary

Closes #14455, migrated from https://github.com/astral-sh/docs/pull/106.
2024-11-21 09:08:02 -06:00
Micha Reiser
87043a2415 Limit type size assertion to 64bit (#14514) 2024-11-21 12:49:55 +00:00
David Peter
f684b6fff4 [red-knot] Fix: Infer type for typing.Union[..] tuple expression (#14510)
## Summary

Fixes a panic related to sub-expressions of `typing.Union` where we fail
to store a type for the `int, str` tuple-expression in code like this:
```
x: Union[int, str] = 1
```

relates to [my
comment](https://github.com/astral-sh/ruff/pull/14499#discussion_r1851794467)
on #14499.

## Test Plan

New corpus test
2024-11-21 11:49:20 +01:00
David Peter
47f39ed1a0 [red-knot] Meta data for Type::Todo (#14500)
## Summary

Adds meta information to `Type::Todo`, allowing developers to easily
trace back the origin of a particular `@Todo` type they encounter.

Instead of `Type::Todo`, we now write either `type_todo!()` which
creates a `@Todo[path/to/source.rs:123]` type with file and line
information, or using `type_todo!("PEP 604 unions not supported")`,
which creates a variant with a custom message.

`Type::Todo` now contains a `TodoType` field. In release mode, this is
just a zero-sized struct, in order not to create any overhead. In debug
mode, this is an `enum` that contains the meta information.

`Type` implements `Copy`, which means that `TodoType` also needs to be
copyable. This limits the design space. We could intern `TodoType`, but
I discarded this option, as it would require us to have access to the
salsa DB everywhere we want to use `Type::Todo`. And it would have made
the macro invocations less ergonomic (requiring us to pass `db`).

So for now, the meta information is simply a `&'static str` / `u32` for
the file/line variant, or a `&'static str` for the custom message.
Anything involving a chain/backtrace of several `@Todo`s or similar is
therefore currently not implemented. Also because we currently don't see
any direct use cases for this, and because all of this will eventually
go away.

Note that the size of `Type` increases from 16 to 24 bytes, but only in
debug mode.

## Test Plan

- Observed the changes in Markdown tests.
- Added custom messages for all `Type::Todo`s that were revealed in the
tests
- Ran red knot in release and debug mode on the following Python file:
  ```py
  def f(x: int) -> int:
      reveal_type(x)
  ```
Prints `@Todo` in release mode and `@Todo(function parameter type)` in
debug mode.
2024-11-21 09:59:47 +01:00
Shaygan Hooshyari
aecdb8c144 [red-knot] support typing.Union in type annotations (#14499)
Fix #14498

## Summary

This PR adds `typing.Union` support

## Test Plan

I created new tests in mdtest.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-11-20 21:55:33 +00:00
213 changed files with 8884 additions and 1660 deletions

View File

@@ -157,6 +157,33 @@ jobs:
name: ruff
path: target/debug/ruff
cargo-test-linux-release:
name: "cargo test (linux, release)"
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@v2
with:
tool: cargo-insta
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
cargo-test-windows:
name: "cargo test (windows)"
runs-on: windows-latest-xlarge
@@ -212,7 +239,6 @@ jobs:
cargo-build-release:
name: "cargo build (release)"
runs-on: macos-latest
needs: determine_changes
if: ${{ github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
@@ -309,7 +335,7 @@ jobs:
# Make executable, since artifact download doesn't preserve this
chmod +x ${{ steps.download-cached-binary.outputs.download-path }}/ruff
python scripts/fuzz-parser/fuzz.py 0-500 --test-executable ${{ steps.download-cached-binary.outputs.download-path }}/ruff
python scripts/fuzz-parser/fuzz.py --bin ruff 0-500 --test-executable ${{ steps.download-cached-binary.outputs.download-path }}/ruff
scripts:
name: "test scripts"

View File

@@ -49,7 +49,7 @@ jobs:
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI
run: cargo build --locked
- name: Fuzz
run: python scripts/fuzz-parser/fuzz.py $(shuf -i 0-9999999999999999999 -n 1000) --test-executable target/debug/ruff
run: python scripts/fuzz-parser/fuzz.py --bin ruff $(shuf -i 0-9999999999999999999 -n 1000) --test-executable target/debug/ruff
create-issue-on-failure:
name: Create an issue if the daily fuzz surfaced any bugs

View File

@@ -1,5 +1,30 @@
# Breaking Changes
## 0.8.0
- **Default to Python 3.9**
Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896))
- **Changed location of `pydoclint` diagnostics**
[`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case.
If you've opted into these preview rules but have them suppressed using
[`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in
some places, this change may mean that you need to move the `noqa` suppression
comments. Most users should be unaffected by this change.
- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer**
Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order).
This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected.
- **Changes to the line width calculation**
Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)).
## 0.7.0
- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments

View File

@@ -1,5 +1,104 @@
# Changelog
## 0.8.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.8.0) for a migration guide and overview of the changes!
### Breaking changes
See also, the "Remapped rules" section which may result in disabled rules.
- **Default to Python 3.9**
Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896))
- **Changed location of `pydoclint` diagnostics**
[`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case.
If you've opted into these preview rules but have them suppressed using
[`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in
some places, this change may mean that you need to move the `noqa` suppression
comments. Most users should be unaffected by this change.
- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer**
Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order).
This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected.
- **Changes to the line width calculation**
Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)).
### Removed Rules
The following deprecated rules have been removed:
- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`)
- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`)
- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`)
- [`pytest-missing-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-missing-fixture-name-underscore/) (`PT004`)
- [`pytest-incorrect-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-incorrect-fixture-name-underscore/) (`PT005`)
- [`unpacked-list-comprehension`](https://docs.astral.sh/ruff/rules/unpacked-list-comprehension/) (`UP027`)
### Remapped rules
The following rules have been remapped to new rule codes:
- [`flake8-type-checking`](https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc): `TCH` to `TC`
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`builtin-import-shadowing`](https://docs.astral.sh/ruff/rules/builtin-import-shadowing/) (`A004`)
- [`mutable-contextvar-default`](https://docs.astral.sh/ruff/rules/mutable-contextvar-default/) (`B039`)
- [`fast-api-redundant-response-model`](https://docs.astral.sh/ruff/rules/fast-api-redundant-response-model/) (`FAST001`)
- [`fast-api-non-annotated-dependency`](https://docs.astral.sh/ruff/rules/fast-api-non-annotated-dependency/) (`FAST002`)
- [`dict-index-missing-items`](https://docs.astral.sh/ruff/rules/dict-index-missing-items/) (`PLC0206`)
- [`pep484-style-positional-only-parameter`](https://docs.astral.sh/ruff/rules/pep484-style-positional-only-parameter/) (`PYI063`)
- [`redundant-final-literal`](https://docs.astral.sh/ruff/rules/redundant-final-literal/) (`PYI064`)
- [`bad-version-info-order`](https://docs.astral.sh/ruff/rules/bad-version-info-order/) (`PYI066`)
- [`parenthesize-chained-operators`](https://docs.astral.sh/ruff/rules/parenthesize-chained-operators/) (`RUF021`)
- [`unsorted-dunder-all`](https://docs.astral.sh/ruff/rules/unsorted-dunder-all/) (`RUF022`)
- [`unsorted-dunder-slots`](https://docs.astral.sh/ruff/rules/unsorted-dunder-slots/) (`RUF023`)
- [`assert-with-print-message`](https://docs.astral.sh/ruff/rules/assert-with-print-message/) (`RUF030`)
- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`)
The following behaviors have been stabilized:
- [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name/) (`E741`): Violations in stub files are now ignored. Stub authors typically don't control variable names.
- [`printf-string-formatting`](https://docs.astral.sh/ruff/rules/printf-string-formatting/) (`UP031`): Report all `printf`-like usages even if no autofix is available
The following fixes have been stabilized:
- [`zip-instead-of-pairwise`](https://docs.astral.sh/ruff/rules/zip-instead-of-pairwise/) (`RUF007`)
### Preview features
- \[`flake8-datetimez`\] Exempt `min.time()` and `max.time()` (`DTZ901`) ([#14394](https://github.com/astral-sh/ruff/pull/14394))
- \[`flake8-pie`\] Mark fix as unsafe if the following statement is a string literal (`PIE790`) ([#14393](https://github.com/astral-sh/ruff/pull/14393))
- \[`flake8-pyi`\] New rule `redundant-none-literal` (`PYI061`) ([#14316](https://github.com/astral-sh/ruff/pull/14316))
- \[`flake8-pyi`\] Add autofix for `redundant-numeric-union` (`PYI041`) ([#14273](https://github.com/astral-sh/ruff/pull/14273))
- \[`ruff`\] New rule `map-int-version-parsing` (`RUF048`) ([#14373](https://github.com/astral-sh/ruff/pull/14373))
- \[`ruff`\] New rule `redundant-bool-literal` (`RUF038`) ([#14319](https://github.com/astral-sh/ruff/pull/14319))
- \[`ruff`\] New rule `unraw-re-pattern` (`RUF039`) ([#14446](https://github.com/astral-sh/ruff/pull/14446))
- \[`pycodestyle`\] Exempt `pytest.importorskip()` calls (`E402`) ([#14474](https://github.com/astral-sh/ruff/pull/14474))
- \[`pylint`\] Autofix suggests using sets when possible (`PLR1714`) ([#14372](https://github.com/astral-sh/ruff/pull/14372))
### Rule changes
- [`invalid-pyproject-toml`](https://docs.astral.sh/ruff/rules/invalid-pyproject-toml/) (`RUF200`): Updated to reflect the provisionally accepted [PEP 639](https://peps.python.org/pep-0639/).
- \[`flake8-pyi`\] Avoid panic in unfixable case (`PYI041`) ([#14402](https://github.com/astral-sh/ruff/pull/14402))
- \[`flake8-type-checking`\] Correctly handle quotes in subscript expression when generating an autofix ([#14371](https://github.com/astral-sh/ruff/pull/14371))
- \[`pylint`\] Suggest correct autofix for `__contains__` (`PLC2801`) ([#14424](https://github.com/astral-sh/ruff/pull/14424))
### Configuration
- Ruff now emits a warning instead of an error when a configuration [`ignore`](https://docs.astral.sh/ruff/settings/#lint_ignore)s a rule that has been removed ([#14435](https://github.com/astral-sh/ruff/pull/14435))
- Ruff now validates that `lint.flake8-import-conventions.aliases` only uses valid module names and aliases ([#14477](https://github.com/astral-sh/ruff/pull/14477))
## 0.7.4
### Preview features
@@ -978,7 +1077,7 @@ The following deprecated CLI commands have been removed:
### Preview features
- \[`flake8-bugbear`\] Implement `return-in-generator` (`B901`) ([#11644](https://github.com/astral-sh/ruff/pull/11644))
- \[`flake8-pyi`\] Implement `PYI063` ([#11699](https://github.com/astral-sh/ruff/pull/11699))
- \[`flake8-pyi`\] Implement `pep484-style-positional-only-parameter` (`PYI063`) ([#11699](https://github.com/astral-sh/ruff/pull/11699))
- \[`pygrep_hooks`\] Check blanket ignores via file-level pragmas (`PGH004`) ([#11540](https://github.com/astral-sh/ruff/pull/11540))
### Rule changes

130
Cargo.lock generated
View File

@@ -413,7 +413,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -693,7 +693,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -704,7 +704,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -758,23 +758,23 @@ dependencies = [
[[package]]
name = "dir-test"
version = "0.3.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c44bdf9319ad5223afb7eb15a7110452b0adf0373ea6756561b2c708eef0dd1"
checksum = "b12781621d53fd9087021f5a338df5c57c04f84a6231c1f4726f45e2e333470b"
dependencies = [
"dir-test-macros",
]
[[package]]
name = "dir-test-macros"
version = "0.3.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "644f96047137dfaa7a09e34d4623f9e52a1926ecc25ba32ad2ba3fc422536b25"
checksum = "1340852f50b2285d01a7f598cc5d08b572669c3e09e614925175cc3c26787b91"
dependencies = [
"glob",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -826,7 +826,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -1068,9 +1068,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.15.1"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "hashlink"
@@ -1246,7 +1246,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -1319,7 +1319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [
"equivalent",
"hashbrown 0.15.1",
"hashbrown 0.15.2",
"serde",
]
@@ -1420,7 +1420,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -1526,9 +1526,9 @@ checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
[[package]]
name = "libcst"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1586dd7a857d8a61a577afde1a24cc9573ff549eff092d5ce968b1ec93cc61b6"
checksum = "fa3e60579a8cba3d86aa4a5f7fc98973cc0fd2ac270bf02f85a9bef09700b075"
dependencies = [
"chic",
"libcst_derive",
@@ -1546,7 +1546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
dependencies = [
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -1710,9 +1710,9 @@ checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
[[package]]
name = "newtype-uuid"
version = "1.1.0"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526cb7c660872e401beaf3297f95f548ce3b4b4bdd8121b7c0713771d7c4a6e"
checksum = "4c8781e2ef64806278a55ad223f0bc875772fd40e1fe6e73e8adbf027817229d"
dependencies = [
"uuid",
]
@@ -2012,7 +2012,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -2127,9 +2127,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.89"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
@@ -2150,24 +2150,24 @@ dependencies = [
[[package]]
name = "quick-junit"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62ffd2f9a162cfae131bed6d9d1ed60adced33be340a94f96952897d7cb0c240"
checksum = "3ed1a693391a16317257103ad06a88c6529ac640846021da7c435a06fffdacd7"
dependencies = [
"chrono",
"indexmap",
"newtype-uuid",
"quick-xml",
"strip-ansi-escapes",
"thiserror 1.0.67",
"thiserror 2.0.3",
"uuid",
]
[[package]]
name = "quick-xml"
version = "0.36.1"
version = "0.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc"
checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03"
dependencies = [
"memchr",
]
@@ -2266,7 +2266,7 @@ dependencies = [
"compact_str",
"countme",
"dir-test",
"hashbrown 0.15.1",
"hashbrown 0.15.2",
"indexmap",
"insta",
"itertools 0.13.0",
@@ -2489,7 +2489,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.7.4"
version = "0.8.0"
dependencies = [
"anyhow",
"argfile",
@@ -2708,7 +2708,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.7.4"
version = "0.8.0"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2775,7 +2775,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3023,7 +3023,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.7.4"
version = "0.8.0"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3072,6 +3072,7 @@ dependencies = [
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_source_file",
"rustc-hash 2.0.0",
"schemars",
@@ -3194,7 +3195,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
"synstructure",
]
@@ -3228,7 +3229,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3277,7 +3278,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3288,7 +3289,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3311,7 +3312,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3352,7 +3353,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3460,7 +3461,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3471,20 +3472,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "1.0.109"
version = "2.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e"
dependencies = [
"proc-macro2",
"quote",
@@ -3499,7 +3489,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3562,7 +3552,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3573,7 +3563,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
"test-case-core",
]
@@ -3603,7 +3593,7 @@ checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3614,7 +3604,7 @@ checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3736,7 +3726,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -3875,9 +3865,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unicode-normalization"
@@ -3952,9 +3942,9 @@ dependencies = [
[[package]]
name = "url"
version = "2.5.3"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [
"form_urlencoded",
"idna",
@@ -4006,7 +3996,7 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -4101,7 +4091,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
"wasm-bindgen-shared",
]
@@ -4135,7 +4125,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4169,7 +4159,7 @@ checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -4472,7 +4462,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
"synstructure",
]
@@ -4493,7 +4483,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]
@@ -4513,7 +4503,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
"synstructure",
]
@@ -4542,7 +4532,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"syn",
]
[[package]]

View File

@@ -65,7 +65,7 @@ compact_str = "0.8.0"
criterion = { version = "0.5.1", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
dir-test = { version = "0.3.0" }
dir-test = { version = "0.4.0" }
dunce = { version = "1.0.5" }
drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.11.0" }

View File

@@ -136,8 +136,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.7.4/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.7.4/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.8.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.8.0/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -170,7 +170,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.7.4
rev: v0.8.0
hooks:
# Run the linter.
- id: ruff

View File

@@ -4,7 +4,6 @@ use std::io::Write;
use std::time::Duration;
use anyhow::{anyhow, Context};
use red_knot_python_semantic::{resolve_module, ModuleName, Program, PythonVersion, SitePackages};
use red_knot_workspace::db::{Db, RootDatabase};
use red_knot_workspace::watch;
@@ -14,7 +13,7 @@ use red_knot_workspace::workspace::WorkspaceMetadata;
use ruff_db::files::{system_path_to_file, File, FileError};
use ruff_db::source::source_text;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use ruff_db::testing::setup_logging;
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
use ruff_db::Upcast;
struct TestCase {
@@ -47,6 +46,8 @@ impl TestCase {
}
fn try_stop_watch(&mut self, timeout: Duration) -> Option<Vec<watch::ChangeEvent>> {
tracing::debug!("Try stopping watch with timeout {:?}", timeout);
let watcher = self
.watcher
.take()
@@ -56,8 +57,11 @@ impl TestCase {
.changes_receiver
.recv_timeout(timeout)
.unwrap_or_default();
watcher.flush();
tracing::debug!("Flushed file watcher");
watcher.stop();
tracing::debug!("Stopping file watcher");
for event in &self.changes_receiver {
all_events.extend(event);
@@ -600,6 +604,8 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
#[test]
fn directory_renamed() -> anyhow::Result<()> {
let _tracing = setup_logging_with_filter("file_watching=TRACE,red_knot=TRACE");
let mut case = setup([
("bar.py", "import sub.a"),
("sub/__init__.py", ""),
@@ -640,6 +646,10 @@ fn directory_renamed() -> anyhow::Result<()> {
let changes = case.stop_watch();
for event in &changes {
tracing::debug!("Event: {:?}", event);
}
case.apply_changes(changes);
// `import sub.a` should no longer resolve

View File

@@ -0,0 +1,62 @@
# NoReturn & Never
`NoReturn` is used to annotate the return type for functions that never return. `Never` is the
bottom type, representing the empty set of Python objects. These two annotations can be used
interchangeably.
## Function Return Type Annotation
```py
from typing import NoReturn
def stop() -> NoReturn:
raise RuntimeError("no way")
# revealed: Never
reveal_type(stop())
```
## Assignment
```py
from typing import NoReturn, Never, Any
# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
x: Never[int]
a1: NoReturn
# TODO: Test `Never` is only available in python >= 3.11
a2: Never
b1: Any
b2: int
def f():
# revealed: Never
reveal_type(a1)
# revealed: Never
reveal_type(a2)
# Never is assignable to all types.
v1: int = a1
v2: str = a1
# Other types are not assignable to Never except for Never (and Any).
v3: Never = b1
v4: Never = a2
v5: Any = b2
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Never`"
v6: Never = 1
```
## Typing Extensions
```py
from typing_extensions import NoReturn, Never
x: NoReturn
y: Never
def f():
# revealed: Never
reveal_type(x)
# revealed: Never
reveal_type(y)
```

View File

@@ -9,10 +9,10 @@ Ts = TypeVarTuple("Ts")
def append_int(*args: *Ts) -> tuple[*Ts, int]:
# TODO: should show some representation of the variadic generic type
reveal_type(args) # revealed: @Todo
reveal_type(args) # revealed: @Todo(function parameter type)
return (*args, 1)
# TODO should be tuple[Literal[True], Literal["a"], int]
reveal_type(append_int(True, "a")) # revealed: @Todo
reveal_type(append_int(True, "a")) # revealed: @Todo(full tuple[...] support)
```

View File

@@ -125,7 +125,6 @@ def f7() -> "\x69nt":
def f8() -> """int""":
return 1
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
def f9() -> "b'int'":
return 1
@@ -189,3 +188,31 @@ reveal_type(d) # revealed: Foo
## Parameter
TODO: Add tests once parameter inference is supported
## Invalid expressions
The expressions in these string annotations aren't valid expressions in this context but we
shouldn't panic.
```py
a: "1 or 2"
b: "(x := 1)"
c: "1 + 2"
d: "lambda x: x"
e: "x if True else y"
f: "{'a': 1, 'b': 2}"
g: "{1, 2}"
h: "[i for i in range(5)]"
i: "{i for i in range(5)}"
j: "{i: i for i in range(5)}"
k: "(i for i in range(5))"
l: "await 1"
# error: [forward-annotation-syntax-error]
m: "yield 1"
# error: [forward-annotation-syntax-error]
n: "yield from 1"
o: "1 < 2"
p: "call()"
r: "[1, 2]"
s: "(1, 2)"
```

View File

@@ -0,0 +1,61 @@
# Union
## Annotation
`typing.Union` can be used to construct union types same as `|` operator.
```py
from typing import Union
a: Union[int, str]
a1: Union[int, bool]
a2: Union[int, Union[float, str]]
a3: Union[int, None]
a4: Union[Union[float, str]]
a5: Union[int]
a6: Union[()]
def f():
# revealed: int | str
reveal_type(a)
# Since bool is a subtype of int we simplify to int here. But we do allow assigning boolean values (see below).
# revealed: int
reveal_type(a1)
# revealed: int | float | str
reveal_type(a2)
# revealed: int | None
reveal_type(a3)
# revealed: float | str
reveal_type(a4)
# revealed: int
reveal_type(a5)
# revealed: Never
reveal_type(a6)
```
## Assignment
```py
from typing import Union
a: Union[int, str]
a = 1
a = ""
a1: Union[int, bool]
a1 = 1
a1 = True
# error: [invalid-assignment] "Object of type `Literal[b""]` is not assignable to `int | str`"
a = b""
```
## Typing Extensions
```py
from typing_extensions import Union
a: Union[int, str]
def f():
# revealed: int | str
reveal_type(a)
```

View File

@@ -51,12 +51,12 @@ reveal_type(c) # revealed: tuple[str, int]
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
# TODO: homogenous tuples, PEP-646 tuples
reveal_type(e) # revealed: @Todo
reveal_type(f) # revealed: @Todo
reveal_type(g) # revealed: @Todo
reveal_type(e) # revealed: @Todo(full tuple[...] support)
reveal_type(f) # revealed: @Todo(full tuple[...] support)
reveal_type(g) # revealed: @Todo(full tuple[...] support)
# TODO: support more kinds of type expressions in annotations
reveal_type(h) # revealed: @Todo
reveal_type(h) # revealed: @Todo(full tuple[...] support)
reveal_type(i) # revealed: tuple[str | int, str | int]
reveal_type(j) # revealed: tuple[str | int]

View File

@@ -317,7 +317,7 @@ reveal_type(1 + A()) # revealed: int
reveal_type(A() + "foo") # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances
# TODO overloads
reveal_type("foo" + A()) # revealed: @Todo
reveal_type("foo" + A()) # revealed: @Todo(return type)
reveal_type(A() + b"foo") # revealed: A
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
@@ -325,7 +325,7 @@ reveal_type(b"foo" + A()) # revealed: bytes
reveal_type(A() + ()) # revealed: A
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
reveal_type(() + A()) # revealed: @Todo
reveal_type(() + A()) # revealed: @Todo(return type)
literal_string_instance = "foo" * 1_000_000_000
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
@@ -334,7 +334,7 @@ reveal_type(literal_string_instance) # revealed: LiteralString
reveal_type(A() + literal_string_instance) # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances
# TODO overloads
reveal_type(literal_string_instance + A()) # revealed: @Todo
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type)
```
## Operations involving instances of classes inheriting from `Any`

View File

@@ -16,7 +16,7 @@ async def get_int_async() -> int:
return 42
# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]`
reveal_type(get_int_async()) # revealed: @Todo
reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
```
## Generic
@@ -36,6 +36,8 @@ from typing import Callable
def foo() -> int:
return 42
# TODO: This should be resolved once we understand `Callable` annotations
# error: [annotation-with-invalid-expression]
def decorator(func) -> Callable[[], int]:
return foo
@@ -44,7 +46,7 @@ def bar() -> str:
return "bar"
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
reveal_type(bar()) # revealed: @Todo
reveal_type(bar()) # revealed: @Todo(return type)
```
## Invalid callable

View File

@@ -50,11 +50,11 @@ def foo(
help()
except x as e:
# TODO: should be `AttributeError`
reveal_type(e) # revealed: @Todo
reveal_type(e) # revealed: @Todo(exception type)
except y as f:
# TODO: should be `OSError | RuntimeError`
reveal_type(f) # revealed: @Todo
reveal_type(f) # revealed: @Todo(exception type)
except z as g:
# TODO: should be `BaseException`
reveal_type(g) # revealed: @Todo
reveal_type(g) # revealed: @Todo(exception type)
```

View File

@@ -22,3 +22,22 @@ reveal_type(1 if None else 2) # revealed: Literal[2]
reveal_type(1 if "" else 2) # revealed: Literal[2]
reveal_type(1 if 0 else 2) # revealed: Literal[2]
```
## Leaked Narrowing Constraint
(issue #14588)
The test inside an if expression should not affect code outside of the expression.
```py
def bool_instance() -> bool:
return True
x: Literal[42, "hello"] = 42 if bool_instance() else "hello"
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
_ = ... if isinstance(x, str) else ...
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
```

View File

@@ -18,7 +18,7 @@ box: MyBox[int] = MyBox(5)
wrong_innards: MyBox[int] = MyBox("five")
# TODO reveal int
reveal_type(box.data) # revealed: @Todo
reveal_type(box.data) # revealed: @Todo(instance attributes)
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
```
@@ -39,7 +39,7 @@ class MySecureBox[T](MyBox[T]): ...
secure_box: MySecureBox[int] = MySecureBox(5)
reveal_type(secure_box) # revealed: MySecureBox
# TODO reveal int
reveal_type(secure_box.data) # revealed: @Todo
reveal_type(secure_box.data) # revealed: @Todo(instance attributes)
```
## Cyclical class definition
@@ -60,52 +60,20 @@ reveal_type(S) # revealed: Literal[S]
## Type params
A PEP695 type variable defines a value of type `typing.TypeVar` with attributes `__name__`,
`__bounds__`, `__constraints__`, and `__default__` (the latter three all lazily evaluated):
A PEP695 type variable defines a value of type `typing.TypeVar`.
```py
def f[T, U: A, V: (A, B), W = A, X: A = A1]():
def f[T]():
reveal_type(T) # revealed: T
reveal_type(T.__name__) # revealed: Literal["T"]
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
reveal_type(T.__default__) # revealed: NoDefault
reveal_type(U) # revealed: U
reveal_type(U.__name__) # revealed: Literal["U"]
reveal_type(U.__bound__) # revealed: type[A]
reveal_type(U.__constraints__) # revealed: tuple[()]
reveal_type(U.__default__) # revealed: NoDefault
reveal_type(V) # revealed: V
reveal_type(V.__name__) # revealed: Literal["V"]
reveal_type(V.__bound__) # revealed: None
reveal_type(V.__constraints__) # revealed: tuple[type[A], type[B]]
reveal_type(V.__default__) # revealed: NoDefault
reveal_type(W) # revealed: W
reveal_type(W.__name__) # revealed: Literal["W"]
reveal_type(W.__bound__) # revealed: None
reveal_type(W.__constraints__) # revealed: tuple[()]
reveal_type(W.__default__) # revealed: type[A]
reveal_type(X) # revealed: X
reveal_type(X.__name__) # revealed: Literal["X"]
reveal_type(X.__bound__) # revealed: type[A]
reveal_type(X.__constraints__) # revealed: tuple[()]
reveal_type(X.__default__) # revealed: type[A1]
class A: ...
class B: ...
class A1(A): ...
```
## Minimum two constraints
A typevar with less than two constraints emits a diagnostic and is treated as unconstrained:
A typevar with less than two constraints emits a diagnostic:
```py
# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types"
def f[T: (int,)]():
reveal_type(T.__constraints__) # revealed: tuple[()]
pass
```

View File

@@ -75,10 +75,11 @@ Literal: _SpecialForm
```py
from other import Literal
# error: [annotation-with-invalid-expression]
a1: Literal[26]
def f():
reveal_type(a1) # revealed: @Todo
reveal_type(a1) # revealed: @Todo(generics)
```
## Detecting typing_extensions.Literal

View File

@@ -18,7 +18,7 @@ async def foo():
pass
# TODO: should reveal `Unknown` because `__aiter__` is not defined
# revealed: @Todo
# revealed: @Todo(async iterables/iterators)
# error: [possibly-unresolved-reference]
reveal_type(x)
```
@@ -40,6 +40,6 @@ async def foo():
pass
# error: [possibly-unresolved-reference]
# revealed: @Todo
# revealed: @Todo(async iterables/iterators)
reveal_type(x)
```

View File

@@ -52,3 +52,29 @@ else:
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[1, 2, 4]
```
## Nested while loops
```py
def flag() -> bool:
return True
x = 1
while flag():
x = 2
while flag():
x = 3
if flag():
break
else:
x = 4
if flag():
break
else:
x = 5
reveal_type(x) # revealed: Literal[3, 4, 5]
```

View File

@@ -171,7 +171,7 @@ def f(*args, **kwargs) -> int: ...
class A(metaclass=f): ...
# TODO should be `type[int]`
reveal_type(A.__class__) # revealed: @Todo
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
```
## Cyclic

View File

@@ -17,8 +17,7 @@ reveal_type(__doc__) # revealed: str | None
# (needs support for `*` imports)
reveal_type(__spec__) # revealed: Unknown | None
# TODO: generics
reveal_type(__path__) # revealed: @Todo
reveal_type(__path__) # revealed: @Todo(generics)
class X:
reveal_type(__name__) # revealed: str
@@ -64,7 +63,7 @@ reveal_type(typing.__class__) # revealed: Literal[type]
# TODO: needs support for attribute access on instances, properties and generics;
# should be `dict[str, Any]`
reveal_type(typing.__dict__) # revealed: @Todo
reveal_type(typing.__dict__) # revealed: @Todo(instance attributes)
```
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
@@ -96,8 +95,8 @@ from foo import __dict__ as foo_dict
# TODO: needs support for attribute access on instances, properties, and generics;
# should be `dict[str, Any]` for both of these:
reveal_type(foo.__dict__) # revealed: @Todo
reveal_type(foo_dict) # revealed: @Todo
reveal_type(foo.__dict__) # revealed: @Todo(instance attributes)
reveal_type(foo_dict) # revealed: @Todo(instance attributes)
```
## Conditionally global or `ModuleType` attribute

View File

@@ -27,7 +27,7 @@ def int_instance() -> int:
a = b"abcde"[int_instance()]
# TODO: Support overloads... Should be `bytes`
reveal_type(a) # revealed: @Todo
reveal_type(a) # revealed: @Todo(return type)
```
## Slices
@@ -47,11 +47,11 @@ def int_instance() -> int: ...
byte_slice1 = b[int_instance() : int_instance()]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice1) # revealed: @Todo
reveal_type(byte_slice1) # revealed: @Todo(return type)
def bytes_instance() -> bytes: ...
byte_slice2 = bytes_instance()[0:5]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice2) # revealed: @Todo
reveal_type(byte_slice2) # revealed: @Todo(return type)
```

View File

@@ -12,13 +12,13 @@ x = [1, 2, 3]
reveal_type(x) # revealed: list
# TODO reveal int
reveal_type(x[0]) # revealed: @Todo
reveal_type(x[0]) # revealed: @Todo(return type)
# TODO reveal list
reveal_type(x[0:1]) # revealed: @Todo
reveal_type(x[0:1]) # revealed: @Todo(return type)
# TODO error
reveal_type(x["a"]) # revealed: @Todo
reveal_type(x["a"]) # revealed: @Todo(return type)
```
## Assignments within list assignment

View File

@@ -23,7 +23,7 @@ def int_instance() -> int: ...
a = "abcde"[int_instance()]
# TODO: Support overloads... Should be `str`
reveal_type(a) # revealed: @Todo
reveal_type(a) # revealed: @Todo(return type)
```
## Slices
@@ -78,13 +78,13 @@ def int_instance() -> int: ...
substring1 = s[int_instance() : int_instance()]
# TODO: Support overloads... Should be `LiteralString`
reveal_type(substring1) # revealed: @Todo
reveal_type(substring1) # revealed: @Todo(return type)
def str_instance() -> str: ...
substring2 = str_instance()[0:5]
# TODO: Support overloads... Should be `str`
reveal_type(substring2) # revealed: @Todo
reveal_type(substring2) # revealed: @Todo(return type)
```
## Unsupported slice types

View File

@@ -71,5 +71,5 @@ def int_instance() -> int: ...
tuple_slice = t[int_instance() : int_instance()]
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
reveal_type(tuple_slice) # revealed: @Todo
reveal_type(tuple_slice) # revealed: @Todo(return type)
```

View File

@@ -112,9 +112,9 @@ properties on instance types:
```py path=b.py
import sys
reveal_type(sys.version_info.micro) # revealed: @Todo
reveal_type(sys.version_info.releaselevel) # revealed: @Todo
reveal_type(sys.version_info.serial) # revealed: @Todo
reveal_type(sys.version_info.micro) # revealed: @Todo(instance attributes)
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(instance attributes)
reveal_type(sys.version_info.serial) # revealed: @Todo(instance attributes)
```
## Accessing fields by index/slice

View File

@@ -0,0 +1,71 @@
# Type aliases
## Basic
```py
type IntOrStr = int | str
reveal_type(IntOrStr) # revealed: typing.TypeAliasType
reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]
x: IntOrStr = 1
reveal_type(x) # revealed: Literal[1]
def f() -> None:
reveal_type(x) # revealed: int | str
```
## `__value__` attribute
```py
type IntOrStr = int | str
# TODO: This should either fall back to the specified type from typeshed,
# which is `Any`, or be the actual type of the runtime value expression
# `int | str`, i.e. `types.UnionType`.
reveal_type(IntOrStr.__value__) # revealed: @Todo(instance attributes)
```
## Invalid assignment
```py
type OptionalInt = int | None
# error: [invalid-assignment]
x: OptionalInt = "1"
```
## Type aliases in type aliases
```py
type IntOrStr = int | str
type IntOrStrOrBytes = IntOrStr | bytes
x: IntOrStrOrBytes = 1
def f() -> None:
reveal_type(x) # revealed: int | str | bytes
```
## Aliased type aliases
```py
type IntOrStr = int | str
MyIntOrStr = IntOrStr
x: MyIntOrStr = 1
# error: [invalid-assignment]
y: MyIntOrStr = None
```
## Generic type aliases
```py
type ListOrSet[T] = list[T] | set[T]
# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`,
# as specified in the `typeshed` stubs.
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(instance attributes)
```

View File

@@ -84,7 +84,7 @@ reveal_type(b) # revealed: Literal[2]
[a, *b, c, d] = (1, 2)
reveal_type(a) # revealed: Literal[1]
# TODO: Should be list[Any] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: Literal[2]
reveal_type(d) # revealed: Unknown
```
@@ -95,7 +95,7 @@ reveal_type(d) # revealed: Unknown
[a, *b, c] = (1, 2)
reveal_type(a) # revealed: Literal[1]
# TODO: Should be list[Any] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: Literal[2]
```
@@ -105,7 +105,7 @@ reveal_type(c) # revealed: Literal[2]
[a, *b, c] = (1, 2, 3)
reveal_type(a) # revealed: Literal[1]
# TODO: Should be list[int] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: Literal[3]
```
@@ -115,7 +115,7 @@ reveal_type(c) # revealed: Literal[3]
[a, *b, c, d] = (1, 2, 3, 4, 5, 6)
reveal_type(a) # revealed: Literal[1]
# TODO: Should be list[int] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: Literal[5]
reveal_type(d) # revealed: Literal[6]
```
@@ -127,7 +127,7 @@ reveal_type(d) # revealed: Literal[6]
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[2]
# TODO: Should be list[int] once support for assigning to starred expression is added
reveal_type(c) # revealed: @Todo
reveal_type(c) # revealed: @Todo(starred unpacking)
```
### Starred expression (6)
@@ -138,7 +138,7 @@ reveal_type(c) # revealed: @Todo
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: @Todo
reveal_type(d) # revealed: @Todo(starred unpacking)
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown
```
@@ -222,7 +222,7 @@ reveal_type(b) # revealed: LiteralString
(a, *b, c, d) = "ab"
reveal_type(a) # revealed: LiteralString
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: LiteralString
reveal_type(d) # revealed: Unknown
```
@@ -233,7 +233,7 @@ reveal_type(d) # revealed: Unknown
(a, *b, c) = "ab"
reveal_type(a) # revealed: LiteralString
# TODO: Should be list[Any] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: LiteralString
```
@@ -243,7 +243,7 @@ reveal_type(c) # revealed: LiteralString
(a, *b, c) = "abc"
reveal_type(a) # revealed: LiteralString
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: LiteralString
```
@@ -253,7 +253,7 @@ reveal_type(c) # revealed: LiteralString
(a, *b, c, d) = "abcdef"
reveal_type(a) # revealed: LiteralString
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: LiteralString
reveal_type(d) # revealed: LiteralString
```
@@ -265,5 +265,5 @@ reveal_type(d) # revealed: LiteralString
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
# TODO: Should be list[int] once support for assigning to starred expression is added
reveal_type(c) # revealed: @Todo
reveal_type(c) # revealed: @Todo(starred unpacking)
```

View File

@@ -17,5 +17,5 @@ class Manager:
async def test():
async with Manager() as f:
reveal_type(f) # revealed: @Todo
reveal_type(f) # revealed: @Todo(async with statement)
```

View File

@@ -36,12 +36,25 @@ use super::definition::{
mod except_handlers;
/// Are we in a state where a `break` statement is allowed?
#[derive(Clone, Copy, Debug)]
enum LoopState {
InLoop,
NotInLoop,
}
impl LoopState {
fn is_inside(self) -> bool {
matches!(self, LoopState::InLoop)
}
}
pub(super) struct SemanticIndexBuilder<'db> {
// Builder state
db: &'db dyn Db,
file: File,
module: &'db ParsedModule,
scope_stack: Vec<FileScopeId>,
scope_stack: Vec<(FileScopeId, LoopState)>,
/// The assignments we're currently visiting, with
/// the most recent visit at the end of the Vec
current_assignments: Vec<CurrentAssignment<'db>>,
@@ -103,9 +116,24 @@ impl<'db> SemanticIndexBuilder<'db> {
*self
.scope_stack
.last()
.map(|(scope, _)| scope)
.expect("Always to have a root scope")
}
fn loop_state(&self) -> LoopState {
self.scope_stack
.last()
.expect("Always to have a root scope")
.1
}
fn set_inside_loop(&mut self, state: LoopState) {
self.scope_stack
.last_mut()
.expect("Always to have a root scope")
.1 = state;
}
fn push_scope(&mut self, node: NodeWithScopeRef) {
let parent = self.current_scope();
self.push_scope_with_parent(node, Some(parent));
@@ -131,15 +159,16 @@ impl<'db> SemanticIndexBuilder<'db> {
let scope_id = ScopeId::new(self.db, self.file, file_scope_id, countme::Count::default());
self.scope_ids_by_scope.push(scope_id);
self.scopes_by_node.insert(node.node_key(), file_scope_id);
let previous = self.scopes_by_node.insert(node.node_key(), file_scope_id);
debug_assert_eq!(previous, None);
debug_assert_eq!(ast_id_scope, file_scope_id);
self.scope_stack.push(file_scope_id);
self.scope_stack.push((file_scope_id, LoopState::NotInLoop));
}
fn pop_scope(&mut self) -> FileScopeId {
let id = self.scope_stack.pop().expect("Root scope to be present");
let (id, _) = self.scope_stack.pop().expect("Root scope to be present");
let children_end = self.scopes.next_index();
let scope = &mut self.scopes[id];
scope.descendents = scope.descendents.start..children_end;
@@ -589,6 +618,27 @@ where
},
);
}
ast::Stmt::TypeAlias(type_alias) => {
let symbol = self.add_symbol(
type_alias
.name
.as_name_expr()
.map(|name| name.id.clone())
.unwrap_or("<unknown>".into()),
);
self.add_definition(symbol, type_alias);
self.visit_expr(&type_alias.name);
self.with_type_params(
NodeWithScopeRef::TypeAliasTypeParameters(type_alias),
type_alias.type_params.as_ref(),
|builder| {
builder.push_scope(NodeWithScopeRef::TypeAlias(type_alias));
builder.visit_expr(&type_alias.value);
builder.pop_scope()
},
);
}
ast::Stmt::Import(node) => {
for alias in &node.names {
let symbol_name = if let Some(asname) = &alias.asname {
@@ -763,7 +813,10 @@ where
// TODO: definitions created inside the body should be fully visible
// to other statements/expressions inside the body --Alex/Carl
let outer_loop_state = self.loop_state();
self.set_inside_loop(LoopState::InLoop);
self.visit_body(body);
self.set_inside_loop(outer_loop_state);
// Get the break states from the body of this loop, and restore the saved outer
// ones.
@@ -802,7 +855,9 @@ where
self.visit_body(body);
}
ast::Stmt::Break(_) => {
self.loop_break_states.push(self.flow_snapshot());
if self.loop_state().is_inside() {
self.loop_break_states.push(self.flow_snapshot());
}
}
ast::Stmt::For(
@@ -829,7 +884,10 @@ where
// TODO: Definitions created by loop variables
// (and definitions created inside the body)
// are fully visible to other statements/expressions inside the body --Alex/Carl
let outer_loop_state = self.loop_state();
self.set_inside_loop(LoopState::InLoop);
self.visit_body(body);
self.set_inside_loop(outer_loop_state);
let break_states =
std::mem::replace(&mut self.loop_break_states, saved_break_states);
@@ -1131,8 +1189,8 @@ where
// AST inspection, so we can't simplify here, need to record test expression for
// later checking)
self.visit_expr(test);
let constraint = self.record_expression_constraint(test);
let pre_if = self.flow_snapshot();
let constraint = self.record_expression_constraint(test);
self.visit_expr(body);
let post_body = self.flow_snapshot();
self.flow_restore(pre_if);

View File

@@ -83,6 +83,7 @@ pub(crate) enum DefinitionNodeRef<'a> {
For(ForStmtDefinitionNodeRef<'a>),
Function(&'a ast::StmtFunctionDef),
Class(&'a ast::StmtClassDef),
TypeAlias(&'a ast::StmtTypeAlias),
NamedExpression(&'a ast::ExprNamed),
Assignment(AssignmentDefinitionNodeRef<'a>),
AnnotatedAssignment(&'a ast::StmtAnnAssign),
@@ -109,6 +110,12 @@ impl<'a> From<&'a ast::StmtClassDef> for DefinitionNodeRef<'a> {
}
}
impl<'a> From<&'a ast::StmtTypeAlias> for DefinitionNodeRef<'a> {
fn from(node: &'a ast::StmtTypeAlias) -> Self {
Self::TypeAlias(node)
}
}
impl<'a> From<&'a ast::ExprNamed> for DefinitionNodeRef<'a> {
fn from(node: &'a ast::ExprNamed) -> Self {
Self::NamedExpression(node)
@@ -265,6 +272,9 @@ impl<'db> DefinitionNodeRef<'db> {
DefinitionNodeRef::Class(class) => {
DefinitionKind::Class(AstNodeRef::new(parsed, class))
}
DefinitionNodeRef::TypeAlias(type_alias) => {
DefinitionKind::TypeAlias(AstNodeRef::new(parsed, type_alias))
}
DefinitionNodeRef::NamedExpression(named) => {
DefinitionKind::NamedExpression(AstNodeRef::new(parsed, named))
}
@@ -358,6 +368,7 @@ impl<'db> DefinitionNodeRef<'db> {
}
Self::Function(node) => node.into(),
Self::Class(node) => node.into(),
Self::TypeAlias(node) => node.into(),
Self::NamedExpression(node) => node.into(),
Self::Assignment(AssignmentDefinitionNodeRef {
value: _,
@@ -434,6 +445,7 @@ pub enum DefinitionKind<'db> {
ImportFrom(ImportFromDefinitionKind),
Function(AstNodeRef<ast::StmtFunctionDef>),
Class(AstNodeRef<ast::StmtClassDef>),
TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
NamedExpression(AstNodeRef<ast::ExprNamed>),
Assignment(AssignmentDefinitionKind<'db>),
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
@@ -456,6 +468,7 @@ impl DefinitionKind<'_> {
// functions, classes, and imports always bind, and we consider them declarations
DefinitionKind::Function(_)
| DefinitionKind::Class(_)
| DefinitionKind::TypeAlias(_)
| DefinitionKind::Import(_)
| DefinitionKind::ImportFrom(_)
| DefinitionKind::TypeVar(_)
@@ -682,6 +695,12 @@ impl From<&ast::StmtClassDef> for DefinitionNodeKey {
}
}
impl From<&ast::StmtTypeAlias> for DefinitionNodeKey {
fn from(node: &ast::StmtTypeAlias) -> Self {
Self(NodeKey::from_node(node))
}
}
impl From<&ast::ExprName> for DefinitionNodeKey {
fn from(node: &ast::ExprName) -> Self {
Self(NodeKey::from_node(node))

View File

@@ -116,14 +116,11 @@ impl<'db> ScopeId<'db> {
// 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.node(db),
NodeWithScopeKind::ClassTypeParameters(_)
| NodeWithScopeKind::FunctionTypeParameters(_)
| NodeWithScopeKind::Function(_)
| NodeWithScopeKind::ListComprehension(_)
| NodeWithScopeKind::SetComprehension(_)
| NodeWithScopeKind::DictComprehension(_)
| NodeWithScopeKind::GeneratorExpression(_)
self.node(db).scope_kind(),
ScopeKind::Annotation
| ScopeKind::Function
| ScopeKind::TypeAlias
| ScopeKind::Comprehension
)
}
@@ -144,6 +141,12 @@ impl<'db> ScopeId<'db> {
}
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>",
@@ -201,6 +204,7 @@ pub enum ScopeKind {
Class,
Function,
Comprehension,
TypeAlias,
}
impl ScopeKind {
@@ -326,6 +330,8 @@ pub(crate) enum NodeWithScopeRef<'a> {
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),
@@ -347,6 +353,12 @@ impl NodeWithScopeRef<'_> {
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))
}
@@ -387,6 +399,12 @@ impl NodeWithScopeRef<'_> {
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))
}
@@ -411,6 +429,8 @@ pub enum NodeWithScopeKind {
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>),
@@ -423,9 +443,11 @@ impl NodeWithScopeKind {
match self {
Self::Module => ScopeKind::Module,
Self::Class(_) => ScopeKind::Class,
Self::Function(_) => ScopeKind::Function,
Self::Lambda(_) => ScopeKind::Function,
Self::FunctionTypeParameters(_) | Self::ClassTypeParameters(_) => ScopeKind::Annotation,
Self::Function(_) | Self::Lambda(_) => ScopeKind::Function,
Self::FunctionTypeParameters(_)
| Self::ClassTypeParameters(_)
| Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation,
Self::TypeAlias(_) => ScopeKind::TypeAlias,
Self::ListComprehension(_)
| Self::SetComprehension(_)
| Self::DictComprehension(_)
@@ -446,6 +468,13 @@ impl NodeWithScopeKind {
_ => panic!("expected function"),
}
}
pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias {
match self {
Self::TypeAlias(type_alias) => type_alias.node(),
_ => panic!("expected type alias"),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
@@ -455,6 +484,8 @@ pub(crate) enum NodeWithScopeKey {
ClassTypeParameters(NodeKey),
Function(NodeKey),
FunctionTypeParameters(NodeKey),
TypeAlias(NodeKey),
TypeAliasTypeParameters(NodeKey),
Lambda(NodeKey),
ListComprehension(NodeKey),
SetComprehension(NodeKey),

View File

@@ -324,6 +324,61 @@ fn declarations_ty<'db>(
}
}
/// Meta data for `Type::Todo`, which represents a known limitation in red-knot.
#[cfg(debug_assertions)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum TodoType {
FileAndLine(&'static str, u32),
Message(&'static str),
}
#[cfg(debug_assertions)]
impl std::fmt::Display for TodoType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TodoType::FileAndLine(file, line) => write!(f, "[{file}:{line}]"),
TodoType::Message(msg) => write!(f, "({msg})"),
}
}
}
#[cfg(not(debug_assertions))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TodoType;
#[cfg(not(debug_assertions))]
impl std::fmt::Display for TodoType {
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}
/// Create a `Type::Todo` variant to represent a known limitation in the type system.
///
/// It can be used with a custom message (preferred): `todo_type!("PEP 604 not supported")`,
/// or simply using `todo_type!()`, which will include information about the file and line.
#[cfg(debug_assertions)]
macro_rules! todo_type {
() => {
Type::Todo(crate::types::TodoType::FileAndLine(file!(), line!()))
};
($message:literal) => {
Type::Todo(crate::types::TodoType::Message($message))
};
}
#[cfg(not(debug_assertions))]
macro_rules! todo_type {
() => {
Type::Todo(crate::types::TodoType)
};
($message:literal) => {
Type::Todo(crate::types::TodoType)
};
}
pub(crate) use todo_type;
/// Representation of a type: a set of possible values at runtime.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub enum Type<'db> {
@@ -340,7 +395,9 @@ pub enum Type<'db> {
/// General rule: `Todo` should only propagate when the presence of the input `Todo` caused the
/// output to be unknown. An output should only be `Todo` if fixing all `Todo` inputs to be not
/// `Todo` would change the output type.
Todo,
///
/// This variant should be created with the `todo_type!` macro.
Todo(TodoType),
/// The empty set of values
Never,
/// A specific function object
@@ -384,7 +441,7 @@ impl<'db> Type<'db> {
}
pub const fn is_todo(&self) -> bool {
matches!(self, Type::Todo)
matches!(self, Type::Todo(_))
}
pub const fn class_literal(class: Class<'db>) -> Self {
@@ -480,6 +537,19 @@ impl<'db> Type<'db> {
.expect("Expected a Type::IntLiteral variant")
}
pub const fn into_known_instance(self) -> Option<KnownInstanceType<'db>> {
match self {
Type::KnownInstance(known_instance) => Some(known_instance),
_ => None,
}
}
#[track_caller]
pub fn expect_known_instance(self) -> KnownInstanceType<'db> {
self.into_known_instance()
.expect("Expected a Type::KnownInstance variant")
}
pub const fn is_boolean_literal(&self) -> bool {
matches!(self, Type::BooleanLiteral(..))
}
@@ -530,8 +600,8 @@ impl<'db> Type<'db> {
return true;
}
match (self, target) {
(Type::Unknown | Type::Any | Type::Todo, _) => false,
(_, Type::Unknown | Type::Any | Type::Todo) => false,
(Type::Unknown | Type::Any | Type::Todo(_), _) => false,
(_, Type::Unknown | Type::Any | Type::Todo(_)) => false,
(Type::Never, _) => true,
(_, Type::Never) => false,
(_, Type::Instance(InstanceType { class }))
@@ -666,8 +736,8 @@ impl<'db> Type<'db> {
return true;
}
match (self, target) {
(Type::Unknown | Type::Any | Type::Todo, _) => true,
(_, Type::Unknown | Type::Any | Type::Todo) => true,
(Type::Unknown | Type::Any | Type::Todo(_), _) => true,
(_, Type::Unknown | Type::Any | Type::Todo(_)) => true,
(Type::Union(union), ty) => union
.elements(db)
.iter()
@@ -703,6 +773,7 @@ impl<'db> Type<'db> {
// of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once
// we understand `sys.version_info` branches.
self == other
|| matches!((self, other), (Type::Todo(_), Type::Todo(_)))
|| matches!((self, other),
(
Type::Instance(InstanceType { class: self_class }),
@@ -726,7 +797,7 @@ impl<'db> Type<'db> {
(Type::Any, _) | (_, Type::Any) => false,
(Type::Unknown, _) | (_, Type::Unknown) => false,
(Type::Todo, _) | (_, Type::Todo) => false,
(Type::Todo(_), _) | (_, Type::Todo(_)) => false,
(Type::Union(union), other) | (other, Type::Union(union)) => union
.elements(db)
@@ -931,7 +1002,7 @@ impl<'db> Type<'db> {
Type::Any
| Type::Never
| Type::Unknown
| Type::Todo
| Type::Todo(_)
| Type::IntLiteral(..)
| Type::StringLiteral(..)
| Type::BytesLiteral(..)
@@ -1007,7 +1078,10 @@ impl<'db> Type<'db> {
Type::Instance(InstanceType { class }) => match class.known(db) {
Some(
KnownClass::NoneType | KnownClass::NoDefaultType | KnownClass::VersionInfo,
KnownClass::NoneType
| KnownClass::NoDefaultType
| KnownClass::VersionInfo
| KnownClass::TypeAliasType,
) => true,
Some(
KnownClass::Bool
@@ -1034,7 +1108,7 @@ impl<'db> Type<'db> {
Type::Any
| Type::Never
| Type::Unknown
| Type::Todo
| Type::Todo(_)
| Type::Union(..)
| Type::Intersection(..)
| Type::LiteralString => false,
@@ -1052,12 +1126,12 @@ impl<'db> Type<'db> {
Type::Any => Type::Any.into(),
Type::Never => {
// TODO: attribute lookup on Never type
Type::Todo.into()
todo_type!().into()
}
Type::Unknown => Type::Unknown.into(),
Type::FunctionLiteral(_) => {
// TODO: attribute lookup on function type
Type::Todo.into()
todo_type!().into()
}
Type::ModuleLiteral(file) => {
// `__dict__` is a very special member that is never overridden by module globals;
@@ -1107,7 +1181,7 @@ impl<'db> Type<'db> {
Type::IntLiteral(Program::get(db).target_version(db).minor.into())
}
// TODO MRO? get_own_instance_member, get_instance_member
_ => Type::Todo,
_ => todo_type!("instance attributes"),
};
ty.into()
}
@@ -1149,36 +1223,36 @@ impl<'db> Type<'db> {
Type::Intersection(_) => {
// TODO perform the get_member on each type in the intersection
// TODO return the intersection of those results
Type::Todo.into()
todo_type!().into()
}
Type::IntLiteral(_) => {
// TODO raise error
Type::Todo.into()
todo_type!().into()
}
Type::BooleanLiteral(_) => Type::Todo.into(),
Type::BooleanLiteral(_) => todo_type!().into(),
Type::StringLiteral(_) => {
// TODO defer to `typing.LiteralString`/`builtins.str` methods
// from typeshed's stubs
Type::Todo.into()
todo_type!().into()
}
Type::LiteralString => {
// TODO defer to `typing.LiteralString`/`builtins.str` methods
// from typeshed's stubs
Type::Todo.into()
todo_type!().into()
}
Type::BytesLiteral(_) => {
// TODO defer to Type::Instance(<bytes from typeshed>).member
Type::Todo.into()
todo_type!().into()
}
Type::SliceLiteral(_) => {
// TODO defer to `builtins.slice` methods
Type::Todo.into()
todo_type!().into()
}
Type::Tuple(_) => {
// TODO: implement tuple methods
Type::Todo.into()
todo_type!().into()
}
Type::Todo => Type::Todo.into(),
&todo @ Type::Todo(_) => todo.into(),
}
}
@@ -1188,7 +1262,7 @@ impl<'db> Type<'db> {
/// when `bool(x)` is called on an object `x`.
fn bool(&self, db: &'db dyn Db) -> Truthiness {
match self {
Type::Any | Type::Todo | Type::Never | Type::Unknown => Truthiness::Ambiguous,
Type::Any | Type::Todo(_) | Type::Never | Type::Unknown => Truthiness::Ambiguous,
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
Type::ClassLiteral(_) => {
@@ -1329,7 +1403,7 @@ impl<'db> Type<'db> {
// `Any` is callable, and its return type is also `Any`.
Type::Any => CallOutcome::callable(Type::Any),
Type::Todo => CallOutcome::callable(Type::Todo),
Type::Todo(_) => CallOutcome::callable(todo_type!()),
Type::Unknown => CallOutcome::callable(Type::Unknown),
@@ -1342,7 +1416,7 @@ impl<'db> Type<'db> {
),
// TODO: intersection types
Type::Intersection(_) => CallOutcome::callable(Type::Todo),
Type::Intersection(_) => CallOutcome::callable(todo_type!()),
_ => CallOutcome::not_callable(self),
}
@@ -1381,7 +1455,7 @@ impl<'db> Type<'db> {
};
}
if matches!(self, Type::Unknown | Type::Any | Type::Todo) {
if matches!(self, Type::Unknown | Type::Any | Type::Todo(_)) {
// Explicit handling of `Unknown` and `Any` necessary until `type[Unknown]` and
// `type[Any]` are not defined as `Todo` anymore.
return IterationOutcome::Iterable { element_ty: self };
@@ -1440,14 +1514,14 @@ impl<'db> Type<'db> {
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::Any => Type::Any,
Type::Todo => Type::Todo,
todo @ Type::Todo(_) => *todo,
Type::Unknown => Type::Unknown,
Type::Never => Type::Never,
Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(*class),
Type::SubclassOf(SubclassOfType { class }) => Type::instance(*class),
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
// TODO: we can probably do better here: --Alex
Type::Intersection(_) => Type::Todo,
Type::Intersection(_) => todo_type!(),
// TODO: calling `.to_instance()` on any of these should result in a diagnostic,
// since they already indicate that the object is an instance of some kind:
Type::BooleanLiteral(_)
@@ -1478,7 +1552,11 @@ impl<'db> Type<'db> {
Type::Unknown => Type::Unknown,
// TODO map this to a new `Type::TypeVar` variant
Type::KnownInstance(KnownInstanceType::TypeVar(_)) => *self,
_ => Type::Todo,
Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => alias.value_ty(db),
Type::KnownInstance(KnownInstanceType::Never | KnownInstanceType::NoReturn) => {
Type::Never
}
_ => todo_type!(),
}
}
@@ -1553,8 +1631,8 @@ impl<'db> Type<'db> {
// TODO: `type[Unknown]`?
Type::Unknown => Type::Unknown,
// TODO intersections
Type::Intersection(_) => Type::Todo,
Type::Todo => Type::Todo,
Type::Intersection(_) => todo_type!(),
todo @ Type::Todo(_) => *todo,
}
}
@@ -1642,6 +1720,7 @@ pub enum KnownClass {
// Typing
SpecialForm,
TypeVar,
TypeAliasType,
NoDefaultType,
// sys
VersionInfo,
@@ -1668,6 +1747,7 @@ impl<'db> KnownClass {
Self::NoneType => "NoneType",
Self::SpecialForm => "_SpecialForm",
Self::TypeVar => "TypeVar",
Self::TypeAliasType => "TypeAliasType",
Self::NoDefaultType => "_NoDefaultType",
// This is the name the type of `sys.version_info` has in typeshed,
// which is different to what `type(sys.version_info).__name__` is at runtime.
@@ -1706,7 +1786,7 @@ impl<'db> KnownClass {
Self::VersionInfo => CoreStdlibModule::Sys,
Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types,
Self::NoneType => CoreStdlibModule::Typeshed,
Self::SpecialForm | Self::TypeVar => CoreStdlibModule::Typing,
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType => CoreStdlibModule::Typing,
// TODO when we understand sys.version_info, we will need an explicit fallback here,
// because typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
// singleton, but not for `typing._NoDefaultType`
@@ -1720,7 +1800,7 @@ impl<'db> KnownClass {
const fn is_singleton(self) -> bool {
// TODO there are other singleton types (EllipsisType, NotImplementedType)
match self {
Self::NoneType | Self::NoDefaultType | Self::VersionInfo => true,
Self::NoneType | Self::NoDefaultType | Self::VersionInfo | Self::TypeAliasType => true,
Self::Bool
| Self::Object
| Self::Bytes
@@ -1762,6 +1842,7 @@ impl<'db> KnownClass {
"NoneType" => Self::NoneType,
"ModuleType" => Self::ModuleType,
"FunctionType" => Self::FunctionType,
"TypeAliasType" => Self::TypeAliasType,
"_SpecialForm" => Self::SpecialForm,
"_NoDefaultType" => Self::NoDefaultType,
"_version_info" => Self::VersionInfo,
@@ -1795,7 +1876,7 @@ impl<'db> KnownClass {
| Self::VersionInfo
| Self::FunctionType => module.name() == self.canonical_module().as_str(),
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
Self::SpecialForm | Self::TypeVar | Self::NoDefaultType => {
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => {
matches!(module.name().as_str(), "typing" | "typing_extensions")
}
}
@@ -1809,24 +1890,42 @@ pub enum KnownInstanceType<'db> {
Literal,
/// 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,
/// A single instance of `typing.TypeVar`
TypeVar(TypeVarInstance<'db>),
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
TypeAliasType(TypeAliasType<'db>),
// TODO: fill this enum out with more special forms, etc.
}
impl<'db> KnownInstanceType<'db> {
pub const fn as_str(self) -> &'static str {
match self {
KnownInstanceType::Literal => "Literal",
KnownInstanceType::Optional => "Optional",
KnownInstanceType::TypeVar(_) => "TypeVar",
Self::Literal => "Literal",
Self::Optional => "Optional",
Self::Union => "Union",
Self::TypeVar(_) => "TypeVar",
Self::NoReturn => "NoReturn",
Self::Never => "Never",
Self::TypeAliasType(_) => "TypeAliasType",
}
}
/// Evaluate the known instance in boolean context
pub const fn bool(self) -> Truthiness {
match self {
Self::Literal | Self::Optional | Self::TypeVar(_) => Truthiness::AlwaysTrue,
Self::Literal
| Self::Optional
| Self::TypeVar(_)
| Self::Union
| Self::NoReturn
| Self::Never
| Self::TypeAliasType(_) => Truthiness::AlwaysTrue,
}
}
@@ -1835,7 +1934,11 @@ impl<'db> KnownInstanceType<'db> {
match self {
Self::Literal => "typing.Literal",
Self::Optional => "typing.Optional",
Self::Union => "typing.Union",
Self::NoReturn => "typing.NoReturn",
Self::Never => "typing.Never",
Self::TypeVar(typevar) => typevar.name(db),
Self::TypeAliasType(_) => "typing.TypeAliasType",
}
}
@@ -1844,7 +1947,11 @@ impl<'db> KnownInstanceType<'db> {
match self {
Self::Literal => KnownClass::SpecialForm,
Self::Optional => KnownClass::SpecialForm,
Self::Union => KnownClass::SpecialForm,
Self::NoReturn => KnownClass::SpecialForm,
Self::Never => KnownClass::SpecialForm,
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
}
}
@@ -1864,6 +1971,9 @@ impl<'db> KnownInstanceType<'db> {
match (module.name().as_str(), instance_name) {
("typing" | "typing_extensions", "Literal") => Some(Self::Literal),
("typing" | "typing_extensions", "Optional") => Some(Self::Optional),
("typing" | "typing_extensions", "Union") => Some(Self::Union),
("typing" | "typing_extensions", "NoReturn") => Some(Self::NoReturn),
("typing" | "typing_extensions", "Never") => Some(Self::Never),
_ => None,
}
}
@@ -1871,23 +1981,7 @@ impl<'db> KnownInstanceType<'db> {
fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
let ty = match (self, name) {
(Self::TypeVar(typevar), "__name__") => Type::string_literal(db, typevar.name(db)),
(Self::TypeVar(typevar), "__bound__") => typevar
.upper_bound(db)
.map(|ty| ty.to_meta_type(db))
.unwrap_or_else(|| KnownClass::NoneType.to_instance(db)),
(Self::TypeVar(typevar), "__constraints__") => {
let tuple_elements: Vec<Type<'db>> = typevar
.constraints(db)
.unwrap_or_default()
.iter()
.map(|ty| ty.to_meta_type(db))
.collect();
Type::tuple(db, &tuple_elements)
}
(Self::TypeVar(typevar), "__default__") => typevar
.default_ty(db)
.map(|ty| ty.to_meta_type(db))
.unwrap_or_else(|| KnownClass::NoDefaultType.to_instance(db)),
(Self::TypeAliasType(alias), "__name__") => Type::string_literal(db, alias.name(db)),
_ => return self.instance_fallback(db).member(db, name),
};
ty.into()
@@ -1919,6 +2013,7 @@ pub struct TypeVarInstance<'db> {
}
impl<'db> TypeVarInstance<'db> {
#[allow(unused)]
pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option<Type<'db>> {
if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) {
Some(ty)
@@ -1927,6 +2022,7 @@ impl<'db> TypeVarInstance<'db> {
}
}
#[allow(unused)]
pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&[Type<'db>]> {
if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) {
Some(tuple.elements(db))
@@ -2599,7 +2695,7 @@ impl<'db> Class<'db> {
// TODO: If the metaclass is not a class, we should verify that it's a callable
// which accepts the same arguments as `type.__new__` (otherwise error), and return
// the meta-type of its return type. (And validate that is a class type?)
return Ok(Type::Todo);
return Ok(todo_type!("metaclass not a class"));
};
// Reconcile all base classes' metaclasses with the candidate metaclass.
@@ -2713,6 +2809,27 @@ impl<'db> Class<'db> {
}
}
#[salsa::interned]
pub struct TypeAliasType<'db> {
#[return_ref]
pub name: ast::name::Name,
rhs_scope: ScopeId<'db>,
}
#[salsa::tracked]
impl<'db> TypeAliasType<'db> {
#[salsa::tracked]
pub fn value_ty(self, db: &'db dyn Db) -> Type<'db> {
let scope = self.rhs_scope(db);
let type_alias_stmt_node = scope.node(db).expect_type_alias();
let definition = semantic_index(db, scope.file(db)).definition(type_alias_stmt_node);
definition_expression_ty(db, definition, &type_alias_stmt_node.value)
}
}
/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct MetaclassCandidate<'db> {
@@ -2899,6 +3016,11 @@ impl<'db> TupleType<'db> {
}
}
// Make sure that the `Type` enum does not grow unexpectedly.
#[cfg(not(debug_assertions))]
#[cfg(target_pointer_width = "64")]
static_assertions::assert_eq_size!(Type, [u8; 16]);
#[cfg(test)]
pub(crate) mod tests {
use super::*;
@@ -2914,13 +3036,6 @@ pub(crate) mod tests {
use ruff_python_ast as ast;
use test_case::test_case;
#[cfg(target_pointer_width = "64")]
#[test]
fn no_bloat_enum_sizes() {
use std::mem::size_of;
assert_eq!(size_of::<Type>(), 16);
}
pub(crate) fn setup_db() -> TestDb {
let db = TestDb::new();
@@ -2974,7 +3089,7 @@ pub(crate) mod tests {
Ty::Unknown => Type::Unknown,
Ty::None => Type::none(db),
Ty::Any => Type::Any,
Ty::Todo => Type::Todo,
Ty::Todo => todo_type!("Ty::Todo"),
Ty::IntLiteral(n) => Type::IntLiteral(n),
Ty::StringLiteral(s) => Type::string_literal(db, s),
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
@@ -3565,4 +3680,95 @@ pub(crate) mod tests {
Ok(())
}
#[test]
fn type_alias_types() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/mod.py",
r#"
type Alias1 = int
type Alias2 = int
"#,
)?;
let mod_py = system_path_to_file(&db, "src/mod.py")?;
let ty_alias1 = global_symbol(&db, mod_py, "Alias1").expect_type();
let ty_alias2 = global_symbol(&db, mod_py, "Alias2").expect_type();
let Type::KnownInstance(KnownInstanceType::TypeAliasType(alias1)) = ty_alias1 else {
panic!("Expected TypeAliasType, got {ty_alias1:?}");
};
assert_eq!(alias1.name(&db), "Alias1");
assert_eq!(alias1.value_ty(&db), KnownClass::Int.to_instance(&db));
// Two type aliases are distinct and disjoint, even if they refer to the same type
assert!(!ty_alias1.is_equivalent_to(&db, ty_alias2));
assert!(ty_alias1.is_disjoint_from(&db, ty_alias2));
Ok(())
}
/// All other tests also make sure that `Type::Todo` works as expected. This particular
/// test makes sure that we handle `Todo` types correctly, even if they originate from
/// different sources.
#[test]
fn todo_types() {
let db = setup_db();
let todo1 = todo_type!("1");
let todo2 = todo_type!("2");
let todo3 = todo_type!();
let todo4 = todo_type!();
assert!(todo1.is_equivalent_to(&db, todo2));
assert!(todo3.is_equivalent_to(&db, todo4));
assert!(todo1.is_equivalent_to(&db, todo3));
assert!(todo1.is_subtype_of(&db, todo2));
assert!(todo2.is_subtype_of(&db, todo1));
assert!(todo3.is_subtype_of(&db, todo4));
assert!(todo4.is_subtype_of(&db, todo3));
assert!(todo1.is_subtype_of(&db, todo3));
assert!(todo3.is_subtype_of(&db, todo1));
let int = KnownClass::Int.to_instance(&db);
assert!(int.is_assignable_to(&db, todo1));
assert!(int.is_assignable_to(&db, todo3));
assert!(todo1.is_assignable_to(&db, int));
assert!(todo3.is_assignable_to(&db, int));
// We lose information when combining several `Todo` types. This is an
// acknowledged limitation of the current implementation. We can not
// easily store the meta information of several `Todo`s in a single
// variant, as `TodoType` needs to implement `Copy`, meaning it can't
// contain `Vec`/`Box`/etc., and can't be boxed itself.
//
// Lifting this restriction would require us to intern `TodoType` in
// salsa, but that would mean we would have to pass in `db` everywhere.
// A union of several `Todo` types collapses to a single `Todo` type:
assert!(UnionType::from_elements(&db, vec![todo1, todo2, todo3, todo4]).is_todo());
// And similar for intersection types:
assert!(IntersectionBuilder::new(&db)
.add_positive(todo1)
.add_positive(todo2)
.add_positive(todo3)
.add_positive(todo4)
.build()
.is_todo());
assert!(IntersectionBuilder::new(&db)
.add_positive(todo1)
.add_negative(todo2)
.add_positive(todo3)
.add_negative(todo4)
.build()
.is_todo());
}
}

View File

@@ -31,7 +31,7 @@ use crate::{Db, FxOrderSet};
use smallvec::SmallVec;
pub(crate) struct UnionBuilder<'db> {
elements: SmallVec<[Type<'db>; 1]>,
elements: Vec<Type<'db>>,
db: &'db dyn Db,
}
@@ -39,7 +39,7 @@ impl<'db> UnionBuilder<'db> {
pub(crate) fn new(db: &'db dyn Db) -> Self {
Self {
db,
elements: SmallVec::new(),
elements: vec![],
}
}
@@ -48,22 +48,13 @@ impl<'db> UnionBuilder<'db> {
match ty {
Type::Union(union) => {
let new_elements = union.elements(self.db);
if self.elements.is_empty() {
self.elements.extend_from_slice(new_elements);
} else {
self.elements.reserve(new_elements.len());
for element in new_elements {
self = self.add(*element);
}
self.elements.reserve(new_elements.len());
for element in new_elements {
self = self.add(*element);
}
}
Type::Never => {}
_ => {
if self.elements.is_empty() {
self.elements.push(ty);
return self;
}
let bool_pair = if let Type::BooleanLiteral(b) = ty {
Some(Type::BooleanLiteral(!b))
} else {
@@ -318,7 +309,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
self.add_positive(db, *neg);
}
}
ty @ (Type::Any | Type::Unknown | Type::Todo) => {
ty @ (Type::Any | Type::Unknown | Type::Todo(_)) => {
// Adding any of these types to the negative side of an intersection
// is equivalent to adding it to the positive side. We do this to
// simplify the representation.
@@ -388,7 +379,7 @@ mod tests {
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::stdlib::typing_symbol;
use crate::types::{global_symbol, KnownClass, UnionBuilder};
use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder};
use crate::ProgramSettings;
use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
@@ -996,7 +987,7 @@ mod tests {
#[test_case(Type::Any)]
#[test_case(Type::Unknown)]
#[test_case(Type::Todo)]
#[test_case(todo_type!())]
fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) {
let db = setup_db();

View File

@@ -77,7 +77,7 @@ impl Display for DisplayRepresentation<'_> {
}
// `[Type::Todo]`'s display should be explicit that is not a valid display of
// any other type
Type::Todo => f.write_str("@Todo"),
Type::Todo(todo) => write!(f, "@Todo{todo}"),
Type::ModuleLiteral(file) => {
write!(f, "<module '{:?}'>", file.path(self.db))
}

View File

@@ -31,6 +31,7 @@ use std::num::NonZeroU32;
use itertools::Itertools;
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::visitor::{self, Visitor};
use ruff_python_ast::{self as ast, AnyNodeRef, Expr, ExprContext, UnaryOp};
use rustc_hash::{FxHashMap, FxHashSet};
use salsa;
@@ -52,11 +53,12 @@ use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder
use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, typing_extensions_symbol,
Boundness, Class, ClassLiteralType, FunctionType, InstanceType, IntersectionBuilder,
IntersectionType, IterationOutcome, KnownClass, KnownFunction, KnownInstanceType,
MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, Symbol, Truthiness, TupleType, Type,
TypeArrayDisplay, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, todo_type,
typing_extensions_symbol, Boundness, Class, ClassLiteralType, FunctionType, InstanceType,
IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction,
KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, Symbol,
Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay, TypeVarBoundOrConstraints,
TypeVarInstance, UnionBuilder, UnionType,
};
use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
@@ -438,6 +440,12 @@ impl<'db> TypeInferenceBuilder<'db> {
NodeWithScopeKind::FunctionTypeParameters(function) => {
self.infer_function_type_params(function.node());
}
NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => {
self.infer_type_alias_type_params(type_alias.node());
}
NodeWithScopeKind::TypeAlias(type_alias) => {
self.infer_type_alias(type_alias.node());
}
NodeWithScopeKind::ListComprehension(comprehension) => {
self.infer_list_comprehension_expression_scope(comprehension.node());
}
@@ -605,6 +613,9 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_function_definition(function.node(), definition);
}
DefinitionKind::Class(class) => self.infer_class_definition(class.node(), definition),
DefinitionKind::TypeAlias(type_alias) => {
self.infer_type_alias_definition(type_alias.node(), definition);
}
DefinitionKind::Import(import) => {
self.infer_import_definition(import.node(), definition);
}
@@ -847,6 +858,19 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_parameters(&function.parameters);
}
fn infer_type_alias_type_params(&mut self, type_alias: &ast::StmtTypeAlias) {
let type_params = type_alias
.type_params
.as_ref()
.expect("type alias type params scope without type params");
self.infer_type_parameters(type_params);
}
fn infer_type_alias(&mut self, type_alias: &ast::StmtTypeAlias) {
self.infer_annotation_expression(&type_alias.value, DeferredExpressionState::Deferred);
}
fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) {
self.infer_body(&function.body);
}
@@ -1027,7 +1051,7 @@ impl<'db> TypeInferenceBuilder<'db> {
) {
// TODO(dhruvmanila): Annotation expression is resolved at the enclosing scope, infer the
// parameter type from there
let annotated_ty = Type::Todo;
let annotated_ty = todo_type!("function parameter type");
if parameter.annotation.is_some() {
self.add_declaration_with_binding(
parameter.into(),
@@ -1107,6 +1131,33 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
fn infer_type_alias_definition(
&mut self,
type_alias: &ast::StmtTypeAlias,
definition: Definition<'db>,
) {
self.infer_expression(&type_alias.name);
let rhs_scope = self
.index
.node_scope(NodeWithScopeRef::TypeAlias(type_alias))
.to_scope_id(self.db, self.file);
let type_alias_ty =
Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::new(
self.db,
&type_alias.name.as_name_expr().unwrap().id,
rhs_scope,
)));
self.add_declaration_with_binding(
type_alias.into(),
definition,
type_alias_ty,
type_alias_ty,
);
}
fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) {
let ast::StmtIf {
range: _,
@@ -1236,7 +1287,7 @@ impl<'db> TypeInferenceBuilder<'db> {
) -> Type<'db> {
// TODO: Handle async with statements (they use `aenter` and `aexit`)
if is_async {
return Type::Todo;
return todo_type!("async with statement");
}
let context_manager_ty = context_expression_ty.to_meta_type(self.db);
@@ -1386,12 +1437,12 @@ impl<'db> TypeInferenceBuilder<'db> {
self.db,
tuple.elements(self.db).iter().map(|ty| {
ty.into_class_literal()
.map_or(Type::Todo, |ClassLiteralType { class }| {
.map_or(todo_type!(), |ClassLiteralType { class }| {
Type::instance(class)
})
}),
),
_ => Type::Todo,
_ => todo_type!("exception type"),
}
};
@@ -1461,7 +1512,7 @@ impl<'db> TypeInferenceBuilder<'db> {
default,
} = node;
self.infer_optional_expression(default.as_deref());
self.add_declaration_with_binding(node.into(), definition, Type::Todo, Type::Todo);
self.add_declaration_with_binding(node.into(), definition, todo_type!(), todo_type!());
}
fn infer_typevartuple_definition(
@@ -1475,7 +1526,7 @@ impl<'db> TypeInferenceBuilder<'db> {
default,
} = node;
self.infer_optional_expression(default.as_deref());
self.add_declaration_with_binding(node.into(), definition, Type::Todo, Type::Todo);
self.add_declaration_with_binding(node.into(), definition, todo_type!(), todo_type!());
}
fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) {
@@ -1510,7 +1561,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// against the subject expression type (which we can query via `infer_expression_types`)
// and extract the type at the `index` position if the pattern matches. This will be
// similar to the logic in `self.infer_assignment_definition`.
self.add_binding(pattern.into(), definition, Type::Todo);
self.add_binding(pattern.into(), definition, todo_type!());
}
fn infer_match_pattern(&mut self, pattern: &ast::Pattern) {
@@ -1829,17 +1880,8 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_augmented_op(assignment, target_type, value_type)
}
fn infer_type_alias_statement(&mut self, type_alias_statement: &ast::StmtTypeAlias) {
let ast::StmtTypeAlias {
range: _,
name,
type_params: _,
value,
} = type_alias_statement;
self.infer_expression(value);
self.infer_expression(name);
// TODO: properly handle generic type aliases, which need their own annotation scope
fn infer_type_alias_statement(&mut self, node: &ast::StmtTypeAlias) {
self.infer_definition(node);
}
fn infer_for_statement(&mut self, for_statement: &ast::StmtFor) {
@@ -1874,8 +1916,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let iterable_ty = self.infer_standalone_expression(iterable);
let loop_var_value_ty = if is_async {
// TODO(Alex): async iterables/iterators!
Type::Todo
todo_type!("async iterables/iterators")
} else {
iterable_ty
.iterate(self.db)
@@ -2203,7 +2244,7 @@ impl<'db> TypeInferenceBuilder<'db> {
ast::Expr::Await(await_expression) => self.infer_await_expression(await_expression),
ast::Expr::IpyEscapeCommand(_) => {
// TODO Implement Ipy escape command support
Type::Todo
todo_type!()
}
};
@@ -2397,7 +2438,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_first_comprehension_iter(generators);
// TODO generator type
Type::Todo
todo_type!()
}
fn infer_list_comprehension_expression(&mut self, listcomp: &ast::ExprListComp) -> Type<'db> {
@@ -2410,7 +2451,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_first_comprehension_iter(generators);
// TODO list type
Type::Todo
todo_type!()
}
fn infer_dict_comprehension_expression(&mut self, dictcomp: &ast::ExprDictComp) -> Type<'db> {
@@ -2424,7 +2465,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_first_comprehension_iter(generators);
// TODO dict type
Type::Todo
todo_type!()
}
fn infer_set_comprehension_expression(&mut self, setcomp: &ast::ExprSetComp) -> Type<'db> {
@@ -2437,7 +2478,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_first_comprehension_iter(generators);
// TODO set type
Type::Todo
todo_type!()
}
fn infer_generator_expression_scope(&mut self, generator: &ast::ExprGenerator) {
@@ -2552,7 +2593,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let target_ty = if is_async {
// TODO: async iterables/iterators! -- Alex
Type::Todo
todo_type!("async iterables/iterators")
} else {
iterable_ty
.iterate(self.db)
@@ -2642,7 +2683,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
// TODO function type
Type::Todo
todo_type!()
}
fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> {
@@ -2673,7 +2714,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.unwrap_with_diagnostic(value.as_ref().into(), &mut self.diagnostics);
// TODO
Type::Todo
todo_type!()
}
fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> {
@@ -2682,7 +2723,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_optional_expression(value.as_deref());
// TODO awaitable type
Type::Todo
todo_type!()
}
fn infer_yield_from_expression(&mut self, yield_from: &ast::ExprYieldFrom) -> Type<'db> {
@@ -2694,7 +2735,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.unwrap_with_diagnostic(value.as_ref().into(), &mut self.diagnostics);
// TODO get type from `ReturnType` of generator
Type::Todo
todo_type!()
}
fn infer_await_expression(&mut self, await_expression: &ast::ExprAwait) -> Type<'db> {
@@ -2703,7 +2744,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_expression(value);
// TODO awaitable type
Type::Todo
todo_type!()
}
/// Look up a name reference that isn't bound in the local scope.
@@ -2979,7 +3020,7 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown
}
}
_ => Type::Todo, // TODO other unary op types
_ => todo_type!(), // TODO other unary op types
}
}
@@ -3227,7 +3268,7 @@ impl<'db> TypeInferenceBuilder<'db> {
(left, Type::BooleanLiteral(bool_value), op) => {
self.infer_binary_expression_type(left, Type::IntLiteral(i64::from(bool_value)), op)
}
_ => Some(Type::Todo), // TODO
_ => Some(todo_type!()), // TODO
}
}
@@ -3665,7 +3706,7 @@ impl<'db> TypeInferenceBuilder<'db> {
).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`");
match eq_result {
Type::Todo => return Ok(Type::Todo),
todo @ Type::Todo(_) => return Ok(todo),
ty => match ty.bool(self.db) {
Truthiness::AlwaysTrue => eq_count += 1,
Truthiness::AlwaysFalse => not_eq_count += 1,
@@ -3690,7 +3731,7 @@ impl<'db> TypeInferenceBuilder<'db> {
);
Ok(match eq_result {
Type::Todo => Type::Todo,
todo @ Type::Todo(_) => todo,
ty => match ty.bool(self.db) {
Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()),
_ => KnownClass::Bool.to_instance(self.db),
@@ -3745,7 +3786,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: handle more types
_ => match op {
ast::CmpOp::Is | ast::CmpOp::IsNot => Ok(KnownClass::Bool.to_instance(self.db)),
_ => Ok(Type::Todo),
_ => Ok(todo_type!()),
},
}
}
@@ -3773,7 +3814,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match pairwise_eq_result {
// If propagation is required, return the result as is
Type::Todo => return Ok(Type::Todo),
todo @ Type::Todo(_) => return Ok(todo),
ty => match ty.bool(self.db) {
// - AlwaysTrue : Continue to the next pair for lexicographic comparison
Truthiness::AlwaysTrue => continue,
@@ -4174,24 +4215,6 @@ impl<'db> TypeInferenceBuilder<'db> {
// Annotation expressions also get special handling for `*args` and `**kwargs`.
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
ast::Expr::BytesLiteral(bytes) => {
self.diagnostics.add(
bytes.into(),
"annotation-byte-string",
format_args!("Type expressions cannot use bytes literal"),
);
Type::Unknown
}
ast::Expr::FString(fstring) => {
self.diagnostics.add(
fstring.into(),
"annotation-f-string",
format_args!("Type expressions cannot use f-strings"),
);
Type::Unknown
}
// All other annotation expressions are (possibly) valid type expressions, so handle
// them there instead.
type_expr => self.infer_type_expression_no_store(type_expr),
@@ -4202,6 +4225,72 @@ impl<'db> TypeInferenceBuilder<'db> {
annotation_ty
}
/// Walk child expressions of the given AST node and store the given type for each of them.
/// Does not store a type for the root expression. Resets to normal type inference for all
/// expressions with a nested scope (lambda, named expression, comprehensions, generators).
fn store_type_for_sub_expressions_of(&mut self, expression: &ast::Expr, ty: Type<'db>) {
struct StoreTypeVisitor<'a, 'db> {
builder: &'a mut TypeInferenceBuilder<'db>,
ty: Type<'db>,
is_root_node: bool,
}
impl<'a, 'db> StoreTypeVisitor<'a, 'db> {
fn new(builder: &'a mut TypeInferenceBuilder<'db>, ty: Type<'db>) -> Self {
Self {
builder,
ty,
is_root_node: true,
}
}
fn store(&mut self, expr: &ast::Expr) {
if self.is_root_node {
self.is_root_node = false;
} else {
self.builder.store_expression_type(expr, self.ty);
}
}
}
impl<'a, 'db, 'ast> Visitor<'ast> for StoreTypeVisitor<'a, 'db> {
fn visit_expr(&mut self, expr: &'ast Expr) {
match expr {
ast::Expr::Lambda(lambda) => {
self.builder.infer_lambda_expression(lambda);
self.store(expr);
}
ast::Expr::Named(named) => {
self.builder.infer_named_expression(named);
self.store(expr);
}
ast::Expr::ListComp(list_comp) => {
self.builder.infer_list_comprehension_expression(list_comp);
self.store(expr);
}
ast::Expr::SetComp(set_comp) => {
self.builder.infer_set_comprehension_expression(set_comp);
self.store(expr);
}
ast::Expr::DictComp(dict_comp) => {
self.builder.infer_dict_comprehension_expression(dict_comp);
self.store(expr);
}
ast::Expr::Generator(generator) => {
self.builder.infer_generator_expression(generator);
self.store(expr);
}
_ => {
self.store(expr);
visitor::walk_expr(self, expr);
}
}
}
}
StoreTypeVisitor::new(self, ty).visit_expr(expression);
}
/// Infer the type of a string annotation expression.
fn infer_string_annotation_expression(&mut self, string: &ast::ExprStringLiteral) -> Type<'db> {
match parse_string_annotation(self.db, self.file, string) {
@@ -4263,7 +4352,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_name_expression(name).in_type_expression(self.db)
}
ast::ExprContext::Invalid => Type::Unknown,
ast::ExprContext::Store | ast::ExprContext::Del => Type::Todo,
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(),
},
ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
@@ -4271,7 +4360,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.infer_attribute_expression(attribute_expression)
.in_type_expression(self.db),
ast::ExprContext::Invalid => Type::Unknown,
ast::ExprContext::Store | ast::ExprContext::Del => Type::Todo,
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(),
},
ast::Expr::NoneLiteral(_literal) => Type::none(self.db),
@@ -4281,14 +4370,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: an Ellipsis literal *on its own* does not have any meaning in annotation
// expressions, but is meaningful in the context of a number of special forms.
ast::Expr::EllipsisLiteral(_literal) => Type::Todo,
// Other literals do not have meaningful values in the annotation expression context.
// However, we will we want to handle these differently when working with special forms,
// since (e.g.) `123` is not valid in an annotation expression but `Literal[123]` is.
ast::Expr::BytesLiteral(_literal) => Type::Todo,
ast::Expr::NumberLiteral(_literal) => Type::Todo,
ast::Expr::BooleanLiteral(_literal) => Type::Todo,
ast::Expr::EllipsisLiteral(_literal) => todo_type!(),
ast::Expr::Subscript(subscript) => {
let ast::ExprSubscript {
@@ -4331,94 +4413,69 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO PEP 646
ast::Expr::Starred(starred) => {
self.infer_starred_expression(starred);
Type::Todo
todo_type!()
}
// Forms which are invalid in the context of annotation expressions: we infer their
// nested expressions as normal expressions, but the type of the top-level expression is
// always `Type::Unknown` in these cases.
ast::Expr::BoolOp(bool_op) => {
self.infer_boolean_expression(bool_op);
Type::Unknown
}
ast::Expr::Named(named) => {
self.infer_named_expression(named);
Type::Unknown
}
ast::Expr::UnaryOp(unary) => {
self.infer_unary_expression(unary);
Type::Unknown
}
ast::Expr::Lambda(lambda_expression) => {
self.infer_lambda_expression(lambda_expression);
Type::Unknown
}
ast::Expr::If(if_expression) => {
self.infer_if_expression(if_expression);
Type::Unknown
}
ast::Expr::Dict(dict) => {
self.infer_dict_expression(dict);
Type::Unknown
}
ast::Expr::Set(set) => {
self.infer_set_expression(set);
Type::Unknown
}
ast::Expr::ListComp(listcomp) => {
self.infer_list_comprehension_expression(listcomp);
Type::Unknown
}
ast::Expr::SetComp(setcomp) => {
self.infer_set_comprehension_expression(setcomp);
Type::Unknown
}
ast::Expr::DictComp(dictcomp) => {
self.infer_dict_comprehension_expression(dictcomp);
Type::Unknown
}
ast::Expr::Generator(generator) => {
self.infer_generator_expression(generator);
Type::Unknown
}
ast::Expr::Await(await_expression) => {
self.infer_await_expression(await_expression);
Type::Unknown
}
ast::Expr::Yield(yield_expression) => {
self.infer_yield_expression(yield_expression);
Type::Unknown
}
ast::Expr::YieldFrom(yield_from) => {
self.infer_yield_from_expression(yield_from);
Type::Unknown
}
ast::Expr::Compare(compare) => {
self.infer_compare_expression(compare);
Type::Unknown
}
ast::Expr::Call(call_expr) => {
self.infer_call_expression(call_expr);
Type::Unknown
}
ast::Expr::FString(fstring) => {
self.infer_fstring_expression(fstring);
Type::Unknown
}
ast::Expr::List(list) => {
self.infer_list_expression(list);
Type::Unknown
}
ast::Expr::Tuple(tuple) => {
self.infer_tuple_expression(tuple);
Type::Unknown
}
ast::Expr::Slice(slice) => {
self.infer_slice_expression(slice);
Type::Unknown
}
// Avoid inferring the types of invalid type expressions that have been parsed from a
// string annotation, as they are not present in the semantic index.
_ if self.deferred_state.in_string_annotation() => Type::Unknown,
ast::Expr::IpyEscapeCommand(_) => todo!("Implement Ipy escape command support"),
// Forms which are invalid in the context of annotation expressions: we store a type of
// `Type::Unknown` for these expressions (and their sub-expressions) to avoid problems
// with invalid-syntax examples like `x: f"{x}"` or `x: lambda y: x`, for which we can
// not infer a meaningful type for the inner `x` expression. The top-level expression
// is also `Type::Unknown` in these cases.
ast::Expr::BoolOp(_)
| ast::Expr::Named(_)
| ast::Expr::UnaryOp(_)
| ast::Expr::Lambda(_)
| ast::Expr::If(_)
| ast::Expr::Dict(_)
| ast::Expr::Set(_)
| ast::Expr::ListComp(_)
| ast::Expr::SetComp(_)
| ast::Expr::DictComp(_)
| ast::Expr::Generator(_)
| ast::Expr::Await(_)
| ast::Expr::Yield(_)
| ast::Expr::YieldFrom(_)
| ast::Expr::Compare(_)
| ast::Expr::Call(_)
| ast::Expr::FString(_)
| ast::Expr::List(_)
| ast::Expr::Tuple(_)
| ast::Expr::Slice(_)
| ast::Expr::IpyEscapeCommand(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::NumberLiteral(_)
| ast::Expr::BooleanLiteral(_) => {
match expression {
ast::Expr::BytesLiteral(bytes) => {
self.diagnostics.add(
bytes.into(),
"annotation-byte-string",
format_args!("Type expressions cannot use bytes literal"),
);
}
ast::Expr::FString(fstring) => {
self.diagnostics.add(
fstring.into(),
"annotation-f-string",
format_args!("Type expressions cannot use f-strings"),
);
}
_ => {
self.diagnostics.add(
expression.into(),
"annotation-with-invalid-expression",
format_args!("Invalid expression in type expression"),
);
}
}
self.store_type_for_sub_expressions_of(expression, Type::Unknown);
Type::Unknown
}
}
}
@@ -4476,7 +4533,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
let ty = if return_todo {
Type::Todo
todo_type!("full tuple[...] support")
} else {
Type::tuple(self.db, &element_types)
};
@@ -4491,7 +4548,7 @@ impl<'db> TypeInferenceBuilder<'db> {
single_element => {
let single_element_ty = self.infer_type_expression(single_element);
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty) {
Type::Todo
todo_type!()
} else {
Type::tuple(self.db, &[single_element_ty])
}
@@ -4507,13 +4564,13 @@ impl<'db> TypeInferenceBuilder<'db> {
if let Some(ClassLiteralType { class }) = name_ty.into_class_literal() {
Type::subclass_of(class)
} else {
Type::Todo
todo_type!()
}
}
// TODO: attributes, unions, subscripts, etc.
_ => {
self.infer_type_expression(slice);
Type::Todo
todo_type!()
}
}
}
@@ -4532,20 +4589,21 @@ impl<'db> TypeInferenceBuilder<'db> {
match value_ty {
Type::KnownInstance(known_instance) => {
self.infer_parameterized_known_instance_type_expression(known_instance, slice)
self.infer_parameterized_known_instance_type_expression(subscript, known_instance)
}
_ => {
self.infer_type_expression(slice);
Type::Todo // TODO: generics
todo_type!("generics")
}
}
}
fn infer_parameterized_known_instance_type_expression(
&mut self,
subscript: &ast::ExprSubscript,
known_instance: KnownInstanceType,
parameters: &ast::Expr,
) -> Type<'db> {
let parameters = &*subscript.slice;
match known_instance {
KnownInstanceType::Literal => match self.infer_literal_parameter_type(parameters) {
Ok(ty) => ty,
@@ -4567,7 +4625,36 @@ impl<'db> TypeInferenceBuilder<'db> {
let param_type = self.infer_type_expression(parameters);
UnionType::from_elements(self.db, [param_type, Type::none(self.db)])
}
KnownInstanceType::TypeVar(_) => Type::Todo,
KnownInstanceType::Union => match parameters {
ast::Expr::Tuple(t) => {
let union_ty = UnionType::from_elements(
self.db,
t.iter().map(|elt| self.infer_type_expression(elt)),
);
self.store_expression_type(parameters, union_ty);
union_ty
}
_ => self.infer_type_expression(parameters),
},
KnownInstanceType::TypeVar(_) => {
self.infer_type_expression(parameters);
todo_type!()
}
KnownInstanceType::TypeAliasType(_) => {
self.infer_type_expression(parameters);
todo_type!("generic type alias")
}
KnownInstanceType::NoReturn | KnownInstanceType::Never => {
self.diagnostics.add(
subscript.into(),
"invalid-type-parameter",
format_args!(
"Type `{}` expected no type parameter",
known_instance.repr(self.db)
),
);
Type::Unknown
}
}
}
@@ -4935,8 +5022,8 @@ fn perform_membership_test_comparison<'db>(
compare_result_opt
.map(|ty| {
if matches!(ty, Type::Todo) {
return Type::Todo;
if matches!(ty, Type::Todo(_)) {
return ty;
}
match op {
@@ -5907,7 +5994,17 @@ mod tests {
// We currently return `Todo` for all async comprehensions,
// including comprehensions that have invalid syntax
assert_scope_ty(&db, "src/a.py", &["foo", "<listcomp>"], "x", "@Todo");
assert_scope_ty(
&db,
"src/a.py",
&["foo", "<listcomp>"],
"x",
if cfg!(debug_assertions) {
"@Todo(async iterables/iterators)"
} else {
"@Todo"
},
);
Ok(())
}
@@ -5931,7 +6028,17 @@ mod tests {
)?;
// TODO async iterables/iterators! --Alex
assert_scope_ty(&db, "src/a.py", &["foo", "<listcomp>"], "x", "@Todo");
assert_scope_ty(
&db,
"src/a.py",
&["foo", "<listcomp>"],
"x",
if cfg!(debug_assertions) {
"@Todo(async iterables/iterators)"
} else {
"@Todo"
},
);
Ok(())
}
@@ -5965,6 +6072,72 @@ mod tests {
);
}
#[test]
fn pep695_type_params() {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
def f[T, U: A, V: (A, B), W = A, X: A = A1, Y: (int,)]():
pass
class A: ...
class B: ...
class A1(A): ...
",
)
.unwrap();
let check_typevar = |var: &'static str,
upper_bound: Option<&'static str>,
constraints: Option<&[&'static str]>,
default: Option<&'static str>| {
let var_ty = get_symbol(&db, "src/a.py", &["f"], var).expect_type();
assert_eq!(var_ty.display(&db).to_string(), var);
let expected_name_ty = format!(r#"Literal["{var}"]"#);
let name_ty = var_ty.member(&db, "__name__").expect_type();
assert_eq!(name_ty.display(&db).to_string(), expected_name_ty);
let KnownInstanceType::TypeVar(typevar) = var_ty.expect_known_instance() else {
panic!("expected TypeVar");
};
assert_eq!(
typevar
.upper_bound(&db)
.map(|ty| ty.display(&db).to_string()),
upper_bound.map(std::borrow::ToOwned::to_owned)
);
assert_eq!(
typevar.constraints(&db).map(|tys| tys
.iter()
.map(|ty| ty.display(&db).to_string())
.collect::<Vec<_>>()),
constraints.map(|strings| strings
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>())
);
assert_eq!(
typevar
.default_ty(&db)
.map(|ty| ty.display(&db).to_string()),
default.map(std::borrow::ToOwned::to_owned)
);
};
check_typevar("T", None, None, None);
check_typevar("U", Some("A"), None, None);
check_typevar("V", None, Some(&["A", "B"]), None);
check_typevar("W", None, None, Some("A"));
check_typevar("X", Some("A"), None, Some("A1"));
// a typevar with less than two constraints is treated as unconstrained
check_typevar("Y", None, None, None);
}
// Incremental inference tests
fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> {

View File

@@ -5,7 +5,7 @@ use itertools::Either;
use rustc_hash::FxHashSet;
use super::{Class, ClassLiteralType, KnownClass, KnownInstanceType, Type};
use crate::Db;
use crate::{types::todo_type, Db};
/// The inferred method resolution order of a given class.
///
@@ -354,7 +354,7 @@ impl<'db> ClassBase<'db> {
match ty {
Type::Any => Some(Self::Any),
Type::Unknown => Some(Self::Unknown),
Type::Todo => Some(Self::Todo),
Type::Todo(_) => Some(Self::Todo),
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
Type::Intersection(_) => None, // TODO -- probably incorrect?
@@ -372,7 +372,11 @@ impl<'db> ClassBase<'db> {
| Type::SubclassOf(_) => None,
Type::KnownInstance(known_instance) => match known_instance {
KnownInstanceType::TypeVar(_)
| KnownInstanceType::TypeAliasType(_)
| KnownInstanceType::Literal
| KnownInstanceType::Union
| KnownInstanceType::NoReturn
| KnownInstanceType::Never
| KnownInstanceType::Optional => None,
},
}
@@ -405,7 +409,7 @@ impl<'db> From<ClassBase<'db>> for Type<'db> {
fn from(value: ClassBase<'db>) -> Self {
match value {
ClassBase::Any => Type::Any,
ClassBase::Todo => Type::Todo,
ClassBase::Todo => todo_type!(),
ClassBase::Unknown => Type::Unknown,
ClassBase::Class(class) => Type::class_literal(class),
}

View File

@@ -1,7 +1,7 @@
#![allow(dead_code)]
use super::{definition_expression_ty, Type};
use crate::semantic_index::definition::Definition;
use crate::Db;
use crate::{semantic_index::definition::Definition, types::todo_type};
use ruff_python_ast::{self as ast, name::Name};
/// A typed callable signature.
@@ -18,7 +18,7 @@ impl<'db> Signature<'db> {
pub(crate) fn todo() -> Self {
Self {
parameters: Parameters::todo(),
return_ty: Type::Todo,
return_ty: todo_type!("return type"),
}
}
@@ -33,8 +33,7 @@ impl<'db> Signature<'db> {
.as_ref()
.map(|returns| {
if function_node.is_async {
// TODO: generic `types.CoroutineType`!
Type::Todo
todo_type!("generic types.CoroutineType")
} else {
definition_expression_ty(db, definition, returns.as_ref())
}
@@ -81,11 +80,11 @@ impl<'db> Parameters<'db> {
Self {
variadic: Some(Parameter {
name: Some(Name::new_static("args")),
annotated_ty: Type::Todo,
annotated_ty: todo_type!(),
}),
keywords: Some(Parameter {
name: Some(Name::new_static("kwargs")),
annotated_ty: Type::Todo,
annotated_ty: todo_type!(),
}),
..Default::default()
}

View File

@@ -6,7 +6,7 @@ use rustc_hash::FxHashMap;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::symbol::ScopeId;
use crate::types::{Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
use crate::types::{todo_type, Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
use crate::Db;
/// Unpacks the value expression type to their respective targets.
@@ -59,7 +59,7 @@ impl<'db> Unpacker<'db> {
// TODO: Combine the types into a list type. If the
// starred_element_types is empty, then it should be `List[Any]`.
// combine_types(starred_element_types);
element_types.push(Type::Todo);
element_types.push(todo_type!("starred unpacking"));
element_types.extend_from_slice(
// SAFETY: Safe because of the length check above.
@@ -72,7 +72,7 @@ impl<'db> Unpacker<'db> {
// index.
element_types.resize(elts.len() - 1, Type::Unknown);
// TODO: This should be `list[Unknown]`
element_types.insert(starred_index, Type::Todo);
element_types.insert(starred_index, todo_type!("starred unpacking"));
Cow::Owned(element_types)
}
} else {

View File

@@ -180,6 +180,16 @@ where
}
}
/// Discard `@Todo`-type metadata from expected types, which is not available
/// when running in release mode.
#[cfg(not(debug_assertions))]
fn discard_todo_metadata(ty: &str) -> std::borrow::Cow<'_, str> {
static TODO_METADATA_REGEX: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap());
TODO_METADATA_REGEX.replace_all(ty, "@Todo")
}
struct Matcher {
line_index: LineIndex,
source: SourceText,
@@ -276,6 +286,9 @@ impl Matcher {
}
}
Assertion::Revealed(expected_type) => {
#[cfg(not(debug_assertions))]
let expected_type = discard_todo_metadata(&expected_type);
let mut matched_revealed_type = None;
let mut matched_undefined_reveal = None;
let expected_reveal_type_message = format!("Revealed type is `{expected_type}`");

View File

@@ -0,0 +1,6 @@
while True:
class A:
x: int
break

View File

@@ -0,0 +1,6 @@
while True:
def b():
x: int
break

View File

@@ -0,0 +1,6 @@
for _ in range(1):
class A:
x: int
break

View File

@@ -0,0 +1,6 @@
for _ in range(1):
def b():
x: int
break

View File

@@ -0,0 +1 @@
../../../../ruff_python_parser/resources/inline/err/type_param_invalid_bound_expr.py

View File

@@ -0,0 +1 @@
x: f"Literal[{1 + 2}]" = 3

View File

@@ -0,0 +1,3 @@
from typing import Union
x: Union[int, str] = 1

View File

@@ -264,26 +264,12 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
}
/// Whether or not the .py/.pyi version of this file is expected to fail
#[rustfmt::skip]
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
// Probably related to missing support for type aliases / type params:
("crates/ruff_python_parser/resources/inline/err/type_param_invalid_bound_expr.py", true, true),
("crates/ruff_python_parser/resources/inline/err/type_param_param_spec_invalid_default_expr.py", true, true),
("crates/ruff_python_parser/resources/inline/err/type_param_type_var_invalid_default_expr.py", true, true),
("crates/ruff_python_parser/resources/inline/err/type_param_type_var_missing_default.py", true, true),
("crates/ruff_python_parser/resources/inline/err/type_param_type_var_tuple_invalid_default_expr.py", true, true),
("crates/ruff_python_parser/resources/inline/ok/type_param_param_spec.py", true, true),
("crates/ruff_python_parser/resources/inline/ok/type_param_type_var_tuple.py", true, true),
("crates/ruff_python_parser/resources/inline/ok/type_param_type_var.py", true, true),
("crates/ruff_python_parser/resources/valid/statement/type.py", true, true),
("crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_15.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_19.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_14.py", false, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_15.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_17.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_20.py", true, true),
// related to circular references in class definitions
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, false),
// Fails for unknown reasons:
("crates/ruff_linter/resources/test/fixtures/pyflakes/F632.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F811_19.py", true, false),
("crates/ruff_linter/resources/test/fixtures/pyupgrade/UP039.py", true, false),
// related to circular references in type aliases (salsa cycle panic):
("crates/ruff_python_parser/resources/inline/err/type_alias_invalid_value_expr.py", true, true),
];

View File

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

View File

@@ -1966,3 +1966,75 @@ fn nested_implicit_namespace_package() -> Result<()> {
Ok(())
}
#[test]
fn flake8_import_convention_invalid_aliases_config_alias_name() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint.flake8-import-conventions.aliases]
"module.name" = "invalid.alias"
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Failed to parse [TMP]/ruff.toml
Cause: TOML parse error at line 2, column 2
|
2 | [lint.flake8-import-conventions.aliases]
| ^^^^
invalid value: string "invalid.alias", expected a Python identifier
"###);});
Ok(())
}
#[test]
fn flake8_import_convention_invalid_aliases_config_module_name() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint.flake8-import-conventions.aliases]
"module..invalid" = "alias"
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Failed to parse [TMP]/ruff.toml
Cause: TOML parse error at line 2, column 2
|
2 | [lint.flake8-import-conventions.aliases]
| ^^^^
invalid value: string "module..invalid", expected a sequence of Python identifiers delimited by periods
"###);});
Ok(())
}

View File

@@ -27,13 +27,17 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
// We don't support `*` imports yet:
"error[unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`",
// We don't support terminal statements in control flow yet:
"error[annotation-with-invalid-expression] /src/tomllib/_parser.py:57:71 Invalid expression in type expression",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined",
"error[annotation-with-invalid-expression] /src/tomllib/_parser.py:69:66 Invalid expression in type expression",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
"error[conflicting-declarations] /src/tomllib/_parser.py:108:17 Conflicting declared types for `second_char`: Unknown, str | None",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
"error[annotation-with-invalid-expression] /src/tomllib/_parser.py:145:27 Invalid expression in type expression",
"error[annotation-with-invalid-expression] /src/tomllib/_parser.py:196:25 Invalid expression in type expression",
"error[conflicting-declarations] /src/tomllib/_parser.py:267:9 Conflicting declared types for `char`: Unknown, str | None",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",

View File

@@ -1,10 +1,11 @@
use super::{write, Arguments, FormatElement};
use crate::format_element::Interned;
use crate::prelude::LineMode;
use crate::prelude::{LineMode, Tag};
use crate::{FormatResult, FormatState};
use rustc_hash::FxHashMap;
use std::any::{Any, TypeId};
use std::fmt::Debug;
use std::num::NonZeroUsize;
use std::ops::{Deref, DerefMut};
/// A trait for writing or formatting into [`FormatElement`]-accepting buffers or streams.
@@ -294,10 +295,11 @@ where
}
}
/// A Buffer that removes any soft line breaks.
/// A Buffer that removes any soft line breaks or [`if_group_breaks`](crate::builders::if_group_breaks) elements.
///
/// - Removes [`lines`](FormatElement::Line) with the mode [`Soft`](LineMode::Soft).
/// - Replaces [`lines`](FormatElement::Line) with the mode [`Soft`](LineMode::SoftOrSpace) with a [`Space`](FormatElement::Space)
/// - Removes [`if_group_breaks`](crate::builders::if_group_breaks) elements.
///
/// # Examples
///
@@ -350,6 +352,8 @@ pub struct RemoveSoftLinesBuffer<'a, Context> {
/// It's fine to not snapshot the cache. The worst that can happen is that it holds on interned elements
/// that are now unused. But there's little harm in that and the cache is cleaned when dropping the buffer.
interned_cache: FxHashMap<Interned, Interned>,
state: RemoveSoftLineBreaksState,
}
impl<'a, Context> RemoveSoftLinesBuffer<'a, Context> {
@@ -357,6 +361,7 @@ impl<'a, Context> RemoveSoftLinesBuffer<'a, Context> {
pub fn new(inner: &'a mut dyn Buffer<Context = Context>) -> Self {
Self {
inner,
state: RemoveSoftLineBreaksState::default(),
interned_cache: FxHashMap::default(),
}
}
@@ -375,6 +380,8 @@ fn clean_interned(
if let Some(cleaned) = interned_cache.get(interned) {
cleaned.clone()
} else {
let mut state = RemoveSoftLineBreaksState::default();
// Find the first soft line break element or interned element that must be changed
let result = interned
.iter()
@@ -382,8 +389,9 @@ fn clean_interned(
.find_map(|(index, element)| match element {
FormatElement::Line(LineMode::Soft | LineMode::SoftOrSpace) => {
let mut cleaned = Vec::new();
cleaned.extend_from_slice(&interned[..index]);
Some((cleaned, &interned[index..]))
let (before, after) = interned.split_at(index);
cleaned.extend_from_slice(before);
Some((cleaned, &after[1..]))
}
FormatElement::Interned(inner) => {
let cleaned_inner = clean_interned(inner, interned_cache);
@@ -398,19 +406,33 @@ fn clean_interned(
}
}
_ => None,
element => {
if state.should_drop(element) {
let mut cleaned = Vec::new();
let (before, after) = interned.split_at(index);
cleaned.extend_from_slice(before);
Some((cleaned, &after[1..]))
} else {
None
}
}
});
let result = match result {
// Copy the whole interned buffer so that becomes possible to change the necessary elements.
Some((mut cleaned, rest)) => {
for element in rest {
if state.should_drop(element) {
continue;
}
let element = match element {
FormatElement::Line(LineMode::Soft) => continue,
FormatElement::Line(LineMode::SoftOrSpace) => FormatElement::Space,
FormatElement::Interned(interned) => {
FormatElement::Interned(clean_interned(interned, interned_cache))
}
element => element.clone(),
};
cleaned.push(element);
@@ -431,12 +453,17 @@ impl<Context> Buffer for RemoveSoftLinesBuffer<'_, Context> {
type Context = Context;
fn write_element(&mut self, element: FormatElement) {
if self.state.should_drop(&element) {
return;
}
let element = match element {
FormatElement::Line(LineMode::Soft) => return,
FormatElement::Line(LineMode::SoftOrSpace) => FormatElement::Space,
FormatElement::Interned(interned) => {
FormatElement::Interned(self.clean_interned(&interned))
}
element => element,
};
@@ -456,14 +483,77 @@ impl<Context> Buffer for RemoveSoftLinesBuffer<'_, Context> {
}
fn snapshot(&self) -> BufferSnapshot {
self.inner.snapshot()
BufferSnapshot::Any(Box::new(RemoveSoftLinebreaksSnapshot {
inner: self.inner.snapshot(),
state: self.state,
}))
}
fn restore_snapshot(&mut self, snapshot: BufferSnapshot) {
self.inner.restore_snapshot(snapshot);
let RemoveSoftLinebreaksSnapshot { inner, state } = snapshot.unwrap_any();
self.inner.restore_snapshot(inner);
self.state = state;
}
}
#[derive(Copy, Clone, Debug, Default)]
enum RemoveSoftLineBreaksState {
#[default]
Default,
InIfGroupBreaks {
conditional_content_level: NonZeroUsize,
},
}
impl RemoveSoftLineBreaksState {
fn should_drop(&mut self, element: &FormatElement) -> bool {
match self {
Self::Default => {
// Entered the start of an `if_group_breaks`
if let FormatElement::Tag(Tag::StartConditionalContent(condition)) = element {
if condition.mode.is_expanded() {
*self = Self::InIfGroupBreaks {
conditional_content_level: NonZeroUsize::new(1).unwrap(),
};
return true;
}
}
false
}
Self::InIfGroupBreaks {
conditional_content_level,
} => {
match element {
// A nested `if_group_breaks` or `if_group_fits`
FormatElement::Tag(Tag::StartConditionalContent(_)) => {
*conditional_content_level = conditional_content_level.saturating_add(1);
}
// The end of an `if_group_breaks` or `if_group_fits`.
FormatElement::Tag(Tag::EndConditionalContent) => {
if let Some(level) = NonZeroUsize::new(conditional_content_level.get() - 1)
{
*conditional_content_level = level;
} else {
// Found the end tag of the initial `if_group_breaks`. Skip this element but retain
// the elements coming after
*self = RemoveSoftLineBreaksState::Default;
}
}
_ => {}
}
true
}
}
}
}
struct RemoveSoftLinebreaksSnapshot {
inner: BufferSnapshot,
state: RemoveSoftLineBreaksState,
}
pub trait BufferExtensions: Buffer + Sized {
/// Returns a new buffer that calls the passed inspector for every element that gets written to the output
#[must_use]

View File

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

View File

@@ -0,0 +1,15 @@
from airflow import DAG, dag
DAG(dag_id="class_default_schedule")
DAG(dag_id="class_schedule", schedule="@hourly")
@dag()
def decorator_default_schedule():
pass
@dag(schedule="0 * * * *")
def decorator_schedule():
pass

View File

@@ -14,6 +14,8 @@ ContextVar("cv", default=frozenset())
ContextVar("cv", default=MappingProxyType({}))
ContextVar("cv", default=re.compile("foo"))
ContextVar("cv", default=float(1))
ContextVar("cv", default=frozenset[str]())
ContextVar[frozenset[str]]("cv", default=frozenset[str]())
# Bad
ContextVar("cv", default=[])
@@ -25,6 +27,8 @@ ContextVar("cv", default=[char for char in "foo"])
ContextVar("cv", default={char for char in "foo"})
ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
ContextVar("cv", default=collections.deque())
ContextVar("cv", default=set[str]())
ContextVar[set[str]]("cv", default=set[str]())
def bar() -> list[int]:
return [1, 2, 3]

View File

@@ -84,3 +84,27 @@ field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union me
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
# Should emit in cases with nested `typing.Union`
field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int`
# Should emit in cases with nested `typing.Union`
field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int`
# Should emit in cases with mixed `typing.Union` and `|`
field28: typing.Union[int | int] # Error
# Should emit twice in cases with multiple nested `typing.Union`
field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error
# Should emit once in cases with multiple nested `typing.Union`
field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error
# Should emit once, and fix to `typing.Union[float, int]`
field31: typing.Union[float, typing.Union[int | int]] # Error
# Should emit once, and fix to `typing.Union[float, int]`
field32: typing.Union[float, typing.Union[int | int | int]] # Error
# Test case for mixed union type fix
field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error

View File

@@ -84,3 +84,27 @@ field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union me
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
# Should emit in cases with nested `typing.Union`
field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int`
# Should emit in cases with nested `typing.Union`
field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int`
# Should emit in cases with mixed `typing.Union` and `|`
field28: typing.Union[int | int] # Error
# Should emit twice in cases with multiple nested `typing.Union`
field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error
# Should emit once in cases with multiple nested `typing.Union`
field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error
# Should emit once, and fix to `typing.Union[float, int]`
field31: typing.Union[float, typing.Union[int | int]] # Error
# Should emit once, and fix to `typing.Union[float, int]`
field32: typing.Union[float, typing.Union[int | int | int]] # Error
# Test case for mixed union type fix
field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error

View File

@@ -39,14 +39,30 @@ async def f4(**kwargs: int | int | float) -> None:
...
def f5(
def f5(arg1: int, *args: Union[int, int, float]) -> None:
...
def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None:
...
def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None:
...
def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None:
...
def f9(
arg: Union[ # comment
float, # another
complex, int]
) -> None:
...
def f6(
def f10(
arg: (
int | # comment
float | # another

View File

@@ -46,6 +46,18 @@ def f6(
)
) -> None: ... # PYI041
def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041
def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041
def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041
def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041
class Foo:
def good(self, arg: int) -> None: ...

View File

@@ -5,6 +5,9 @@ A: str | Literal["foo"]
B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...

View File

@@ -5,6 +5,9 @@ A: str | Literal["foo"]
B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...

View File

@@ -1,11 +1,14 @@
import builtins
from typing import Union
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
x: type[int] | type[str] | type[float]
y: builtins.type[int] | type[str] | builtins.type[complex]
z: Union[type[float], type[complex]]
z: Union[type[float, int], type[complex]]
s: builtins.type[int] | builtins.type[str] | builtins.type[complex]
t: type[int] | type[str] | type[float]
u: builtins.type[int] | type[str] | builtins.type[complex]
v: Union[type[float], type[complex]]
w: Union[type[float, int], type[complex]]
x: Union[Union[type[float, int], type[complex]]]
y: Union[Union[Union[type[float, int], type[complex]]]]
z: Union[type[complex], Union[Union[type[float, int]]]]
def func(arg: type[int] | str | type[float]) -> None:

View File

@@ -1,11 +1,14 @@
import builtins
from typing import Union
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
x: type[int] | type[str] | type[float]
y: builtins.type[int] | type[str] | builtins.type[complex]
z: Union[type[float], type[complex]]
z: Union[type[float, int], type[complex]]
s: builtins.type[int] | builtins.type[str] | builtins.type[complex]
t: type[int] | type[str] | type[float]
u: builtins.type[int] | type[str] | builtins.type[complex]
v: Union[type[float], type[complex]]
w: Union[type[float, int], type[complex]]
x: Union[Union[type[float, int], type[complex]]]
y: Union[Union[Union[type[float, int], type[complex]]]]
z: Union[type[complex], Union[Union[type[float, int]]]]
def func(arg: type[int] | str | type[float]) -> None: ...

View File

@@ -1,4 +1,4 @@
from typing import Literal
from typing import Literal, Union
def func1(arg1: Literal[None]):
@@ -17,7 +17,7 @@ def func4(arg1: Literal[int, None, float]):
...
def func5(arg1: Literal[None, None]):
def func5(arg1: Literal[None, None]):
...
@@ -25,13 +25,21 @@ def func6(arg1: Literal[
"hello",
None # Comment 1
, "world"
]):
]):
...
def func7(arg1: Literal[
None # Comment 1
]):
]):
...
def func8(arg1: Literal[None] | None):
...
def func9(arg1: Union[Literal[None], None]):
...
@@ -58,3 +66,16 @@ Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replac
# and there are no None members in the Literal[] slice,
# only emit Y062:
Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True"
# Regression tests for https://github.com/astral-sh/ruff/issues/14567
x: Literal[None] | None
y: None | Literal[None]
z: Union[Literal[None], None]
a: int | Literal[None] | None
b: None | Literal[None] | None
c: (None | Literal[None]) | None
d: None | (Literal[None] | None)
e: None | ((None | Literal[None]) | None) | None
f: Literal[None] | Literal[None]

View File

@@ -1,4 +1,4 @@
from typing import Literal
from typing import Literal, Union
def func1(arg1: Literal[None]): ...
@@ -28,6 +28,12 @@ def func7(arg1: Literal[
]): ...
def func8(arg1: Literal[None] | None):...
def func9(arg1: Union[Literal[None], None]): ...
# OK
def good_func(arg1: Literal[int] | None): ...
@@ -35,3 +41,16 @@ def good_func(arg1: Literal[int] | None): ...
# From flake8-pyi
Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None"
Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None"
# Regression tests for https://github.com/astral-sh/ruff/issues/14567
x: Literal[None] | None
y: None | Literal[None]
z: Union[Literal[None], None]
a: int | Literal[None] | None
b: None | Literal[None] | None
c: (None | Literal[None]) | None
d: None | (Literal[None] | None)
e: None | ((None | Literal[None]) | None) | None
f: Literal[None] | Literal[None]

View File

@@ -25,3 +25,9 @@ Literal[
MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
n: Literal["No", "duplicates", "here", 1, "1"]
# nested literals, all equivalent to `Literal[1]`
Literal[Literal[1]] # no duplicate
Literal[Literal[Literal[1], Literal[1]]] # once
Literal[Literal[1], Literal[Literal[Literal[1]]]] # once

View File

@@ -25,3 +25,9 @@ Literal[
MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
n: Literal["No", "duplicates", "here", 1, "1"]
# nested literals, all equivalent to `Literal[1]`
Literal[Literal[1]] # no duplicate
Literal[Literal[Literal[1], Literal[1]]] # once
Literal[Literal[1], Literal[Literal[Literal[1]]]] # once

View File

@@ -69,3 +69,15 @@ def test_implicit_str_concat_with_multi_parens(param1, param2, param3):
@pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
def test_csv_with_parens(param1, param2):
...
parametrize = pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
@parametrize
def test_not_decorator(param1, param2):
...
@pytest.mark.parametrize(argnames=("param1,param2"), argvalues=[(1, 2), (3, 4)])
def test_keyword_arguments(param1, param2):
...

View File

@@ -0,0 +1,62 @@
def f():
from typing import cast
cast(int, 3.0) # TC006
def f():
from typing import cast
cast(list[tuple[bool | float | int | str]], 3.0) # TC006
def f():
from typing import Union, cast
cast(list[tuple[Union[bool, float, int, str]]], 3.0) # TC006
def f():
from typing import cast
cast("int", 3.0) # OK
def f():
from typing import cast
cast("list[tuple[bool | float | int | str]]", 3.0) # OK
def f():
from typing import Union, cast
cast("list[tuple[Union[bool, float, int, str]]]", 3.0) # OK
def f():
from typing import cast as typecast
typecast(int, 3.0) # TC006
def f():
import typing
typing.cast(int, 3.0) # TC006
def f():
import typing as t
t.cast(t.Literal["3.0", '3'], 3.0) # TC006
def f():
from typing import cast
cast(
int # TC006 (unsafe, because it will get rid of this comment)
| None,
3.0
)

View File

@@ -0,0 +1,21 @@
"""Regression test for #13824.
Don't report an error when the function being annotated has the
`@no_type_check` decorator.
However, we still want to ignore this annotation on classes. See
https://github.com/python/typing/pull/1615/files and the discussion on #14615.
"""
from typing import no_type_check
@no_type_check
def f(arg: "this isn't python") -> "this isn't python either":
x: "this also isn't python" = 0
@no_type_check
class C:
def f(arg: "this isn't python") -> "this isn't python either":
x: "this also isn't python" = 1

View File

@@ -0,0 +1,21 @@
"""Regression test for #13824.
Don't report an error when the function being annotated has the
`@no_type_check` decorator.
However, we still want to ignore this annotation on classes. See
https://github.com/python/typing/pull/1615/files and the discussion on #14615.
"""
import typing
@typing.no_type_check
def f(arg: "A") -> "R":
x: "A" = 1
@typing.no_type_check
class C:
def f(self, arg: "B") -> "S":
x: "B" = 1

View File

@@ -12,3 +12,4 @@ os.getenv("AA", "GOOD %s" % "BAR")
os.getenv("B", Z)
os.getenv("AA", "GOOD" if Z else "BAR")
os.getenv("AA", 1 if Z else "BAR") # [invalid-envvar-default]
os.environ.get("TEST", 12) # [invalid-envvar-default]

View File

@@ -0,0 +1,234 @@
if len('TEST'): # [PLC1802]
pass
if not len('TEST'): # [PLC1802]
pass
z = []
if z and len(['T', 'E', 'S', 'T']): # [PLC1802]
pass
if True or len('TEST'): # [PLC1802]
pass
if len('TEST') == 0: # Should be fine
pass
if len('TEST') < 1: # Should be fine
pass
if len('TEST') <= 0: # Should be fine
pass
if 1 > len('TEST'): # Should be fine
pass
if 0 >= len('TEST'): # Should be fine
pass
if z and len('TEST') == 0: # Should be fine
pass
if 0 == len('TEST') < 10: # Should be fine
pass
# Should be fine
if 0 < 1 <= len('TEST') < 10: # [comparison-of-constants]
pass
if 10 > len('TEST') != 0: # Should be fine
pass
if 10 > len('TEST') > 1 > 0: # Should be fine
pass
if 0 <= len('TEST') < 100: # Should be fine
pass
if z or 10 > len('TEST') != 0: # Should be fine
pass
if z:
pass
elif len('TEST'): # [PLC1802]
pass
if z:
pass
elif not len('TEST'): # [PLC1802]
pass
while len('TEST'): # [PLC1802]
pass
while not len('TEST'): # [PLC1802]
pass
while z and len('TEST'): # [PLC1802]
pass
while not len('TEST') and z: # [PLC1802]
pass
assert len('TEST') > 0 # Should be fine
x = 1 if len('TEST') != 0 else 2 # Should be fine
f_o_o = len('TEST') or 42 # Should be fine
a = x and len(x) # Should be fine
def some_func():
return len('TEST') > 0 # Should be fine
def github_issue_1325():
l = [1, 2, 3]
length = len(l) if l else 0 # Should be fine
return length
def github_issue_1331(*args):
assert False, len(args) # Should be fine
def github_issue_1331_v2(*args):
assert len(args), args # [PLC1802]
def github_issue_1331_v3(*args):
assert len(args) or z, args # [PLC1802]
def github_issue_1331_v4(*args):
assert z and len(args), args # [PLC1802]
def github_issue_1331_v5(**args):
assert z and len(args), args # [PLC1802]
b = bool(len(z)) # [PLC1802]
c = bool(len('TEST') or 42) # [PLC1802]
def github_issue_1879():
class ClassWithBool(list):
def __bool__(self):
return True
class ClassWithoutBool(list):
pass
class ChildClassWithBool(ClassWithBool):
pass
class ChildClassWithoutBool(ClassWithoutBool):
pass
assert len(ClassWithBool())
assert len(ChildClassWithBool())
assert len(ClassWithoutBool()) # unintuitive?, in pylint: [PLC1802]
assert len(ChildClassWithoutBool()) # unintuitive?, in pylint: [PLC1802]
assert len(range(0)) # [PLC1802]
assert len([t + 1 for t in []]) # [PLC1802]
# assert len(u + 1 for u in []) generator has no len
assert len({"1":(v + 1) for v in {}}) # [PLC1802]
assert len(set((w + 1) for w in set())) # [PLC1802]
import numpy
numpy_array = numpy.array([0])
if len(numpy_array) > 0:
print('numpy_array')
if len(numpy_array):
print('numpy_array')
if numpy_array:
print('b')
import pandas as pd
pandas_df = pd.DataFrame()
if len(pandas_df):
print("this works, but pylint tells me not to use len() without comparison")
if len(pandas_df) > 0:
print("this works and pylint likes it, but it's not the solution intended by PEP-8")
if pandas_df:
print("this does not work (truth value of dataframe is ambiguous)")
def function_returning_list(r):
if r==1:
return [1]
return [2]
def function_returning_int(r):
if r==1:
return 1
return 2
def function_returning_generator(r):
for i in [r, 1, 2, 3]:
yield i
def function_returning_comprehension(r):
return [x+1 for x in [r, 1, 2, 3]]
def function_returning_function(r):
return function_returning_generator(r)
assert len(function_returning_list(z)) # [PLC1802] differs from pylint
assert len(function_returning_int(z))
# This should raise a PLC1802 once astroid can infer it
# See https://github.com/pylint-dev/pylint/pull/3821#issuecomment-743771514
assert len(function_returning_generator(z))
assert len(function_returning_comprehension(z))
assert len(function_returning_function(z))
def github_issue_4215():
# Test undefined variables
# https://github.com/pylint-dev/pylint/issues/4215
if len(undefined_var): # [undefined-variable]
pass
if len(undefined_var2[0]): # [undefined-variable]
pass
def f(cond:bool):
x = [1,2,3]
if cond:
x = [4,5,6]
if len(x): # this should be addressed
print(x)
def g(cond:bool):
x = [1,2,3]
if cond:
x = [4,5,6]
if len(x): # this should be addressed
print(x)
del x
def h(cond:bool):
x = [1,2,3]
x = 123
if len(x): # ok
print(x)
def outer():
x = [1,2,3]
def inner(x:int):
return x+1
if len(x): # [PLC1802]
print(x)
def redefined():
x = 123
x = [1, 2, 3]
if len(x): # this should be addressed
print(x)
global_seq = [1, 2, 3]
def i():
global global_seq
if len(global_seq): # ok
print(global_seq)
def j():
if False:
x = [1, 2, 3]
if len(x): # [PLC1802] should be fine
print(x)

View File

@@ -102,3 +102,6 @@ blah = lambda: {"a": 1}.__delitem__("a") # OK
blah = dict[{"a": 1}.__delitem__("a")] # OK
"abc".__contains__("a")
# https://github.com/astral-sh/ruff/issues/14597
assert "abc".__str__() == "abc"

View File

@@ -0,0 +1,125 @@
import attr
from attrs import define, field, frozen, mutable
foo = int
@define # auto_attribs = None => True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@define() # auto_attribs = None => True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@define(auto_attribs=None) # auto_attribs = None => True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@frozen # auto_attribs = None => True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@frozen() # auto_attribs = None => True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@frozen(auto_attribs=None) # auto_attribs = None => True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@mutable # auto_attribs = None => True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@mutable() # auto_attribs = None => True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@mutable(auto_attribs=None) # auto_attribs = None => True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@attr.s # auto_attribs = False
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@attr.s() # auto_attribs = False
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@attr.s(auto_attribs=None) # auto_attribs = None => True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@attr.s(auto_attribs=False) # auto_attribs = False
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@attr.s(auto_attribs=True) # auto_attribs = True
class C:
a: str = 0
b = field()
c: int = foo()
d = list()
@attr.s(auto_attribs=[1, 2, 3]) # auto_attribs = False
class C:
a: str = 0
b = field()
c: int = foo()
d = list()

View File

@@ -6,3 +6,12 @@ Never | int
NoReturn | int
Union[Union[Never, int], Union[NoReturn, int]]
Union[NoReturn, int, float]
# Regression tests for https://github.com/astral-sh/ruff/issues/14567
x: None | Never | None
y: (None | Never) | None
z: None | (Never | None)
a: int | Never | None
b: Never | Never | None

View File

@@ -0,0 +1,5 @@
fruits = ["apples", "plums", "pear"]
fruits.filter(lambda fruit: fruit.startwith("p"))
assert len(fruits), 2
assert True, "always true"

View File

@@ -11,8 +11,8 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
use crate::rules::{
flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins,
flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
airflow, flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear,
flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging,
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_use_pathlib, flynt, numpy,
@@ -494,6 +494,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::SuperWithoutBrackets) {
pylint::rules::super_without_brackets(checker, func);
}
if checker.enabled(Rule::LenTest) {
pylint::rules::len_test(checker, call);
}
if checker.enabled(Rule::BitCount) {
refurb::rules::bit_count(checker, call);
}
@@ -856,6 +859,15 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.settings.preview.is_enabled()
&& checker.any_enabled(&[
Rule::PytestParametrizeNamesWrongType,
Rule::PytestParametrizeValuesWrongType,
Rule::PytestDuplicateParametrizeTestCases,
])
{
flake8_pytest_style::rules::parametrize(checker, call);
}
if checker.enabled(Rule::PytestUnittestAssertion) {
if let Some(diagnostic) = flake8_pytest_style::rules::unittest_assertion(
checker, expr, func, args, keywords,
@@ -1061,6 +1073,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnrawRePattern) {
ruff::rules::unraw_re_pattern(checker, call);
}
if checker.enabled(Rule::AirflowDagNoScheduleArgument) {
airflow::rules::dag_no_schedule_argument(checker, expr);
}
}
Expr::Dict(dict) => {
if checker.any_enabled(&[

View File

@@ -309,12 +309,20 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
body,
);
}
if checker.any_enabled(&[
Rule::PytestParametrizeNamesWrongType,
Rule::PytestParametrizeValuesWrongType,
Rule::PytestDuplicateParametrizeTestCases,
]) {
flake8_pytest_style::rules::parametrize(checker, decorator_list);
// In preview mode, calls are analyzed. To avoid duplicate diagnostics,
// skip analyzing the decorators.
if !checker.settings.preview.is_enabled()
&& checker.any_enabled(&[
Rule::PytestParametrizeNamesWrongType,
Rule::PytestParametrizeValuesWrongType,
Rule::PytestDuplicateParametrizeTestCases,
])
{
for decorator in decorator_list {
if let Some(call) = decorator.expression.as_call_expr() {
flake8_pytest_style::rules::parametrize(checker, call);
}
}
}
if checker.any_enabled(&[
Rule::PytestIncorrectMarkParenthesesStyle,
@@ -1268,6 +1276,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::AssertWithPrintMessage) {
ruff::rules::assert_with_print_message(checker, assert_stmt);
}
if checker.enabled(Rule::InvalidAssertMessageLiteralArgument) {
ruff::rules::invalid_assert_message_literal_argument(checker, assert_stmt);
}
}
Stmt::With(
with_stmt @ ast::StmtWith {

View File

@@ -723,6 +723,12 @@ impl<'a> Visitor<'a> for Checker<'a> {
// Visit the decorators and arguments, but avoid the body, which will be
// deferred.
for decorator in decorator_list {
if self
.semantic
.match_typing_expr(&decorator.expression, "no_type_check")
{
self.semantic.flags |= SemanticModelFlags::NO_TYPE_CHECK;
}
self.visit_decorator(decorator);
}
@@ -1280,6 +1286,10 @@ impl<'a> Visitor<'a> for Checker<'a> {
let mut args = arguments.args.iter();
if let Some(arg) = args.next() {
self.visit_type_definition(arg);
if self.enabled(Rule::RuntimeCastValue) {
flake8_type_checking::rules::runtime_cast_value(self, arg);
}
}
for arg in args {
self.visit_expr(arg);
@@ -1847,6 +1857,9 @@ impl<'a> Checker<'a> {
/// Visit an [`Expr`], and treat it as a type definition.
fn visit_type_definition(&mut self, expr: &'a Expr) {
if self.semantic.in_no_type_check() {
return;
}
let snapshot = self.semantic.flags;
self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION;
self.visit_expr(expr);

View File

@@ -192,6 +192,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet),
(Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias),
(Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel),
(Pylint, "C1802") => (RuleGroup::Preview, rules::pylint::rules::LenTest),
(Pylint, "C1901") => (RuleGroup::Preview, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C2401") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiName),
(Pylint, "C2403") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiImportName),
@@ -856,6 +857,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8TypeChecking, "003") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport),
(Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
(Flake8TypeChecking, "005") => (RuleGroup::Stable, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
(Flake8TypeChecking, "006") => (RuleGroup::Preview, rules::flake8_type_checking::rules::RuntimeCastValue),
(Flake8TypeChecking, "010") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeStringUnion),
// tryceratops
@@ -977,6 +979,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "038") => (RuleGroup::Preview, rules::ruff::rules::RedundantBoolLiteral),
(Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing),
(Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern),
(Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
@@ -1031,6 +1034,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// airflow
(Airflow, "001") => (RuleGroup::Stable, rules::airflow::rules::AirflowVariableNameTaskIdMismatch),
(Airflow, "301") => (RuleGroup::Preview, rules::airflow::rules::AirflowDagNoScheduleArgument),
// perflint
(Perflint, "101") => (RuleGroup::Stable, rules::perflint::rules::UnnecessaryListCast),

View File

@@ -13,6 +13,7 @@ mod tests {
use crate::{assert_messages, settings};
#[test_case(Rule::AirflowVariableNameTaskIdMismatch, Path::new("AIR001.py"))]
#[test_case(Rule::AirflowDagNoScheduleArgument, Path::new("AIR301.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -0,0 +1,84 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Expr;
use ruff_python_ast::{self as ast};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for a `DAG()` class or `@dag()` decorator without an explicit
/// `schedule` parameter.
///
/// ## Why is this bad?
/// The default `schedule` value on Airflow 2 is `timedelta(days=1)`, which is
/// almost never what a user is looking for. Airflow 3 changes this the default
/// to *None*, and would break existing DAGs using the implicit default.
///
/// If your DAG does not have an explicit `schedule` argument, Airflow 2
/// schedules a run for it every day (at the time determined by `start_date`).
/// Such a DAG will no longer be scheduled on Airflow 3 at all, without any
/// exceptions or other messages visible to the user.
///
/// ## Example
/// ```python
/// from airflow import DAG
///
///
/// # Using the implicit default schedule.
/// dag = DAG(dag_id="my_dag")
/// ```
///
/// Use instead:
/// ```python
/// from datetime import timedelta
///
/// from airflow import DAG
///
///
/// dag = DAG(dag_id="my_dag", schedule=timedelta(days=1))
/// ```
#[violation]
pub struct AirflowDagNoScheduleArgument;
impl Violation for AirflowDagNoScheduleArgument {
#[derive_message_formats]
fn message(&self) -> String {
"DAG should have an explicit `schedule` argument".to_string()
}
}
/// AIR301
pub(crate) fn dag_no_schedule_argument(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::AIRFLOW) {
return;
}
// Don't check non-call expressions.
let Expr::Call(ast::ExprCall {
func, arguments, ..
}) = expr
else {
return;
};
// We don't do anything unless this is a `DAG` (class) or `dag` (decorator
// function) from Airflow.
if !checker
.semantic()
.resolve_qualified_name(func)
.is_some_and(|qualname| matches!(qualname.segments(), ["airflow", .., "DAG" | "dag"]))
{
return;
}
// If there's a `schedule` keyword argument, we are good.
if arguments.find_keyword("schedule").is_some() {
return;
}
// Produce a diagnostic when the `schedule` keyword argument is not found.
let diagnostic = Diagnostic::new(AirflowDagNoScheduleArgument, expr.range());
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,3 +1,5 @@
pub(crate) use dag_schedule_argument::*;
pub(crate) use task_variable_name::*;
mod dag_schedule_argument;
mod task_variable_name;

View File

@@ -0,0 +1,20 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR301.py:3:1: AIR301 DAG should have an explicit `schedule` argument
|
1 | from airflow import DAG, dag
2 |
3 | DAG(dag_id="class_default_schedule")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301
4 |
5 | DAG(dag_id="class_schedule", schedule="@hourly")
|
AIR301.py:8:2: AIR301 DAG should have an explicit `schedule` argument
|
8 | @dag()
| ^^^^^ AIR301
9 | def decorator_default_schedule():
10 | pass
|

View File

@@ -311,7 +311,8 @@ pub(crate) fn shell_injection(checker: &mut Checker, call: &ast::ExprCall) {
}
// S603
Some(ShellKeyword {
truthiness: Truthiness::False | Truthiness::Falsey | Truthiness::Unknown,
truthiness:
Truthiness::False | Truthiness::Falsey | Truthiness::None | Truthiness::Unknown,
}) => {
if checker.enabled(Rule::SubprocessWithoutShellEqualsTrue) {
checker.diagnostics.push(Diagnostic::new(

View File

@@ -106,6 +106,10 @@ pub(crate) fn boolean_default_value_positional_argument(
decorator_list: &[Decorator],
parameters: &Parameters,
) {
// https://github.com/astral-sh/ruff/issues/14535
if checker.source_type.is_stub() {
return;
}
// Allow Boolean defaults in explicitly-allowed functions.
if is_allowed_func_def(name) {
return;

View File

@@ -115,6 +115,10 @@ pub(crate) fn boolean_type_hint_positional_argument(
decorator_list: &[Decorator],
parameters: &Parameters,
) {
// https://github.com/astral-sh/ruff/issues/14535
if checker.source_type.is_stub() {
return;
}
// Allow Boolean type hints in explicitly-allowed functions.
if is_allowed_func_def(name) {
return;

View File

@@ -11,12 +11,14 @@ use crate::checkers::ast::Checker;
use crate::registry::Rule;
/// ## What it does
/// Checks for abstract classes without abstract methods.
/// Checks for abstract classes without abstract methods or properties.
/// Annotated but unassigned class variables are regarded as abstract.
///
/// ## Why is this bad?
/// Abstract base classes are used to define interfaces. If an abstract base
/// class has no abstract methods, you may have forgotten to add an abstract
/// method to the class or omitted an `@abstractmethod` decorator.
/// class has no abstract methods or properties, you may have forgotten
/// to add an abstract method or property to the class,
/// or omitted an `@abstractmethod` decorator.
///
/// If the class is _not_ meant to be used as an interface, consider removing
/// the `ABC` base class from the class definition.
@@ -24,9 +26,12 @@ use crate::registry::Rule;
/// ## Example
/// ```python
/// from abc import ABC
/// from typing import ClassVar
///
///
/// class Foo(ABC):
/// class_var: ClassVar[str] = "assigned"
///
/// def method(self):
/// bar()
/// ```
@@ -34,9 +39,12 @@ use crate::registry::Rule;
/// Use instead:
/// ```python
/// from abc import ABC, abstractmethod
/// from typing import ClassVar
///
///
/// class Foo(ABC):
/// class_var: ClassVar[str] # unassigned
///
/// @abstractmethod
/// def method(self):
/// bar()
@@ -44,6 +52,7 @@ use crate::registry::Rule;
///
/// ## References
/// - [Python documentation: `abc`](https://docs.python.org/3/library/abc.html)
/// - [Python documentation: `typing.ClassVar`](https://docs.python.org/3/library/typing.html#typing.ClassVar)
#[violation]
pub struct AbstractBaseClassWithoutAbstractMethod {
name: String,
@@ -53,7 +62,7 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod {
#[derive_message_formats]
fn message(&self) -> String {
let AbstractBaseClassWithoutAbstractMethod { name } = self;
format!("`{name}` is an abstract base class, but it has no abstract methods")
format!("`{name}` is an abstract base class, but it has no abstract methods or properties")
}
}

View File

@@ -1,5 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_expr, is_mutable_func};
@@ -96,7 +97,7 @@ pub(crate) fn mutable_contextvar_default(checker: &mut Checker, call: &ast::Expr
&& !is_immutable_func(func, checker.semantic(), &extend_immutable_calls)))
&& checker
.semantic()
.resolve_qualified_name(&call.func)
.resolve_qualified_name(map_subscript(&call.func))
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["contextvars", "ContextVar"])
})

View File

@@ -11,10 +11,11 @@ use crate::checkers::ast::Checker;
///
/// ## Why is this bad?
/// The `warnings.warn` method uses a `stacklevel` of 1 by default, which
/// limits the rendered stack trace to that of the line on which the
/// `warn` method is called.
/// will output a stack frame of the line on which the "warn" method
/// is called. Setting it to a higher number will output a stack frame
/// from higher up the stack.
///
/// It's recommended to use a `stacklevel` of 2 or higher, give the caller
/// It's recommended to use a `stacklevel` of 2 or higher, to give the caller
/// more context about the warning.
///
/// ## Example

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
snapshot_kind: text
---
B024.py:18:7: B024 `Base_1` is an abstract base class, but it has no abstract methods
B024.py:18:7: B024 `Base_1` is an abstract base class, but it has no abstract methods or properties
|
18 | class Base_1(ABC): # error
| ^^^^^^ B024
@@ -10,7 +10,7 @@ B024.py:18:7: B024 `Base_1` is an abstract base class, but it has no abstract me
20 | foo()
|
B024.py:71:7: B024 `MetaBase_1` is an abstract base class, but it has no abstract methods
B024.py:71:7: B024 `MetaBase_1` is an abstract base class, but it has no abstract methods or properties
|
71 | class MetaBase_1(metaclass=ABCMeta): # error
| ^^^^^^^^^^ B024
@@ -18,7 +18,7 @@ B024.py:71:7: B024 `MetaBase_1` is an abstract base class, but it has no abstrac
73 | foo()
|
B024.py:82:7: B024 `abc_Base_1` is an abstract base class, but it has no abstract methods
B024.py:82:7: B024 `abc_Base_1` is an abstract base class, but it has no abstract methods or properties
|
82 | class abc_Base_1(abc.ABC): # error
| ^^^^^^^^^^ B024
@@ -26,7 +26,7 @@ B024.py:82:7: B024 `abc_Base_1` is an abstract base class, but it has no abstrac
84 | foo()
|
B024.py:87:7: B024 `abc_Base_2` is an abstract base class, but it has no abstract methods
B024.py:87:7: B024 `abc_Base_2` is an abstract base class, but it has no abstract methods or properties
|
87 | class abc_Base_2(metaclass=abc.ABCMeta): # error
| ^^^^^^^^^^ B024
@@ -34,7 +34,7 @@ B024.py:87:7: B024 `abc_Base_2` is an abstract base class, but it has no abstrac
89 | foo()
|
B024.py:92:7: B024 `notabc_Base_1` is an abstract base class, but it has no abstract methods
B024.py:92:7: B024 `notabc_Base_1` is an abstract base class, but it has no abstract methods or properties
|
92 | class notabc_Base_1(notabc.ABC): # error
| ^^^^^^^^^^^^^ B024
@@ -42,21 +42,21 @@ B024.py:92:7: B024 `notabc_Base_1` is an abstract base class, but it has no abst
94 | foo()
|
B024.py:132:7: B024 `abc_set_class_variable_2` is an abstract base class, but it has no abstract methods
B024.py:132:7: B024 `abc_set_class_variable_2` is an abstract base class, but it has no abstract methods or properties
|
132 | class abc_set_class_variable_2(ABC): # error (not an abstract attribute)
| ^^^^^^^^^^^^^^^^^^^^^^^^ B024
133 | foo = 2
|
B024.py:136:7: B024 `abc_set_class_variable_3` is an abstract base class, but it has no abstract methods
B024.py:136:7: B024 `abc_set_class_variable_3` is an abstract base class, but it has no abstract methods or properties
|
136 | class abc_set_class_variable_3(ABC): # error (not an abstract attribute)
| ^^^^^^^^^^^^^^^^^^^^^^^^ B024
137 | foo: int = 2
|
B024.py:141:7: B024 `abc_set_class_variable_4` is an abstract base class, but it has no abstract methods
B024.py:141:7: B024 `abc_set_class_variable_4` is an abstract base class, but it has no abstract methods or properties
|
140 | # this doesn't actually declare a class variable, it's just an expression
141 | class abc_set_class_variable_4(ABC): # error

View File

@@ -2,127 +2,149 @@
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
snapshot_kind: text
---
B039.py:19:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
18 | # Bad
19 | ContextVar("cv", default=[])
| ^^ B039
20 | ContextVar("cv", default={})
21 | ContextVar("cv", default=list())
|
= help: Replace with `None`; initialize with `.set()``
B039.py:20:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
18 | # Bad
19 | ContextVar("cv", default=[])
20 | ContextVar("cv", default={})
| ^^ B039
21 | ContextVar("cv", default=list())
22 | ContextVar("cv", default=set())
|
= help: Replace with `None`; initialize with `.set()``
B039.py:21:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
19 | ContextVar("cv", default=[])
20 | ContextVar("cv", default={})
21 | ContextVar("cv", default=list())
| ^^^^^^ B039
22 | ContextVar("cv", default=set())
23 | ContextVar("cv", default=dict())
20 | # Bad
21 | ContextVar("cv", default=[])
| ^^ B039
22 | ContextVar("cv", default={})
23 | ContextVar("cv", default=list())
|
= help: Replace with `None`; initialize with `.set()``
B039.py:22:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
20 | ContextVar("cv", default={})
21 | ContextVar("cv", default=list())
22 | ContextVar("cv", default=set())
| ^^^^^ B039
23 | ContextVar("cv", default=dict())
24 | ContextVar("cv", default=[char for char in "foo"])
20 | # Bad
21 | ContextVar("cv", default=[])
22 | ContextVar("cv", default={})
| ^^ B039
23 | ContextVar("cv", default=list())
24 | ContextVar("cv", default=set())
|
= help: Replace with `None`; initialize with `.set()``
B039.py:23:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
21 | ContextVar("cv", default=list())
22 | ContextVar("cv", default=set())
23 | ContextVar("cv", default=dict())
21 | ContextVar("cv", default=[])
22 | ContextVar("cv", default={})
23 | ContextVar("cv", default=list())
| ^^^^^^ B039
24 | ContextVar("cv", default=[char for char in "foo"])
25 | ContextVar("cv", default={char for char in "foo"})
24 | ContextVar("cv", default=set())
25 | ContextVar("cv", default=dict())
|
= help: Replace with `None`; initialize with `.set()``
B039.py:24:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
22 | ContextVar("cv", default=set())
23 | ContextVar("cv", default=dict())
24 | ContextVar("cv", default=[char for char in "foo"])
| ^^^^^^^^^^^^^^^^^^^^^^^^ B039
25 | ContextVar("cv", default={char for char in "foo"})
26 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
22 | ContextVar("cv", default={})
23 | ContextVar("cv", default=list())
24 | ContextVar("cv", default=set())
| ^^^^^ B039
25 | ContextVar("cv", default=dict())
26 | ContextVar("cv", default=[char for char in "foo"])
|
= help: Replace with `None`; initialize with `.set()``
B039.py:25:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
23 | ContextVar("cv", default=dict())
24 | ContextVar("cv", default=[char for char in "foo"])
25 | ContextVar("cv", default={char for char in "foo"})
| ^^^^^^^^^^^^^^^^^^^^^^^^ B039
26 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
27 | ContextVar("cv", default=collections.deque())
23 | ContextVar("cv", default=list())
24 | ContextVar("cv", default=set())
25 | ContextVar("cv", default=dict())
| ^^^^^^ B039
26 | ContextVar("cv", default=[char for char in "foo"])
27 | ContextVar("cv", default={char for char in "foo"})
|
= help: Replace with `None`; initialize with `.set()``
B039.py:26:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
24 | ContextVar("cv", default=[char for char in "foo"])
25 | ContextVar("cv", default={char for char in "foo"})
26 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B039
27 | ContextVar("cv", default=collections.deque())
24 | ContextVar("cv", default=set())
25 | ContextVar("cv", default=dict())
26 | ContextVar("cv", default=[char for char in "foo"])
| ^^^^^^^^^^^^^^^^^^^^^^^^ B039
27 | ContextVar("cv", default={char for char in "foo"})
28 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
= help: Replace with `None`; initialize with `.set()``
B039.py:27:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
25 | ContextVar("cv", default={char for char in "foo"})
26 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
27 | ContextVar("cv", default=collections.deque())
25 | ContextVar("cv", default=dict())
26 | ContextVar("cv", default=[char for char in "foo"])
27 | ContextVar("cv", default={char for char in "foo"})
| ^^^^^^^^^^^^^^^^^^^^^^^^ B039
28 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
29 | ContextVar("cv", default=collections.deque())
|
= help: Replace with `None`; initialize with `.set()``
B039.py:28:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
26 | ContextVar("cv", default=[char for char in "foo"])
27 | ContextVar("cv", default={char for char in "foo"})
28 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B039
29 | ContextVar("cv", default=collections.deque())
30 | ContextVar("cv", default=set[str]())
|
= help: Replace with `None`; initialize with `.set()``
B039.py:29:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
27 | ContextVar("cv", default={char for char in "foo"})
28 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
29 | ContextVar("cv", default=collections.deque())
| ^^^^^^^^^^^^^^^^^^^ B039
28 |
29 | def bar() -> list[int]:
30 | ContextVar("cv", default=set[str]())
31 | ContextVar[set[str]]("cv", default=set[str]())
|
= help: Replace with `None`; initialize with `.set()``
B039.py:32:26: B039 Do not use mutable data structures for `ContextVar` defaults
B039.py:30:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
30 | return [1, 2, 3]
31 |
32 | ContextVar("cv", default=bar())
| ^^^^^ B039
33 | ContextVar("cv", default=time.time())
28 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
29 | ContextVar("cv", default=collections.deque())
30 | ContextVar("cv", default=set[str]())
| ^^^^^^^^^^ B039
31 | ContextVar[set[str]]("cv", default=set[str]())
|
= help: Replace with `None`; initialize with `.set()``
B039.py:33:26: B039 Do not use mutable data structures for `ContextVar` defaults
B039.py:31:36: B039 Do not use mutable data structures for `ContextVar` defaults
|
32 | ContextVar("cv", default=bar())
33 | ContextVar("cv", default=time.time())
| ^^^^^^^^^^^ B039
34 |
35 | def baz(): ...
29 | ContextVar("cv", default=collections.deque())
30 | ContextVar("cv", default=set[str]())
31 | ContextVar[set[str]]("cv", default=set[str]())
| ^^^^^^^^^^ B039
32 |
33 | def bar() -> list[int]:
|
= help: Replace with `None`; initialize with `.set()``
B039.py:36:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
35 | def baz(): ...
36 | ContextVar("cv", default=baz())
34 | return [1, 2, 3]
35 |
36 | ContextVar("cv", default=bar())
| ^^^^^ B039
37 | ContextVar("cv", default=time.time())
|
= help: Replace with `None`; initialize with `.set()``
B039.py:37:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
36 | ContextVar("cv", default=bar())
37 | ContextVar("cv", default=time.time())
| ^^^^^^^^^^^ B039
38 |
39 | def baz(): ...
|
= help: Replace with `None`; initialize with `.set()``
B039.py:40:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
39 | def baz(): ...
40 | ContextVar("cv", default=baz())
| ^^^^^ B039
|
= help: Replace with `None`; initialize with `.set()``

View File

@@ -36,6 +36,10 @@ mod tests {
Rule::BuiltinModuleShadowing,
Path::new("A005/modules/package/bisect.py")
)]
#[test_case(
Rule::BuiltinModuleShadowing,
Path::new("A005/modules/_abc/__init__.py")
)]
#[test_case(Rule::BuiltinModuleShadowing, Path::new("A005/modules/package/xml.py"))]
#[test_case(Rule::BuiltinLambdaArgumentShadowing, Path::new("A006.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
@@ -91,6 +95,10 @@ mod tests {
Rule::BuiltinModuleShadowing,
Path::new("A005/modules/package/bisect.py")
)]
#[test_case(
Rule::BuiltinModuleShadowing,
Path::new("A005/modules/_abc/__init__.py")
)]
#[test_case(Rule::BuiltinModuleShadowing, Path::new("A005/modules/package/xml.py"))]
fn builtins_allowed_modules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(

View File

@@ -1,7 +1,5 @@
use std::path::Path;
use crate::package::PackageRoot;
use crate::settings::types::PythonVersion;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::PySourceType;
@@ -9,6 +7,9 @@ use ruff_python_stdlib::path::is_module_file;
use ruff_python_stdlib::sys::is_known_standard_library;
use ruff_text_size::TextRange;
use crate::package::PackageRoot;
use crate::settings::types::PythonVersion;
/// ## What it does
/// Checks for modules that use the same names as Python builtin modules.
///
@@ -47,25 +48,35 @@ pub(crate) fn builtin_module_shadowing(
return None;
}
if let Some(package) = package {
let module_name = if is_module_file(path) {
package.path().file_name().unwrap().to_string_lossy()
} else {
path.file_stem().unwrap().to_string_lossy()
};
let package = package?;
if is_known_standard_library(target_version.minor(), &module_name)
&& allowed_modules
.iter()
.all(|allowed_module| allowed_module != &module_name)
{
return Some(Diagnostic::new(
BuiltinModuleShadowing {
name: module_name.to_string(),
},
TextRange::default(),
));
}
let module_name = if is_module_file(path) {
package.path().file_name().unwrap().to_string_lossy()
} else {
path.file_stem().unwrap().to_string_lossy()
};
if !is_known_standard_library(target_version.minor(), &module_name) {
return None;
}
None
// Shadowing private stdlib modules is okay.
// https://github.com/astral-sh/ruff/issues/12949
if module_name.starts_with('_') && !module_name.starts_with("__") {
return None;
}
if allowed_modules
.iter()
.any(|allowed_module| allowed_module == &module_name)
{
return None;
}
Some(Diagnostic::new(
BuiltinModuleShadowing {
name: module_name.to_string(),
},
TextRange::default(),
))
}

View File

@@ -0,0 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
snapshot_kind: text
---

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