Compare commits

...

86 Commits

Author SHA1 Message Date
Dhruv Manilawala
b3b2c982cd Update CHANGELOG.md with the new commits for 0.8.1 (#14664)
The 0.8.1 release was delayed, so this PR updates the CHANGELOG.md with
the latest commits on `main`.
2024-11-29 03:15:36 +00:00
Simon Brugman
abb3c6ea95 [flake8-pyi] Avoid rewriting invalid type expressions in unnecessary-type-union (PYI055) (#14660) 2024-11-28 18:30:50 +00:00
Brent Westbrook
224fe75a76 [ruff] Implement unnecessary-regular-expression (RUF055) (#14659)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Simon Brugman <sbrugman@users.noreply.github.com>
2024-11-28 18:29:23 +00:00
Simon Brugman
dc29f52750 [flake8-pyi, ruff] Fix traversal of nested literals and unions (PYI016, PYI051, PYI055, PYI062, RUF041) (#14641) 2024-11-28 18:07:12 +00:00
David Salvisberg
d9cbf2fe44 Avoids unnecessary overhead for TC004, when TC001-003 are disabled (#14657) 2024-11-28 16:28:24 +01:00
Samodya Abeysiriwardane
3f6c65e78c [red-knot] Fix merged type after if-else without explicit else branch (#14621)
## Summary

Closes: https://github.com/astral-sh/ruff/issues/14593

The final type of a variable after if-statement without explicit else
branch should be similar to having an explicit else branch.

## Test Plan

Originally failed test cases from the bug are added.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-11-28 06:23:55 -08:00
Dhruv Manilawala
976c37a849 Bump version to 0.8.1 (#14655) 2024-11-28 19:12:50 +05:30
David Peter
a378ff38dc [red-knot] Fix Boolean flags in mdtests (#14654)
## Summary

Similar to #14652, but now with conditions that are `Literal[True]`
(instead of `Literal[False]`), where we want them to be `bool`.
2024-11-28 14:29:35 +01:00
Alex Waygood
d8bca0d3a2 Fix bug where methods defined using lambdas were flagged by FURB118 (#14639) 2024-11-28 12:58:23 +00:00
David Peter
6f1cf5b686 [red-knot] Minor fix in MRO tests (#14652)
## Summary

`bool()` is equal to `False`, and we infer `Literal[False]` for it. Which
means that the test here will fail as soon as we treat the body of
this `if` as unreachable.
2024-11-28 10:17:15 +01:00
David Peter
8639f8c1a6 CI: Treat mdtest Markdown files as code (#14653)
## Summary

Make sure we run the tests for mdtest-only changes.

## Test Plan

Tested if positive glob patterns override negative patterns here:
https://codepen.io/mrmlnc/pen/OXQjMe
2024-11-28 10:04:20 +01:00
Alex Waygood
f1b2e85339 py-fuzzer: recommend using uvx rather than uv run to run the fuzzer (#14645) 2024-11-27 22:19:52 +00:00
David Salvisberg
6d61c8aa16 Fixes minor bug in SemanticModel::lookup_symbol (#14643)
## Summary

This came up as part of #12927 when implementing
`SemanticModel::simulate_runtime_load`.

Should be fairly self-explanatory, if the scope returns a binding with
`BindingKind::Annotation` the bottom part of the loop gets skipped, so
there's no chance for `seen_function` to have been updated. So unless
there's something subtle going on here, like function scopes never
containing bindings with `BindingKind::Annotation`, this seems like a
bug.

## Test Plan

`cargo nextest run`
2024-11-27 16:50:19 -05:00
David Salvisberg
8a7ba5d2df [flake8-type-checking] Fixes quote_type_expression (#14634) 2024-11-27 18:58:48 +01:00
Brent Westbrook
6fcbe8efb4 [ruff] Detect redirected-noqa in file-level comments (RUF101) (#14635) 2024-11-27 18:25:47 +01:00
Alexandra Valentine-Ketchum
c40b37aa36 N811 & N814: eliminate false positives for single-letter names (#14584)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-11-27 14:38:36 +00:00
Alex Waygood
ef0e2a6e1b Refactor crates/ruff_python_stdlib/src/builtins.rs to make it easier to add support for new Python versions (#14632) 2024-11-27 12:20:21 +00:00
Alex Waygood
4fb1416bf4 Minor stylistic improvements for functions detecting PEP-604 unions (#14633) 2024-11-27 11:29:37 +00:00
Simon Brugman
8a860b89b4 Add social icons to the footer (#14591)
## Summary

Add social icons to the footer

`mkdocs-material` update is required for the `x-twitter` icon.

## Test Plan

Tested locally. 

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-11-27 11:07:45 +00:00
Dhruv Manilawala
f96fa6b0e2 Do not consider f-strings with escaped newlines as multiline (#14624)
## Summary

This PR fixes a bug in the f-string formatting to not consider the
escaped newlines for `is_multiline`. This is done by checking if the
f-string is triple-quoted or not similar to normal string literals.

This is not required to be gated behind preview because the logic change
for `is_multiline` was added in
https://github.com/astral-sh/ruff/pull/14454.

## Test Plan

Add a test case which formats differently on `main`:
https://play.ruff.rs/ea3c55c2-f0fe-474e-b6b8-e3365e0ede5e
2024-11-27 10:25:38 +00:00
Dhruv Manilawala
4cd2b9926e Gate is_multiline change behind preview (#14630)
## Summary

Ref:
https://github.com/astral-sh/ruff/pull/14624#pullrequestreview-2464127254

## Test Plan

The test case in the follow-up PR showcases the difference between
preview and non-preview formatting:
https://github.com/astral-sh/ruff/pull/14624/files#diff-dc25bd4df280d9a9180598075b5bc2d0bac30af956767b373561029309c8f024
2024-11-27 15:50:28 +05:30
Simon Brugman
11a2929ed7 [ruff] Implement unnecessary-nested-literal (RUF041) (#14323)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-11-27 10:01:50 +00:00
InSync
187974eff4 [flake8-use-pathlib] Recommend Path.iterdir() over os.listdir() (PTH208) (#14509)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-11-27 09:53:13 +00:00
Micha Reiser
14ba469fc0 Use a derive macro for Violations (#14557)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-11-27 09:41:40 +00:00
David Salvisberg
6fd10e2fe7 [flake8-type-checking] Adds implementation for TC007 and TC008 (#12927)
Co-authored-by: Simon Brugman <sbrugman@users.noreply.github.com>
Co-authored-by: Carl Meyer <carl@oddbird.net>
2024-11-27 09:51:20 +01:00
Alex Waygood
e0f3eaf1dd Turn the fuzz-parser script into a properly packaged Python project (#14606)
## Summary

This PR gets rid of the `requirements.in` and `requirements.txt` files
in the `scripts/fuzz-parser` directory, and replaces them with
`pyproject.toml` and `uv.lock` files. The script is renamed from
`fuzz-parser` to `py-fuzzer` (since it can now also be used to fuzz
red-knot as well as the parser, following
https://github.com/astral-sh/ruff/pull/14566), and moved from the
`scripts/` directory to the `python/` directory, since it's now a
(uv)-pip-installable project in its own right.

I've been resisting this for a while, because conceptually this script
just doesn't feel "complicated" enough to me for it to be a full-blown
package. However, I think it's time to do this. Making it a proper
package has several advantages:
- It means we can run it from the project root using `uv run` without
having to activate a virtual environment and ensure that all required
dependencies are installed into that environment
- Using a `pyproject.toml` file means that we can express that the
project requires Python 3.12+ to run properly; this wasn't possible
before
- I've been running mypy on the project locally when I've been working
on it or reviewing other people's PRs; now I can put the mypy config for
the project in the `pyproject.toml` file

## Test Plan

I manually tested that all the commands detailed in
`python/py-fuzzer/README.md` work for me locally.

---------

Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
2024-11-27 08:09:04 +00:00
Dhruv Manilawala
c84c690f1e Avoid invalid syntax for format-spec with quotes for all Python versions (#14625)
## Summary

fixes: #14608

The logic that was only applied for 3.12+ target version needs to be
applied for other versions as well.

## Test Plan

I've moved the existing test cases for 3.12 only to `f_string.py` so
that it's tested against the default target version.

I think we should probably enabled testing for two target version (pre
3.12 and 3.12) but it won't highlight any issue because the parser
doesn't consider this. Maybe we should enable this once we have target
version specific syntax errors in place
(https://github.com/astral-sh/ruff/issues/6591).
2024-11-27 13:19:33 +05:30
Dhruv Manilawala
0d649f9afd Check that airflow module is seen for AIR001 (#14627) 2024-11-27 07:25:08 +00: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
871 changed files with 15731 additions and 4604 deletions

5
.github/CODEOWNERS vendored
View File

@@ -13,9 +13,10 @@
# flake8-pyi
/crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood
# Script for fuzzing the parser
/scripts/fuzz-parser/ @AlexWaygood
# Script for fuzzing the parser/red-knot etc.
/python/py-fuzzer/ @AlexWaygood
# red-knot
/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp
/scripts/knot_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp

View File

@@ -49,7 +49,7 @@ jobs:
- crates/ruff_text_size/**
- crates/ruff_python_ast/**
- crates/ruff_python_parser/**
- scripts/fuzz-parser/**
- python/py-fuzzer/**
- .github/workflows/ci.yaml
linter:
@@ -82,6 +82,7 @@ jobs:
code:
- "**/*"
- "!**/*.md"
- "crates/red_knot_python_semantic/resources/mdtest/**/*.md"
- "!docs/**"
- "!assets/**"
@@ -157,6 +158,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 +240,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:
@@ -291,13 +318,7 @@ jobs:
FORCE_COLOR: 1
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install Python requirements
run: uv pip install -r scripts/fuzz-parser/requirements.txt --system
- uses: astral-sh/setup-uv@v4
- uses: actions/download-artifact@v4
name: Download Ruff binary to test
id: download-cached-binary
@@ -309,7 +330,15 @@ 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
(
uvx \
--python=${{ env.PYTHON_VERSION }} \
--from=./python/py-fuzzer \
fuzz \
--test-executable=${{ steps.download-cached-binary.outputs.download-path }}/ruff \
--bin=ruff \
0-500
)
scripts:
name: "test scripts"

View File

@@ -32,13 +32,7 @@ jobs:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install Python requirements
run: uv pip install -r scripts/fuzz-parser/requirements.txt --system
- uses: astral-sh/setup-uv@v4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -49,7 +43,16 @@ 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: |
(
uvx \
--python=3.12 \
--from=./python/py-fuzzer \
fuzz \
--test-executable=target/debug/ruff \
--bin=ruff \
$(shuf -i 0-9999999999999999999 -n 1000)
)
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,145 @@
# Changelog
## 0.8.1
### Preview features
- Formatter: Avoid invalid syntax for format-spec with quotes for all Python versions ([#14625](https://github.com/astral-sh/ruff/pull/14625))
- Formatter: Consider quotes inside format-specs when choosing the quotes for an f-string ([#14493](https://github.com/astral-sh/ruff/pull/14493))
- Formatter: Do not consider f-strings with escaped newlines as multiline ([#14624](https://github.com/astral-sh/ruff/pull/14624))
- Formatter: Fix f-string formatting in assignment statement ([#14454](https://github.com/astral-sh/ruff/pull/14454))
- Formatter: Fix unnecessary space around power operator (`**`) in overlong f-string expressions ([#14489](https://github.com/astral-sh/ruff/pull/14489))
- \[`airflow`\] Avoid implicit `schedule` argument to `DAG` and `@dag` (`AIR301`) ([#14581](https://github.com/astral-sh/ruff/pull/14581))
- \[`flake8-builtins`\] Exempt private built-in modules (`A005`) ([#14505](https://github.com/astral-sh/ruff/pull/14505))
- \[`flake8-pytest-style`\] Fix `pytest.mark.parametrize` rules to check calls instead of decorators ([#14515](https://github.com/astral-sh/ruff/pull/14515))
- \[`flake8-type-checking`\] Implement `runtime-cast-value` (`TC006`) ([#14511](https://github.com/astral-sh/ruff/pull/14511))
- \[`flake8-type-checking`\] Implement `unquoted-type-alias` (`TC007`) and `quoted-type-alias` (`TC008`) ([#12927](https://github.com/astral-sh/ruff/pull/12927))
- \[`flake8-use-pathlib`\] Recommend `Path.iterdir()` over `os.listdir()` (`PTH208`) ([#14509](https://github.com/astral-sh/ruff/pull/14509))
- \[`pylint`\] Extend `invalid-envvar-default` to detect `os.environ.get` (`PLW1508`) ([#14512](https://github.com/astral-sh/ruff/pull/14512))
- \[`pylint`\] Implement `len-test` (`PLC1802`) ([#14309](https://github.com/astral-sh/ruff/pull/14309))
- \[`refurb`\] Fix bug where methods defined using lambdas were flagged by `FURB118` ([#14639](https://github.com/astral-sh/ruff/pull/14639))
- \[`ruff`\] Auto-add `r` prefix when string has no backslashes for `unraw-re-pattern` (`RUF039`) ([#14536](https://github.com/astral-sh/ruff/pull/14536))
- \[`ruff`\] Implement `invalid-assert-message-literal-argument` (`RUF040`) ([#14488](https://github.com/astral-sh/ruff/pull/14488))
- \[`ruff`\] Implement `unnecessary-nested-literal` (`RUF041`) ([#14323](https://github.com/astral-sh/ruff/pull/14323))
- \[`ruff`\] Implement `unnecessary-regular-expression` (`RUF055`) ([#14659](https://github.com/astral-sh/ruff/pull/14659))
### Rule changes
- Ignore more rules for stub files ([#14541](https://github.com/astral-sh/ruff/pull/14541))
- \[`pep8-naming`\] Eliminate false positives for single-letter names (`N811`, `N814`) ([#14584](https://github.com/astral-sh/ruff/pull/14584))
- \[`pyflakes`\] Avoid false positives in `@no_type_check` contexts (`F821`, `F722`) ([#14615](https://github.com/astral-sh/ruff/pull/14615))
- \[`ruff`\] Detect redirected-noqa in file-level comments (`RUF101`) ([#14635](https://github.com/astral-sh/ruff/pull/14635))
- \[`ruff`\] Mark fixes for `unsorted-dunder-all` and `unsorted-dunder-slots` as unsafe when there are complex comments in the sequence (`RUF022`, `RUF023`) ([#14560](https://github.com/astral-sh/ruff/pull/14560))
### Bug fixes
- Avoid fixing code to `None | None` for `redundant-none-literal` (`PYI061`) and `never-union` (`RUF020`) ([#14583](https://github.com/astral-sh/ruff/pull/14583), [#14589](https://github.com/astral-sh/ruff/pull/14589))
- \[`flake8-bugbear`\] Fix `mutable-contextvar-default` to resolve annotated function calls properly (`B039`) ([#14532](https://github.com/astral-sh/ruff/pull/14532))
- \[`flake8-pyi`, `ruff`\] Fix traversal of nested literals and unions (`PYI016`, `PYI051`, `PYI055`, `PYI062`, `RUF041`) ([#14641](https://github.com/astral-sh/ruff/pull/14641))
- \[`flake8-pyi`\] Avoid rewriting invalid type expressions in `unnecessary-type-union` (`PYI055`) ([#14660](https://github.com/astral-sh/ruff/pull/14660))
- \[`flake8-type-checking`\] Avoid syntax errors and type checking problem for quoted annotations autofix (`TC003`, `TC006`) ([#14634](https://github.com/astral-sh/ruff/pull/14634))
- \[`pylint`\] Do not wrap function calls in parentheses in the fix for unnecessary-dunder-call (`PLC2801`) ([#14601](https://github.com/astral-sh/ruff/pull/14601))
- \[`ruff`\] Handle `attrs`'s `auto_attribs` correctly (`RUF009`) ([#14520](https://github.com/astral-sh/ruff/pull/14520))
## 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 +1118,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

View File

@@ -139,7 +139,7 @@ At a high level, the steps involved in adding a new lint rule are as follows:
1. Create a file for your rule (e.g., `crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs`).
1. In that file, define a violation struct (e.g., `pub struct AssertFalse`). You can grep for
`#[violation]` to see examples.
`#[derive(ViolationMetadata)]` to see examples.
1. In that file, define a function that adds the violation to the diagnostic list as appropriate
(e.g., `pub(crate) fn assert_false`) based on whatever inputs are required for the rule (e.g.,

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.1"
dependencies = [
"anyhow",
"argfile",
@@ -2708,7 +2708,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.7.4"
version = "0.8.1"
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.1"
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.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.8.1/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.1
hooks:
# Run the linter.
- id: ruff

View File

@@ -1,21 +1,25 @@
doc-valid-idents = [
"..",
"CodeQL",
"FastAPI",
"IPython",
"LangChain",
"LibCST",
"McCabe",
"NumPy",
"SCREAMING_SNAKE_CASE",
"SQLAlchemy",
"StackOverflow",
"PyCharm",
"..",
"CodeQL",
"FastAPI",
"IPython",
"LangChain",
"LibCST",
"McCabe",
"NumPy",
"SCREAMING_SNAKE_CASE",
"SQLAlchemy",
"StackOverflow",
"PyCharm",
"SNMPv1",
"SNMPv2",
"SNMPv3",
"PyFlakes"
]
ignore-interior-mutability = [
# Interned is read-only. The wrapped `Rc` never gets updated.
"ruff_formatter::format_element::Interned",
# The expression is read-only.
"ruff_python_ast::hashable::HashableExpr",
# Interned is read-only. The wrapped `Rc` never gets updated.
"ruff_formatter::format_element::Interned",
# The expression is read-only.
"ruff_python_ast::hashable::HashableExpr",
]

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

@@ -189,3 +189,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
@@ -44,7 +44,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

@@ -78,7 +78,7 @@ from other import Literal
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

@@ -256,7 +256,7 @@ class O: ...
class X(O): ...
class Y(O): ...
if bool():
if returns_bool():
foo = Y
else:
foo = object

View File

@@ -0,0 +1,64 @@
# Consolidating narrowed types after if statement
## After if-else statements, narrowing has no effect if the variable is not mutated in any branch
```py
def optional_int() -> int | None: ...
x = optional_int()
if x is None:
pass
else:
pass
reveal_type(x) # revealed: int | None
```
## Narrowing can have a persistent effect if the variable is mutated in one branch
```py
def optional_int() -> int | None: ...
x = optional_int()
if x is None:
x = 10
else:
pass
reveal_type(x) # revealed: int
```
## An if statement without an explicit `else` branch is equivalent to one with a no-op `else` branch
```py
def optional_int() -> int | None: ...
x = optional_int()
y = optional_int()
if x is None:
x = 0
if y is None:
pass
reveal_type(x) # revealed: int
reveal_type(y) # revealed: int | None
```
## An if-elif without an explicit else branch is equivalent to one with an empty else branch
```py
def optional_int() -> int | None: ...
x = optional_int()
if x is None:
x = 0
elif x > 50:
x = 50
reveal_type(x) # revealed: int
```

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

@@ -21,10 +21,11 @@ reveal_type(Identity[0]) # revealed: str
## Class getitem union
```py
flag = True
def bool_instance() -> bool:
return True
class UnionClassGetItem:
if flag:
if bool_instance():
def __class_getitem__(cls, item: int) -> str:
return item
@@ -59,9 +60,10 @@ reveal_type(x[0]) # revealed: str | int
## Class getitem with unbound method union
```py
flag = True
def bool_instance() -> bool:
return True
if flag:
if bool_instance():
class Spam:
def __class_getitem__(self, x: int) -> str:
return "foo"
@@ -77,9 +79,10 @@ reveal_type(Spam[42])
## TODO: Class getitem non-class union
```py
flag = True
def bool_instance() -> bool:
return True
if flag:
if bool_instance():
class Eggs:
def __class_getitem__(self, x: int) -> str:
return "foo"

View File

@@ -30,10 +30,11 @@ reveal_type(Identity()[0]) # revealed: int
## Getitem union
```py
flag = True
def bool_instance() -> bool:
return True
class Identity:
if flag:
if bool_instance():
def __getitem__(self, index: int) -> int:
return index

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 {
@@ -719,7 +769,22 @@ where
let mut constraints = vec![constraint];
self.visit_body(&node.body);
let mut post_clauses: Vec<FlowSnapshot> = vec![];
for clause in &node.elif_else_clauses {
let elif_else_clauses = node
.elif_else_clauses
.iter()
.map(|clause| (clause.test.as_ref(), clause.body.as_slice()));
let has_else = node
.elif_else_clauses
.last()
.is_some_and(|clause| clause.test.is_none());
let elif_else_clauses = elif_else_clauses.chain(if has_else {
// if there's an `else` clause already, we don't need to add another
None
} else {
// if there's no `else` branch, we should add a no-op `else` branch
Some((None, Default::default()))
});
for (clause_test, clause_body) in elif_else_clauses {
// snapshot after every block except the last; the last one will just become
// the state that we merge the other snapshots into
post_clauses.push(self.flow_snapshot());
@@ -729,24 +794,15 @@ where
for constraint in &constraints {
self.record_negated_constraint(*constraint);
}
if let Some(elif_test) = &clause.test {
if let Some(elif_test) = clause_test {
self.visit_expr(elif_test);
constraints.push(self.record_expression_constraint(elif_test));
}
self.visit_body(&clause.body);
self.visit_body(clause_body);
}
for post_clause_state in post_clauses {
self.flow_merge(post_clause_state);
}
let has_else = node
.elif_else_clauses
.last()
.is_some_and(|clause| clause.test.is_none());
if !has_else {
// if there's no else clause, then it's possible we took none of the branches,
// and the pre_if state can reach here
self.flow_merge(pre_if);
}
}
ast::Stmt::While(ast::StmtWhile {
test,
@@ -763,7 +819,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 +861,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 +890,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 +1195,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

@@ -309,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.
@@ -379,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};
@@ -987,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

@@ -52,11 +52,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 +439,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 +612,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 +857,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 +1050,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 +1130,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 +1286,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 +1436,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 +1511,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 +1525,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 +1560,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 +1879,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 +1915,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 +2243,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 +2437,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 +2450,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 +2464,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 +2477,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 +2592,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 +2682,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 +2713,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 +2722,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 +2734,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 +2743,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 +3019,7 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown
}
}
_ => Type::Todo, // TODO other unary op types
_ => todo_type!(), // TODO other unary op types
}
}
@@ -3227,7 +3267,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 +3705,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 +3730,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 +3785,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 +3813,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,
@@ -4189,6 +4229,7 @@ impl<'db> TypeInferenceBuilder<'db> {
"annotation-f-string",
format_args!("Type expressions cannot use f-strings"),
);
self.infer_fstring_expression(fstring);
Type::Unknown
}
@@ -4263,7 +4304,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 +4312,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 +4322,14 @@ 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,
ast::Expr::EllipsisLiteral(_literal) => todo_type!(),
// 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::BytesLiteral(_literal) => todo_type!(),
ast::Expr::NumberLiteral(_literal) => todo_type!(),
ast::Expr::BooleanLiteral(_literal) => todo_type!(),
ast::Expr::Subscript(subscript) => {
let ast::ExprSubscript {
@@ -4331,9 +4372,13 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO PEP 646
ast::Expr::Starred(starred) => {
self.infer_starred_expression(starred);
Type::Todo
todo_type!()
}
// 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,
// 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.
@@ -4417,7 +4462,6 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_slice_expression(slice);
Type::Unknown
}
ast::Expr::IpyEscapeCommand(_) => todo!("Implement Ipy escape command support"),
}
}
@@ -4476,7 +4520,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 +4535,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 +4551,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 +4576,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 +4612,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 +5009,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 +5981,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 +6015,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 +6059,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,15 @@ 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),
// related to circular references in f-string annotations (invalid syntax)
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_15.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_14.py", false, true),
];

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.7.4"
version = "0.8.1"
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

@@ -2,7 +2,7 @@ pub use diagnostic::{Diagnostic, DiagnosticKind};
pub use edit::Edit;
pub use fix::{Applicability, Fix, IsolationLevel};
pub use source_map::{SourceMap, SourceMarker};
pub use violation::{AlwaysFixableViolation, FixAvailability, Violation};
pub use violation::{AlwaysFixableViolation, FixAvailability, Violation, ViolationMetadata};
mod diagnostic;
mod edit;

View File

@@ -1,3 +1,4 @@
use crate::DiagnosticKind;
use std::fmt::{Debug, Display};
#[derive(Debug, Copy, Clone)]
@@ -17,7 +18,16 @@ impl Display for FixAvailability {
}
}
pub trait Violation: Debug + PartialEq + Eq {
pub trait ViolationMetadata {
/// Returns the rule name of this violation
fn rule_name() -> &'static str;
/// Returns an explanation of what this violation catches,
/// why it's bad, and what users should do instead.
fn explain() -> Option<&'static str>;
}
pub trait Violation: ViolationMetadata {
/// `None` in the case a fix is never available or otherwise Some
/// [`FixAvailability`] describing the available fix.
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
@@ -41,7 +51,7 @@ pub trait Violation: Debug + PartialEq + Eq {
/// This trait exists just to make implementing the [`Violation`] trait more
/// convenient for violations that can always be fixed.
pub trait AlwaysFixableViolation: Debug + PartialEq + Eq {
pub trait AlwaysFixableViolation: ViolationMetadata {
/// The message used to describe the violation.
fn message(&self) -> String;
@@ -69,3 +79,16 @@ impl<V: AlwaysFixableViolation> Violation for V {
<Self as AlwaysFixableViolation>::message_formats()
}
}
impl<T> From<T> for DiagnosticKind
where
T: Violation,
{
fn from(value: T) -> Self {
Self {
body: Violation::message(&value),
suggestion: Violation::fix_title(&value),
name: T::rule_name().to_string(),
}
}
}

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.1"
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,30 @@ 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
# Test case for mixed union type
field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error

View File

@@ -84,3 +84,30 @@ 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
# Test case for mixed union type
field34: typing.Union[list[int], str] | typing.Union[bytes, list[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[Union[float, int]], type[complex]]]
y: Union[Union[Union[type[float | int], type[complex]]]]
z: Union[type[complex], Union[Union[type[Union[float, int]]]]]
def func(arg: type[int] | str | type[float]) -> None:
@@ -13,8 +16,8 @@ def func(arg: type[int] | str | type[float]) -> None:
# OK
x: type[int, str, float]
y: builtins.type[int, str, complex]
x: type[int | str | float]
y: builtins.type[int | str | complex]
z: Union[float, complex]
@@ -65,3 +68,11 @@ def convert_union(union: UnionType) -> _T | None:
Union[type[_T] | type[Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055
] = union.__args__
...
# `type[float, int]`` is not valid, use `type[float|int]` or `type[Union[float, int]]`
# OK for PYI055, should be covered by another check.
a: Union[type[float, int], type[complex]]
b: Union[Union[type[float, int], type[complex]]]
c: Union[Union[Union[type[float, int], type[complex]]]]
d: Union[type[complex], Union[Union[type[float, int]]]]

View File

@@ -1,17 +1,20 @@
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[Union[float, int]], type[complex]]
x: Union[Union[type[Union[float, int]], type[complex]]]
y: Union[Union[Union[type[Union[float, int]], type[complex]]]]
z: Union[type[complex], Union[Union[type[Union[float, int]]]]]
def func(arg: type[int] | str | type[float]) -> None: ...
# OK
x: type[int, str, float]
y: builtins.type[int, str, complex]
x: type[int | str | float]
y: builtins.type[int | str | complex]
z: Union[float, complex]
def func(arg: type[int, float] | str) -> None: ...
@@ -26,3 +29,10 @@ def func():
item3: Union[ # comment
type[requests_mock.Mocker], # another comment
type[httpretty], type[str]] = requests_mock.Mocker
# OK
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]]]]

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,82 @@
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
)
def f():
# Regression test for #14554
import typing
typing.cast(M-())
def f():
# Simple case with Literal that should lead to nested quotes
from typing import cast, Literal
cast(Literal["A"], 'A')
def f():
# Really complex case with nested forward references
from typing import cast, Annotated, Literal
cast(list[Annotated["list['Literal[\"A\"]']", "Foo"]], ['A'])

View File

@@ -0,0 +1,31 @@
from __future__ import annotations
from typing import Dict, TypeAlias, TYPE_CHECKING
if TYPE_CHECKING:
from typing import Dict
from foo import Foo
OptStr: TypeAlias = str | None
Bar: TypeAlias = Foo[int]
a: TypeAlias = int # OK
b: TypeAlias = Dict # OK
c: TypeAlias = Foo # TC007
d: TypeAlias = Foo | None # TC007
e: TypeAlias = OptStr # TC007
f: TypeAlias = Bar # TC007
g: TypeAlias = Foo | Bar # TC007 x2
h: TypeAlias = Foo[str] # TC007
i: TypeAlias = (Foo | # TC007 x2 (fix removes comment currently)
Bar)
type C = Foo # OK
type D = Foo | None # OK
type E = OptStr # OK
type F = Bar # OK
type G = Foo | Bar # OK
type H = Foo[str] # OK
type I = (Foo | # OK
Bar)

View File

@@ -0,0 +1,52 @@
from __future__ import annotations
from typing import TypeAlias, TYPE_CHECKING
from foo import Foo
if TYPE_CHECKING:
from typing import Dict
OptStr: TypeAlias = str | None
Bar: TypeAlias = Foo[int]
else:
Bar = Foo
a: TypeAlias = 'int' # TC008
b: TypeAlias = 'Dict' # OK
c: TypeAlias = 'Foo' # TC008
d: TypeAlias = 'Foo[str]' # OK
e: TypeAlias = 'Foo.bar' # OK
f: TypeAlias = 'Foo | None' # TC008
g: TypeAlias = 'OptStr' # OK
h: TypeAlias = 'Bar' # TC008
i: TypeAlias = Foo['str'] # TC008
j: TypeAlias = 'Baz' # OK
k: TypeAlias = 'k | None' # OK
l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled)
m: TypeAlias = ('int' # TC008
| None)
n: TypeAlias = ('int' # TC008 (fix removes comment currently)
' | None')
type B = 'Dict' # TC008
type D = 'Foo[str]' # TC008
type E = 'Foo.bar' # TC008
type G = 'OptStr' # TC008
type I = Foo['str'] # TC008
type J = 'Baz' # TC008
type K = 'K | None' # TC008
type L = 'int' | None # TC008 (because TC010 is not enabled)
type M = ('int' # TC008
| None)
type N = ('int' # TC008 (fix removes comment currently)
' | None')
class Baz:
a: TypeAlias = 'Baz' # OK
type A = 'Baz' # TC008
class Nested:
a: TypeAlias = 'Baz' # OK
type A = 'Baz' # TC008

View File

@@ -101,3 +101,12 @@ def f():
def test_annotated_non_typing_reference(user: Annotated[str, Depends(get_foo)]):
pass
def f():
from typing import TypeAlias, TYPE_CHECKING
if TYPE_CHECKING:
from pandas import DataFrame
x: TypeAlias = DataFrame | None

View File

@@ -58,7 +58,7 @@ def f():
from typing import Literal
from third_party import Type
def test_string_contains_opposite_quote_do_not_fix(self, type1: Type[Literal["'"]], type2: Type[Literal["\'"]]):
def test_string_contains_opposite_quote(self, type1: Type[Literal["'"]], type2: Type[Literal["\'"]]):
pass

View File

@@ -0,0 +1,23 @@
import os
os.listdir('.')
os.listdir(b'.')
string_path = '.'
os.listdir(string_path)
bytes_path = b'.'
os.listdir(bytes_path)
from pathlib import Path
path_path = Path('.')
os.listdir(path_path)
if os.listdir("dir"):
...
if "file" in os.listdir("dir"):
...

View File

@@ -1,3 +1,9 @@
import mod.CONST as const
from mod import CONSTANT as constant
from mod import ANOTHER_CONSTANT as another_constant
import mod.CON as c
from mod import C as c
# These are all OK:
import django.db.models.Q as Query1
from django.db.models import Q as Query2

View File

@@ -1,3 +1,7 @@
import mod.Camel as CAMEL
from mod import CamelCase as CAMELCASE
from mod import AnotherCamelCase as ANOTHER_CAMELCASE
# These are all OK:
import mod.AppleFruit as A
from mod import BananaFruit as B

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

@@ -93,3 +93,53 @@ op_itemgetter = lambda x: x[1, :]
# Without a slice, trivia is retained
op_itemgetter = lambda x: x[1, 2]
# All methods in classes are ignored, even those defined using lambdas:
class Foo:
def x(self, other):
return self == other
class Bar:
y = lambda self, other: self == other
from typing import Callable
class Baz:
z: Callable = lambda self, other: self == other
# Lambdas wrapped in function calls could also still be method definitions!
# To avoid false positives, we shouldn't flag any of these either:
from typing import final, override, no_type_check
class Foo:
a = final(lambda self, other: self == other)
b = override(lambda self, other: self == other)
c = no_type_check(lambda self, other: self == other)
d = final(override(no_type_check(lambda self, other: self == other)))
# lambdas used in decorators do not constitute method definitions,
# so these *should* be flagged:
class TheLambdasHereAreNotMethods:
@pytest.mark.parametrize(
"slicer, expected",
[
(lambda x: x[-2:], "foo"),
(lambda x: x[-5:-3], "bar"),
],
)
def test_inlet_asset_alias_extra_slice(self, slicer, expected):
assert slice("whatever") == expected
class NotAMethodButHardToDetect:
# In an ideal world, perhaps we'd emit a diagnostic here,
# since this `lambda` is clearly not a method definition,
# and *could* be safely replaced with an `operator` function.
# Practically speaking, however, it's hard to see how we'd accurately determine
# that the `lambda` is *not* a method definition
# without risking false positives elsewhere or introducing complex heuristics
# that users would find surprising and confusing
FOO = sorted([x for x in BAR], key=lambda x: x.baz)

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

@@ -0,0 +1,31 @@
from typing import Literal
import typing as t
import typing_extensions
y: Literal[1, print("hello"), 3, Literal[4, 1]]
Literal[1, Literal[1]]
Literal[1, 2, Literal[1, 2]]
Literal[1, Literal[1], Literal[1]]
Literal[1, Literal[2], Literal[2]]
t.Literal[1, t.Literal[2, t.Literal[1]]]
Literal[
1, # comment 1
Literal[ # another comment
1 # yet annother comment
]
] # once
# Ensure issue is only raised once, even on nested literals
MyType = Literal["foo", Literal[True, False, True], "bar"]
# nested literals, all equivalent to `Literal[1]`
Literal[Literal[1]]
Literal[Literal[Literal[1], Literal[1]]]
Literal[Literal[1], Literal[Literal[Literal[1]]]]
# OK
x: Literal[True, False, True, False]
z: Literal[{1, 3, 5}, "foobar", {1,3,5}]
typing_extensions.Literal[1, 1, 1]
n: Literal["No", "duplicates", "here", 1, "1"]

View File

@@ -0,0 +1,31 @@
from typing import Literal
import typing as t
import typing_extensions
y: Literal[1, print("hello"), 3, Literal[4, 1]]
Literal[1, Literal[1]]
Literal[1, 2, Literal[1, 2]]
Literal[1, Literal[1], Literal[1]]
Literal[1, Literal[2], Literal[2]]
t.Literal[1, t.Literal[2, t.Literal[1]]]
Literal[
1, # comment 1
Literal[ # another comment
1 # yet annother comment
]
] # once
# Ensure issue is only raised once, even on nested literals
MyType = Literal["foo", Literal[True, False, True], "bar"]
# nested literals, all equivalent to `Literal[1]`
Literal[Literal[1]]
Literal[Literal[Literal[1], Literal[1]]]
Literal[Literal[1], Literal[Literal[Literal[1]]]]
# OK
x: Literal[True, False, True, False]
z: Literal[{1, 3, 5}, "foobar", {1,3,5}]
typing_extensions.Literal[1, 1, 1]
n: Literal["No", "duplicates", "here", 1, "1"]

View File

@@ -0,0 +1,76 @@
import re
s = "str"
# this should be replaced with s.replace("abc", "")
re.sub("abc", "", s)
# this example, adapted from https://docs.python.org/3/library/re.html#re.sub,
# should *not* be replaced because repl is a function, not a string
def dashrepl(matchobj):
if matchobj.group(0) == "-":
return " "
else:
return "-"
re.sub("-", dashrepl, "pro----gram-files")
# this one should be replaced with s.startswith("abc") because the Match is
# used in an if context for its truth value
if re.match("abc", s):
pass
if m := re.match("abc", s): # this should *not* be replaced
pass
re.match("abc", s) # this should not be replaced because match returns a Match
# this should be replaced with "abc" in s
if re.search("abc", s):
pass
re.search("abc", s) # this should not be replaced
# this should be replaced with "abc" == s
if re.fullmatch("abc", s):
pass
re.fullmatch("abc", s) # this should not be replaced
# this should be replaced with s.split("abc")
re.split("abc", s)
# these currently should not be modified because the patterns contain regex
# metacharacters
re.sub("ab[c]", "", s)
re.match("ab[c]", s)
re.search("ab[c]", s)
re.fullmatch("ab[c]", s)
re.split("ab[c]", s)
# test that all of the metacharacters prevent the rule from triggering, also
# use raw strings in line with RUF039
re.sub(r"abc.", "", s)
re.sub(r"^abc", "", s)
re.sub(r"abc$", "", s)
re.sub(r"abc*", "", s)
re.sub(r"abc+", "", s)
re.sub(r"abc?", "", s)
re.sub(r"abc{2,3}", "", s)
re.sub(r"abc\n", "", s) # this one could be fixed but is not currently
re.sub(r"abc|def", "", s)
re.sub(r"(a)bc", "", s)
# and these should not be modified because they have extra arguments
re.sub("abc", "", s, flags=re.A)
re.match("abc", s, flags=re.I)
re.search("abc", s, flags=re.L)
re.fullmatch("abc", s, flags=re.M)
re.split("abc", s, maxsplit=2)
# this should trigger an unsafe fix because of the presence of comments
re.sub(
# pattern
"abc",
# repl
"",
s, # string
)

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