Compare commits

..

46 Commits

Author SHA1 Message Date
David Peter
500b9a2691 EllipsisType test 2024-11-29 22:15:37 +01:00
David Peter
15476be531 Add TODO 2024-11-29 21:52:17 +01:00
David Peter
e7a361699d Fix behavior for NoDefault 2024-11-29 21:49:24 +01:00
David Peter
a218e1901b Declarations 2024-11-29 21:16:34 +01:00
David Peter
f108043d2d Reset typeshed 2024-11-28 22:51:10 +01:00
David Peter
77b45aeee9 Remove hard-coded EllipsisType 2024-11-28 22:49:25 +01:00
David Peter
c6f4c106b0 Updates 2024-11-28 22:47:18 +01:00
David Peter
dc55b4c8a2 Properly restore constraints 2024-11-28 22:38:08 +01:00
David Peter
41d19c3c29 Update 2024-11-28 20:00:58 +01:00
David Peter
99d44299e8 Revert typing change 2024-11-28 19:58:26 +01:00
David Peter
32ad489d79 More debugging output 2024-11-28 19:42:24 +01:00
David Peter
a3e7e7d8b6 Remove TODOs 2024-11-28 19:39:58 +01:00
David Peter
aea4bbbb30 Adapt debug test 2024-11-28 19:39:33 +01:00
David Peter
167e445243 Reset test file 2024-11-28 19:39:16 +01:00
David Peter
dccfd6e4f8 New tests 2024-11-28 19:38:56 +01:00
David Peter
eb4ae2b910 statically known to be True branches 2024-11-28 17:33:35 +01:00
David Peter
5be842b1c3 Patch typeshed and increase default Python version to 3.13 2024-11-28 17:33:35 +01:00
David Peter
2a21d79ec4 Cleanup 2024-11-28 17:33:35 +01:00
David Peter
1964ecdbb7 Base check on type, not expr 2024-11-28 17:33:35 +01:00
David Peter
6d167672f1 First version based on end-of-branch constraints 2024-11-28 17:33:35 +01:00
David Peter
ae45b897ea Restore to main 2024-11-28 17:33:35 +01:00
David Peter
90f48f45b0 Dump save 2024-11-28 17:33:35 +01: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
748 changed files with 7678 additions and 3633 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/**"
@@ -317,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
@@ -335,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 --bin ruff 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 --bin ruff $(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,43 @@
# 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))
### 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-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!

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.,

6
Cargo.lock generated
View File

@@ -2489,7 +2489,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.8.0"
version = "0.8.1"
dependencies = [
"anyhow",
"argfile",
@@ -2708,7 +2708,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.8.0"
version = "0.8.1"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -3023,7 +3023,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.8.0"
version = "0.8.1"
dependencies = [
"console_error_panic_hook",
"console_log",

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.8.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.8.0/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.8.0
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

@@ -5,11 +5,11 @@
pub enum TargetVersion {
Py37,
Py38,
#[default]
Py39,
Py310,
Py311,
Py312,
#[default]
Py313,
}

View File

@@ -125,6 +125,7 @@ def f7() -> "\x69nt":
def f8() -> """int""":
return 1
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
def f9() -> "b'int'":
return 1

View File

@@ -38,7 +38,7 @@ if (x := 1) and bool_instance():
if True or (x := 1):
# TODO: infer that the second arm is never executed, and raise `unresolved-reference`.
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
reveal_type(x) # revealed: Never
if True and (x := 1):
# TODO: infer that the second arm is always executed, do not raise a diagnostic

View File

@@ -36,8 +36,6 @@ from typing import Callable
def foo() -> int:
return 42
# TODO: This should be resolved once we understand `Callable` annotations
# error: [annotation-with-invalid-expression]
def decorator(func) -> Callable[[], int]:
return foo

View File

@@ -75,7 +75,6 @@ Literal: _SpecialForm
```py
from other import Literal
# error: [annotation-with-invalid-expression]
a1: Literal[26]
def f():

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

@@ -0,0 +1,303 @@
# Statically-known branches
## Always false
### If
```py
x = 1
if False:
x = 2
reveal_type(x) # revealed: Literal[1]
```
### Else
```py
x = 1
if True:
pass
else:
x = 2
reveal_type(x) # revealed: Literal[1]
```
## Always true
### If
```py
x = 1
if True:
x = 2
reveal_type(x) # revealed: Literal[2]
```
### Else
```py
x = 1
if False:
pass
else:
x = 2
reveal_type(x) # revealed: Literal[2]
```
## Combination
```py
x = 1
if True:
x = 2
else:
x = 3
reveal_type(x) # revealed: Literal[2]
```
## Nested
```py path=nested_if_true_if_true.py
x = 1
if True:
if True:
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2]
```
```py path=nested_if_true_if_false.py
x = 1
if True:
if False:
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3]
```
```py path=nested_if_true_if_bool.py
def flag() -> bool: ...
x = 1
if True:
if flag():
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2, 3]
```
```py path=nested_if_bool_if_true.py
def flag() -> bool: ...
x = 1
if flag():
if True:
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2, 4]
```
```py path=nested_else_if_true.py
x = 1
if False:
x = 2
else:
if True:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3]
```
```py path=nested_else_if_false.py
x = 1
if False:
x = 2
else:
if False:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[4]
```
```py path=nested_else_if_bool.py
def flag() -> bool: ...
x = 1
if False:
x = 2
else:
if flag():
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3, 4]
```
## If-expressions
### Always true
```py
x = 1 if True else 2
reveal_type(x) # revealed: Literal[1]
```
### Always false
```py
x = 1 if False else 2
reveal_type(x) # revealed: Literal[2]
```
## Boolean expressions
### Always true
```py
(x := 1) == 1 or (x := 2)
reveal_type(x) # revealed: Literal[1]
```
### Always false
```py
(x := 1) == 0 or (x := 2)
reveal_type(x) # revealed: Literal[2]
```
## Conditional declarations
```py path=if_false.py
x: str
if False:
x: int
def f() -> None:
reveal_type(x) # revealed: str
```
```py path=if_true_else.py
x: str
if True:
pass
else:
x: int
def f() -> None:
reveal_type(x) # revealed: str
```
```py path=if_true.py
x: str
if True:
x: int
def f() -> None:
reveal_type(x) # revealed: int
```
```py path=if_false_else.py
x: str
if False:
pass
else:
x: int
def f() -> None:
reveal_type(x) # revealed: int
```
```py path=if_bool.py
def flag() -> bool: ...
x: str
if flag():
x: int
def f() -> None:
reveal_type(x) # revealed: str | int
```
## Conditionally defined functions
```py
def f() -> int: ...
def g() -> int: ...
if True:
def f() -> str: ...
else:
def g() -> str: ...
reveal_type(f()) # revealed: str
reveal_type(g()) # revealed: int
```
## Conditionally defined class attributes
```py
class C:
if True:
x: int = 1
else:
x: str = "a"
reveal_type(C.x) # revealed: int
```
## TODO
- declarations vs bindings => NoDefault: NoDefaultType
- conditional imports
- conditional class definitions
- compare with tests in if.md=>Statically known branches
- boundness
- TODO in `issubclass.md`

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

@@ -49,14 +49,14 @@ sometimes not:
```py
import sys
reveal_type(sys.version_info >= (3, 9, 1)) # revealed: bool
reveal_type(sys.version_info >= (3, 9, 1, "final", 0)) # revealed: bool
reveal_type(sys.version_info >= (3, 9, 1)) # revealed: Literal[True]
reveal_type(sys.version_info >= (3, 9, 1, "final", 0)) # revealed: Literal[True]
# TODO: While this won't fail at runtime, the user has probably made a mistake
# if they're comparing a tuple of length >5 with `sys.version_info`
# (`sys.version_info` is a tuple of length 5). It might be worth
# emitting a lint diagnostic of some kind warning them about the probable error?
reveal_type(sys.version_info >= (3, 9, 1, "final", 0, 5)) # revealed: bool
reveal_type(sys.version_info >= (3, 9, 1, "final", 0, 5)) # revealed: Literal[True]
reveal_type(sys.version_info == (3, 8, 1, "finallllll", 0)) # revealed: Literal[False]
```
@@ -102,8 +102,8 @@ The fields of `sys.version_info` can be accessed by name:
import sys
reveal_type(sys.version_info.major >= 3) # revealed: Literal[True]
reveal_type(sys.version_info.minor >= 9) # revealed: Literal[True]
reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
reveal_type(sys.version_info.minor >= 13) # revealed: Literal[True]
reveal_type(sys.version_info.minor >= 14) # revealed: Literal[False]
```
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
@@ -125,14 +125,14 @@ The fields of `sys.version_info` can be accessed by index or by slice:
import sys
reveal_type(sys.version_info[0] < 3) # revealed: Literal[False]
reveal_type(sys.version_info[1] > 9) # revealed: Literal[False]
reveal_type(sys.version_info[1] > 13) # revealed: Literal[False]
# revealed: tuple[Literal[3], Literal[9], int, Literal["alpha", "beta", "candidate", "final"], int]
# revealed: tuple[Literal[3], Literal[13], int, Literal["alpha", "beta", "candidate", "final"], int]
reveal_type(sys.version_info[:5])
reveal_type(sys.version_info[:2] >= (3, 9)) # revealed: Literal[True]
reveal_type(sys.version_info[0:2] >= (3, 10)) # revealed: Literal[False]
reveal_type(sys.version_info[:3] >= (3, 10, 1)) # revealed: Literal[False]
reveal_type(sys.version_info[:2] >= (3, 13)) # revealed: Literal[True]
reveal_type(sys.version_info[0:2] >= (3, 14)) # revealed: Literal[False]
reveal_type(sys.version_info[:3] >= (3, 14, 1)) # revealed: Literal[False]
reveal_type(sys.version_info[3] == "final") # revealed: bool
reveal_type(sys.version_info[3] == "finalllllll") # revealed: Literal[False]
```

View File

@@ -39,7 +39,7 @@ impl PythonVersion {
impl Default for PythonVersion {
fn default() -> Self {
Self::PY39
Self::PY313 // TODO: temporarily changed to 3.13 to activate all sys.version_info branches
}
}

View File

@@ -1229,4 +1229,32 @@ match 1:
assert!(matches!(binding.kind(&db), DefinitionKind::For(_)));
}
#[test]
#[ignore]
fn if_statement() {
let TestCase { db, file } = test_case(
"
x = False
if True:
x: bool
",
);
let index = semantic_index(&db, file);
// let global_table = index.symbol_table(FileScopeId::global());
let use_def = index.use_def_map(FileScopeId::global());
// use_def
use_def.print(&db);
assert!(false);
// let binding = use_def
// .first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists"))
// .expect("Expected with item definition for {name}");
// assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_)));
}
}

View File

@@ -23,7 +23,7 @@ use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId,
SymbolTableBuilder,
};
use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
use crate::semantic_index::use_def::{ActiveConstraintsSnapshot, FlowSnapshot, UseDefMapBuilder};
use crate::semantic_index::SemanticIndex;
use crate::unpack::Unpack;
use crate::Db;
@@ -200,12 +200,20 @@ impl<'db> SemanticIndexBuilder<'db> {
self.current_use_def_map().snapshot()
}
fn flow_restore(&mut self, state: FlowSnapshot) {
self.current_use_def_map_mut().restore(state);
fn constraints_snapshot(&self) -> ActiveConstraintsSnapshot {
self.current_use_def_map().constraints_snapshot()
}
fn flow_merge(&mut self, state: FlowSnapshot) {
fn flow_restore(&mut self, state: FlowSnapshot, active_constraints: ActiveConstraintsSnapshot) {
self.current_use_def_map_mut().restore(state);
self.current_use_def_map_mut()
.restore_constraints(active_constraints);
}
fn flow_merge(&mut self, state: FlowSnapshot, active_constraints: ActiveConstraintsSnapshot) {
self.current_use_def_map_mut().merge(state);
self.current_use_def_map_mut()
.restore_constraints(active_constraints);
}
fn add_symbol(&mut self, name: Name) -> ScopedSymbolId {
@@ -765,37 +773,44 @@ where
ast::Stmt::If(node) => {
self.visit_expr(&node.test);
let pre_if = self.flow_snapshot();
let pre_if_constraints = self.constraints_snapshot();
let constraint = self.record_expression_constraint(&node.test);
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());
// we can only take an elif/else branch if none of the previous ones were
// taken, so the block entry state is always `pre_if`
self.flow_restore(pre_if.clone());
self.flow_restore(pre_if.clone(), pre_if_constraints.clone());
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);
self.flow_merge(post_clause_state, pre_if_constraints.clone());
}
}
ast::Stmt::While(ast::StmtWhile {
@@ -807,6 +822,7 @@ where
self.visit_expr(test);
let pre_loop = self.flow_snapshot();
let pre_loop_constraints = self.constraints_snapshot();
// Save aside any break states from an outer loop
let saved_break_states = std::mem::take(&mut self.loop_break_states);
@@ -825,13 +841,13 @@ where
// We may execute the `else` clause without ever executing the body, so merge in
// the pre-loop state before visiting `else`.
self.flow_merge(pre_loop);
self.flow_merge(pre_loop, pre_loop_constraints.clone());
self.visit_body(orelse);
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
// states after visiting `else`.
for break_state in break_states {
self.flow_merge(break_state);
self.flow_merge(break_state, pre_loop_constraints.clone()); // TODO?
}
}
ast::Stmt::With(ast::StmtWith {
@@ -874,6 +890,7 @@ where
self.visit_expr(iter);
let pre_loop = self.flow_snapshot();
let pre_loop_constraints = self.constraints_snapshot();
let saved_break_states = std::mem::take(&mut self.loop_break_states);
debug_assert_eq!(&self.current_assignments, &[]);
@@ -894,13 +911,13 @@ where
// We may execute the `else` clause without ever executing the body, so merge in
// the pre-loop state before visiting `else`.
self.flow_merge(pre_loop);
self.flow_merge(pre_loop, pre_loop_constraints.clone());
self.visit_body(orelse);
// Breaking out of a `for` loop bypasses the `else` clause, so merge in the break
// states after visiting `else`.
for break_state in break_states {
self.flow_merge(break_state);
self.flow_merge(break_state, pre_loop_constraints.clone());
}
}
ast::Stmt::Match(ast::StmtMatch {
@@ -912,6 +929,7 @@ where
self.visit_expr(subject);
let after_subject = self.flow_snapshot();
let after_subject_cs = self.constraints_snapshot();
let Some((first, remaining)) = cases.split_first() else {
return;
};
@@ -921,18 +939,18 @@ where
let mut post_case_snapshots = vec![];
for case in remaining {
post_case_snapshots.push(self.flow_snapshot());
self.flow_restore(after_subject.clone());
self.flow_restore(after_subject.clone(), after_subject_cs.clone());
self.add_pattern_constraint(subject, &case.pattern);
self.visit_match_case(case);
}
for post_clause_state in post_case_snapshots {
self.flow_merge(post_clause_state);
self.flow_merge(post_clause_state, after_subject_cs.clone());
}
if !cases
.last()
.is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard())
{
self.flow_merge(after_subject);
self.flow_merge(after_subject, after_subject_cs.clone());
}
}
ast::Stmt::Try(ast::StmtTry {
@@ -950,6 +968,7 @@ where
// We will merge this state with all of the intermediate
// states during the `try` block before visiting those suites.
let pre_try_block_state = self.flow_snapshot();
let pre_try_block_constraints = self.constraints_snapshot();
self.try_node_context_stack_manager.push_context();
@@ -970,14 +989,17 @@ where
// as there necessarily must have been 0 `except` blocks executed
// if we hit the `else` block.
let post_try_block_state = self.flow_snapshot();
let post_try_block_constraints = self.constraints_snapshot();
// Prepare for visiting the `except` block(s)
self.flow_restore(pre_try_block_state);
self.flow_restore(pre_try_block_state, pre_try_block_constraints.clone());
for state in try_block_snapshots {
self.flow_merge(state);
self.flow_merge(state, pre_try_block_constraints.clone());
// TODO?
}
let pre_except_state = self.flow_snapshot();
let pre_except_constraints = self.constraints_snapshot();
let num_handlers = handlers.len();
for (i, except_handler) in handlers.iter().enumerate() {
@@ -1016,19 +1038,22 @@ where
// as we'll immediately call `self.flow_restore()` to a different state
// as soon as this loop over the handlers terminates.
if i < (num_handlers - 1) {
self.flow_restore(pre_except_state.clone());
self.flow_restore(
pre_except_state.clone(),
pre_except_constraints.clone(),
);
}
}
// If we get to the `else` block, we know that 0 of the `except` blocks can have been executed,
// and the entire `try` block must have been executed:
self.flow_restore(post_try_block_state);
self.flow_restore(post_try_block_state, post_try_block_constraints);
}
self.visit_body(orelse);
for post_except_state in post_except_states {
self.flow_merge(post_except_state);
self.flow_merge(post_except_state, pre_try_block_constraints.clone());
}
// TODO: there's lots of complexity here that isn't yet handled by our model.
@@ -1185,19 +1210,17 @@ where
ast::Expr::If(ast::ExprIf {
body, test, orelse, ..
}) => {
// TODO detect statically known truthy or falsy test (via type inference, not naive
// AST inspection, so we can't simplify here, need to record test expression for
// later checking)
self.visit_expr(test);
let pre_if = self.flow_snapshot();
let pre_if_constraints = self.constraints_snapshot();
let constraint = self.record_expression_constraint(test);
self.visit_expr(body);
let post_body = self.flow_snapshot();
self.flow_restore(pre_if);
self.flow_restore(pre_if, pre_if_constraints.clone());
self.record_negated_constraint(constraint);
self.visit_expr(orelse);
self.flow_merge(post_body);
self.flow_merge(post_body, pre_if_constraints);
}
ast::Expr::ListComp(
list_comprehension @ ast::ExprListComp {
@@ -1258,7 +1281,7 @@ where
// AST inspection, so we can't simplify here, need to record test expression for
// later checking)
let mut snapshots = vec![];
let pre_op_constraints = self.constraints_snapshot();
for (index, value) in values.iter().enumerate() {
self.visit_expr(value);
// In the last value we don't need to take a snapshot nor add a constraint
@@ -1273,7 +1296,7 @@ where
}
}
for snapshot in snapshots {
self.flow_merge(snapshot);
self.flow_merge(snapshot, pre_op_constraints.clone());
}
}
_ => {

View File

@@ -221,6 +221,8 @@
//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it
//! visits a `StmtIf` node.
use std::collections::HashSet;
use self::symbol_state::{
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
ScopedConstraintId, ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
@@ -268,6 +270,109 @@ pub(crate) struct UseDefMap<'db> {
}
impl<'db> UseDefMap<'db> {
#[cfg(test)]
pub(crate) fn print(&self, db: &dyn crate::db::Db) {
use crate::semantic_index::constraint::ConstraintNode;
println!("all_definitions:");
println!("================");
for (id, d) in self.all_definitions.iter_enumerated() {
println!(
"{:?}: {:?} {:?} {:?}",
id,
d.category(db),
d.scope(db),
d.symbol(db),
);
println!(" {:?}", d.kind(db));
println!();
}
println!("all_constraints:");
println!("================");
for (id, c) in self.all_constraints.iter_enumerated() {
println!("{:?}: {:?}", id, c.node);
if let ConstraintNode::Expression(e) = c.node {
println!(" {:?}", e.node_ref(db));
}
}
println!();
println!("bindings_by_use:");
println!("================");
for (id, bindings) in self.bindings_by_use.iter_enumerated() {
println!("{:?}:", id);
for binding in bindings.iter() {
let definition = self.all_definitions[binding.definition];
let mut constraint_ids = binding.constraint_ids.peekable();
let mut active_constraint_ids =
binding.constraints_active_at_binding_ids.peekable();
println!(" * {:?}", definition);
if constraint_ids.peek().is_some() {
println!(" Constraints:");
for constraint_id in constraint_ids {
println!(" {:?}", self.all_constraints[constraint_id]);
}
} else {
println!(" No constraints");
}
println!();
if active_constraint_ids.peek().is_some() {
println!(" Active constraints at binding:");
for constraint_id in active_constraint_ids {
println!(" {:?}", self.all_constraints[constraint_id]);
}
} else {
println!(" No active constraints at binding");
}
}
}
println!();
println!("public_symbols:");
println!("================");
for (id, symbol) in self.public_symbols.iter_enumerated() {
println!("{:?}:", id);
println!(" * Bindings:");
for binding in symbol.bindings().iter() {
let definition = self.all_definitions[binding.definition];
let mut constraint_ids = binding.constraint_ids.peekable();
println!(" {:?}", definition);
if constraint_ids.peek().is_some() {
println!(" Constraints:");
for constraint_id in constraint_ids {
println!(" {:?}", self.all_constraints[constraint_id]);
}
} else {
println!(" No constraints");
}
}
println!(" * Declarations:");
for (declaration, _) in symbol.declarations().iter() {
let definition = self.all_definitions[declaration];
println!(" {:?}", definition);
}
println!();
}
println!();
println!();
}
pub(crate) fn bindings_at_use(
&self,
use_id: ScopedUseId,
@@ -352,6 +457,7 @@ impl<'db> UseDefMap<'db> {
) -> DeclarationsIterator<'a, 'db> {
DeclarationsIterator {
all_definitions: &self.all_definitions,
all_constraints: &self.all_constraints,
inner: declarations.iter(),
may_be_undeclared: declarations.may_be_undeclared(),
}
@@ -365,7 +471,7 @@ enum SymbolDefinitions {
Declarations(SymbolDeclarations),
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
@@ -384,6 +490,10 @@ impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
all_constraints: self.all_constraints,
constraint_ids: def_id_with_constraints.constraint_ids,
},
constraints_active_at_binding: ConstraintsIterator {
all_constraints: self.all_constraints,
constraint_ids: def_id_with_constraints.constraints_active_at_binding_ids,
},
})
}
}
@@ -393,8 +503,10 @@ impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
pub(crate) struct BindingWithConstraints<'map, 'db> {
pub(crate) binding: Definition<'db>,
pub(crate) constraints: ConstraintsIterator<'map, 'db>,
pub(crate) constraints_active_at_binding: ConstraintsIterator<'map, 'db>,
}
#[derive(Debug, Clone)]
pub(crate) struct ConstraintsIterator<'map, 'db> {
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
constraint_ids: ConstraintIdIterator<'map>,
@@ -414,6 +526,7 @@ impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
pub(crate) struct DeclarationsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
inner: DeclarationIdIterator<'map>,
may_be_undeclared: bool,
}
@@ -425,10 +538,18 @@ impl DeclarationsIterator<'_, '_> {
}
impl<'map, 'db> Iterator for DeclarationsIterator<'map, 'db> {
type Item = Definition<'db>;
type Item = (Definition<'db>, ConstraintsIterator<'map, 'db>);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|def_id| self.all_definitions[def_id])
self.inner.next().map(|(def_id, constraints)| {
(
self.all_definitions[def_id],
ConstraintsIterator {
all_constraints: self.all_constraints,
constraint_ids: constraints,
},
)
})
}
}
@@ -440,6 +561,9 @@ pub(super) struct FlowSnapshot {
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
}
#[derive(Clone, Debug)]
pub(super) struct ActiveConstraintsSnapshot(HashSet<ScopedConstraintId>);
#[derive(Debug, Default)]
pub(super) struct UseDefMapBuilder<'db> {
/// Append-only array of [`Definition`].
@@ -448,6 +572,8 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Append-only array of [`Constraint`].
all_constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
active_constraints: HashSet<ScopedConstraintId>,
/// Live bindings at each so-far-recorded use.
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
@@ -471,7 +597,7 @@ impl<'db> UseDefMapBuilder<'db> {
binding,
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
);
symbol_state.record_binding(def_id);
symbol_state.record_binding(def_id, &self.active_constraints);
}
pub(super) fn record_constraint(&mut self, constraint: Constraint<'db>) {
@@ -479,6 +605,7 @@ impl<'db> UseDefMapBuilder<'db> {
for state in &mut self.symbol_states {
state.record_constraint(constraint_id);
}
self.active_constraints.insert(constraint_id);
}
pub(super) fn record_declaration(
@@ -492,7 +619,7 @@ impl<'db> UseDefMapBuilder<'db> {
declaration,
SymbolDefinitions::Bindings(symbol_state.bindings().clone()),
);
symbol_state.record_declaration(def_id);
symbol_state.record_declaration(def_id, &self.active_constraints);
}
pub(super) fn record_declaration_and_binding(
@@ -503,8 +630,8 @@ impl<'db> UseDefMapBuilder<'db> {
// We don't need to store anything in self.definitions_by_definition.
let def_id = self.all_definitions.push(definition);
let symbol_state = &mut self.symbol_states[symbol];
symbol_state.record_declaration(def_id);
symbol_state.record_binding(def_id);
symbol_state.record_declaration(def_id, &self.active_constraints);
symbol_state.record_binding(def_id, &self.active_constraints);
}
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
@@ -523,6 +650,10 @@ impl<'db> UseDefMapBuilder<'db> {
}
}
pub(super) fn constraints_snapshot(&self) -> ActiveConstraintsSnapshot {
ActiveConstraintsSnapshot(self.active_constraints.clone())
}
/// Restore the current builder symbols state to the given snapshot.
pub(super) fn restore(&mut self, snapshot: FlowSnapshot) {
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
@@ -541,6 +672,10 @@ impl<'db> UseDefMapBuilder<'db> {
.resize(num_symbols, SymbolState::undefined());
}
pub(super) fn restore_constraints(&mut self, snapshot: ActiveConstraintsSnapshot) {
self.active_constraints = snapshot.0;
}
/// Merge the given snapshot into the current state, reflecting that we might have taken either
/// path to get here. The new state for each symbol should include definitions from both the
/// prior state and the snapshot.

View File

@@ -122,7 +122,7 @@ impl<const B: usize> BitSet<B> {
}
/// Iterator over values in a [`BitSet`].
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(super) struct BitSetIterator<'a, const B: usize> {
/// The blocks we are iterating over.
blocks: &'a [u64],

View File

@@ -43,6 +43,8 @@
//!
//! Tracking live declarations is simpler, since constraints are not involved, but otherwise very
//! similar to tracking live bindings.
use std::collections::HashSet;
use super::bitset::{BitSet, BitSetIterator};
use ruff_index::newtype_index;
use smallvec::SmallVec;
@@ -87,6 +89,8 @@ pub(super) struct SymbolDeclarations {
/// [`BitSet`]: which declarations (as [`ScopedDefinitionId`]) can reach the current location?
live_declarations: Declarations,
constraints_active_at_declaration: Constraints, // TODO: rename to constraints_active_at_declaration
/// Could the symbol be un-declared at this point?
may_be_undeclared: bool,
}
@@ -95,14 +99,27 @@ impl SymbolDeclarations {
fn undeclared() -> Self {
Self {
live_declarations: Declarations::default(),
constraints_active_at_declaration: Constraints::default(),
may_be_undeclared: true,
}
}
/// Record a newly-encountered declaration for this symbol.
fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
fn record_declaration(
&mut self,
declaration_id: ScopedDefinitionId,
active_constraints: &HashSet<ScopedConstraintId>,
) {
self.live_declarations = Declarations::with(declaration_id.into());
self.may_be_undeclared = false;
// TODO: unify code with below
self.constraints_active_at_declaration = Constraints::with_capacity(1);
self.constraints_active_at_declaration
.push(BitSet::default());
for active_constraint_id in active_constraints {
self.constraints_active_at_declaration[0].insert(active_constraint_id.as_u32());
}
}
/// Add undeclared as a possibility for this symbol.
@@ -114,6 +131,7 @@ impl SymbolDeclarations {
pub(super) fn iter(&self) -> DeclarationIdIterator {
DeclarationIdIterator {
inner: self.live_declarations.iter(),
constraints_active_at_binding: self.constraints_active_at_declaration.iter(),
}
}
@@ -138,6 +156,8 @@ pub(super) struct SymbolBindings {
/// binding in `live_bindings`.
constraints: Constraints,
constraints_active_at_binding: Constraints,
/// Could the symbol be unbound at this point?
may_be_unbound: bool,
}
@@ -147,6 +167,7 @@ impl SymbolBindings {
Self {
live_bindings: Bindings::default(),
constraints: Constraints::default(),
constraints_active_at_binding: Constraints::default(),
may_be_unbound: true,
}
}
@@ -157,12 +178,21 @@ impl SymbolBindings {
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
pub(super) fn record_binding(
&mut self,
binding_id: ScopedDefinitionId,
active_constraints: &HashSet<ScopedConstraintId>,
) {
// The new binding replaces all previous live bindings in this path, and has no
// constraints.
self.live_bindings = Bindings::with(binding_id.into());
self.constraints = Constraints::with_capacity(1);
self.constraints.push(BitSet::default());
self.constraints_active_at_binding = Constraints::with_capacity(1);
self.constraints_active_at_binding.push(BitSet::default());
for active_constraint_id in active_constraints {
self.constraints_active_at_binding[0].insert(active_constraint_id.as_u32());
}
self.may_be_unbound = false;
}
@@ -178,6 +208,7 @@ impl SymbolBindings {
BindingIdWithConstraintsIterator {
definitions: self.live_bindings.iter(),
constraints: self.constraints.iter(),
constraints_active_at_binding: self.constraints_active_at_binding.iter(),
}
}
@@ -207,8 +238,12 @@ impl SymbolState {
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
self.bindings.record_binding(binding_id);
pub(super) fn record_binding(
&mut self,
binding_id: ScopedDefinitionId,
active_constraints: &HashSet<ScopedConstraintId>,
) {
self.bindings.record_binding(binding_id, active_constraints);
}
/// Add given constraint to all live bindings.
@@ -222,8 +257,13 @@ impl SymbolState {
}
/// Record a newly-encountered declaration of this symbol.
pub(super) fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
self.declarations.record_declaration(declaration_id);
pub(super) fn record_declaration(
&mut self,
declaration_id: ScopedDefinitionId,
active_constraints: &HashSet<ScopedConstraintId>,
) {
self.declarations
.record_declaration(declaration_id, active_constraints);
}
/// Merge another [`SymbolState`] into this one.
@@ -232,24 +272,93 @@ impl SymbolState {
bindings: SymbolBindings {
live_bindings: Bindings::default(),
constraints: Constraints::default(),
constraints_active_at_binding: Constraints::default(), // TODO
may_be_unbound: self.bindings.may_be_unbound || b.bindings.may_be_unbound,
},
declarations: SymbolDeclarations {
live_declarations: self.declarations.live_declarations.clone(),
constraints_active_at_declaration: Constraints::default(), // TODO
may_be_undeclared: self.declarations.may_be_undeclared
|| b.declarations.may_be_undeclared,
},
};
// let mut constraints_active_at_binding = BitSet::default();
// for active_constraint_id in active_constraints.0 {
// constraints_active_at_binding.insert(active_constraint_id.as_u32());
// }
std::mem::swap(&mut a, self);
self.declarations
.live_declarations
.union(&b.declarations.live_declarations);
// self.declarations
// .live_declarations
// .union(&b.declarations.live_declarations);
let mut a_decls_iter = a.declarations.live_declarations.iter();
let mut b_decls_iter = b.declarations.live_declarations.iter();
let mut a_constraints_active_at_declaration_iter =
a.declarations.constraints_active_at_declaration.into_iter();
let mut b_constraints_active_at_declaration_iter =
b.declarations.constraints_active_at_declaration.into_iter();
let mut opt_a_decl: Option<u32> = a_decls_iter.next();
let mut opt_b_decl: Option<u32> = b_decls_iter.next();
let push = |decl,
constraints_active_at_declaration_iter: &mut ConstraintsIntoIterator,
merged: &mut Self| {
merged.declarations.live_declarations.insert(decl);
let constraints_active_at_binding = constraints_active_at_declaration_iter
.next()
.expect("declarations and constraints_active_at_binding length mismatch");
merged
.declarations
.constraints_active_at_declaration
.push(constraints_active_at_binding);
};
loop {
match (opt_a_decl, opt_b_decl) {
(Some(a_decl), Some(b_decl)) => match a_decl.cmp(&b_decl) {
std::cmp::Ordering::Less => {
push(a_decl, &mut a_constraints_active_at_declaration_iter, self);
opt_a_decl = a_decls_iter.next();
}
std::cmp::Ordering::Greater => {
push(b_decl, &mut b_constraints_active_at_declaration_iter, self);
opt_b_decl = b_decls_iter.next();
}
std::cmp::Ordering::Equal => {
push(a_decl, &mut b_constraints_active_at_declaration_iter, self);
self.declarations
.constraints_active_at_declaration
.last_mut()
.unwrap()
.intersect(&a_constraints_active_at_declaration_iter.next().unwrap());
opt_a_decl = a_decls_iter.next();
opt_b_decl = b_decls_iter.next();
}
},
(Some(a_decl), None) => {
push(a_decl, &mut a_constraints_active_at_declaration_iter, self);
opt_a_decl = a_decls_iter.next();
}
(None, Some(b_decl)) => {
push(b_decl, &mut b_constraints_active_at_declaration_iter, self);
opt_b_decl = b_decls_iter.next();
}
(None, None) => break,
}
}
let mut a_defs_iter = a.bindings.live_bindings.iter();
let mut b_defs_iter = b.bindings.live_bindings.iter();
let mut a_constraints_iter = a.bindings.constraints.into_iter();
let mut b_constraints_iter = b.bindings.constraints.into_iter();
let mut a_constraints_active_at_binding_iter =
a.bindings.constraints_active_at_binding.into_iter();
let mut b_constraints_active_at_binding_iter =
b.bindings.constraints_active_at_binding.into_iter();
let mut opt_a_def: Option<u32> = a_defs_iter.next();
let mut opt_b_def: Option<u32> = b_defs_iter.next();
@@ -261,7 +370,10 @@ impl SymbolState {
// path is irrelevant.
// Helper to push `def`, with constraints in `constraints_iter`, onto `self`.
let push = |def, constraints_iter: &mut ConstraintsIntoIterator, merged: &mut Self| {
let push = |def,
constraints_iter: &mut ConstraintsIntoIterator,
constraints_active_at_binding_iter: &mut ConstraintsIntoIterator,
merged: &mut Self| {
merged.bindings.live_bindings.insert(def);
// SAFETY: we only ever create SymbolState with either no definitions and no constraint
// bitsets (`::unbound`) or one definition and one constraint bitset (`::with`), and
@@ -271,7 +383,14 @@ impl SymbolState {
let constraints = constraints_iter
.next()
.expect("definitions and constraints length mismatch");
let constraints_active_at_binding = constraints_active_at_binding_iter
.next()
.expect("definitions and constraints_active_at_binding length mismatch");
merged.bindings.constraints.push(constraints);
merged
.bindings
.constraints_active_at_binding
.push(constraints_active_at_binding);
};
loop {
@@ -279,17 +398,32 @@ impl SymbolState {
(Some(a_def), Some(b_def)) => match a_def.cmp(&b_def) {
std::cmp::Ordering::Less => {
// Next definition ID is only in `a`, push it to `self` and advance `a`.
push(a_def, &mut a_constraints_iter, self);
push(
a_def,
&mut a_constraints_iter,
&mut a_constraints_active_at_binding_iter,
self,
);
opt_a_def = a_defs_iter.next();
}
std::cmp::Ordering::Greater => {
// Next definition ID is only in `b`, push it to `self` and advance `b`.
push(b_def, &mut b_constraints_iter, self);
push(
b_def,
&mut b_constraints_iter,
&mut b_constraints_active_at_binding_iter,
self,
);
opt_b_def = b_defs_iter.next();
}
std::cmp::Ordering::Equal => {
// Next definition is in both; push to `self` and intersect constraints.
push(a_def, &mut b_constraints_iter, self);
push(
a_def,
&mut b_constraints_iter,
&mut b_constraints_active_at_binding_iter,
self,
);
// SAFETY: we only ever create SymbolState with either no definitions and
// no constraint bitsets (`::unbound`) or one definition and one constraint
// bitset (`::with`), and `::merge` always pushes one definition and one
@@ -298,6 +432,11 @@ impl SymbolState {
let a_constraints = a_constraints_iter
.next()
.expect("definitions and constraints length mismatch");
// let _a_constraints_active_at_binding =
// a_constraints_active_at_binding_iter.next().expect(
// "definitions and constraints_active_at_binding length mismatch",
// ); // TODO: perform check that we see the same constraints in both paths
// If the same definition is visible through both paths, any constraint
// that applies on only one path is irrelevant to the resulting type from
// unioning the two paths, so we intersect the constraints.
@@ -306,18 +445,29 @@ impl SymbolState {
.last_mut()
.unwrap()
.intersect(&a_constraints);
opt_a_def = a_defs_iter.next();
opt_b_def = b_defs_iter.next();
}
},
(Some(a_def), None) => {
// We've exhausted `b`, just push the def from `a` and move on to the next.
push(a_def, &mut a_constraints_iter, self);
push(
a_def,
&mut a_constraints_iter,
&mut a_constraints_active_at_binding_iter,
self,
);
opt_a_def = a_defs_iter.next();
}
(None, Some(b_def)) => {
// We've exhausted `a`, just push the def from `b` and move on to the next.
push(b_def, &mut b_constraints_iter, self);
push(
b_def,
&mut b_constraints_iter,
&mut b_constraints_active_at_binding_iter,
self,
);
opt_b_def = b_defs_iter.next();
}
(None, None) => break,
@@ -353,26 +503,37 @@ impl Default for SymbolState {
pub(super) struct BindingIdWithConstraints<'a> {
pub(super) definition: ScopedDefinitionId,
pub(super) constraint_ids: ConstraintIdIterator<'a>,
pub(super) constraints_active_at_binding_ids: ConstraintIdIterator<'a>,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(super) struct BindingIdWithConstraintsIterator<'a> {
definitions: BindingsIterator<'a>,
constraints: ConstraintsIterator<'a>,
constraints_active_at_binding: ConstraintsIterator<'a>,
}
impl<'a> Iterator for BindingIdWithConstraintsIterator<'a> {
type Item = BindingIdWithConstraints<'a>;
fn next(&mut self) -> Option<Self::Item> {
match (self.definitions.next(), self.constraints.next()) {
(None, None) => None,
(Some(def), Some(constraints)) => Some(BindingIdWithConstraints {
definition: ScopedDefinitionId::from_u32(def),
constraint_ids: ConstraintIdIterator {
wrapped: constraints.iter(),
},
}),
match (
self.definitions.next(),
self.constraints.next(),
self.constraints_active_at_binding.next(),
) {
(None, None, None) => None,
(Some(def), Some(constraints), Some(constraints_active_at_binding)) => {
Some(BindingIdWithConstraints {
definition: ScopedDefinitionId::from_u32(def),
constraint_ids: ConstraintIdIterator {
wrapped: constraints.iter(),
},
constraints_active_at_binding_ids: ConstraintIdIterator {
wrapped: constraints_active_at_binding.iter(),
},
})
}
// SAFETY: see above.
_ => unreachable!("definitions and constraints length mismatch"),
}
@@ -381,7 +542,7 @@ impl<'a> Iterator for BindingIdWithConstraintsIterator<'a> {
impl std::iter::FusedIterator for BindingIdWithConstraintsIterator<'_> {}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(super) struct ConstraintIdIterator<'a> {
wrapped: BitSetIterator<'a, INLINE_CONSTRAINT_BLOCKS>,
}
@@ -399,13 +560,25 @@ impl std::iter::FusedIterator for ConstraintIdIterator<'_> {}
#[derive(Debug)]
pub(super) struct DeclarationIdIterator<'a> {
inner: DeclarationsIterator<'a>,
constraints_active_at_binding: ConstraintsIterator<'a>,
}
impl<'a> Iterator for DeclarationIdIterator<'a> {
type Item = ScopedDefinitionId;
type Item = (ScopedDefinitionId, ConstraintIdIterator<'a>);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(ScopedDefinitionId::from_u32)
// self.inner.next().map(ScopedDefinitionId::from_u32)
match (self.inner.next(), self.constraints_active_at_binding.next()) {
(None, None) => None,
(Some(declaration), Some(constraints_active_at_binding)) => Some((
ScopedDefinitionId::from_u32(declaration),
ConstraintIdIterator {
wrapped: constraints_active_at_binding.iter(),
},
)),
// SAFETY: see above.
_ => unreachable!("declarations and constraints_active_at_binding length mismatch"),
}
}
}
@@ -413,7 +586,7 @@ impl std::iter::FusedIterator for DeclarationIdIterator<'_> {}
#[cfg(test)]
mod tests {
use super::{ScopedConstraintId, ScopedDefinitionId, SymbolState};
use super::{ScopedConstraintId, SymbolState};
fn assert_bindings(symbol: &SymbolState, may_be_unbound: bool, expected: &[&str]) {
assert_eq!(symbol.may_be_unbound(), may_be_unbound);
@@ -445,7 +618,7 @@ mod tests {
let actual = symbol
.declarations()
.iter()
.map(ScopedDefinitionId::as_u32)
.map(|(d, _)| d.as_u32()) // TODO: constraints
.collect::<Vec<_>>();
assert_eq!(actual, expected);
}
@@ -457,76 +630,76 @@ mod tests {
assert_bindings(&sym, true, &[]);
}
#[test]
fn with() {
let mut sym = SymbolState::undefined();
sym.record_binding(ScopedDefinitionId::from_u32(0));
// #[test]
// fn with() {
// let mut sym = SymbolState::undefined();
// sym.record_binding(ScopedDefinitionId::from_u32(0));
assert_bindings(&sym, false, &["0<>"]);
}
// assert_bindings(&sym, false, &["0<>"]);
// }
#[test]
fn set_may_be_unbound() {
let mut sym = SymbolState::undefined();
sym.record_binding(ScopedDefinitionId::from_u32(0));
sym.set_may_be_unbound();
// #[test]
// fn set_may_be_unbound() {
// let mut sym = SymbolState::undefined();
// sym.record_binding(ScopedDefinitionId::from_u32(0));
// sym.set_may_be_unbound();
assert_bindings(&sym, true, &["0<>"]);
}
// assert_bindings(&sym, true, &["0<>"]);
// }
#[test]
fn record_constraint() {
let mut sym = SymbolState::undefined();
sym.record_binding(ScopedDefinitionId::from_u32(0));
sym.record_constraint(ScopedConstraintId::from_u32(0));
// #[test]
// fn record_constraint() {
// let mut sym = SymbolState::undefined();
// sym.record_binding(ScopedDefinitionId::from_u32(0));
// sym.record_constraint(ScopedConstraintId::from_u32(0));
assert_bindings(&sym, false, &["0<0>"]);
}
// assert_bindings(&sym, false, &["0<0>"]);
// }
#[test]
fn merge() {
// merging the same definition with the same constraint keeps the constraint
let mut sym0a = SymbolState::undefined();
sym0a.record_binding(ScopedDefinitionId::from_u32(0));
sym0a.record_constraint(ScopedConstraintId::from_u32(0));
// #[test]
// fn merge() {
// // merging the same definition with the same constraint keeps the constraint
// let mut sym0a = SymbolState::undefined();
// sym0a.record_binding(ScopedDefinitionId::from_u32(0));
// sym0a.record_constraint(ScopedConstraintId::from_u32(0));
let mut sym0b = SymbolState::undefined();
sym0b.record_binding(ScopedDefinitionId::from_u32(0));
sym0b.record_constraint(ScopedConstraintId::from_u32(0));
// let mut sym0b = SymbolState::undefined();
// sym0b.record_binding(ScopedDefinitionId::from_u32(0));
// sym0b.record_constraint(ScopedConstraintId::from_u32(0));
sym0a.merge(sym0b);
let mut sym0 = sym0a;
assert_bindings(&sym0, false, &["0<0>"]);
// sym0a.merge(sym0b);
// let mut sym0 = sym0a;
// assert_bindings(&sym0, false, &["0<0>"]);
// merging the same definition with differing constraints drops all constraints
let mut sym1a = SymbolState::undefined();
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
sym1a.record_constraint(ScopedConstraintId::from_u32(1));
// // merging the same definition with differing constraints drops all constraints
// let mut sym1a = SymbolState::undefined();
// sym1a.record_binding(ScopedDefinitionId::from_u32(1));
// sym1a.record_constraint(ScopedConstraintId::from_u32(1));
let mut sym1b = SymbolState::undefined();
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
// let mut sym1b = SymbolState::undefined();
// sym1b.record_binding(ScopedDefinitionId::from_u32(1));
// sym1b.record_constraint(ScopedConstraintId::from_u32(2));
sym1a.merge(sym1b);
let sym1 = sym1a;
assert_bindings(&sym1, false, &["1<>"]);
// sym1a.merge(sym1b);
// let sym1 = sym1a;
// assert_bindings(&sym1, false, &["1<>"]);
// merging a constrained definition with unbound keeps both
let mut sym2a = SymbolState::undefined();
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
sym2a.record_constraint(ScopedConstraintId::from_u32(3));
// // merging a constrained definition with unbound keeps both
// let mut sym2a = SymbolState::undefined();
// sym2a.record_binding(ScopedDefinitionId::from_u32(2));
// sym2a.record_constraint(ScopedConstraintId::from_u32(3));
let sym2b = SymbolState::undefined();
// let sym2b = SymbolState::undefined();
sym2a.merge(sym2b);
let sym2 = sym2a;
assert_bindings(&sym2, true, &["2<3>"]);
// sym2a.merge(sym2b);
// let sym2 = sym2a;
// assert_bindings(&sym2, true, &["2<3>"]);
// merging different definitions keeps them each with their existing constraints
sym0.merge(sym2);
let sym = sym0;
assert_bindings(&sym, true, &["0<0>", "2<3>"]);
}
// // merging different definitions keeps them each with their existing constraints
// sym0.merge(sym2);
// let sym = sym0;
// assert_bindings(&sym, true, &["0<0>", "2<3>"]);
// }
#[test]
fn no_declaration() {
@@ -535,54 +708,54 @@ mod tests {
assert_declarations(&sym, true, &[]);
}
#[test]
fn record_declaration() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
// #[test]
// fn record_declaration() {
// let mut sym = SymbolState::undefined();
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
assert_declarations(&sym, false, &[1]);
}
// assert_declarations(&sym, false, &[1]);
// }
#[test]
fn record_declaration_override() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
sym.record_declaration(ScopedDefinitionId::from_u32(2));
// #[test]
// fn record_declaration_override() {
// let mut sym = SymbolState::undefined();
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
// sym.record_declaration(ScopedDefinitionId::from_u32(2));
assert_declarations(&sym, false, &[2]);
}
// assert_declarations(&sym, false, &[2]);
// }
#[test]
fn record_declaration_merge() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
// #[test]
// fn record_declaration_merge() {
// let mut sym = SymbolState::undefined();
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
let mut sym2 = SymbolState::undefined();
sym2.record_declaration(ScopedDefinitionId::from_u32(2));
// let mut sym2 = SymbolState::undefined();
// sym2.record_declaration(ScopedDefinitionId::from_u32(2));
sym.merge(sym2);
// sym.merge(sym2);
assert_declarations(&sym, false, &[1, 2]);
}
// assert_declarations(&sym, false, &[1, 2]);
// }
#[test]
fn record_declaration_merge_partial_undeclared() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
// #[test]
// fn record_declaration_merge_partial_undeclared() {
// let mut sym = SymbolState::undefined();
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
let sym2 = SymbolState::undefined();
// let sym2 = SymbolState::undefined();
sym.merge(sym2);
// sym.merge(sym2);
assert_declarations(&sym, true, &[1]);
}
// assert_declarations(&sym, true, &[1]);
// }
#[test]
fn set_may_be_undeclared() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(0));
sym.set_may_be_undeclared();
// #[test]
// fn set_may_be_undeclared() {
// let mut sym = SymbolState::undefined();
// sym.record_declaration(ScopedDefinitionId::from_u32(0));
// sym.set_may_be_undeclared();
assert_declarations(&sym, true, &[0]);
}
// assert_declarations(&sym, true, &[0]);
// }
}

View File

@@ -15,6 +15,7 @@ pub(crate) use self::infer::{
pub(crate) use self::signatures::Signature;
use crate::module_resolver::file_to_module;
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::constraint::ConstraintNode;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::{self as symbol, ScopeId, ScopedSymbolId};
use crate::semantic_index::{
@@ -222,6 +223,12 @@ fn definition_expression_ty<'db>(
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum UnconditionallyVisible {
Yes,
No,
}
/// Infer the combined type of an iterator of bindings.
///
/// Will return a union if there is more than one binding.
@@ -229,29 +236,88 @@ fn bindings_ty<'db>(
db: &'db dyn Db,
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
) -> Option<Type<'db>> {
let mut def_types = bindings_with_constraints.map(
let def_types = bindings_with_constraints.map(
|BindingWithConstraints {
binding,
constraints,
constraints_active_at_binding,
}| {
let mut constraint_tys = constraints
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
.peekable();
let test_expr_tys = || {
constraints_active_at_binding.clone().map(|c| {
let ty = if let ConstraintNode::Expression(test_expr) = c.node {
let inference = infer_expression_types(db, test_expr);
let scope = test_expr.scope(db);
inference
.expression_ty(test_expr.node_ref(db).scoped_expression_id(db, scope))
} else {
// TODO: handle other constraint nodes
todo_type!()
};
let binding_ty = binding_ty(db, binding);
if constraint_tys.peek().is_some() {
constraint_tys
.fold(
IntersectionBuilder::new(db).add_positive(binding_ty),
IntersectionBuilder::add_positive,
)
.build()
(c, ty)
})
};
if test_expr_tys().any(|(c, test_expr_ty)| {
if c.is_positive {
test_expr_ty.bool(db).is_always_false()
} else {
test_expr_ty.bool(db).is_always_true()
}
}) {
// TODO: do we need to call binding_ty(…) even if we don't need the result?
(Type::Never, UnconditionallyVisible::No)
} else {
binding_ty
let mut test_expr_tys_iter = test_expr_tys().peekable();
let unconditionally_visible = if test_expr_tys_iter.peek().is_some()
&& test_expr_tys_iter.all(|(c, test_expr_ty)| {
if c.is_positive {
test_expr_ty.bool(db).is_always_true()
} else {
test_expr_ty.bool(db).is_always_false()
}
}) {
UnconditionallyVisible::Yes
} else {
UnconditionallyVisible::No
};
let mut constraint_tys = constraints
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
.peekable();
let binding_ty = binding_ty(db, binding);
if constraint_tys.peek().is_some() {
let intersection_ty = constraint_tys
.fold(
IntersectionBuilder::new(db).add_positive(binding_ty),
IntersectionBuilder::add_positive,
)
.build();
(intersection_ty, unconditionally_visible)
} else {
(binding_ty, unconditionally_visible)
}
}
},
);
// TODO: get rid of all the collects and clean up, obviously
let def_types: Vec<_> = def_types.collect();
// shrink the vector to only include everything from the last unconditionally visible binding
let def_types: Vec<_> = def_types
.iter()
.rev()
.take_while_inclusive(|(_, unconditionally_visible)| {
*unconditionally_visible != UnconditionallyVisible::Yes
})
.map(|(ty, _)| *ty)
.collect();
let mut def_types = def_types.into_iter().rev();
if let Some(first) = def_types.next() {
if let Some(second) = def_types.next() {
Some(UnionType::from_elements(
@@ -287,7 +353,63 @@ fn declarations_ty<'db>(
declarations: DeclarationsIterator<'_, 'db>,
undeclared_ty: Option<Type<'db>>,
) -> DeclaredTypeResult<'db> {
let decl_types = declarations.map(|declaration| declaration_ty(db, declaration));
let decl_types = declarations.map(|(declaration, constraints_active_at_declaration)| {
let test_expr_tys = || {
constraints_active_at_declaration.clone().map(|c| {
let ty = if let ConstraintNode::Expression(test_expr) = c.node {
let inference = infer_expression_types(db, test_expr);
let scope = test_expr.scope(db);
inference.expression_ty(test_expr.node_ref(db).scoped_expression_id(db, scope))
} else {
// TODO: handle other constraint nodes
todo_type!()
};
(c, ty)
})
};
if test_expr_tys().any(|(c, test_expr_ty)| {
if c.is_positive {
test_expr_ty.bool(db).is_always_false()
} else {
test_expr_ty.bool(db).is_always_true()
}
}) {
(Type::Never, UnconditionallyVisible::No)
} else {
let mut test_expr_tys_iter = test_expr_tys().peekable();
if test_expr_tys_iter.peek().is_some()
&& test_expr_tys_iter.all(|(c, test_expr_ty)| {
if c.is_positive {
test_expr_ty.bool(db).is_always_true()
} else {
test_expr_ty.bool(db).is_always_false()
}
})
{
(declaration_ty(db, declaration), UnconditionallyVisible::Yes)
} else {
(declaration_ty(db, declaration), UnconditionallyVisible::No)
}
}
});
// TODO: get rid of all the collects and clean up, obviously
let decl_types: Vec<_> = decl_types.collect();
// shrink the vector to only include everything from the last unconditionally visible binding
let decl_types: Vec<_> = decl_types
.iter()
.rev()
.take_while_inclusive(|(_, unconditionally_visible)| {
*unconditionally_visible != UnconditionallyVisible::Yes
})
.map(|(ty, _)| *ty)
.collect();
let decl_types = decl_types.into_iter().rev();
let mut all_types = undeclared_ty.into_iter().chain(decl_types);
@@ -768,23 +890,7 @@ impl<'db> Type<'db> {
// TODO: Once we have support for final classes, we can establish that
// `Type::SubclassOf('FinalClass')` is equivalent to `Type::ClassLiteral('FinalClass')`.
// TODO: The following is a workaround that is required to unify the two different versions
// 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 }),
Type::Instance(InstanceType { class: target_class })
)
if {
let self_known = self_class.known(db);
matches!(self_known, Some(KnownClass::NoneType | KnownClass::NoDefaultType))
&& self_known == target_class.known(db)
}
)
self == other || matches!((self, other), (Type::Todo(_), Type::Todo(_)))
}
/// Return true if this type and `other` have no common elements.
@@ -1763,13 +1869,13 @@ impl<'db> KnownClass {
}
pub fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
core_module_symbol(db, self.canonical_module(), self.as_str())
core_module_symbol(db, self.canonical_module(db), self.as_str())
.ignore_possibly_unbound()
.unwrap_or(Type::Unknown)
}
/// Return the module in which we should look up the definition for this class
pub(crate) const fn canonical_module(self) -> CoreStdlibModule {
pub(crate) fn canonical_module(self, db: &'db dyn Db) -> CoreStdlibModule {
match self {
Self::Bool
| Self::Object
@@ -1787,10 +1893,18 @@ impl<'db> KnownClass {
Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types,
Self::NoneType => CoreStdlibModule::Typeshed,
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`
Self::NoDefaultType => CoreStdlibModule::TypingExtensions,
Self::NoDefaultType => {
let python_version = Program::get(db).target_version(db);
// typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
// singleton, but not for `typing._NoDefaultType`. So we need to switch
// to `typing.NoDefault` for newer versions:
if python_version.major >= 3 && python_version.minor >= 13 {
CoreStdlibModule::Typing
} else {
CoreStdlibModule::TypingExtensions
}
}
}
}
@@ -1850,11 +1964,11 @@ impl<'db> KnownClass {
};
let module = file_to_module(db, file)?;
candidate.check_module(&module).then_some(candidate)
candidate.check_module(db, &module).then_some(candidate)
}
/// Return `true` if the module of `self` matches `module_name`
fn check_module(self, module: &Module) -> bool {
fn check_module(self, db: &dyn Db, module: &Module) -> bool {
if !module.search_path().is_standard_library() {
return false;
}
@@ -1874,7 +1988,7 @@ impl<'db> KnownClass {
| Self::GenericAlias
| Self::ModuleType
| Self::VersionInfo
| Self::FunctionType => module.name() == self.canonical_module().as_str(),
| Self::FunctionType => module.name() == self.canonical_module(db).as_str(),
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => {
matches!(module.name().as_str(), "typing" | "typing_extensions")
@@ -2396,6 +2510,14 @@ impl Truthiness {
matches!(self, Truthiness::Ambiguous)
}
const fn is_always_false(self) -> bool {
matches!(self, Truthiness::AlwaysFalse)
}
const fn is_always_true(self) -> bool {
matches!(self, Truthiness::AlwaysTrue)
}
const fn negate(self) -> Self {
match self {
Self::AlwaysTrue => Self::AlwaysFalse,
@@ -3036,7 +3158,7 @@ pub(crate) mod tests {
use ruff_python_ast as ast;
use test_case::test_case;
pub(crate) fn setup_db() -> TestDb {
pub(crate) fn setup_db_with_python_version(python_version: PythonVersion) -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
@@ -3047,7 +3169,7 @@ pub(crate) mod tests {
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
target_version: python_version,
search_paths: SearchPathSettings::new(src_root),
},
)
@@ -3056,6 +3178,10 @@ pub(crate) mod tests {
db
}
pub(crate) fn setup_db() -> TestDb {
setup_db_with_python_version(PythonVersion::default())
}
/// A test representation of a type that can be transformed unambiguously into a real Type,
/// given a db.
#[derive(Debug, Clone)]
@@ -3503,13 +3629,23 @@ pub(crate) mod tests {
#[test_case(Ty::None)]
#[test_case(Ty::BooleanLiteral(true))]
#[test_case(Ty::BooleanLiteral(false))]
#[test_case(Ty::KnownClassInstance(KnownClass::NoDefaultType))]
fn is_singleton(from: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_singleton(&db));
}
/// TODO: test documentation
#[test_case(PythonVersion::PY312)]
#[test_case(PythonVersion::PY313)]
fn no_default_type_is_singleton(python_version: PythonVersion) {
let db = setup_db_with_python_version(python_version);
let no_default = Ty::KnownClassInstance(KnownClass::NoDefaultType).into_type(&db);
assert!(no_default.is_singleton(&db));
}
#[test_case(Ty::None)]
#[test_case(Ty::BooleanLiteral(true))]
#[test_case(Ty::IntLiteral(1))]

View File

@@ -31,7 +31,6 @@ use std::num::NonZeroU32;
use itertools::Itertools;
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::visitor::{self, Visitor};
use ruff_python_ast::{self as ast, AnyNodeRef, Expr, ExprContext, UnaryOp};
use rustc_hash::{FxHashMap, FxHashSet};
use salsa;
@@ -4215,6 +4214,25 @@ impl<'db> TypeInferenceBuilder<'db> {
// Annotation expressions also get special handling for `*args` and `**kwargs`.
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
ast::Expr::BytesLiteral(bytes) => {
self.diagnostics.add(
bytes.into(),
"annotation-byte-string",
format_args!("Type expressions cannot use bytes literal"),
);
Type::Unknown
}
ast::Expr::FString(fstring) => {
self.diagnostics.add(
fstring.into(),
"annotation-f-string",
format_args!("Type expressions cannot use f-strings"),
);
self.infer_fstring_expression(fstring);
Type::Unknown
}
// All other annotation expressions are (possibly) valid type expressions, so handle
// them there instead.
type_expr => self.infer_type_expression_no_store(type_expr),
@@ -4225,72 +4243,6 @@ impl<'db> TypeInferenceBuilder<'db> {
annotation_ty
}
/// Walk child expressions of the given AST node and store the given type for each of them.
/// Does not store a type for the root expression. Resets to normal type inference for all
/// expressions with a nested scope (lambda, named expression, comprehensions, generators).
fn store_type_for_sub_expressions_of(&mut self, expression: &ast::Expr, ty: Type<'db>) {
struct StoreTypeVisitor<'a, 'db> {
builder: &'a mut TypeInferenceBuilder<'db>,
ty: Type<'db>,
is_root_node: bool,
}
impl<'a, 'db> StoreTypeVisitor<'a, 'db> {
fn new(builder: &'a mut TypeInferenceBuilder<'db>, ty: Type<'db>) -> Self {
Self {
builder,
ty,
is_root_node: true,
}
}
fn store(&mut self, expr: &ast::Expr) {
if self.is_root_node {
self.is_root_node = false;
} else {
self.builder.store_expression_type(expr, self.ty);
}
}
}
impl<'a, 'db, 'ast> Visitor<'ast> for StoreTypeVisitor<'a, 'db> {
fn visit_expr(&mut self, expr: &'ast Expr) {
match expr {
ast::Expr::Lambda(lambda) => {
self.builder.infer_lambda_expression(lambda);
self.store(expr);
}
ast::Expr::Named(named) => {
self.builder.infer_named_expression(named);
self.store(expr);
}
ast::Expr::ListComp(list_comp) => {
self.builder.infer_list_comprehension_expression(list_comp);
self.store(expr);
}
ast::Expr::SetComp(set_comp) => {
self.builder.infer_set_comprehension_expression(set_comp);
self.store(expr);
}
ast::Expr::DictComp(dict_comp) => {
self.builder.infer_dict_comprehension_expression(dict_comp);
self.store(expr);
}
ast::Expr::Generator(generator) => {
self.builder.infer_generator_expression(generator);
self.store(expr);
}
_ => {
self.store(expr);
visitor::walk_expr(self, expr);
}
}
}
}
StoreTypeVisitor::new(self, ty).visit_expr(expression);
}
/// Infer the type of a string annotation expression.
fn infer_string_annotation_expression(&mut self, string: &ast::ExprStringLiteral) -> Type<'db> {
match parse_string_annotation(self.db, self.file, string) {
@@ -4372,6 +4324,13 @@ impl<'db> TypeInferenceBuilder<'db> {
// expressions, but is meaningful in the context of a number of special forms.
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) => todo_type!(),
ast::Expr::NumberLiteral(_literal) => todo_type!(),
ast::Expr::BooleanLiteral(_literal) => todo_type!(),
ast::Expr::Subscript(subscript) => {
let ast::ExprSubscript {
value,
@@ -4420,62 +4379,90 @@ impl<'db> TypeInferenceBuilder<'db> {
// 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 store a type of
// `Type::Unknown` for these expressions (and their sub-expressions) to avoid problems
// with invalid-syntax examples like `x: f"{x}"` or `x: lambda y: x`, for which we can
// not infer a meaningful type for the inner `x` expression. The top-level expression
// is also `Type::Unknown` in these cases.
ast::Expr::BoolOp(_)
| ast::Expr::Named(_)
| ast::Expr::UnaryOp(_)
| ast::Expr::Lambda(_)
| ast::Expr::If(_)
| ast::Expr::Dict(_)
| ast::Expr::Set(_)
| ast::Expr::ListComp(_)
| ast::Expr::SetComp(_)
| ast::Expr::DictComp(_)
| ast::Expr::Generator(_)
| ast::Expr::Await(_)
| ast::Expr::Yield(_)
| ast::Expr::YieldFrom(_)
| ast::Expr::Compare(_)
| ast::Expr::Call(_)
| ast::Expr::FString(_)
| ast::Expr::List(_)
| ast::Expr::Tuple(_)
| ast::Expr::Slice(_)
| ast::Expr::IpyEscapeCommand(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::NumberLiteral(_)
| ast::Expr::BooleanLiteral(_) => {
match expression {
ast::Expr::BytesLiteral(bytes) => {
self.diagnostics.add(
bytes.into(),
"annotation-byte-string",
format_args!("Type expressions cannot use bytes literal"),
);
}
ast::Expr::FString(fstring) => {
self.diagnostics.add(
fstring.into(),
"annotation-f-string",
format_args!("Type expressions cannot use f-strings"),
);
}
_ => {
self.diagnostics.add(
expression.into(),
"annotation-with-invalid-expression",
format_args!("Invalid expression in type expression"),
);
}
}
self.store_type_for_sub_expressions_of(expression, Type::Unknown);
// Forms which are invalid in the context of annotation expressions: we infer their
// nested expressions as normal expressions, but the type of the top-level expression is
// always `Type::Unknown` in these cases.
ast::Expr::BoolOp(bool_op) => {
self.infer_boolean_expression(bool_op);
Type::Unknown
}
ast::Expr::Named(named) => {
self.infer_named_expression(named);
Type::Unknown
}
ast::Expr::UnaryOp(unary) => {
self.infer_unary_expression(unary);
Type::Unknown
}
ast::Expr::Lambda(lambda_expression) => {
self.infer_lambda_expression(lambda_expression);
Type::Unknown
}
ast::Expr::If(if_expression) => {
self.infer_if_expression(if_expression);
Type::Unknown
}
ast::Expr::Dict(dict) => {
self.infer_dict_expression(dict);
Type::Unknown
}
ast::Expr::Set(set) => {
self.infer_set_expression(set);
Type::Unknown
}
ast::Expr::ListComp(listcomp) => {
self.infer_list_comprehension_expression(listcomp);
Type::Unknown
}
ast::Expr::SetComp(setcomp) => {
self.infer_set_comprehension_expression(setcomp);
Type::Unknown
}
ast::Expr::DictComp(dictcomp) => {
self.infer_dict_comprehension_expression(dictcomp);
Type::Unknown
}
ast::Expr::Generator(generator) => {
self.infer_generator_expression(generator);
Type::Unknown
}
ast::Expr::Await(await_expression) => {
self.infer_await_expression(await_expression);
Type::Unknown
}
ast::Expr::Yield(yield_expression) => {
self.infer_yield_expression(yield_expression);
Type::Unknown
}
ast::Expr::YieldFrom(yield_from) => {
self.infer_yield_from_expression(yield_from);
Type::Unknown
}
ast::Expr::Compare(compare) => {
self.infer_compare_expression(compare);
Type::Unknown
}
ast::Expr::Call(call_expr) => {
self.infer_call_expression(call_expr);
Type::Unknown
}
ast::Expr::FString(fstring) => {
self.infer_fstring_expression(fstring);
Type::Unknown
}
ast::Expr::List(list) => {
self.infer_list_expression(list);
Type::Unknown
}
ast::Expr::Tuple(tuple) => {
self.infer_tuple_expression(tuple);
Type::Unknown
}
ast::Expr::Slice(slice) => {
self.infer_slice_expression(slice);
Type::Unknown
}
ast::Expr::IpyEscapeCommand(_) => todo!("Implement Ipy escape command support"),
}
}
@@ -5044,7 +5031,7 @@ mod tests {
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::python_version::{self, PythonVersion};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::FileScopeId;
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
@@ -5054,10 +5041,11 @@ mod tests {
use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_db::testing::assert_function_query_was_not_run;
use test_case::test_case;
use super::*;
fn setup_db() -> TestDb {
fn setup_db_with_python_version(python_version: PythonVersion) -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
@@ -5068,7 +5056,7 @@ mod tests {
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
target_version: python_version,
search_paths: SearchPathSettings::new(src_root),
},
)
@@ -5077,6 +5065,10 @@ mod tests {
db
}
fn setup_db() -> TestDb {
setup_db_with_python_version(PythonVersion::default())
}
fn setup_db_with_custom_typeshed<'a>(
typeshed: &str,
files: impl IntoIterator<Item = (&'a str, &'a str)>,
@@ -5348,9 +5340,10 @@ mod tests {
Ok(())
}
#[test]
fn ellipsis_type() -> anyhow::Result<()> {
let mut db = setup_db();
#[test_case(PythonVersion::PY39, "ellipsis")]
#[test_case(PythonVersion::PY310, "EllipsisType")]
fn ellipsis_type(version: PythonVersion, expected_type: &str) -> anyhow::Result<()> {
let mut db = setup_db_with_python_version(version);
db.write_dedented(
"src/a.py",
@@ -5359,8 +5352,7 @@ mod tests {
",
)?;
// TODO: sys.version_info
assert_public_ty(&db, "src/a.py", "x", "EllipsisType | ellipsis");
assert_public_ty(&db, "src/a.py", "x", expected_type);
Ok(())
}

View File

@@ -185,11 +185,11 @@ impl Settings {
pub enum TargetVersion {
Py37,
Py38,
#[default]
Py39,
Py310,
Py311,
Py312,
#[default]
Py313,
}

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: "&workspace"
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -24,7 +23,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: workspace
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -24,7 +23,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: workspace
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -24,7 +23,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: workspace
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -24,7 +23,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: workspace
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -37,7 +36,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -1,7 +1,6 @@
---
source: crates/red_knot_workspace/src/workspace/metadata.rs
expression: workspace
snapshot_kind: text
---
WorkspaceMetadata(
root: "/app",
@@ -50,7 +49,7 @@ WorkspaceMetadata(
program: ProgramSettings(
target_version: PythonVersion(
major: 3,
minor: 9,
minor: 13,
),
search_paths: SearchPathSettings(
extra_paths: [],

View File

@@ -272,4 +272,7 @@ const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
("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.8.0"
version = "0.8.1"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

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

View File

@@ -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,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.8.0"
version = "0.8.1"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -60,3 +60,23 @@ def f():
| 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

@@ -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,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,13 @@
"""Regression test for #14531.
RUF101 should trigger here because the TCH rules have been recoded to TC.
"""
# ruff: noqa: TCH002
from __future__ import annotations
import local_module
def func(sized: local_module.Container) -> int:
return len(sized)

View File

@@ -3,7 +3,9 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint, ruff};
use crate::rules::{
flake8_import_conventions, flake8_pyi, flake8_type_checking, pyflakes, pylint, ruff,
};
/// Run lint rules over the [`Binding`]s.
pub(crate) fn bindings(checker: &mut Checker) {
@@ -15,6 +17,7 @@ pub(crate) fn bindings(checker: &mut Checker) {
Rule::UnconventionalImportAlias,
Rule::UnsortedDunderSlots,
Rule::UnusedVariable,
Rule::UnquotedTypeAlias,
]) {
return;
}
@@ -72,6 +75,13 @@ pub(crate) fn bindings(checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::UnquotedTypeAlias) {
if let Some(diagnostics) =
flake8_type_checking::rules::unquoted_type_alias(checker, binding)
{
checker.diagnostics.extend(diagnostics);
}
}
if checker.enabled(Rule::UnsortedDunderSlots) {
if let Some(diagnostic) = ruff::rules::sort_dunder_slots(checker, binding) {
checker.diagnostics.push(diagnostic);

View File

@@ -2,7 +2,7 @@ use ruff_python_ast::Expr;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_builtins, flake8_pie, pylint, refurb};
use crate::rules::{flake8_builtins, flake8_pie, pylint};
/// Run lint rules over all deferred lambdas in the [`SemanticModel`].
pub(crate) fn deferred_lambdas(checker: &mut Checker) {
@@ -21,9 +21,6 @@ pub(crate) fn deferred_lambdas(checker: &mut Checker) {
if checker.enabled(Rule::ReimplementedContainerBuiltin) {
flake8_pie::rules::reimplemented_container_builtin(checker, lambda);
}
if checker.enabled(Rule::ReimplementedOperator) {
refurb::rules::reimplemented_operator(checker, &lambda.into());
}
if checker.enabled(Rule::BuiltinLambdaArgumentShadowing) {
flake8_builtins::rules::builtin_lambda_argument_shadowing(checker, lambda);
}

View File

@@ -52,14 +52,13 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let enforce_typing_imports = !checker.source_type.is_stub()
let enforce_typing_only_imports = !checker.source_type.is_stub()
&& checker.any_enabled(&[
Rule::RuntimeImportInTypeCheckingBlock,
Rule::TypingOnlyFirstPartyImport,
Rule::TypingOnlyStandardLibraryImport,
Rule::TypingOnlyThirdPartyImport,
]);
let runtime_imports: Vec<Vec<&Binding>> = if enforce_typing_imports {
let runtime_imports: Vec<Vec<&Binding>> = if enforce_typing_only_imports {
checker
.semantic
.scopes
@@ -375,7 +374,16 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if enforce_typing_imports {
if !checker.source_type.is_stub()
&& checker.enabled(Rule::RuntimeImportInTypeCheckingBlock)
{
flake8_type_checking::rules::runtime_import_in_type_checking_block(
checker,
scope,
&mut diagnostics,
);
}
if enforce_typing_only_imports {
let runtime_imports: Vec<&Binding> = checker
.semantic
.scopes
@@ -384,26 +392,12 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
.copied()
.collect();
if checker.enabled(Rule::RuntimeImportInTypeCheckingBlock) {
flake8_type_checking::rules::runtime_import_in_type_checking_block(
checker,
scope,
&mut diagnostics,
);
}
if checker.any_enabled(&[
Rule::TypingOnlyFirstPartyImport,
Rule::TypingOnlyStandardLibraryImport,
Rule::TypingOnlyThirdPartyImport,
]) {
flake8_type_checking::rules::typing_only_runtime_import(
checker,
scope,
&runtime_imports,
&mut diagnostics,
);
}
flake8_type_checking::rules::typing_only_runtime_import(
checker,
scope,
&runtime_imports,
&mut diagnostics,
);
}
if checker.enabled(Rule::UnusedImport) {

View File

@@ -108,6 +108,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::DuplicateLiteralMember,
Rule::RedundantBoolLiteral,
Rule::RedundantNoneLiteral,
Rule::UnnecessaryNestedLiteral,
]) {
if !checker.semantic.in_nested_literal() {
if checker.enabled(Rule::DuplicateLiteralMember) {
@@ -119,6 +120,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::RedundantNoneLiteral) {
flake8_pyi::rules::redundant_none_literal(checker, expr);
}
if checker.enabled(Rule::UnnecessaryNestedLiteral) {
ruff::rules::unnecessary_nested_literal(checker, expr);
}
}
}
@@ -965,6 +969,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::OsPathGetmtime,
Rule::OsPathGetctime,
Rule::Glob,
Rule::OsListdir,
]) {
flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call);
}
@@ -1645,6 +1650,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
ruff::rules::assignment_in_assert(checker, expr);
}
}
Expr::Lambda(lambda_expr) => {
if checker.enabled(Rule::ReimplementedOperator) {
refurb::rules::reimplemented_operator(checker, &lambda_expr.into());
}
}
_ => {}
};
}

View File

@@ -888,9 +888,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
if let Some(type_params) = type_params {
self.visit_type_params(type_params);
}
self.visit
.type_param_definitions
.push((value, self.semantic.snapshot()));
self.visit_deferred_type_alias_value(value);
self.semantic.pop_scope();
self.visit_expr(name);
}
@@ -961,7 +959,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
if let Some(expr) = value {
if self.semantic.match_typing_expr(annotation, "TypeAlias") {
self.visit_type_definition(expr);
self.visit_annotated_type_alias_value(expr);
} else {
self.visit_expr(expr);
}
@@ -1855,6 +1853,45 @@ impl<'a> Checker<'a> {
self.semantic.flags = snapshot;
}
/// Visit an [`Expr`], and treat it as the value expression
/// of a [PEP 613] type alias.
///
/// For example:
/// ```python
/// from typing import TypeAlias
///
/// OptStr: TypeAlias = str | None # We're visiting the RHS
/// ```
///
/// [PEP 613]: https://peps.python.org/pep-0613/
fn visit_annotated_type_alias_value(&mut self, expr: &'a Expr) {
let snapshot = self.semantic.flags;
self.semantic.flags |= SemanticModelFlags::ANNOTATED_TYPE_ALIAS;
self.visit_type_definition(expr);
self.semantic.flags = snapshot;
}
/// Visit an [`Expr`], and treat it as the value expression
/// of a [PEP 695] type alias.
///
/// For example:
/// ```python
/// type OptStr = str | None # We're visiting the RHS
/// ```
///
/// [PEP 695]: https://peps.python.org/pep-0695/#generic-type-alias
fn visit_deferred_type_alias_value(&mut self, expr: &'a Expr) {
let snapshot = self.semantic.flags;
// even though we don't visit these nodes immediately we need to
// modify the semantic flags before we push the expression and its
// corresponding semantic snapshot
self.semantic.flags |= SemanticModelFlags::DEFERRED_TYPE_ALIAS;
self.visit
.type_param_definitions
.push((expr, self.semantic.snapshot()));
self.semantic.flags = snapshot;
}
/// Visit an [`Expr`], and treat it as a type definition.
fn visit_type_definition(&mut self, expr: &'a Expr) {
if self.semantic.in_no_type_check() {
@@ -2017,6 +2054,21 @@ impl<'a> Checker<'a> {
flags.insert(BindingFlags::UNPACKED_ASSIGNMENT);
}
match parent {
Stmt::TypeAlias(_) => flags.insert(BindingFlags::DEFERRED_TYPE_ALIAS),
Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. }) => {
// TODO: It is a bit unfortunate that we do this check twice
// maybe we should change how we visit this statement
// so the semantic flag for the type alias sticks around
// until after we've handled this store, so we can check
// the flag instead of duplicating this check
if self.semantic.match_typing_expr(annotation, "TypeAlias") {
flags.insert(BindingFlags::ANNOTATED_TYPE_ALIAS);
}
}
_ => {}
}
let scope = self.semantic.current_scope();
if scope.kind.is_module()
@@ -2272,7 +2324,17 @@ impl<'a> Checker<'a> {
self.semantic.flags |=
SemanticModelFlags::TYPE_DEFINITION | type_definition_flag;
self.visit_expr(parsed_annotation.expression());
let parsed_expr = parsed_annotation.expression();
self.visit_expr(parsed_expr);
if self.semantic.in_type_alias_value() {
if self.enabled(Rule::QuotedTypeAlias) {
flake8_type_checking::rules::quoted_type_alias(
self,
parsed_expr,
string_expr,
);
}
}
self.parsed_type_annotation = None;
} else {
if self.enabled(Rule::ForwardAnnotationSyntaxError) {

View File

@@ -211,6 +211,7 @@ pub(crate) fn check_noqa(
&& !exemption.includes(Rule::RedirectedNOQA)
{
ruff::rules::redirected_noqa(diagnostics, &noqa_directives);
ruff::rules::redirected_file_noqa(diagnostics, &file_noqa_directives);
}
if settings.rules.enabled(Rule::BlanketNOQA)

View File

@@ -858,6 +858,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
(Flake8TypeChecking, "005") => (RuleGroup::Stable, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
(Flake8TypeChecking, "006") => (RuleGroup::Preview, rules::flake8_type_checking::rules::RuntimeCastValue),
(Flake8TypeChecking, "007") => (RuleGroup::Preview, rules::flake8_type_checking::rules::UnquotedTypeAlias),
(Flake8TypeChecking, "008") => (RuleGroup::Preview, rules::flake8_type_checking::rules::QuotedTypeAlias),
(Flake8TypeChecking, "010") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeStringUnion),
// tryceratops
@@ -906,6 +908,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "205") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathGetctime),
(Flake8UsePathlib, "206") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsSepSplit),
(Flake8UsePathlib, "207") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::Glob),
(Flake8UsePathlib, "208") => (RuleGroup::Preview, rules::flake8_use_pathlib::violations::OsListdir),
// flake8-logging-format
(Flake8LoggingFormat, "001") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingStringFormat),
@@ -977,9 +980,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "035") => (RuleGroup::Preview, rules::ruff::rules::UnsafeMarkupUse),
(Ruff, "036") => (RuleGroup::Preview, rules::ruff::rules::NoneNotAtEndOfUnion),
(Ruff, "038") => (RuleGroup::Preview, rules::ruff::rules::RedundantBoolLiteral),
(Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing),
(Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern),
(Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument),
(Ruff, "041") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryNestedLiteral),
(Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),

View File

@@ -319,7 +319,7 @@ impl<'a> From<&'a FileNoqaDirectives<'a>> for FileExemption<'a> {
if directives
.lines()
.iter()
.any(|line| ParsedFileExemption::All == line.parsed_file_exemption)
.any(|line| matches!(line.parsed_file_exemption, ParsedFileExemption::All))
{
FileExemption::All(codes)
} else {
@@ -362,7 +362,7 @@ impl<'a> FileNoqaDirectives<'a> {
let mut lines = vec![];
for range in comment_ranges {
match ParsedFileExemption::try_extract(&locator.contents()[range]) {
match ParsedFileExemption::try_extract(range, locator.contents()) {
Err(err) => {
#[allow(deprecated)]
let line = locator.compute_line_index(range.start());
@@ -384,6 +384,7 @@ impl<'a> FileNoqaDirectives<'a> {
}
ParsedFileExemption::Codes(codes) => {
codes.iter().filter_map(|code| {
let code = code.as_str();
// Ignore externally-defined rules.
if external.iter().any(|external| code.starts_with(external)) {
return None;
@@ -424,21 +425,26 @@ impl<'a> FileNoqaDirectives<'a> {
/// An individual file-level exemption (e.g., `# ruff: noqa` or `# ruff: noqa: F401, F841`). Like
/// [`FileNoqaDirectives`], but only for a single line, as opposed to an aggregated set of exemptions
/// across a source file.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug)]
pub(crate) enum ParsedFileExemption<'a> {
/// The file-level exemption ignores all rules (e.g., `# ruff: noqa`).
All,
/// The file-level exemption ignores specific rules (e.g., `# ruff: noqa: F401, F841`).
Codes(Vec<&'a str>),
Codes(Codes<'a>),
}
impl<'a> ParsedFileExemption<'a> {
/// Return a [`ParsedFileExemption`] for a given comment line.
fn try_extract(line: &'a str) -> Result<Option<Self>, ParseError> {
/// Return a [`ParsedFileExemption`] for a given `comment_range` in `source`.
fn try_extract(comment_range: TextRange, source: &'a str) -> Result<Option<Self>, ParseError> {
let line = &source[comment_range];
let offset = comment_range.start();
let init_line_len = line.text_len();
let line = Self::lex_whitespace(line);
let Some(line) = Self::lex_char(line, '#') else {
return Ok(None);
};
let comment_start = init_line_len - line.text_len() - '#'.text_len();
let line = Self::lex_whitespace(line);
let Some(line) = Self::lex_flake8(line).or_else(|| Self::lex_ruff(line)) else {
@@ -469,7 +475,11 @@ impl<'a> ParsedFileExemption<'a> {
let mut codes = vec![];
let mut line = line;
while let Some(code) = Self::lex_code(line) {
codes.push(code);
let codes_end = init_line_len - line.text_len();
codes.push(Code {
code,
range: TextRange::at(codes_end, code.text_len()).add(offset),
});
line = &line[code.len()..];
// Codes can be comma- or whitespace-delimited.
@@ -485,7 +495,12 @@ impl<'a> ParsedFileExemption<'a> {
return Err(ParseError::MissingCodes);
}
Self::Codes(codes)
let codes_end = init_line_len - line.text_len();
let range = TextRange::new(comment_start, codes_end);
Self::Codes(Codes {
range: range.add(offset),
codes,
})
}))
}
@@ -1059,7 +1074,7 @@ mod tests {
use ruff_diagnostics::{Diagnostic, Edit};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::LineEnding;
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption};
use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon};
@@ -1226,50 +1241,74 @@ mod tests {
#[test]
fn flake8_exemption_all() {
let source = "# flake8: noqa";
assert_debug_snapshot!(ParsedFileExemption::try_extract(source));
assert_debug_snapshot!(ParsedFileExemption::try_extract(
TextRange::up_to(source.text_len()),
source,
));
}
#[test]
fn ruff_exemption_all() {
let source = "# ruff: noqa";
assert_debug_snapshot!(ParsedFileExemption::try_extract(source));
assert_debug_snapshot!(ParsedFileExemption::try_extract(
TextRange::up_to(source.text_len()),
source,
));
}
#[test]
fn flake8_exemption_all_no_space() {
let source = "#flake8:noqa";
assert_debug_snapshot!(ParsedFileExemption::try_extract(source));
assert_debug_snapshot!(ParsedFileExemption::try_extract(
TextRange::up_to(source.text_len()),
source,
));
}
#[test]
fn ruff_exemption_all_no_space() {
let source = "#ruff:noqa";
assert_debug_snapshot!(ParsedFileExemption::try_extract(source));
assert_debug_snapshot!(ParsedFileExemption::try_extract(
TextRange::up_to(source.text_len()),
source,
));
}
#[test]
fn flake8_exemption_codes() {
// Note: Flake8 doesn't support this; it's treated as a blanket exemption.
let source = "# flake8: noqa: F401, F841";
assert_debug_snapshot!(ParsedFileExemption::try_extract(source));
assert_debug_snapshot!(ParsedFileExemption::try_extract(
TextRange::up_to(source.text_len()),
source,
));
}
#[test]
fn ruff_exemption_codes() {
let source = "# ruff: noqa: F401, F841";
assert_debug_snapshot!(ParsedFileExemption::try_extract(source));
assert_debug_snapshot!(ParsedFileExemption::try_extract(
TextRange::up_to(source.text_len()),
source,
));
}
#[test]
fn flake8_exemption_all_case_insensitive() {
let source = "# flake8: NoQa";
assert_debug_snapshot!(ParsedFileExemption::try_extract(source));
assert_debug_snapshot!(ParsedFileExemption::try_extract(
TextRange::up_to(source.text_len()),
source,
));
}
#[test]
fn ruff_exemption_all_case_insensitive() {
let source = "# ruff: NoQa";
assert_debug_snapshot!(ParsedFileExemption::try_extract(source));
assert_debug_snapshot!(ParsedFileExemption::try_extract(
TextRange::up_to(source.text_len()),
source,
));
}
#[test]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::Expr;
use ruff_python_ast::{self as ast};
use ruff_python_semantic::Modules;
@@ -39,8 +39,8 @@ use crate::checkers::ast::Checker;
///
/// dag = DAG(dag_id="my_dag", schedule=timedelta(days=1))
/// ```
#[violation]
pub struct AirflowDagNoScheduleArgument;
#[derive(ViolationMetadata)]
pub(crate) struct AirflowDagNoScheduleArgument;
impl Violation for AirflowDagNoScheduleArgument {
#[derive_message_formats]

View File

@@ -1,7 +1,8 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast as ast;
use ruff_python_ast::Expr;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -30,8 +31,8 @@ use crate::checkers::ast::Checker;
///
/// my_task = PythonOperator(task_id="my_task")
/// ```
#[violation]
pub struct AirflowVariableNameTaskIdMismatch {
#[derive(ViolationMetadata)]
pub(crate) struct AirflowVariableNameTaskIdMismatch {
task_id: String,
}
@@ -49,6 +50,10 @@ pub(crate) fn variable_name_task_id(
targets: &[Expr],
value: &Expr,
) -> Option<Diagnostic> {
if !checker.semantic().seen_module(Modules::AIRFLOW) {
return None;
}
// If we have more than one target, we can't do anything.
let [target] = targets else {
return None;
@@ -69,7 +74,7 @@ pub(crate) fn variable_name_task_id(
if !checker
.semantic()
.resolve_qualified_name(func)
.is_some_and(|qualified_name| matches!(qualified_name.segments()[0], "airflow"))
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["airflow", ..]))
{
return None;
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::{LineRanges, UniversalNewlineIterator};
use ruff_text_size::TextRange;
@@ -29,8 +29,8 @@ use super::super::detection::comment_contains_code;
/// - `lint.task-tags`
///
/// [#4845]: https://github.com/astral-sh/ruff/issues/4845
#[violation]
pub struct CommentedOutCode;
#[derive(ViolationMetadata)]
pub(crate) struct CommentedOutCode;
impl Violation for CommentedOutCode {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast as ast;
use ruff_python_ast::helpers::map_callable;
use ruff_python_semantic::Modules;
@@ -63,8 +63,8 @@ use crate::settings::types::PythonVersion;
/// [fastAPI documentation]: https://fastapi.tiangolo.com/tutorial/query-params-str-validations/?h=annotated#advantages-of-annotated
/// [typing.Annotated]: https://docs.python.org/3/library/typing.html#typing.Annotated
/// [typing_extensions]: https://typing-extensions.readthedocs.io/en/stable/
#[violation]
pub struct FastApiNonAnnotatedDependency {
#[derive(ViolationMetadata)]
pub(crate) struct FastApiNonAnnotatedDependency {
py_version: PythonVersion,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{Decorator, Expr, ExprCall, Keyword, StmtFunctionDef};
use ruff_python_semantic::{Modules, SemanticModel};
use ruff_text_size::Ranged;
@@ -59,8 +59,8 @@ use crate::rules::fastapi::rules::is_fastapi_route_decorator;
/// return item
/// ```
#[violation]
pub struct FastApiRedundantResponseModel;
#[derive(ViolationMetadata)]
pub(crate) struct FastApiRedundantResponseModel;
impl AlwaysFixableViolation for FastApiRedundantResponseModel {
#[derive_message_formats]

View File

@@ -4,7 +4,7 @@ use std::str::CharIndices;
use ruff_diagnostics::Fix;
use ruff_diagnostics::{Diagnostic, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast as ast;
use ruff_python_ast::{Expr, Parameter, ParameterWithDefault};
use ruff_python_semantic::{Modules, SemanticModel};
@@ -62,8 +62,8 @@ use crate::rules::fastapi::rules::is_fastapi_route_decorator;
/// ## Fix safety
/// This rule's fix is marked as unsafe, as modifying a function signature can
/// change the behavior of the code.
#[violation]
pub struct FastApiUnusedPathParameter {
#[derive(ViolationMetadata)]
pub(crate) struct FastApiUnusedPathParameter {
arg_name: String,
function_name: String,
is_positional: bool,

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_text_size::Ranged;
@@ -40,8 +40,8 @@ use super::super::helpers::is_sys;
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionCmpStr3;
#[derive(ViolationMetadata)]
pub(crate) struct SysVersionCmpStr3;
impl Violation for SysVersionCmpStr3 {
#[derive_message_formats]
@@ -87,8 +87,8 @@ impl Violation for SysVersionCmpStr3 {
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionInfo0Eq3;
#[derive(ViolationMetadata)]
pub(crate) struct SysVersionInfo0Eq3;
impl Violation for SysVersionInfo0Eq3 {
#[derive_message_formats]
@@ -127,8 +127,8 @@ impl Violation for SysVersionInfo0Eq3 {
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionInfo1CmpInt;
#[derive(ViolationMetadata)]
pub(crate) struct SysVersionInfo1CmpInt;
impl Violation for SysVersionInfo1CmpInt {
#[derive_message_formats]
@@ -169,8 +169,8 @@ impl Violation for SysVersionInfo1CmpInt {
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionInfoMinorCmpInt;
#[derive(ViolationMetadata)]
pub(crate) struct SysVersionInfoMinorCmpInt;
impl Violation for SysVersionInfoMinorCmpInt {
#[derive_message_formats]
@@ -212,8 +212,8 @@ impl Violation for SysVersionInfoMinorCmpInt {
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionCmpStr10;
#[derive(ViolationMetadata)]
pub(crate) struct SysVersionCmpStr10;
impl Violation for SysVersionCmpStr10 {
#[derive_message_formats]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::Expr;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
@@ -35,8 +35,8 @@ use crate::checkers::ast::Checker;
/// - [PyPI: `six`](https://pypi.org/project/six/)
/// - [Six documentation: `six.PY2`](https://six.readthedocs.io/#six.PY2)
/// - [Six documentation: `six.PY3`](https://six.readthedocs.io/#six.PY3)
#[violation]
pub struct SixPY3;
#[derive(ViolationMetadata)]
pub(crate) struct SixPY3;
impl Violation for SixPY3 {
#[derive_message_formats]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
@@ -37,8 +37,8 @@ use crate::rules::flake8_2020::helpers::is_sys;
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionSlice3;
#[derive(ViolationMetadata)]
pub(crate) struct SysVersionSlice3;
impl Violation for SysVersionSlice3 {
#[derive_message_formats]
@@ -77,8 +77,8 @@ impl Violation for SysVersionSlice3 {
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersion2;
#[derive(ViolationMetadata)]
pub(crate) struct SysVersion2;
impl Violation for SysVersion2 {
#[derive_message_formats]
@@ -117,8 +117,8 @@ impl Violation for SysVersion2 {
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersion0;
#[derive(ViolationMetadata)]
pub(crate) struct SysVersion0;
impl Violation for SysVersion0 {
#[derive_message_formats]
@@ -157,8 +157,8 @@ impl Violation for SysVersion0 {
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionSlice1;
#[derive(ViolationMetadata)]
pub(crate) struct SysVersionSlice1;
impl Violation for SysVersionSlice1 {
#[derive_message_formats]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor::Visitor;
@@ -33,8 +33,8 @@ use crate::rules::ruff::typing::type_hint_resolves_to_any;
/// ```python
/// def foo(x: int): ...
/// ```
#[violation]
pub struct MissingTypeFunctionArgument {
#[derive(ViolationMetadata)]
pub(crate) struct MissingTypeFunctionArgument {
name: String,
}
@@ -65,8 +65,8 @@ impl Violation for MissingTypeFunctionArgument {
/// ```python
/// def foo(*args: int): ...
/// ```
#[violation]
pub struct MissingTypeArgs {
#[derive(ViolationMetadata)]
pub(crate) struct MissingTypeArgs {
name: String,
}
@@ -97,8 +97,8 @@ impl Violation for MissingTypeArgs {
/// ```python
/// def foo(**kwargs: int): ...
/// ```
#[violation]
pub struct MissingTypeKwargs {
#[derive(ViolationMetadata)]
pub(crate) struct MissingTypeKwargs {
name: String,
}
@@ -137,9 +137,9 @@ impl Violation for MissingTypeKwargs {
/// class Foo:
/// def bar(self: "Foo"): ...
/// ```
#[violation]
#[derive(ViolationMetadata)]
#[deprecated(note = "ANN101 has been removed")]
pub struct MissingTypeSelf;
pub(crate) struct MissingTypeSelf;
#[allow(deprecated)]
impl Violation for MissingTypeSelf {
@@ -181,9 +181,9 @@ impl Violation for MissingTypeSelf {
/// @classmethod
/// def bar(cls: Type["Foo"]): ...
/// ```
#[violation]
#[derive(ViolationMetadata)]
#[deprecated(note = "ANN102 has been removed")]
pub struct MissingTypeCls;
pub(crate) struct MissingTypeCls;
#[allow(deprecated)]
impl Violation for MissingTypeCls {
@@ -215,8 +215,8 @@ impl Violation for MissingTypeCls {
/// def add(a: int, b: int) -> int:
/// return a + b
/// ```
#[violation]
pub struct MissingReturnTypeUndocumentedPublicFunction {
#[derive(ViolationMetadata)]
pub(crate) struct MissingReturnTypeUndocumentedPublicFunction {
name: String,
annotation: Option<String>,
}
@@ -258,8 +258,8 @@ impl Violation for MissingReturnTypeUndocumentedPublicFunction {
/// def _add(a: int, b: int) -> int:
/// return a + b
/// ```
#[violation]
pub struct MissingReturnTypePrivateFunction {
#[derive(ViolationMetadata)]
pub(crate) struct MissingReturnTypePrivateFunction {
name: String,
annotation: Option<String>,
}
@@ -314,8 +314,8 @@ impl Violation for MissingReturnTypePrivateFunction {
/// def __init__(self, x: int) -> None:
/// self.x = x
/// ```
#[violation]
pub struct MissingReturnTypeSpecialMethod {
#[derive(ViolationMetadata)]
pub(crate) struct MissingReturnTypeSpecialMethod {
name: String,
annotation: Option<String>,
}
@@ -361,8 +361,8 @@ impl Violation for MissingReturnTypeSpecialMethod {
/// def bar() -> int:
/// return 1
/// ```
#[violation]
pub struct MissingReturnTypeStaticMethod {
#[derive(ViolationMetadata)]
pub(crate) struct MissingReturnTypeStaticMethod {
name: String,
annotation: Option<String>,
}
@@ -408,8 +408,8 @@ impl Violation for MissingReturnTypeStaticMethod {
/// def bar(cls) -> int:
/// return 1
/// ```
#[violation]
pub struct MissingReturnTypeClassMethod {
#[derive(ViolationMetadata)]
pub(crate) struct MissingReturnTypeClassMethod {
name: String,
annotation: Option<String>,
}
@@ -474,8 +474,8 @@ impl Violation for MissingReturnTypeClassMethod {
/// - [Typing spec: `Any`](https://typing.readthedocs.io/en/latest/spec/special-types.html#any)
/// - [Python documentation: `typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any)
/// - [Mypy documentation: The Any type](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-any-type)
#[violation]
pub struct AnyType {
#[derive(ViolationMetadata)]
pub(crate) struct AnyType {
name: String,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_text_size::Ranged;
@@ -37,8 +37,8 @@ use crate::rules::flake8_async::helpers::AsyncModule;
/// - [`asyncio` events](https://docs.python.org/3/library/asyncio-sync.html#asyncio.Event)
/// - [`anyio` events](https://trio.readthedocs.io/en/latest/reference-core.html#trio.Event)
/// - [`trio` events](https://anyio.readthedocs.io/en/latest/api.html#anyio.Event)
#[violation]
pub struct AsyncBusyWait {
#[derive(ViolationMetadata)]
pub(crate) struct AsyncBusyWait {
module: AsyncModule,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast as ast;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
@@ -8,6 +8,7 @@ use crate::checkers::ast::Checker;
use crate::rules::flake8_async::helpers::AsyncModule;
use crate::settings::types::PythonVersion;
#[allow(clippy::doc_link_with_quotes)]
/// ## What it does
/// Checks for `async` function definitions with `timeout` parameters.
///
@@ -63,8 +64,8 @@ use crate::settings::types::PythonVersion;
/// - [`trio` timeouts](https://trio.readthedocs.io/en/stable/reference-core.html#cancellation-and-timeouts)
///
/// ["structured concurrency"]: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#timeouts-and-cancellation
#[violation]
pub struct AsyncFunctionWithTimeout {
#[derive(ViolationMetadata)]
pub(crate) struct AsyncFunctionWithTimeout {
module: AsyncModule,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr, ExprCall, Int, Number};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
@@ -32,8 +32,8 @@ use crate::rules::flake8_async::helpers::AsyncModule;
/// async def func():
/// await trio.lowlevel.checkpoint()
/// ```
#[violation]
pub struct AsyncZeroSleep {
#[derive(ViolationMetadata)]
pub(crate) struct AsyncZeroSleep {
module: AsyncModule,
}

View File

@@ -1,7 +1,7 @@
use ruff_python_ast::ExprCall;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::name::QualifiedName;
use ruff_text_size::Ranged;
@@ -31,8 +31,8 @@ use crate::checkers::ast::Checker;
/// async with session.get("https://example.com/foo/bar") as resp:
/// ...
/// ```
#[violation]
pub struct BlockingHttpCallInAsyncFunction;
#[derive(ViolationMetadata)]
pub(crate) struct BlockingHttpCallInAsyncFunction;
impl Violation for BlockingHttpCallInAsyncFunction {
#[derive_message_formats]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::{analyze, SemanticModel};
use ruff_text_size::Ranged;
@@ -33,8 +33,8 @@ use crate::checkers::ast::Checker;
/// async with await anyio.open_file("bar.txt") as f:
/// contents = await f.read()
/// ```
#[violation]
pub struct BlockingOpenCallInAsyncFunction;
#[derive(ViolationMetadata)]
pub(crate) struct BlockingOpenCallInAsyncFunction;
impl Violation for BlockingOpenCallInAsyncFunction {
#[derive_message_formats]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::find_assigned_value;
use ruff_python_semantic::SemanticModel;
@@ -30,8 +30,8 @@ use crate::registry::AsRule;
/// async def foo():
/// asyncio.create_subprocess_shell(cmd)
/// ```
#[violation]
pub struct CreateSubprocessInAsyncFunction;
#[derive(ViolationMetadata)]
pub(crate) struct CreateSubprocessInAsyncFunction;
impl Violation for CreateSubprocessInAsyncFunction {
#[derive_message_formats]
@@ -62,8 +62,8 @@ impl Violation for CreateSubprocessInAsyncFunction {
/// async def foo():
/// asyncio.create_subprocess_shell(cmd)
/// ```
#[violation]
pub struct RunProcessInAsyncFunction;
#[derive(ViolationMetadata)]
pub(crate) struct RunProcessInAsyncFunction;
impl Violation for RunProcessInAsyncFunction {
#[derive_message_formats]
@@ -98,8 +98,8 @@ impl Violation for RunProcessInAsyncFunction {
/// async def foo():
/// await asyncio.loop.run_in_executor(None, wait_for_process)
/// ```
#[violation]
pub struct WaitForProcessInAsyncFunction;
#[derive(ViolationMetadata)]
pub(crate) struct WaitForProcessInAsyncFunction;
impl Violation for WaitForProcessInAsyncFunction {
#[derive_message_formats]

View File

@@ -1,7 +1,7 @@
use ruff_python_ast::ExprCall;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::name::QualifiedName;
use ruff_text_size::Ranged;
@@ -28,8 +28,8 @@ use crate::checkers::ast::Checker;
/// async def fetch():
/// await asyncio.sleep(1)
/// ```
#[violation]
pub struct BlockingSleepInAsyncFunction;
#[derive(ViolationMetadata)]
pub(crate) struct BlockingSleepInAsyncFunction;
impl Violation for BlockingSleepInAsyncFunction {
#[derive_message_formats]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::{any_over_body, AwaitVisitor};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{Expr, StmtWith, WithItem};
@@ -38,8 +38,8 @@ use crate::rules::flake8_async::helpers::MethodName;
/// - [`asyncio` timeouts](https://docs.python.org/3/library/asyncio-task.html#timeouts)
/// - [`anyio` timeouts](https://anyio.readthedocs.io/en/stable/cancellation.html)
/// - [`trio` timeouts](https://trio.readthedocs.io/en/stable/reference-core.html#cancellation-and-timeouts)
#[violation]
pub struct CancelScopeNoCheckpoint {
#[derive(ViolationMetadata)]
pub(crate) struct CancelScopeNoCheckpoint {
method_name: MethodName,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{Expr, ExprCall, ExprNumberLiteral, Number};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
@@ -34,8 +34,8 @@ use crate::rules::flake8_async::helpers::AsyncModule;
/// async def func():
/// await trio.sleep_forever()
/// ```
#[violation]
pub struct LongSleepNotForever {
#[derive(ViolationMetadata)]
pub(crate) struct LongSleepNotForever {
module: AsyncModule,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{Expr, ExprCall};
use ruff_python_semantic::Modules;
use ruff_text_size::{Ranged, TextRange};
@@ -31,8 +31,8 @@ use crate::rules::flake8_async::helpers::MethodName;
/// ## Fix safety
/// This rule's fix is marked as unsafe, as adding an `await` to a function
/// call changes its semantics and runtime behavior.
#[violation]
pub struct TrioSyncCall {
#[derive(ViolationMetadata)]
pub(crate) struct TrioSyncCall {
method_name: MethodName,
}

View File

@@ -2,7 +2,7 @@ use ruff_python_ast::Stmt;
use ruff_text_size::{TextLen, TextRange};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_text_size::Ranged;
/// ## What it does
@@ -30,8 +30,8 @@ use ruff_text_size::Ranged;
/// if x <= 0:
/// raise ValueError("Expected positive value.")
/// ```
#[violation]
pub struct Assert;
#[derive(ViolationMetadata)]
pub(crate) struct Assert;
impl Violation for Assert {
#[derive_message_formats]

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_semantic::{Modules, SemanticModel};
@@ -34,8 +34,8 @@ use crate::checkers::ast::Checker;
/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod)
/// - [Python documentation: `stat`](https://docs.python.org/3/library/stat.html)
/// - [Common Weakness Enumeration: CWE-732](https://cwe.mitre.org/data/definitions/732.html)
#[violation]
pub struct BadFilePermissions {
#[derive(ViolationMetadata)]
pub(crate) struct BadFilePermissions {
reason: Reason,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr, ExprAttribute};
use ruff_text_size::Ranged;
@@ -33,8 +33,8 @@ use crate::checkers::ast::Checker;
/// ## References
/// - [Django documentation: SQL injection protection](https://docs.djangoproject.com/en/dev/topics/security/#sql-injection-protection)
/// - [Common Weakness Enumeration: CWE-89](https://cwe.mitre.org/data/definitions/89.html)
#[violation]
pub struct DjangoExtra;
#[derive(ViolationMetadata)]
pub(crate) struct DjangoExtra;
impl Violation for DjangoExtra {
#[derive_message_formats]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
@@ -24,8 +24,8 @@ use crate::checkers::ast::Checker;
/// ## References
/// - [Django documentation: SQL injection protection](https://docs.djangoproject.com/en/dev/topics/security/#sql-injection-protection)
/// - [Common Weakness Enumeration: CWE-89](https://cwe.mitre.org/data/definitions/89.html)
#[violation]
pub struct DjangoRawSql;
#[derive(ViolationMetadata)]
pub(crate) struct DjangoRawSql;
impl Violation for DjangoRawSql {
#[derive_message_formats]

View File

@@ -1,7 +1,7 @@
use ruff_python_ast::Expr;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -21,8 +21,8 @@ use crate::checkers::ast::Checker;
/// ## References
/// - [Python documentation: `exec`](https://docs.python.org/3/library/functions.html#exec)
/// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html)
#[violation]
pub struct ExecBuiltin;
#[derive(ViolationMetadata)]
pub(crate) struct ExecBuiltin;
impl Violation for ExecBuiltin {
#[derive_message_formats]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::{Expr, ExprAttribute, ExprCall};
use ruff_python_semantic::analyze::typing;
@@ -36,8 +36,8 @@ use crate::checkers::ast::Checker;
///
/// ## References
/// - [Flask documentation: Debug Mode](https://flask.palletsprojects.com/en/latest/quickstart/#debug-mode)
#[violation]
pub struct FlaskDebugTrue;
#[derive(ViolationMetadata)]
pub(crate) struct FlaskDebugTrue;
impl Violation for FlaskDebugTrue {
#[derive_message_formats]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, StringLike};
use ruff_text_size::Ranged;
@@ -26,8 +26,8 @@ use crate::checkers::ast::Checker;
///
/// ## References
/// - [Common Weakness Enumeration: CWE-200](https://cwe.mitre.org/data/definitions/200.html)
#[violation]
pub struct HardcodedBindAllInterfaces;
#[derive(ViolationMetadata)]
pub(crate) struct HardcodedBindAllInterfaces;
impl Violation for HardcodedBindAllInterfaces {
#[derive_message_formats]

View File

@@ -1,7 +1,7 @@
use ruff_python_ast::{Expr, Parameter, ParameterWithDefault, Parameters};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -38,8 +38,8 @@ use super::super::helpers::{matches_password_name, string_literal};
///
/// ## References
/// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html)
#[violation]
pub struct HardcodedPasswordDefault {
#[derive(ViolationMetadata)]
pub(crate) struct HardcodedPasswordDefault {
name: String,
}

View File

@@ -1,7 +1,7 @@
use ruff_python_ast::Keyword;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -34,8 +34,8 @@ use super::super::helpers::{matches_password_name, string_literal};
///
/// ## References
/// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html)
#[violation]
pub struct HardcodedPasswordFuncArg {
#[derive(ViolationMetadata)]
pub(crate) struct HardcodedPasswordFuncArg {
name: String,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
@@ -33,8 +33,8 @@ use super::super::helpers::{matches_password_name, string_literal};
///
/// ## References
/// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html)
#[violation]
pub struct HardcodedPasswordString {
#[derive(ViolationMetadata)]
pub(crate) struct HardcodedPasswordString {
name: String,
}

View File

@@ -3,7 +3,7 @@ use std::sync::LazyLock;
use regex::Regex;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::str::raw_contents;
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_text_size::Ranged;
@@ -35,8 +35,8 @@ static SQL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
/// ## References
/// - [B608: Test for SQL injection](https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html)
/// - [psycopg3: Server-side binding](https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html#server-side-binding)
#[violation]
pub struct HardcodedSQLExpression;
#[derive(ViolationMetadata)]
pub(crate) struct HardcodedSQLExpression;
impl Violation for HardcodedSQLExpression {
#[derive_message_formats]

View File

@@ -2,7 +2,7 @@ use ruff_python_ast::{self as ast, Expr, StringLike};
use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use crate::checkers::ast::Checker;
@@ -39,8 +39,8 @@ use crate::checkers::ast::Checker;
/// - [Common Weakness Enumeration: CWE-377](https://cwe.mitre.org/data/definitions/377.html)
/// - [Common Weakness Enumeration: CWE-379](https://cwe.mitre.org/data/definitions/379.html)
/// - [Python documentation: `tempfile`](https://docs.python.org/3/library/tempfile.html)
#[violation]
pub struct HardcodedTempFile {
#[derive(ViolationMetadata)]
pub(crate) struct HardcodedTempFile {
string: String,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::is_const_false;
use ruff_python_ast::{self as ast, Arguments};
use ruff_text_size::Ranged;
@@ -48,8 +48,8 @@ use super::super::helpers::string_literal;
/// - [Common Weakness Enumeration: CWE-327](https://cwe.mitre.org/data/definitions/327.html)
/// - [Common Weakness Enumeration: CWE-328](https://cwe.mitre.org/data/definitions/328.html)
/// - [Common Weakness Enumeration: CWE-916](https://cwe.mitre.org/data/definitions/916.html)
#[violation]
pub struct HashlibInsecureHashFunction {
#[derive(ViolationMetadata)]
pub(crate) struct HashlibInsecureHashFunction {
library: String,
string: String,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
@@ -34,8 +34,8 @@ use crate::checkers::ast::Checker;
/// ## References
/// - [Jinja documentation: API](https://jinja.palletsprojects.com/en/latest/api/#autoescaping)
/// - [Common Weakness Enumeration: CWE-94](https://cwe.mitre.org/data/definitions/94.html)
#[violation]
pub struct Jinja2AutoescapeFalse {
#[derive(ViolationMetadata)]
pub(crate) struct Jinja2AutoescapeFalse {
value: bool,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
@@ -24,8 +24,8 @@ use crate::checkers::ast::Checker;
///
/// ## References
/// - [Python documentation: `logging.config.listen()`](https://docs.python.org/3/library/logging.config.html#logging.config.listen)
#[violation]
pub struct LoggingConfigInsecureListen;
#[derive(ViolationMetadata)]
pub(crate) struct LoggingConfigInsecureListen;
impl Violation for LoggingConfigInsecureListen {
#[derive_message_formats]

View File

@@ -1,6 +1,6 @@
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
@@ -31,8 +31,8 @@ use ruff_text_size::Ranged;
/// - [Mako documentation](https://www.makotemplates.org/)
/// - [OpenStack security: Cross site scripting XSS](https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html)
/// - [Common Weakness Enumeration: CWE-80](https://cwe.mitre.org/data/definitions/80.html)
#[violation]
pub struct MakoTemplates;
#[derive(ViolationMetadata)]
pub(crate) struct MakoTemplates;
impl Violation for MakoTemplates {
#[derive_message_formats]

View File

@@ -1,7 +1,7 @@
use ruff_python_ast::Expr;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -25,8 +25,8 @@ use crate::checkers::ast::Checker;
/// ## References
/// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html)
/// - [Paramiko documentation: `SSHClient.exec_command()`](https://docs.paramiko.org/en/stable/api/client.html#paramiko.client.SSHClient.exec_command)
#[violation]
pub struct ParamikoCall;
#[derive(ViolationMetadata)]
pub(crate) struct ParamikoCall;
impl Violation for ParamikoCall {
#[derive_message_formats]

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast as ast;
use ruff_python_ast::helpers::is_const_false;
use ruff_text_size::Ranged;
@@ -30,8 +30,8 @@ use crate::checkers::ast::Checker;
///
/// ## References
/// - [Common Weakness Enumeration: CWE-295](https://cwe.mitre.org/data/definitions/295.html)
#[violation]
pub struct RequestWithNoCertValidation {
#[derive(ViolationMetadata)]
pub(crate) struct RequestWithNoCertValidation {
string: String,
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
@@ -32,8 +32,8 @@ use crate::checkers::ast::Checker;
/// ## References
/// - [Requests documentation: Timeouts](https://requests.readthedocs.io/en/latest/user/advanced/#timeouts)
/// - [httpx documentation: Timeouts](https://www.python-httpx.org/advanced/timeouts/)
#[violation]
pub struct RequestWithoutTimeout {
#[derive(ViolationMetadata)]
pub(crate) struct RequestWithoutTimeout {
implicit: bool,
module: String,
}

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