Compare commits
54 Commits
salsa-redu
...
0.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee258caed7 | ||
|
|
b4d9d26020 | ||
|
|
a99832088a | ||
|
|
770ef2ab27 | ||
|
|
c6023c03a2 | ||
|
|
df694ca1c1 | ||
|
|
2e75cfbfe7 | ||
|
|
cfafaa7637 | ||
|
|
3e9c7adeee | ||
|
|
81cd438d88 | ||
|
|
483748c188 | ||
|
|
eb3dc37faa | ||
|
|
aba1802828 | ||
|
|
96b42b0c8f | ||
|
|
e6d0c4a65d | ||
|
|
4e1b289a67 | ||
|
|
a5ef124201 | ||
|
|
390bb43276 | ||
|
|
fe8b15291f | ||
|
|
c8e01d7c53 | ||
|
|
c4d628cc4c | ||
|
|
ab3648c4c5 | ||
|
|
a822fd6642 | ||
|
|
f8f2e2a442 | ||
|
|
0b5828a1e8 | ||
|
|
5af48337a5 | ||
|
|
39ad6b9472 | ||
|
|
41dec93cd2 | ||
|
|
aee2caa733 | ||
|
|
fe5544e137 | ||
|
|
14c014a48b | ||
|
|
ecd0597d6b | ||
|
|
202271fba6 | ||
|
|
4bdb0b4f86 | ||
|
|
2286f916c1 | ||
|
|
1e4c944251 | ||
|
|
f50f8732e9 | ||
|
|
ecab04e338 | ||
|
|
8c09496b07 | ||
|
|
d19fd1b91c | ||
|
|
99df859e20 | ||
|
|
2d5fe9a6d3 | ||
|
|
1f2cb09853 | ||
|
|
cfe25ab465 | ||
|
|
551ed2706b | ||
|
|
21c5606793 | ||
|
|
c73a7bb929 | ||
|
|
4f6accb5c6 | ||
|
|
1ca14e4335 | ||
|
|
b9c8113a8a | ||
|
|
2edd32aa31 | ||
|
|
02c4373a49 | ||
|
|
d37e2e5d33 | ||
|
|
d1d067896c |
@@ -20,7 +20,7 @@
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"rust-lang.rust-analyzer",
|
||||
"serayuzgur.crates",
|
||||
"fill-labs.dependi",
|
||||
"tamasfe.even-better-toml",
|
||||
"Swellaby.vscode-rust-test-adapter",
|
||||
"charliermarsh.ruff"
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: tj-actions/changed-files@v44
|
||||
- uses: tj-actions/changed-files@v45
|
||||
id: changed
|
||||
with:
|
||||
files_yaml: |
|
||||
|
||||
@@ -45,7 +45,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.23.6
|
||||
rev: v1.24.1
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -59,7 +59,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.1
|
||||
rev: v0.6.2
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
56
CHANGELOG.md
56
CHANGELOG.md
@@ -1,5 +1,61 @@
|
||||
# Changelog
|
||||
|
||||
## 0.6.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-simplify`\] Extend `open-file-with-context-handler` to work with `dbm.sqlite3` (`SIM115`) ([#13104](https://github.com/astral-sh/ruff/pull/13104))
|
||||
- \[`pycodestyle`\] Disable `E741` in stub files (`.pyi`) ([#13119](https://github.com/astral-sh/ruff/pull/13119))
|
||||
- \[`pydoclint`\] Avoid `DOC201` on explicit returns in functions that only return `None` ([#13064](https://github.com/astral-sh/ruff/pull/13064))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-async`\] Disable check for `asyncio` before Python 3.11 (`ASYNC109`) ([#13023](https://github.com/astral-sh/ruff/pull/13023))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`FastAPI`\] Avoid introducing invalid syntax in fix for `fast-api-non-annotated-dependency` (`FAST002`) ([#13133](https://github.com/astral-sh/ruff/pull/13133))
|
||||
- \[`flake8-implicit-str-concat`\] Normalize octals before merging concatenated strings in `single-line-implicit-string-concatenation` (`ISC001`) ([#13118](https://github.com/astral-sh/ruff/pull/13118))
|
||||
- \[`flake8-pytest-style`\] Improve help message for `pytest-incorrect-mark-parentheses-style` (`PT023`) ([#13092](https://github.com/astral-sh/ruff/pull/13092))
|
||||
- \[`pylint`\] Avoid autofix for calls that aren't `min` or `max` as starred expression (`PLW3301`) ([#13089](https://github.com/astral-sh/ruff/pull/13089))
|
||||
- \[`ruff`\] Add `datetime.time`, `datetime.tzinfo`, and `datetime.timezone` as immutable function calls (`RUF009`) ([#13109](https://github.com/astral-sh/ruff/pull/13109))
|
||||
- \[`ruff`\] Extend comment deletion for `RUF100` to include trailing text from `noqa` directives while preserving any following comments on the same line, if any ([#13105](https://github.com/astral-sh/ruff/pull/13105))
|
||||
- Fix dark theme on initial page load for the Ruff playground ([#13077](https://github.com/astral-sh/ruff/pull/13077))
|
||||
|
||||
## 0.6.2
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-simplify`\] Extend `open-file-with-context-handler` to work with other standard-library IO modules (`SIM115`) ([#12959](https://github.com/astral-sh/ruff/pull/12959))
|
||||
- \[`ruff`\] Avoid `unused-async` for functions with FastAPI route decorator (`RUF029`) ([#12938](https://github.com/astral-sh/ruff/pull/12938))
|
||||
- \[`ruff`\] Ignore `fstring-missing-syntax` (`RUF027`) for `fastAPI` paths ([#12939](https://github.com/astral-sh/ruff/pull/12939))
|
||||
- \[`ruff`\] Implement check for Decimal called with a float literal (RUF032) ([#12909](https://github.com/astral-sh/ruff/pull/12909))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-bugbear`\] Update diagnostic message when expression is at the end of function (`B015`) ([#12944](https://github.com/astral-sh/ruff/pull/12944))
|
||||
- \[`flake8-pyi`\] Skip type annotations in `string-or-bytes-too-long` (`PYI053`) ([#13002](https://github.com/astral-sh/ruff/pull/13002))
|
||||
- \[`flake8-type-checking`\] Always recognise relative imports as first-party ([#12994](https://github.com/astral-sh/ruff/pull/12994))
|
||||
- \[`flake8-unused-arguments`\] Ignore unused arguments on stub functions (`ARG001`) ([#12966](https://github.com/astral-sh/ruff/pull/12966))
|
||||
- \[`pylint`\] Ignore augmented assignment for `self-cls-assignment` (`PLW0642`) ([#12957](https://github.com/astral-sh/ruff/pull/12957))
|
||||
|
||||
### Server
|
||||
|
||||
- Show full context in error log messages ([#13029](https://github.com/astral-sh/ruff/pull/13029))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`pep8-naming`\] Don't flag `from` imports following conventional import names (`N817`) ([#12946](https://github.com/astral-sh/ruff/pull/12946))
|
||||
- \[`pylint`\] - Allow `__new__` methods to have `cls` as their first argument even if decorated with `@staticmethod` for `bad-staticmethod-argument` (`PLW0211`) ([#12958](https://github.com/astral-sh/ruff/pull/12958))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add `hyperfine` installation instructions; update `hyperfine` code samples ([#13034](https://github.com/astral-sh/ruff/pull/13034))
|
||||
- Expand note to use Ruff with other language server in Kate ([#12806](https://github.com/astral-sh/ruff/pull/12806))
|
||||
- Update example for `PT001` as per the new default behavior ([#13019](https://github.com/astral-sh/ruff/pull/13019))
|
||||
- \[`perflint`\] Improve docs for `try-except-in-loop` (`PERF203`) ([#12947](https://github.com/astral-sh/ruff/pull/12947))
|
||||
- \[`pydocstyle`\] Add reference to `lint.pydocstyle.ignore-decorators` setting to rule docs ([#12996](https://github.com/astral-sh/ruff/pull/12996))
|
||||
|
||||
## 0.6.1
|
||||
|
||||
This is a hotfix release to address an issue with `ruff-pre-commit`. In v0.6,
|
||||
|
||||
@@ -530,6 +530,8 @@ You can run the benchmarks with
|
||||
cargo benchmark
|
||||
```
|
||||
|
||||
`cargo benchmark` is an alias for `cargo bench -p ruff_benchmark --bench linter --bench formatter --`
|
||||
|
||||
#### Benchmark-driven Development
|
||||
|
||||
Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use
|
||||
@@ -568,7 +570,7 @@ cargo install critcmp
|
||||
|
||||
#### Tips
|
||||
|
||||
- Use `cargo bench -p ruff_benchmark <filter>` to only run specific benchmarks. For example: `cargo benchmark lexer`
|
||||
- Use `cargo bench -p ruff_benchmark <filter>` to only run specific benchmarks. For example: `cargo bench -p ruff_benchmark lexer`
|
||||
to only run the lexer benchmarks.
|
||||
- Use `cargo bench -p ruff_benchmark -- --quiet` for a more cleaned up output (without statistical relevance)
|
||||
- Use `cargo bench -p ruff_benchmark -- --quick` to get faster results (more prone to noise)
|
||||
|
||||
33
Cargo.lock
generated
33
Cargo.lock
generated
@@ -1256,9 +1256,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.157"
|
||||
version = "0.2.158"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "374af5f94e54fa97cf75e945cce8a6b201e88a1a07e688b47dfd2a59c66dbd86"
|
||||
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -1827,9 +1827,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -1926,6 +1926,7 @@ dependencies = [
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_literal",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_source_file",
|
||||
@@ -1988,6 +1989,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"crossbeam",
|
||||
"notify",
|
||||
"rayon",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
@@ -2088,7 +2090,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2146,6 +2148,7 @@ dependencies = [
|
||||
"criterion",
|
||||
"mimalloc",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_workspace",
|
||||
"ruff_db",
|
||||
@@ -2280,7 +2283,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2600,7 +2603,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -2827,9 +2830,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.208"
|
||||
version = "1.0.209"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
|
||||
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -2847,9 +2850,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.208"
|
||||
version = "1.0.209"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
|
||||
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2869,9 +2872,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.125"
|
||||
version = "1.0.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
|
||||
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3030,9 +3033,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.75"
|
||||
version = "2.0.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
|
||||
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
22
README.md
22
README.md
@@ -110,7 +110,7 @@ For more, see the [documentation](https://docs.astral.sh/ruff/).
|
||||
1. [Who's Using Ruff?](#whos-using-ruff)
|
||||
1. [License](#license)
|
||||
|
||||
## Getting Started
|
||||
## Getting Started<a id="getting-started"></a>
|
||||
|
||||
For more, see the [documentation](https://docs.astral.sh/ruff/).
|
||||
|
||||
@@ -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.6.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.6.1/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.6.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.6.3/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.6.1
|
||||
rev: v0.6.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
- uses: chartboost/ruff-action@v1
|
||||
```
|
||||
|
||||
### Configuration
|
||||
### Configuration<a id="configuration"></a>
|
||||
|
||||
Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml` file (see:
|
||||
[_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/)
|
||||
@@ -291,7 +291,7 @@ features that may change prior to stabilization.
|
||||
See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format`
|
||||
for more on the linting and formatting commands, respectively.
|
||||
|
||||
## Rules
|
||||
## Rules<a id="rules"></a>
|
||||
|
||||
<!-- Begin section: Rules -->
|
||||
|
||||
@@ -367,21 +367,21 @@ quality tools, including:
|
||||
|
||||
For a complete enumeration of the supported rules, see [_Rules_](https://docs.astral.sh/ruff/rules/).
|
||||
|
||||
## Contributing
|
||||
## Contributing<a id="contributing"></a>
|
||||
|
||||
Contributions are welcome and highly appreciated. To get started, check out the
|
||||
[**contributing guidelines**](https://docs.astral.sh/ruff/contributing/).
|
||||
|
||||
You can also join us on [**Discord**](https://discord.com/invite/astral-sh).
|
||||
|
||||
## Support
|
||||
## Support<a id="support"></a>
|
||||
|
||||
Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues),
|
||||
or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new).
|
||||
|
||||
You can also ask for help on [**Discord**](https://discord.com/invite/astral-sh).
|
||||
|
||||
## Acknowledgements
|
||||
## Acknowledgements<a id="acknowledgements"></a>
|
||||
|
||||
Ruff's linter draws on both the APIs and implementation details of many other
|
||||
tools in the Python ecosystem, especially [Flake8](https://github.com/PyCQA/flake8), [Pyflakes](https://github.com/PyCQA/pyflakes),
|
||||
@@ -405,7 +405,7 @@ Ruff is the beneficiary of a large number of [contributors](https://github.com/a
|
||||
|
||||
Ruff is released under the MIT license.
|
||||
|
||||
## Who's Using Ruff?
|
||||
## Who's Using Ruff?<a id="whos-using-ruff"></a>
|
||||
|
||||
Ruff is used by a number of major open-source projects and companies, including:
|
||||
|
||||
@@ -524,7 +524,7 @@ If you're using Ruff, consider adding the Ruff badge to your project's `README.m
|
||||
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff" style="max-width:100%;"></a>
|
||||
```
|
||||
|
||||
## License
|
||||
## License<a id="license"></a>
|
||||
|
||||
This repository is licensed under the [MIT License](https://github.com/astral-sh/ruff/blob/main/LICENSE)
|
||||
|
||||
|
||||
@@ -13,12 +13,17 @@ The CLI supports different verbosity levels.
|
||||
- `-vv` activates `debug!` and timestamps: This should be enough information to get to the bottom of bug reports. When you're processing many packages or files, you'll get pages and pages of output, but each line is link to a specific action or state change.
|
||||
- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you almost certainly want to use `RED_KNOT_LOG` to filter it down to the area your investigating.
|
||||
|
||||
## `RED_KNOT_LOG`
|
||||
## Better logging with `RED_KNOT_LOG` and `RAYON_NUM_THREADS`
|
||||
|
||||
By default, the CLI shows messages from the `ruff` and `red_knot` crates. Tracing messages from other crates are not shown.
|
||||
The `RED_KNOT_LOG` environment variable allows you to customize which messages are shown by specifying one
|
||||
or more [filter directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives).
|
||||
|
||||
The `RAYON_NUM_THREADS` environment variable, meanwhile, can be used to control the level of concurrency red-knot uses.
|
||||
By default, red-knot will attempt to parallelize its work so that multiple files are checked simultaneously,
|
||||
but this can result in a confused logging output where messages from different threads are intertwined.
|
||||
To switch off concurrency entirely and have more readable logs, use `RAYON_NUM_THREADS=1`.
|
||||
|
||||
### Examples
|
||||
|
||||
#### Show all debug messages
|
||||
|
||||
@@ -17,6 +17,7 @@ ruff_python_ast = { workspace = true }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ruff_python_literal = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
|
||||
@@ -575,7 +575,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert_eq!(names(&global_table), vec!["f", "str", "int"]);
|
||||
assert_eq!(names(&global_table), vec!["str", "int", "f"]);
|
||||
|
||||
let [(function_scope_id, _function_scope)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
@@ -1095,4 +1095,56 @@ match subject:
|
||||
vec!["subject", "a", "b", "c", "d", "f", "e", "h", "g", "Foo", "i", "j", "k", "l"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_loops_single_assignment() {
|
||||
let TestCase { db, file } = test_case("for x in a: pass");
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(&names(&global_table), &["a", "x"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let definition = use_def
|
||||
.first_public_definition(global_table.symbol_id_by_name("x").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(definition.node(&db), DefinitionKind::For(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_loops_simple_unpacking() {
|
||||
let TestCase { db, file } = test_case("for (x, y) in a: pass");
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(&names(&global_table), &["a", "x", "y"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let x_definition = use_def
|
||||
.first_public_definition(global_table.symbol_id_by_name("x").unwrap())
|
||||
.unwrap();
|
||||
let y_definition = use_def
|
||||
.first_public_definition(global_table.symbol_id_by_name("y").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(x_definition.node(&db), DefinitionKind::For(_)));
|
||||
assert!(matches!(y_definition.node(&db), DefinitionKind::For(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_loops_complex_unpacking() {
|
||||
let TestCase { db, file } = test_case("for [((a,) b), (c, d)] in e: pass");
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(&names(&global_table), &["e", "a", "b", "c", "d"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let definition = use_def
|
||||
.first_public_definition(global_table.symbol_id_by_name("a").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(definition.node(&db), DefinitionKind::For(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||
use crate::semantic_index::definition::{
|
||||
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionNodeKey,
|
||||
DefinitionNodeRef, ImportFromDefinitionNodeRef,
|
||||
DefinitionNodeRef, ForStmtDefinitionNodeRef, ImportFromDefinitionNodeRef,
|
||||
};
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{
|
||||
@@ -392,20 +392,6 @@ where
|
||||
self.visit_decorator(decorator);
|
||||
}
|
||||
|
||||
let symbol = self
|
||||
.add_or_update_symbol(function_def.name.id.clone(), SymbolFlags::IS_DEFINED);
|
||||
self.add_definition(symbol, function_def);
|
||||
|
||||
// The default value of the parameters needs to be evaluated in the
|
||||
// enclosing scope.
|
||||
for default in function_def
|
||||
.parameters
|
||||
.iter_non_variadic_params()
|
||||
.filter_map(|param| param.default.as_deref())
|
||||
{
|
||||
self.visit_expr(default);
|
||||
}
|
||||
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::FunctionTypeParameters(function_def),
|
||||
function_def.type_params.as_deref(),
|
||||
@@ -426,6 +412,21 @@ where
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
// The default value of the parameters needs to be evaluated in the
|
||||
// enclosing scope.
|
||||
for default in function_def
|
||||
.parameters
|
||||
.iter_non_variadic_params()
|
||||
.filter_map(|param| param.default.as_deref())
|
||||
{
|
||||
self.visit_expr(default);
|
||||
}
|
||||
// The symbol for the function name itself has to be evaluated
|
||||
// at the end to match the runtime evaluation of parameter defaults
|
||||
// and return-type annotations.
|
||||
let symbol = self
|
||||
.add_or_update_symbol(function_def.name.id.clone(), SymbolFlags::IS_DEFINED);
|
||||
self.add_definition(symbol, function_def);
|
||||
}
|
||||
ast::Stmt::ClassDef(class) => {
|
||||
for decorator in &class.decorator_list {
|
||||
@@ -578,6 +579,27 @@ where
|
||||
ast::Stmt::Break(_) => {
|
||||
self.loop_break_states.push(self.flow_snapshot());
|
||||
}
|
||||
|
||||
ast::Stmt::For(
|
||||
for_stmt @ ast::StmtFor {
|
||||
range: _,
|
||||
is_async: _,
|
||||
target,
|
||||
iter,
|
||||
body,
|
||||
orelse,
|
||||
},
|
||||
) => {
|
||||
// TODO add control flow similar to `ast::Stmt::While` above
|
||||
self.add_standalone_expression(iter);
|
||||
self.visit_expr(iter);
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
self.current_assignment = Some(for_stmt.into());
|
||||
self.visit_expr(target);
|
||||
self.current_assignment = None;
|
||||
self.visit_body(body);
|
||||
self.visit_body(orelse);
|
||||
}
|
||||
_ => {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
@@ -624,6 +646,15 @@ where
|
||||
Some(CurrentAssignment::AugAssign(aug_assign)) => {
|
||||
self.add_definition(symbol, aug_assign);
|
||||
}
|
||||
Some(CurrentAssignment::For(node)) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
ForStmtDefinitionNodeRef {
|
||||
iterable: &node.iter,
|
||||
target: name_node,
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(CurrentAssignment::Named(named)) => {
|
||||
// TODO(dhruvmanila): If the current scope is a comprehension, then the
|
||||
// named expression is implicitly nonlocal. This is yet to be
|
||||
@@ -658,11 +689,11 @@ where
|
||||
}
|
||||
ast::Expr::Named(node) => {
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
self.current_assignment = Some(node.into());
|
||||
// TODO walrus in comprehensions is implicitly nonlocal
|
||||
self.visit_expr(&node.value);
|
||||
self.current_assignment = Some(node.into());
|
||||
self.visit_expr(&node.target);
|
||||
self.current_assignment = None;
|
||||
self.visit_expr(&node.value);
|
||||
}
|
||||
ast::Expr::Lambda(lambda) => {
|
||||
if let Some(parameters) = &lambda.parameters {
|
||||
@@ -796,6 +827,7 @@ enum CurrentAssignment<'a> {
|
||||
Assign(&'a ast::StmtAssign),
|
||||
AnnAssign(&'a ast::StmtAnnAssign),
|
||||
AugAssign(&'a ast::StmtAugAssign),
|
||||
For(&'a ast::StmtFor),
|
||||
Named(&'a ast::ExprNamed),
|
||||
Comprehension {
|
||||
node: &'a ast::Comprehension,
|
||||
@@ -822,6 +854,12 @@ impl<'a> From<&'a ast::StmtAugAssign> for CurrentAssignment<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtFor> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::StmtFor) -> Self {
|
||||
Self::For(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprNamed> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::ExprNamed) -> Self {
|
||||
Self::Named(value)
|
||||
|
||||
@@ -39,6 +39,7 @@ impl<'db> Definition<'db> {
|
||||
pub(crate) enum DefinitionNodeRef<'a> {
|
||||
Import(&'a ast::Alias),
|
||||
ImportFrom(ImportFromDefinitionNodeRef<'a>),
|
||||
For(ForStmtDefinitionNodeRef<'a>),
|
||||
Function(&'a ast::StmtFunctionDef),
|
||||
Class(&'a ast::StmtClassDef),
|
||||
NamedExpression(&'a ast::ExprNamed),
|
||||
@@ -92,6 +93,12 @@ impl<'a> From<ImportFromDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ForStmtDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(value: ForStmtDefinitionNodeRef<'a>) -> Self {
|
||||
Self::For(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AssignmentDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node_ref: AssignmentDefinitionNodeRef<'a>) -> Self {
|
||||
Self::Assignment(node_ref)
|
||||
@@ -134,6 +141,12 @@ pub(crate) struct WithItemDefinitionNodeRef<'a> {
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
|
||||
pub(crate) iterable: &'a ast::Expr,
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ComprehensionDefinitionNodeRef<'a> {
|
||||
pub(crate) node: &'a ast::Comprehension,
|
||||
@@ -174,6 +187,12 @@ impl DefinitionNodeRef<'_> {
|
||||
DefinitionNodeRef::AugmentedAssignment(augmented_assignment) => {
|
||||
DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment))
|
||||
}
|
||||
DefinitionNodeRef::For(ForStmtDefinitionNodeRef { iterable, target }) => {
|
||||
DefinitionKind::For(ForStmtDefinitionKind {
|
||||
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
})
|
||||
}
|
||||
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef { node, first }) => {
|
||||
DefinitionKind::Comprehension(ComprehensionDefinitionKind {
|
||||
node: AstNodeRef::new(parsed, node),
|
||||
@@ -212,6 +231,10 @@ impl DefinitionNodeRef<'_> {
|
||||
}) => target.into(),
|
||||
Self::AnnotatedAssignment(node) => node.into(),
|
||||
Self::AugmentedAssignment(node) => node.into(),
|
||||
Self::For(ForStmtDefinitionNodeRef {
|
||||
iterable: _,
|
||||
target,
|
||||
}) => target.into(),
|
||||
Self::Comprehension(ComprehensionDefinitionNodeRef { node, first: _ }) => node.into(),
|
||||
Self::Parameter(node) => match node {
|
||||
ast::AnyParameterRef::Variadic(parameter) => parameter.into(),
|
||||
@@ -232,6 +255,7 @@ pub enum DefinitionKind {
|
||||
Assignment(AssignmentDefinitionKind),
|
||||
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
|
||||
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
|
||||
For(ForStmtDefinitionKind),
|
||||
Comprehension(ComprehensionDefinitionKind),
|
||||
Parameter(AstNodeRef<ast::Parameter>),
|
||||
ParameterWithDefault(AstNodeRef<ast::ParameterWithDefault>),
|
||||
@@ -302,6 +326,22 @@ impl WithItemDefinitionKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ForStmtDefinitionKind {
|
||||
iterable: AstNodeRef<ast::Expr>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
}
|
||||
|
||||
impl ForStmtDefinitionKind {
|
||||
pub(crate) fn iterable(&self) -> &ast::Expr {
|
||||
self.iterable.node()
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||
self.target.node()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub(crate) struct DefinitionNodeKey(NodeKey);
|
||||
|
||||
@@ -347,6 +387,12 @@ impl From<&ast::StmtAugAssign> for DefinitionNodeKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::StmtFor> for DefinitionNodeKey {
|
||||
fn from(value: &ast::StmtFor) -> Self {
|
||||
Self(NodeKey::from_node(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::Comprehension> for DefinitionNodeKey {
|
||||
fn from(node: &ast::Comprehension) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::builtins::builtins_scope;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::definition::{Definition, DefinitionKind};
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId};
|
||||
use crate::semantic_index::{
|
||||
global_scope, semantic_index, symbol_table, use_def_map, DefinitionWithConstraints,
|
||||
@@ -14,7 +15,8 @@ use crate::{Db, FxOrderSet};
|
||||
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
||||
pub(crate) use self::diagnostic::TypeCheckDiagnostics;
|
||||
pub(crate) use self::infer::{
|
||||
infer_definition_types, infer_expression_types, infer_scope_types, TypeInference,
|
||||
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
|
||||
TypeInference,
|
||||
};
|
||||
|
||||
mod builder;
|
||||
@@ -88,6 +90,24 @@ pub(crate) fn definition_ty<'db>(db: &'db dyn Db, definition: Definition<'db>) -
|
||||
inference.definition_ty(definition)
|
||||
}
|
||||
|
||||
/// Infer the type of a (possibly deferred) sub-expression of a [`Definition`].
|
||||
///
|
||||
/// ## Panics
|
||||
/// If the given expression is not a sub-expression of the given [`Definition`].
|
||||
pub(crate) fn definition_expression_ty<'db>(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
expression: &ast::Expr,
|
||||
) -> Type<'db> {
|
||||
let expr_id = expression.scoped_ast_id(db, definition.scope(db));
|
||||
let inference = infer_definition_types(db, definition);
|
||||
if let Some(ty) = inference.try_expression_ty(expr_id) {
|
||||
ty
|
||||
} else {
|
||||
infer_deferred_types(db, definition).expression_ty(expr_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer the combined type of an array of [`Definition`]s, plus one optional "unbound type".
|
||||
///
|
||||
/// Will return a union if there is more than one definition, or at least one plus an unbound
|
||||
@@ -181,6 +201,13 @@ pub enum Type<'db> {
|
||||
IntLiteral(i64),
|
||||
/// A boolean literal, either `True` or `False`.
|
||||
BooleanLiteral(bool),
|
||||
/// A string literal
|
||||
StringLiteral(StringLiteralType<'db>),
|
||||
/// A string known to originate only from literal values, but whose value is not known (unlike
|
||||
/// `StringLiteral` above).
|
||||
LiteralString,
|
||||
/// A bytes literal
|
||||
BytesLiteral(BytesLiteralType<'db>),
|
||||
// TODO protocols, callable types, overloads, generics, type vars
|
||||
}
|
||||
|
||||
@@ -236,7 +263,7 @@ impl<'db> Type<'db> {
|
||||
/// us to explicitly consider whether to handle an error or propagate
|
||||
/// it up the call stack.
|
||||
#[must_use]
|
||||
pub fn member(&self, db: &'db dyn Db, name: &Name) -> Type<'db> {
|
||||
pub fn member(&self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
|
||||
match self {
|
||||
Type::Any => Type::Any,
|
||||
Type::Never => {
|
||||
@@ -276,6 +303,20 @@ impl<'db> Type<'db> {
|
||||
Type::Unknown
|
||||
}
|
||||
Type::BooleanLiteral(_) => Type::Unknown,
|
||||
Type::StringLiteral(_) => {
|
||||
// TODO defer to `typing.LiteralString`/`builtins.str` methods
|
||||
// from typeshed's stubs
|
||||
Type::Unknown
|
||||
}
|
||||
Type::LiteralString => {
|
||||
// TODO defer to `typing.LiteralString`/`builtins.str` methods
|
||||
// from typeshed's stubs
|
||||
Type::Unknown
|
||||
}
|
||||
Type::BytesLiteral(_) => {
|
||||
// TODO defer to Type::Instance(<bytes from typeshed>).member
|
||||
Type::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +334,7 @@ impl<'db> Type<'db> {
|
||||
#[salsa::interned]
|
||||
pub struct FunctionType<'db> {
|
||||
/// name of the function at definition
|
||||
pub name: Name,
|
||||
pub name: ast::name::Name,
|
||||
|
||||
/// types of all decorators on this function
|
||||
decorators: Vec<Type<'db>>,
|
||||
@@ -308,19 +349,33 @@ impl<'db> FunctionType<'db> {
|
||||
#[salsa::interned]
|
||||
pub struct ClassType<'db> {
|
||||
/// Name of the class at definition
|
||||
pub name: Name,
|
||||
pub name: ast::name::Name,
|
||||
|
||||
/// Types of all class bases
|
||||
bases: Vec<Type<'db>>,
|
||||
definition: Definition<'db>,
|
||||
|
||||
body_scope: ScopeId<'db>,
|
||||
}
|
||||
|
||||
impl<'db> ClassType<'db> {
|
||||
/// Return an iterator over the types of this class's bases.
|
||||
///
|
||||
/// # Panics:
|
||||
/// If `definition` is not a `DefinitionKind::Class`.
|
||||
pub fn bases(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {
|
||||
let definition = self.definition(db);
|
||||
let DefinitionKind::Class(class_stmt_node) = definition.node(db) else {
|
||||
panic!("Class type definition must have DefinitionKind::Class");
|
||||
};
|
||||
class_stmt_node
|
||||
.bases()
|
||||
.iter()
|
||||
.map(move |base_expr| definition_expression_ty(db, definition, base_expr))
|
||||
}
|
||||
|
||||
/// Returns the class member of this class named `name`.
|
||||
///
|
||||
/// The member resolves to a member of the class itself or any of its bases.
|
||||
pub fn class_member(self, db: &'db dyn Db, name: &Name) -> Type<'db> {
|
||||
pub fn class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
|
||||
let member = self.own_class_member(db, name);
|
||||
if !member.is_unbound() {
|
||||
return member;
|
||||
@@ -330,12 +385,12 @@ impl<'db> ClassType<'db> {
|
||||
}
|
||||
|
||||
/// Returns the inferred type of the class member named `name`.
|
||||
pub fn own_class_member(self, db: &'db dyn Db, name: &Name) -> Type<'db> {
|
||||
pub fn own_class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
|
||||
let scope = self.body_scope(db);
|
||||
symbol_ty_by_name(db, scope, name)
|
||||
}
|
||||
|
||||
pub fn inherited_class_member(self, db: &'db dyn Db, name: &Name) -> Type<'db> {
|
||||
pub fn inherited_class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
|
||||
for base in self.bases(db) {
|
||||
let member = base.member(db, name);
|
||||
if !member.is_unbound() {
|
||||
@@ -372,6 +427,18 @@ pub struct IntersectionType<'db> {
|
||||
negative: FxOrderSet<Type<'db>>,
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
pub struct StringLiteralType<'db> {
|
||||
#[return_ref]
|
||||
value: Box<str>,
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
pub struct BytesLiteralType<'db> {
|
||||
#[return_ref]
|
||||
value: Box<[u8]>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Context;
|
||||
@@ -420,7 +487,7 @@ mod tests {
|
||||
let foo = system_path_to_file(&db, "src/foo.py").context("Failed to resolve foo.py")?;
|
||||
|
||||
let diagnostics = super::check_types(&db, foo);
|
||||
assert_diagnostic_messages(&diagnostics, &["Import 'bar' could not be resolved."]);
|
||||
assert_diagnostic_messages(&diagnostics, &["Cannot resolve import 'bar'."]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -433,7 +500,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let foo = system_path_to_file(&db, "src/foo.py").unwrap();
|
||||
let diagnostics = super::check_types(&db, foo);
|
||||
assert_diagnostic_messages(&diagnostics, &["Import 'bar' could not be resolved."]);
|
||||
assert_diagnostic_messages(&diagnostics, &["Cannot resolve import 'bar'."]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -445,15 +512,9 @@ mod tests {
|
||||
|
||||
let b_file = system_path_to_file(&db, "/src/b.py").unwrap();
|
||||
let b_file_diagnostics = super::check_types(&db, b_file);
|
||||
assert_diagnostic_messages(
|
||||
&b_file_diagnostics,
|
||||
&["Could not resolve import of 'thing' from 'a'"],
|
||||
);
|
||||
assert_diagnostic_messages(&b_file_diagnostics, &["Module 'a' has no member 'thing'"]);
|
||||
}
|
||||
|
||||
#[ignore = "\
|
||||
A spurious second 'Unresolved import' diagnostic message is emitted on `b.py`, \
|
||||
despite the symbol existing in the symbol table for `a.py`"]
|
||||
#[test]
|
||||
fn resolved_import_of_symbol_from_unresolved_import() {
|
||||
let mut db = setup_db();
|
||||
@@ -466,10 +527,7 @@ despite the symbol existing in the symbol table for `a.py`"]
|
||||
|
||||
let a_file = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let a_file_diagnostics = super::check_types(&db, a_file);
|
||||
assert_diagnostic_messages(
|
||||
&a_file_diagnostics,
|
||||
&["Import 'foo' could not be resolved."],
|
||||
);
|
||||
assert_diagnostic_messages(&a_file_diagnostics, &["Cannot resolve import 'foo'."]);
|
||||
|
||||
// Importing the unresolved import into a second first-party file should not trigger
|
||||
// an additional "unresolved import" violation
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::{IntersectionType, Type, UnionType};
|
||||
use crate::Db;
|
||||
|
||||
@@ -38,6 +41,20 @@ impl Display for DisplayType<'_> {
|
||||
Type::BooleanLiteral(boolean) => {
|
||||
write!(f, "Literal[{}]", if *boolean { "True" } else { "False" })
|
||||
}
|
||||
Type::StringLiteral(string) => write!(
|
||||
f,
|
||||
r#"Literal["{}"]"#,
|
||||
string.value(self.db).replace('"', r#"\""#)
|
||||
),
|
||||
Type::LiteralString => write!(f, "LiteralString"),
|
||||
Type::BytesLiteral(bytes) => {
|
||||
let escape =
|
||||
AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double);
|
||||
|
||||
f.write_str("Literal[")?;
|
||||
escape.bytes_repr().write(f)?;
|
||||
f.write_str("]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,11 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::panic::PanicInfo;
|
||||
|
||||
use lsp_server as lsp;
|
||||
use lsp_types as types;
|
||||
use lsp_server::Message;
|
||||
use lsp_types::{
|
||||
ClientCapabilities, DiagnosticOptions, NotebookCellSelector, NotebookDocumentSyncOptions,
|
||||
NotebookSelector, TextDocumentSyncCapability, TextDocumentSyncOptions,
|
||||
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, MessageType,
|
||||
ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
|
||||
Url,
|
||||
};
|
||||
|
||||
use self::connection::{Connection, ConnectionInitializer};
|
||||
@@ -74,7 +74,7 @@ impl Server {
|
||||
init_params.client_info.as_ref(),
|
||||
);
|
||||
|
||||
let mut workspace_for_url = |url: lsp_types::Url| {
|
||||
let mut workspace_for_url = |url: Url| {
|
||||
let Some(workspace_settings) = workspace_settings.as_mut() else {
|
||||
return (url, ClientSettings::default());
|
||||
};
|
||||
@@ -93,13 +93,18 @@ impl Server {
|
||||
}).collect())
|
||||
.or_else(|| {
|
||||
tracing::warn!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace...");
|
||||
let uri = types::Url::from_file_path(std::env::current_dir().ok()?).ok()?;
|
||||
let uri = Url::from_file_path(std::env::current_dir().ok()?).ok()?;
|
||||
Some(vec![workspace_for_url(uri)])
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Failed to get the current working directory while creating a default workspace.")
|
||||
})?;
|
||||
|
||||
if workspaces.len() > 1 {
|
||||
// TODO(dhruvmanila): Support multi-root workspaces
|
||||
anyhow::bail!("Multi-root workspaces are not supported yet");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
connection,
|
||||
worker_threads,
|
||||
@@ -149,7 +154,7 @@ impl Server {
|
||||
try_show_message(
|
||||
"The Ruff language server exited with a panic. See the logs for more details."
|
||||
.to_string(),
|
||||
lsp_types::MessageType::ERROR,
|
||||
MessageType::ERROR,
|
||||
)
|
||||
.ok();
|
||||
}));
|
||||
@@ -182,9 +187,9 @@ impl Server {
|
||||
break;
|
||||
}
|
||||
let task = match msg {
|
||||
lsp::Message::Request(req) => api::request(req),
|
||||
lsp::Message::Notification(notification) => api::notification(notification),
|
||||
lsp::Message::Response(response) => scheduler.response(response),
|
||||
Message::Request(req) => api::request(req),
|
||||
Message::Notification(notification) => api::notification(notification),
|
||||
Message::Response(response) => scheduler.response(response),
|
||||
};
|
||||
scheduler.dispatch(task);
|
||||
}
|
||||
@@ -206,28 +211,17 @@ impl Server {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn server_capabilities(position_encoding: PositionEncoding) -> types::ServerCapabilities {
|
||||
types::ServerCapabilities {
|
||||
fn server_capabilities(position_encoding: PositionEncoding) -> ServerCapabilities {
|
||||
ServerCapabilities {
|
||||
position_encoding: Some(position_encoding.into()),
|
||||
diagnostic_provider: Some(types::DiagnosticServerCapabilities::Options(
|
||||
DiagnosticOptions {
|
||||
identifier: Some(crate::DIAGNOSTIC_NAME.into()),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
notebook_document_sync: Some(types::OneOf::Left(NotebookDocumentSyncOptions {
|
||||
save: Some(false),
|
||||
notebook_selector: [NotebookSelector::ByCells {
|
||||
notebook: None,
|
||||
cells: vec![NotebookCellSelector {
|
||||
language: "python".to_string(),
|
||||
}],
|
||||
}]
|
||||
.to_vec(),
|
||||
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
|
||||
identifier: Some(crate::DIAGNOSTIC_NAME.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||
TextDocumentSyncOptions {
|
||||
open_close: Some(true),
|
||||
change: Some(TextDocumentSyncKind::INCREMENTAL),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use crate::{server::schedule::Task, session::Session, system::url_to_system_path};
|
||||
use lsp_server as server;
|
||||
|
||||
use crate::server::schedule::Task;
|
||||
use crate::session::Session;
|
||||
use crate::system::{url_to_any_system_path, AnySystemPath};
|
||||
|
||||
mod diagnostics;
|
||||
mod notifications;
|
||||
mod requests;
|
||||
mod traits;
|
||||
|
||||
use notifications as notification;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use requests as request;
|
||||
|
||||
use self::traits::{NotificationHandler, RequestHandler};
|
||||
@@ -43,6 +45,7 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
|
||||
match notif.method.as_str() {
|
||||
notification::DidCloseTextDocumentHandler::METHOD => local_notification_task::<notification::DidCloseTextDocumentHandler>(notif),
|
||||
notification::DidOpenTextDocumentHandler::METHOD => local_notification_task::<notification::DidOpenTextDocumentHandler>(notif),
|
||||
notification::DidChangeTextDocumentHandler::METHOD => local_notification_task::<notification::DidChangeTextDocumentHandler>(notif),
|
||||
notification::DidOpenNotebookHandler::METHOD => {
|
||||
local_notification_task::<notification::DidOpenNotebookHandler>(notif)
|
||||
}
|
||||
@@ -82,12 +85,18 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>(
|
||||
Ok(Task::background(schedule, move |session: &Session| {
|
||||
let url = R::document_url(¶ms).into_owned();
|
||||
|
||||
let Ok(path) = url_to_system_path(&url) else {
|
||||
let Ok(path) = url_to_any_system_path(&url) else {
|
||||
return Box::new(|_, _| {});
|
||||
};
|
||||
let db = session
|
||||
.workspace_db_for_path(path.as_std_path())
|
||||
.map(RootDatabase::snapshot);
|
||||
let db = match path {
|
||||
AnySystemPath::System(path) => {
|
||||
match session.workspace_db_for_path(path.as_std_path()) {
|
||||
Some(db) => db.snapshot(),
|
||||
None => session.default_workspace_db().snapshot(),
|
||||
}
|
||||
}
|
||||
AnySystemPath::SystemVirtual(_) => session.default_workspace_db().snapshot(),
|
||||
};
|
||||
|
||||
let Some(snapshot) = session.take_snapshot(url) else {
|
||||
return Box::new(|_, _| {});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
mod did_change;
|
||||
mod did_close;
|
||||
mod did_close_notebook;
|
||||
mod did_open;
|
||||
mod did_open_notebook;
|
||||
mod set_trace;
|
||||
|
||||
pub(super) use did_change::DidChangeTextDocumentHandler;
|
||||
pub(super) use did_close::DidCloseTextDocumentHandler;
|
||||
pub(super) use did_close_notebook::DidCloseNotebookHandler;
|
||||
pub(super) use did_open::DidOpenTextDocumentHandler;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use lsp_server::ErrorCode;
|
||||
use lsp_types::notification::DidChangeTextDocument;
|
||||
use lsp_types::DidChangeTextDocumentParams;
|
||||
|
||||
use red_knot_workspace::watch::ChangeEvent;
|
||||
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
use crate::server::api::LSPResult;
|
||||
use crate::server::client::{Notifier, Requester};
|
||||
use crate::server::Result;
|
||||
use crate::session::Session;
|
||||
use crate::system::{url_to_any_system_path, AnySystemPath};
|
||||
|
||||
pub(crate) struct DidChangeTextDocumentHandler;
|
||||
|
||||
impl NotificationHandler for DidChangeTextDocumentHandler {
|
||||
type NotificationType = DidChangeTextDocument;
|
||||
}
|
||||
|
||||
impl SyncNotificationHandler for DidChangeTextDocumentHandler {
|
||||
fn run(
|
||||
session: &mut Session,
|
||||
_notifier: Notifier,
|
||||
_requester: &mut Requester,
|
||||
params: DidChangeTextDocumentParams,
|
||||
) -> Result<()> {
|
||||
let Ok(path) = url_to_any_system_path(¶ms.text_document.uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let key = session.key_from_url(params.text_document.uri);
|
||||
|
||||
session
|
||||
.update_text_document(&key, params.content_changes, params.text_document.version)
|
||||
.with_failure_code(ErrorCode::InternalError)?;
|
||||
|
||||
match path {
|
||||
AnySystemPath::System(path) => {
|
||||
let db = match session.workspace_db_for_path_mut(path.as_std_path()) {
|
||||
Some(db) => db,
|
||||
None => session.default_workspace_db_mut(),
|
||||
};
|
||||
db.apply_changes(vec![ChangeEvent::file_content_changed(path)], None);
|
||||
}
|
||||
AnySystemPath::SystemVirtual(virtual_path) => {
|
||||
let db = session.default_workspace_db_mut();
|
||||
db.apply_changes(vec![ChangeEvent::ChangedVirtual(virtual_path)], None);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(dhruvmanila): Publish diagnostics if the client doesnt support pull diagnostics
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use lsp_server::ErrorCode;
|
||||
use lsp_types::notification::DidCloseTextDocument;
|
||||
use lsp_types::DidCloseTextDocumentParams;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use red_knot_workspace::watch::ChangeEvent;
|
||||
|
||||
use crate::server::api::diagnostics::clear_diagnostics;
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
@@ -10,7 +9,7 @@ use crate::server::api::LSPResult;
|
||||
use crate::server::client::{Notifier, Requester};
|
||||
use crate::server::Result;
|
||||
use crate::session::Session;
|
||||
use crate::system::url_to_system_path;
|
||||
use crate::system::{url_to_any_system_path, AnySystemPath};
|
||||
|
||||
pub(crate) struct DidCloseTextDocumentHandler;
|
||||
|
||||
@@ -25,7 +24,7 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
|
||||
_requester: &mut Requester,
|
||||
params: DidCloseTextDocumentParams,
|
||||
) -> Result<()> {
|
||||
let Ok(path) = url_to_system_path(¶ms.text_document.uri) else {
|
||||
let Ok(path) = url_to_any_system_path(¶ms.text_document.uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -34,8 +33,9 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
|
||||
.close_document(&key)
|
||||
.with_failure_code(ErrorCode::InternalError)?;
|
||||
|
||||
if let Some(db) = session.workspace_db_for_path_mut(path.as_std_path()) {
|
||||
File::sync_path(db, &path);
|
||||
if let AnySystemPath::SystemVirtual(virtual_path) = path {
|
||||
let db = session.default_workspace_db_mut();
|
||||
db.apply_changes(vec![ChangeEvent::DeletedVirtual(virtual_path)], None);
|
||||
}
|
||||
|
||||
clear_diagnostics(key.url(), ¬ifier)?;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use lsp_types::notification::DidCloseNotebookDocument;
|
||||
use lsp_types::DidCloseNotebookDocumentParams;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use red_knot_workspace::watch::ChangeEvent;
|
||||
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
use crate::server::api::LSPResult;
|
||||
use crate::server::client::{Notifier, Requester};
|
||||
use crate::server::Result;
|
||||
use crate::session::Session;
|
||||
use crate::system::url_to_system_path;
|
||||
use crate::system::{url_to_any_system_path, AnySystemPath};
|
||||
|
||||
pub(crate) struct DidCloseNotebookHandler;
|
||||
|
||||
@@ -23,7 +23,7 @@ impl SyncNotificationHandler for DidCloseNotebookHandler {
|
||||
_requester: &mut Requester,
|
||||
params: DidCloseNotebookDocumentParams,
|
||||
) -> Result<()> {
|
||||
let Ok(path) = url_to_system_path(¶ms.notebook_document.uri) else {
|
||||
let Ok(path) = url_to_any_system_path(¶ms.notebook_document.uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -32,8 +32,9 @@ impl SyncNotificationHandler for DidCloseNotebookHandler {
|
||||
.close_document(&key)
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
|
||||
if let Some(db) = session.workspace_db_for_path_mut(path.as_std_path()) {
|
||||
File::sync_path(db, &path);
|
||||
if let AnySystemPath::SystemVirtual(virtual_path) = path {
|
||||
let db = session.default_workspace_db_mut();
|
||||
db.apply_changes(vec![ChangeEvent::DeletedVirtual(virtual_path)], None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use lsp_types::notification::DidOpenTextDocument;
|
||||
use lsp_types::DidOpenTextDocumentParams;
|
||||
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use red_knot_workspace::watch::ChangeEvent;
|
||||
use ruff_db::Db;
|
||||
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
use crate::server::client::{Notifier, Requester};
|
||||
use crate::server::Result;
|
||||
use crate::session::Session;
|
||||
use crate::system::url_to_system_path;
|
||||
use crate::system::{url_to_any_system_path, AnySystemPath};
|
||||
use crate::TextDocument;
|
||||
|
||||
pub(crate) struct DidOpenTextDocumentHandler;
|
||||
@@ -23,17 +24,25 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
|
||||
_requester: &mut Requester,
|
||||
params: DidOpenTextDocumentParams,
|
||||
) -> Result<()> {
|
||||
let Ok(path) = url_to_system_path(¶ms.text_document.uri) else {
|
||||
let Ok(path) = url_to_any_system_path(¶ms.text_document.uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let document = TextDocument::new(params.text_document.text, params.text_document.version);
|
||||
session.open_text_document(params.text_document.uri, document);
|
||||
|
||||
if let Some(db) = session.workspace_db_for_path_mut(path.as_std_path()) {
|
||||
// TODO(dhruvmanila): Store the `file` in `DocumentController`
|
||||
let file = system_path_to_file(db, &path).unwrap();
|
||||
file.sync(db);
|
||||
match path {
|
||||
AnySystemPath::System(path) => {
|
||||
let db = match session.workspace_db_for_path_mut(path.as_std_path()) {
|
||||
Some(db) => db,
|
||||
None => session.default_workspace_db_mut(),
|
||||
};
|
||||
db.apply_changes(vec![ChangeEvent::Opened(path)], None);
|
||||
}
|
||||
AnySystemPath::SystemVirtual(virtual_path) => {
|
||||
let db = session.default_workspace_db_mut();
|
||||
db.files().virtual_file(db, &virtual_path);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics
|
||||
|
||||
@@ -2,7 +2,8 @@ use lsp_server::ErrorCode;
|
||||
use lsp_types::notification::DidOpenNotebookDocument;
|
||||
use lsp_types::DidOpenNotebookDocumentParams;
|
||||
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use red_knot_workspace::watch::ChangeEvent;
|
||||
use ruff_db::Db;
|
||||
|
||||
use crate::edit::NotebookDocument;
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
@@ -10,7 +11,7 @@ use crate::server::api::LSPResult;
|
||||
use crate::server::client::{Notifier, Requester};
|
||||
use crate::server::Result;
|
||||
use crate::session::Session;
|
||||
use crate::system::url_to_system_path;
|
||||
use crate::system::{url_to_any_system_path, AnySystemPath};
|
||||
|
||||
pub(crate) struct DidOpenNotebookHandler;
|
||||
|
||||
@@ -25,7 +26,7 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
|
||||
_requester: &mut Requester,
|
||||
params: DidOpenNotebookDocumentParams,
|
||||
) -> Result<()> {
|
||||
let Ok(path) = url_to_system_path(¶ms.notebook_document.uri) else {
|
||||
let Ok(path) = url_to_any_system_path(¶ms.notebook_document.uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -38,10 +39,18 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
|
||||
.with_failure_code(ErrorCode::InternalError)?;
|
||||
session.open_notebook_document(params.notebook_document.uri.clone(), notebook);
|
||||
|
||||
if let Some(db) = session.workspace_db_for_path_mut(path.as_std_path()) {
|
||||
// TODO(dhruvmanila): Store the `file` in `DocumentController`
|
||||
let file = system_path_to_file(db, &path).unwrap();
|
||||
file.sync(db);
|
||||
match path {
|
||||
AnySystemPath::System(path) => {
|
||||
let db = match session.workspace_db_for_path_mut(path.as_std_path()) {
|
||||
Some(db) => db,
|
||||
None => session.default_workspace_db_mut(),
|
||||
};
|
||||
db.apply_changes(vec![ChangeEvent::Opened(path)], None);
|
||||
}
|
||||
AnySystemPath::SystemVirtual(virtual_path) => {
|
||||
let db = session.default_workspace_db_mut();
|
||||
db.files().virtual_file(db, &virtual_path);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics
|
||||
|
||||
@@ -26,13 +26,11 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
snapshot: DocumentSnapshot,
|
||||
db: Option<RootDatabase>,
|
||||
db: RootDatabase,
|
||||
_notifier: Notifier,
|
||||
_params: DocumentDiagnosticParams,
|
||||
) -> Result<DocumentDiagnosticReportResult> {
|
||||
let diagnostics = db
|
||||
.map(|db| compute_diagnostics(&snapshot, &db))
|
||||
.unwrap_or_default();
|
||||
let diagnostics = compute_diagnostics(&snapshot, &db);
|
||||
|
||||
Ok(DocumentDiagnosticReportResult::Report(
|
||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
@@ -48,10 +46,19 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
|
||||
|
||||
fn compute_diagnostics(snapshot: &DocumentSnapshot, db: &RootDatabase) -> Vec<Diagnostic> {
|
||||
let Some(file) = snapshot.file(db) else {
|
||||
tracing::info!(
|
||||
"No file found for snapshot for '{}'",
|
||||
snapshot.query().file_url()
|
||||
);
|
||||
return vec![];
|
||||
};
|
||||
let Ok(diagnostics) = db.check_file(file) else {
|
||||
return vec![];
|
||||
|
||||
let diagnostics = match db.check_file(file) {
|
||||
Ok(diagnostics) => diagnostics,
|
||||
Err(cancelled) => {
|
||||
tracing::info!("Diagnostics computation {cancelled}");
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
|
||||
diagnostics
|
||||
@@ -65,12 +72,12 @@ fn to_lsp_diagnostic(message: &str) -> Diagnostic {
|
||||
let words = message.split(':').collect::<Vec<_>>();
|
||||
|
||||
let (range, message) = match words.as_slice() {
|
||||
[_filename, line, column, message] => {
|
||||
let line = line.parse::<u32>().unwrap_or_default();
|
||||
[_, _, line, column, message] | [_, line, column, message] => {
|
||||
let line = line.parse::<u32>().unwrap_or_default().saturating_sub(1);
|
||||
let column = column.parse::<u32>().unwrap_or_default();
|
||||
(
|
||||
Range::new(
|
||||
Position::new(line.saturating_sub(1), column.saturating_sub(1)),
|
||||
Position::new(line, column.saturating_sub(1)),
|
||||
Position::new(line, column),
|
||||
),
|
||||
message.trim(),
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
snapshot: DocumentSnapshot,
|
||||
db: Option<RootDatabase>,
|
||||
db: RootDatabase,
|
||||
notifier: Notifier,
|
||||
params: <<Self as RequestHandler>::RequestType as Request>::Params,
|
||||
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
|
||||
|
||||
@@ -6,15 +6,16 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use lsp_types::{ClientCapabilities, Url};
|
||||
use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url};
|
||||
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::SystemPath;
|
||||
use ruff_db::Db;
|
||||
|
||||
use crate::edit::{DocumentKey, NotebookDocument};
|
||||
use crate::system::{url_to_system_path, LSPSystem};
|
||||
use crate::edit::{DocumentKey, DocumentVersion, NotebookDocument};
|
||||
use crate::system::{url_to_any_system_path, AnySystemPath, LSPSystem};
|
||||
use crate::{PositionEncoding, TextDocument};
|
||||
|
||||
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
||||
@@ -82,6 +83,12 @@ impl Session {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(dhruvmanila): Ideally, we should have a single method for `workspace_db_for_path_mut`
|
||||
// and `default_workspace_db_mut` but the borrow checker doesn't allow that.
|
||||
// https://github.com/astral-sh/ruff/pull/13041#discussion_r1726725437
|
||||
|
||||
/// Returns a reference to the workspace [`RootDatabase`] corresponding to the given path, if
|
||||
/// any.
|
||||
pub(crate) fn workspace_db_for_path(&self, path: impl AsRef<Path>) -> Option<&RootDatabase> {
|
||||
self.workspaces
|
||||
.range(..=path.as_ref().to_path_buf())
|
||||
@@ -89,6 +96,8 @@ impl Session {
|
||||
.map(|(_, db)| db)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the workspace [`RootDatabase`] corresponding to the given
|
||||
/// path, if any.
|
||||
pub(crate) fn workspace_db_for_path_mut(
|
||||
&mut self,
|
||||
path: impl AsRef<Path>,
|
||||
@@ -99,6 +108,19 @@ impl Session {
|
||||
.map(|(_, db)| db)
|
||||
}
|
||||
|
||||
/// Returns a reference to the default workspace [`RootDatabase`]. The default workspace is the
|
||||
/// minimum root path in the workspace map.
|
||||
pub(crate) fn default_workspace_db(&self) -> &RootDatabase {
|
||||
// SAFETY: Currently, red knot only support a single workspace.
|
||||
self.workspaces.values().next().unwrap()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the default workspace [`RootDatabase`].
|
||||
pub(crate) fn default_workspace_db_mut(&mut self) -> &mut RootDatabase {
|
||||
// SAFETY: Currently, red knot only support a single workspace.
|
||||
self.workspaces.values_mut().next().unwrap()
|
||||
}
|
||||
|
||||
pub fn key_from_url(&self, url: Url) -> DocumentKey {
|
||||
self.index().key_from_url(url)
|
||||
}
|
||||
@@ -125,6 +147,20 @@ impl Session {
|
||||
self.index_mut().open_text_document(url, document);
|
||||
}
|
||||
|
||||
/// Updates a text document at the associated `key`.
|
||||
///
|
||||
/// The document key must point to a text document, or this will throw an error.
|
||||
pub(crate) fn update_text_document(
|
||||
&mut self,
|
||||
key: &DocumentKey,
|
||||
content_changes: Vec<TextDocumentContentChangeEvent>,
|
||||
new_version: DocumentVersion,
|
||||
) -> crate::Result<()> {
|
||||
let position_encoding = self.position_encoding;
|
||||
self.index_mut()
|
||||
.update_text_document(key, content_changes, new_version, position_encoding)
|
||||
}
|
||||
|
||||
/// De-registers a document, specified by its key.
|
||||
/// Calling this multiple times for the same document is a logic error.
|
||||
pub(crate) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> {
|
||||
@@ -211,6 +247,7 @@ impl Drop for MutIndexGuard<'_> {
|
||||
|
||||
/// An immutable snapshot of `Session` that references
|
||||
/// a specific document.
|
||||
#[derive(Debug)]
|
||||
pub struct DocumentSnapshot {
|
||||
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
|
||||
document_ref: index::DocumentQuery,
|
||||
@@ -231,7 +268,12 @@ impl DocumentSnapshot {
|
||||
}
|
||||
|
||||
pub(crate) fn file(&self, db: &RootDatabase) -> Option<File> {
|
||||
let path = url_to_system_path(self.document_ref.file_url()).ok()?;
|
||||
system_path_to_file(db, path).ok()
|
||||
match url_to_any_system_path(self.document_ref.file_url()).ok()? {
|
||||
AnySystemPath::System(path) => system_path_to_file(db, path).ok(),
|
||||
AnySystemPath::SystemVirtual(virtual_path) => db
|
||||
.files()
|
||||
.try_virtual_file(&virtual_path)
|
||||
.map(|virtual_file| virtual_file.file()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,27 +8,40 @@ use ruff_db::file_revision::FileRevision;
|
||||
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
|
||||
use ruff_db::system::{
|
||||
DirectoryEntry, FileType, Metadata, OsSystem, Result, System, SystemPath, SystemPathBuf,
|
||||
SystemVirtualPath,
|
||||
SystemVirtualPath, SystemVirtualPathBuf,
|
||||
};
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
|
||||
use crate::session::index::Index;
|
||||
use crate::DocumentQuery;
|
||||
|
||||
/// Converts the given [`Url`] to a [`SystemPathBuf`].
|
||||
/// Converts the given [`Url`] to an [`AnySystemPath`].
|
||||
///
|
||||
/// If the URL scheme is `file`, then the path is converted to a [`SystemPathBuf`]. Otherwise, the
|
||||
/// URL is converted to a [`SystemVirtualPathBuf`].
|
||||
///
|
||||
/// This fails in the following cases:
|
||||
/// * The URL scheme is not `file`.
|
||||
/// * The URL cannot be converted to a file path (refer to [`Url::to_file_path`]).
|
||||
/// * If the URL is not a valid UTF-8 string.
|
||||
pub(crate) fn url_to_system_path(url: &Url) -> std::result::Result<SystemPathBuf, ()> {
|
||||
pub(crate) fn url_to_any_system_path(url: &Url) -> std::result::Result<AnySystemPath, ()> {
|
||||
if url.scheme() == "file" {
|
||||
Ok(SystemPathBuf::from_path_buf(url.to_file_path()?).map_err(|_| ())?)
|
||||
Ok(AnySystemPath::System(
|
||||
SystemPathBuf::from_path_buf(url.to_file_path()?).map_err(|_| ())?,
|
||||
))
|
||||
} else {
|
||||
Err(())
|
||||
Ok(AnySystemPath::SystemVirtual(
|
||||
SystemVirtualPath::new(url.as_str()).to_path_buf(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents either a [`SystemPath`] or a [`SystemVirtualPath`].
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AnySystemPath {
|
||||
System(SystemPathBuf),
|
||||
SystemVirtual(SystemVirtualPathBuf),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LSPSystem {
|
||||
/// A read-only copy of the index where the server stores all the open documents and settings.
|
||||
@@ -144,19 +157,6 @@ impl System for LSPSystem {
|
||||
}
|
||||
}
|
||||
|
||||
fn virtual_path_metadata(&self, path: &SystemVirtualPath) -> Result<Metadata> {
|
||||
// Virtual paths only exists in the LSP system, so we don't need to check the OS system.
|
||||
let document = self
|
||||
.system_virtual_path_to_document_ref(path)?
|
||||
.ok_or_else(|| virtual_path_not_found(path))?;
|
||||
|
||||
Ok(Metadata::new(
|
||||
document_revision(&document),
|
||||
None,
|
||||
FileType::File,
|
||||
))
|
||||
}
|
||||
|
||||
fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String> {
|
||||
let document = self
|
||||
.system_virtual_path_to_document_ref(path)?
|
||||
|
||||
@@ -234,13 +234,6 @@ impl System for WasmSystem {
|
||||
Notebook::from_source_code(&content)
|
||||
}
|
||||
|
||||
fn virtual_path_metadata(
|
||||
&self,
|
||||
_path: &SystemVirtualPath,
|
||||
) -> ruff_db::system::Result<Metadata> {
|
||||
Err(not_found())
|
||||
}
|
||||
|
||||
fn read_virtual_path_to_string(
|
||||
&self,
|
||||
_path: &SystemVirtualPath,
|
||||
|
||||
@@ -11,14 +11,14 @@ fn check() {
|
||||
};
|
||||
let mut workspace = Workspace::new("/", &settings).expect("Workspace to be created");
|
||||
|
||||
let test = workspace
|
||||
workspace
|
||||
.open_file("test.py", "import random22\n")
|
||||
.expect("File to be opened");
|
||||
|
||||
let result = workspace.check_file(&test).expect("Check to succeed");
|
||||
let result = workspace.check().expect("Check to succeed");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
vec!["/test.py:1:8: Import 'random22' could not be resolved.",]
|
||||
vec!["/test.py:1:8: Cannot resolve import 'random22'."]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,12 +22,13 @@ ruff_text_size = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
crossbeam = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["testing"]}
|
||||
ruff_db = { workspace = true, features = ["testing"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
x = 0
|
||||
(x := x + 1)
|
||||
@@ -0,0 +1,3 @@
|
||||
x = 0
|
||||
if x := x + 1:
|
||||
pass
|
||||
@@ -0,0 +1,11 @@
|
||||
def bool(x) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class MyClass: ...
|
||||
|
||||
|
||||
def MyClass() -> MyClass: ...
|
||||
|
||||
|
||||
def x(self) -> x: ...
|
||||
@@ -0,0 +1,2 @@
|
||||
def bool(x=bool):
|
||||
return x
|
||||
@@ -0,0 +1,3 @@
|
||||
match x:
|
||||
case [1, 0] if x := x[:0]:
|
||||
y = 1
|
||||
@@ -56,6 +56,8 @@ impl RootDatabase {
|
||||
}
|
||||
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<String>, Cancelled> {
|
||||
let _span = tracing::debug_span!("check_file", file=%file.path(self)).entered();
|
||||
|
||||
self.with_db(|db| check_file(db, file))
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ impl RootDatabase {
|
||||
};
|
||||
|
||||
for change in changes {
|
||||
if let Some(path) = change.path() {
|
||||
if let Some(path) = change.system_path() {
|
||||
if matches!(
|
||||
path.file_name(),
|
||||
Some(".gitignore" | ".ignore" | "ruff.toml" | ".ruff.toml" | "pyproject.toml")
|
||||
@@ -72,7 +72,8 @@ impl RootDatabase {
|
||||
}
|
||||
|
||||
match change {
|
||||
watch::ChangeEvent::Changed { path, kind: _ } => sync_path(self, &path),
|
||||
watch::ChangeEvent::Changed { path, kind: _ }
|
||||
| watch::ChangeEvent::Opened(path) => sync_path(self, &path),
|
||||
|
||||
watch::ChangeEvent::Created { kind, path } => {
|
||||
match kind {
|
||||
@@ -130,6 +131,17 @@ impl RootDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
watch::ChangeEvent::CreatedVirtual(path)
|
||||
| watch::ChangeEvent::ChangedVirtual(path) => {
|
||||
File::sync_virtual_path(self, &path);
|
||||
}
|
||||
|
||||
watch::ChangeEvent::DeletedVirtual(path) => {
|
||||
if let Some(virtual_file) = self.files().try_virtual_file(&path) {
|
||||
virtual_file.close(self);
|
||||
}
|
||||
}
|
||||
|
||||
watch::ChangeEvent::Rescan => {
|
||||
workspace_change = true;
|
||||
Files::sync_all(self);
|
||||
|
||||
@@ -7,7 +7,7 @@ use red_knot_python_semantic::types::Type;
|
||||
use red_knot_python_semantic::{HasTy, ModuleName, SemanticModel};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::{parsed_module, ParsedModule};
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_db::source::{source_text, SourceText};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
@@ -48,19 +48,6 @@ pub(crate) fn lint_syntax(db: &dyn Db, file_id: File) -> Vec<String> {
|
||||
};
|
||||
visitor.visit_body(&ast.body);
|
||||
diagnostics = visitor.diagnostics;
|
||||
} else {
|
||||
let path = file_id.path(db);
|
||||
let line_index = line_index(db.upcast(), file_id);
|
||||
diagnostics.extend(parsed.errors().iter().map(|err| {
|
||||
let source_location = line_index.source_location(err.location.start(), source.as_str());
|
||||
format!(
|
||||
"{}:{}:{}: {}",
|
||||
path.as_str(),
|
||||
source_location.row,
|
||||
source_location.column,
|
||||
err,
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
diagnostics
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf, SystemVirtualPathBuf};
|
||||
pub use watcher::{directory_watcher, EventHandler, Watcher};
|
||||
pub use workspace_watcher::WorkspaceWatcher;
|
||||
|
||||
@@ -20,6 +20,9 @@ mod workspace_watcher;
|
||||
/// event instead of emitting an event for each file or subdirectory in that path.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum ChangeEvent {
|
||||
/// The file corresponding to the given path was opened in an editor.
|
||||
Opened(SystemPathBuf),
|
||||
|
||||
/// A new path was created
|
||||
Created {
|
||||
path: SystemPathBuf,
|
||||
@@ -38,6 +41,15 @@ pub enum ChangeEvent {
|
||||
kind: DeletedKind,
|
||||
},
|
||||
|
||||
/// A new virtual path was created.
|
||||
CreatedVirtual(SystemVirtualPathBuf),
|
||||
|
||||
/// The content of a virtual path was changed.
|
||||
ChangedVirtual(SystemVirtualPathBuf),
|
||||
|
||||
/// A virtual path was deleted.
|
||||
DeletedVirtual(SystemVirtualPathBuf),
|
||||
|
||||
/// The file watcher failed to observe some changes and now is out of sync with the file system.
|
||||
///
|
||||
/// This can happen if many files are changed at once. The consumer should rescan all files to catch up
|
||||
@@ -46,16 +58,27 @@ pub enum ChangeEvent {
|
||||
}
|
||||
|
||||
impl ChangeEvent {
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
self.path().and_then(|path| path.file_name())
|
||||
/// Creates a new [`Changed`] event for the file content at the given path.
|
||||
///
|
||||
/// [`Changed`]: ChangeEvent::Changed
|
||||
pub fn file_content_changed(path: SystemPathBuf) -> ChangeEvent {
|
||||
ChangeEvent::Changed {
|
||||
path,
|
||||
kind: ChangedKind::FileContent,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<&SystemPath> {
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
self.system_path().and_then(|path| path.file_name())
|
||||
}
|
||||
|
||||
pub fn system_path(&self) -> Option<&SystemPath> {
|
||||
match self {
|
||||
ChangeEvent::Created { path, .. }
|
||||
ChangeEvent::Opened(path)
|
||||
| ChangeEvent::Created { path, .. }
|
||||
| ChangeEvent::Changed { path, .. }
|
||||
| ChangeEvent::Deleted { path, .. } => Some(path),
|
||||
ChangeEvent::Rescan => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use salsa::{Durability, Setter as _};
|
||||
pub use metadata::{PackageMetadata, WorkspaceMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use red_knot_python_semantic::SearchPathSettings;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{line_index, source_text, SourceDiagnostic};
|
||||
use ruff_db::{
|
||||
files::{system_path_to_file, File},
|
||||
@@ -14,7 +15,8 @@ use ruff_db::{
|
||||
use ruff_python_ast::{name::Name, PySourceType};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::workspace::files::{Index, Indexed, PackageFiles};
|
||||
use crate::db::RootDatabase;
|
||||
use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
|
||||
use crate::{
|
||||
db::Db,
|
||||
lint::{lint_semantic, lint_syntax},
|
||||
@@ -189,23 +191,35 @@ impl Workspace {
|
||||
}
|
||||
|
||||
/// Checks all open files in the workspace and its dependencies.
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub fn check(self, db: &dyn Db) -> Vec<String> {
|
||||
pub fn check(self, db: &RootDatabase) -> Vec<String> {
|
||||
let workspace_span = tracing::debug_span!("check_workspace");
|
||||
let _span = workspace_span.enter();
|
||||
|
||||
tracing::debug!("Checking workspace");
|
||||
let files = WorkspaceFiles::new(db, self);
|
||||
let result = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let inner_result = Arc::clone(&result);
|
||||
|
||||
let mut result = Vec::new();
|
||||
let db = db.snapshot();
|
||||
let workspace_span = workspace_span.clone();
|
||||
|
||||
if let Some(open_files) = self.open_files(db) {
|
||||
for file in open_files {
|
||||
result.extend_from_slice(&check_file(db, *file));
|
||||
rayon::scope(move |scope| {
|
||||
for file in &files {
|
||||
let result = inner_result.clone();
|
||||
let db = db.snapshot();
|
||||
let workspace_span = workspace_span.clone();
|
||||
|
||||
scope.spawn(move |_| {
|
||||
let check_file_span = tracing::debug_span!(parent: &workspace_span, "check_file", file=%file.path(&db));
|
||||
let _entered = check_file_span.entered();
|
||||
|
||||
let file_diagnostics = check_file(&db, file);
|
||||
result.lock().unwrap().extend(file_diagnostics);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for package in self.packages(db) {
|
||||
result.extend(package.check(db));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
result
|
||||
Arc::into_inner(result).unwrap().into_inner().unwrap()
|
||||
}
|
||||
|
||||
/// Opens a file in the workspace.
|
||||
@@ -323,19 +337,6 @@ impl Package {
|
||||
index.insert(file);
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub(crate) fn check(self, db: &dyn Db) -> Vec<String> {
|
||||
tracing::debug!("Checking package '{}'", self.root(db));
|
||||
|
||||
let mut result = Vec::new();
|
||||
for file in &self.files(db) {
|
||||
let diagnostics = check_file(db, file);
|
||||
result.extend_from_slice(&diagnostics);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns the files belonging to this package.
|
||||
pub fn files(self, db: &dyn Db) -> Indexed<'_> {
|
||||
let files = self.file_set(db);
|
||||
@@ -383,9 +384,7 @@ impl Package {
|
||||
|
||||
#[salsa::tracked]
|
||||
pub(super) fn check_file(db: &dyn Db, file: File) -> Vec<String> {
|
||||
let path = file.path(db);
|
||||
let _span = tracing::debug_span!("check_file", file=%path).entered();
|
||||
tracing::debug!("Checking file '{path}'");
|
||||
tracing::debug!("Checking file '{path}'", path = file.path(db));
|
||||
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
@@ -404,6 +403,17 @@ pub(super) fn check_file(db: &dyn Db, file: File) -> Vec<String> {
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
|
||||
if !parsed.errors().is_empty() {
|
||||
let path = file.path(db);
|
||||
let line_index = line_index(db.upcast(), file);
|
||||
diagnostics.extend(parsed.errors().iter().map(|err| {
|
||||
let source_location = line_index.source_location(err.location.start(), source.as_str());
|
||||
format!("{path}:{source_location}: {message}", message = err.error)
|
||||
}));
|
||||
}
|
||||
|
||||
for diagnostic in check_types(db.upcast(), file) {
|
||||
let index = line_index(db.upcast(), diagnostic.file());
|
||||
let location = index.source_location(diagnostic.start(), source.as_str());
|
||||
@@ -462,6 +472,73 @@ fn discover_package_files(db: &dyn Db, path: &SystemPath) -> FxHashSet<File> {
|
||||
files
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum WorkspaceFiles<'a> {
|
||||
OpenFiles(&'a FxHashSet<File>),
|
||||
PackageFiles(Vec<Indexed<'a>>),
|
||||
}
|
||||
|
||||
impl<'a> WorkspaceFiles<'a> {
|
||||
fn new(db: &'a dyn Db, workspace: Workspace) -> Self {
|
||||
if let Some(open_files) = workspace.open_files(db) {
|
||||
WorkspaceFiles::OpenFiles(open_files)
|
||||
} else {
|
||||
WorkspaceFiles::PackageFiles(
|
||||
workspace
|
||||
.packages(db)
|
||||
.map(|package| package.files(db))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a WorkspaceFiles<'a> {
|
||||
type Item = File;
|
||||
type IntoIter = WorkspaceFilesIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
match self {
|
||||
WorkspaceFiles::OpenFiles(files) => WorkspaceFilesIter::OpenFiles(files.iter()),
|
||||
WorkspaceFiles::PackageFiles(package_files) => {
|
||||
let mut package_files = package_files.iter();
|
||||
WorkspaceFilesIter::PackageFiles {
|
||||
current: package_files.next().map(IntoIterator::into_iter),
|
||||
package_files,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum WorkspaceFilesIter<'db> {
|
||||
OpenFiles(std::collections::hash_set::Iter<'db, File>),
|
||||
PackageFiles {
|
||||
package_files: std::slice::Iter<'db, Indexed<'db>>,
|
||||
current: Option<IndexedIter<'db>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Iterator for WorkspaceFilesIter<'_> {
|
||||
type Item = File;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
WorkspaceFilesIter::OpenFiles(files) => files.next().copied(),
|
||||
WorkspaceFilesIter::PackageFiles {
|
||||
package_files,
|
||||
current,
|
||||
} => loop {
|
||||
if let Some(file) = current.as_mut().and_then(Iterator::next) {
|
||||
return Some(file);
|
||||
}
|
||||
|
||||
*current = Some(package_files.next()?.into_iter());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
|
||||
@@ -158,9 +158,11 @@ impl Deref for Indexed<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) type IndexedIter<'a> = std::iter::Copied<std::collections::hash_set::Iter<'a, File>>;
|
||||
|
||||
impl<'a> IntoIterator for &'a Indexed<'_> {
|
||||
type Item = File;
|
||||
type IntoIter = std::iter::Copied<std::collections::hash_set::Iter<'a, File>>;
|
||||
type IntoIter = IndexedIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.files.iter().copied()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -37,13 +37,14 @@ name = "red_knot"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true }
|
||||
criterion = { workspace = true, default-features = false }
|
||||
once_cell = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
url = { workspace = true }
|
||||
ureq = { workspace = true }
|
||||
criterion = { workspace = true, default-features = false }
|
||||
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![allow(clippy::disallowed_names)]
|
||||
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::watch::{ChangeEvent, ChangedKind};
|
||||
@@ -20,18 +21,9 @@ struct Case {
|
||||
|
||||
const TOMLLIB_312_URL: &str = "https://raw.githubusercontent.com/python/cpython/8e8a4baf652f6e1cee7acde9d78c4b6154539748/Lib/tomllib";
|
||||
|
||||
// This first "unresolved import" is because we don't understand `*` imports yet.
|
||||
// The following "unresolved import" violations are because we can't distinguish currently from
|
||||
// "Symbol exists in the module but its type is unknown" and
|
||||
// "Symbol does not exist in the module"
|
||||
// The "unresolved import" is because we don't understand `*` imports yet.
|
||||
static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||
"/src/tomllib/_parser.py:7:29: Could not resolve import of 'Iterable' from 'collections.abc'",
|
||||
"/src/tomllib/_parser.py:10:20: Could not resolve import of 'Any' from 'typing'",
|
||||
"/src/tomllib/_parser.py:13:5: Could not resolve import of 'RE_DATETIME' from '._re'",
|
||||
"/src/tomllib/_parser.py:14:5: Could not resolve import of 'RE_LOCALTIME' from '._re'",
|
||||
"/src/tomllib/_parser.py:15:5: Could not resolve import of 'RE_NUMBER' from '._re'",
|
||||
"/src/tomllib/_parser.py:20:21: Could not resolve import of 'Key' from '._types'",
|
||||
"/src/tomllib/_parser.py:20:26: Could not resolve import of 'ParseFloat' from '._types'",
|
||||
"/src/tomllib/_parser.py:7:29: Module 'collections.abc' has no member 'Iterable'",
|
||||
"Line 69 is too long (89 characters)",
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
@@ -40,23 +32,8 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
"/src/tomllib/_parser.py:153:22: Name 'key' used when not defined.",
|
||||
"/src/tomllib/_parser.py:153:27: Name 'flag' used when not defined.",
|
||||
"/src/tomllib/_parser.py:159:16: Name 'k' used when not defined.",
|
||||
"/src/tomllib/_parser.py:161:25: Name 'k' used when not defined.",
|
||||
"/src/tomllib/_parser.py:168:16: Name 'k' used when not defined.",
|
||||
"/src/tomllib/_parser.py:169:22: Name 'k' used when not defined.",
|
||||
"/src/tomllib/_parser.py:170:25: Name 'k' used when not defined.",
|
||||
"/src/tomllib/_parser.py:180:16: Name 'k' used when not defined.",
|
||||
"/src/tomllib/_parser.py:182:31: Name 'k' used when not defined.",
|
||||
"/src/tomllib/_parser.py:206:16: Name 'k' used when not defined.",
|
||||
"/src/tomllib/_parser.py:207:22: Name 'k' used when not defined.",
|
||||
"/src/tomllib/_parser.py:208:25: Name 'k' used when not defined.",
|
||||
"/src/tomllib/_parser.py:330:32: Name 'header' used when not defined.",
|
||||
"/src/tomllib/_parser.py:330:41: Name 'key' used when not defined.",
|
||||
"/src/tomllib/_parser.py:333:26: Name 'cont_key' used when not defined.",
|
||||
"/src/tomllib/_parser.py:334:71: Name 'cont_key' used when not defined.",
|
||||
"/src/tomllib/_parser.py:337:31: Name 'cont_key' used when not defined.",
|
||||
"/src/tomllib/_parser.py:628:75: Name 'e' used when not defined.",
|
||||
"/src/tomllib/_parser.py:686:23: Name 'parse_float' used when not defined.",
|
||||
];
|
||||
@@ -112,7 +89,25 @@ fn setup_case() -> Case {
|
||||
}
|
||||
}
|
||||
|
||||
static RAYON_INITIALIZED: std::sync::Once = std::sync::Once::new();
|
||||
|
||||
fn setup_rayon() {
|
||||
// Initialize the rayon thread pool outside the benchmark because it has a significant cost.
|
||||
// We limit the thread pool to only one (the current thread) because we're focused on
|
||||
// where red knot spends time and less about how well the code runs concurrently.
|
||||
// We might want to add a benchmark focusing on concurrency to detect congestion in the future.
|
||||
RAYON_INITIALIZED.call_once(|| {
|
||||
ThreadPoolBuilder::new()
|
||||
.num_threads(1)
|
||||
.use_current_thread()
|
||||
.build_global()
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
fn benchmark_incremental(criterion: &mut Criterion) {
|
||||
setup_rayon();
|
||||
|
||||
criterion.bench_function("red_knot_check_file[incremental]", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| {
|
||||
@@ -149,6 +144,8 @@ fn benchmark_incremental(criterion: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn benchmark_cold(criterion: &mut Criterion) {
|
||||
setup_rayon();
|
||||
|
||||
criterion.bench_function("red_knot_check_file[cold]", |b| {
|
||||
b.iter_batched_ref(
|
||||
setup_case,
|
||||
|
||||
@@ -8,11 +8,12 @@ use salsa::{Durability, Setter};
|
||||
pub use file_root::{FileRoot, FileRootKind};
|
||||
pub use path::FilePath;
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
use ruff_python_ast::PySourceType;
|
||||
|
||||
use crate::file_revision::FileRevision;
|
||||
use crate::files::file_root::FileRoots;
|
||||
use crate::files::private::FileStatus;
|
||||
use crate::system::{Metadata, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf};
|
||||
use crate::system::{SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf};
|
||||
use crate::vendored::{VendoredPath, VendoredPathBuf};
|
||||
use crate::{vendored, Db, FxDashMap};
|
||||
|
||||
@@ -60,8 +61,8 @@ struct FilesInner {
|
||||
/// so that queries that depend on the existence of a file are re-executed when the file is created.
|
||||
system_by_path: FxDashMap<SystemPathBuf, File>,
|
||||
|
||||
/// Lookup table that maps [`SystemVirtualPathBuf`]s to salsa interned [`File`] instances.
|
||||
system_virtual_by_path: FxDashMap<SystemVirtualPathBuf, File>,
|
||||
/// Lookup table that maps [`SystemVirtualPathBuf`]s to [`VirtualFile`] instances.
|
||||
system_virtual_by_path: FxDashMap<SystemVirtualPathBuf, VirtualFile>,
|
||||
|
||||
/// Lookup table that maps vendored files to the salsa [`File`] ingredients.
|
||||
vendored_by_path: FxDashMap<VendoredPathBuf, File>,
|
||||
@@ -147,31 +148,31 @@ impl Files {
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// Looks up a virtual file by its `path`.
|
||||
/// Create a new virtual file at the given path and store it for future lookups.
|
||||
///
|
||||
/// For a non-existing file, creates a new salsa [`File`] ingredient and stores it for future lookups.
|
||||
///
|
||||
/// The operations fails if the system failed to provide a metadata for the path.
|
||||
pub fn add_virtual_file(&self, db: &dyn Db, path: &SystemVirtualPath) -> Option<File> {
|
||||
let file = match self.inner.system_virtual_by_path.entry(path.to_path_buf()) {
|
||||
Entry::Occupied(entry) => *entry.get(),
|
||||
Entry::Vacant(entry) => {
|
||||
let metadata = db.system().virtual_path_metadata(path).ok()?;
|
||||
/// This will always create a new file, overwriting any existing file at `path` in the internal
|
||||
/// storage.
|
||||
pub fn virtual_file(&self, db: &dyn Db, path: &SystemVirtualPath) -> VirtualFile {
|
||||
tracing::trace!("Adding virtual file {}", path);
|
||||
let virtual_file = VirtualFile(
|
||||
File::builder(FilePath::SystemVirtual(path.to_path_buf()))
|
||||
.status(FileStatus::Exists)
|
||||
.revision(FileRevision::zero())
|
||||
.permissions(None)
|
||||
.new(db),
|
||||
);
|
||||
self.inner
|
||||
.system_virtual_by_path
|
||||
.insert(path.to_path_buf(), virtual_file);
|
||||
virtual_file
|
||||
}
|
||||
|
||||
tracing::trace!("Adding virtual file '{}'", path);
|
||||
|
||||
let file = File::builder(FilePath::SystemVirtual(path.to_path_buf()))
|
||||
.revision(metadata.revision())
|
||||
.permissions(metadata.permissions())
|
||||
.new(db);
|
||||
|
||||
entry.insert(file);
|
||||
|
||||
file
|
||||
}
|
||||
};
|
||||
|
||||
Some(file)
|
||||
/// Tries to look up a virtual file by its path. Returns `None` if no such file exists yet.
|
||||
pub fn try_virtual_file(&self, path: &SystemVirtualPath) -> Option<VirtualFile> {
|
||||
self.inner
|
||||
.system_virtual_by_path
|
||||
.get(&path.to_path_buf())
|
||||
.map(|entry| *entry.value())
|
||||
}
|
||||
|
||||
/// Looks up the closest root for `path`. Returns `None` if `path` isn't enclosed by any source root.
|
||||
@@ -318,6 +319,9 @@ impl File {
|
||||
}
|
||||
FilePath::Vendored(vendored) => db.vendored().read_to_string(vendored),
|
||||
FilePath::SystemVirtual(system_virtual) => {
|
||||
// Add a dependency on the revision to ensure the operation gets re-executed when the file changes.
|
||||
let _ = self.revision(db);
|
||||
|
||||
db.system().read_virtual_path_to_string(system_virtual)
|
||||
}
|
||||
}
|
||||
@@ -342,6 +346,9 @@ impl File {
|
||||
"Reading a notebook from the vendored file system is not supported.",
|
||||
))),
|
||||
FilePath::SystemVirtual(system_virtual) => {
|
||||
// Add a dependency on the revision to ensure the operation gets re-executed when the file changes.
|
||||
let _ = self.revision(db);
|
||||
|
||||
db.system().read_virtual_path_to_notebook(system_virtual)
|
||||
}
|
||||
}
|
||||
@@ -354,6 +361,13 @@ impl File {
|
||||
Self::sync_system_path(db, &absolute, None);
|
||||
}
|
||||
|
||||
/// Increments the revision for the virtual file at `path`.
|
||||
pub fn sync_virtual_path(db: &mut dyn Db, path: &SystemVirtualPath) {
|
||||
if let Some(virtual_file) = db.files().try_virtual_file(path) {
|
||||
virtual_file.sync(db);
|
||||
}
|
||||
}
|
||||
|
||||
/// Syncs the [`File`]'s state with the state of the file on the system.
|
||||
pub fn sync(self, db: &mut dyn Db) {
|
||||
let path = self.path(db).clone();
|
||||
@@ -366,29 +380,20 @@ impl File {
|
||||
FilePath::Vendored(_) => {
|
||||
// Readonly, can never be out of date.
|
||||
}
|
||||
FilePath::SystemVirtual(system_virtual) => {
|
||||
Self::sync_system_virtual_path(db, &system_virtual, self);
|
||||
FilePath::SystemVirtual(_) => {
|
||||
VirtualFile(self).sync(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Private method providing the implementation for [`Self::sync_path`] and [`Self::sync`] for
|
||||
/// system paths.
|
||||
fn sync_system_path(db: &mut dyn Db, path: &SystemPath, file: Option<File>) {
|
||||
let Some(file) = file.or_else(|| db.files().try_system(db, path)) else {
|
||||
return;
|
||||
};
|
||||
let metadata = db.system().path_metadata(path);
|
||||
Self::sync_impl(db, metadata, file);
|
||||
}
|
||||
|
||||
fn sync_system_virtual_path(db: &mut dyn Db, path: &SystemVirtualPath, file: File) {
|
||||
let metadata = db.system().virtual_path_metadata(path);
|
||||
Self::sync_impl(db, metadata, file);
|
||||
}
|
||||
|
||||
/// Private method providing the implementation for [`Self::sync_system_path`] and
|
||||
/// [`Self::sync_system_virtual_path`].
|
||||
fn sync_impl(db: &mut dyn Db, metadata: crate::system::Result<Metadata>, file: File) {
|
||||
let (status, revision, permission) = match metadata {
|
||||
let (status, revision, permission) = match db.system().path_metadata(path) {
|
||||
Ok(metadata) if metadata.file_type().is_file() => (
|
||||
FileStatus::Exists,
|
||||
metadata.revision(),
|
||||
@@ -420,6 +425,42 @@ impl File {
|
||||
pub fn exists(self, db: &dyn Db) -> bool {
|
||||
self.status(db) == FileStatus::Exists
|
||||
}
|
||||
|
||||
/// Returns `true` if the file should be analyzed as a type stub.
|
||||
pub fn is_stub(self, db: &dyn Db) -> bool {
|
||||
self.path(db)
|
||||
.extension()
|
||||
.is_some_and(|extension| PySourceType::from_extension(extension).is_stub())
|
||||
}
|
||||
}
|
||||
|
||||
/// A virtual file that doesn't exist on the file system.
|
||||
///
|
||||
/// This is a wrapper around a [`File`] that provides additional methods to interact with a virtual
|
||||
/// file.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VirtualFile(File);
|
||||
|
||||
impl VirtualFile {
|
||||
/// Returns the underlying [`File`].
|
||||
pub fn file(&self) -> File {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Increments the revision of the underlying [`File`].
|
||||
fn sync(&self, db: &mut dyn Db) {
|
||||
let file = self.0;
|
||||
tracing::debug!("Updating the revision of '{}'", file.path(db));
|
||||
let current_revision = file.revision(db);
|
||||
file.set_revision(db)
|
||||
.to(FileRevision::new(current_revision.as_u128() + 1));
|
||||
}
|
||||
|
||||
/// Closes the virtual file.
|
||||
pub fn close(&self, db: &mut dyn Db) {
|
||||
tracing::debug!("Closing virtual file '{}'", self.0.path(db));
|
||||
self.0.set_status(db).to(FileStatus::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
// The types in here need to be public because they're salsa ingredients but we
|
||||
|
||||
@@ -121,9 +121,9 @@ mod tests {
|
||||
|
||||
db.write_virtual_file(path, "x = 10");
|
||||
|
||||
let file = db.files().add_virtual_file(&db, path).unwrap();
|
||||
let virtual_file = db.files().virtual_file(&db, path);
|
||||
|
||||
let parsed = parsed_module(&db, file);
|
||||
let parsed = parsed_module(&db, virtual_file.file());
|
||||
|
||||
assert!(parsed.is_valid());
|
||||
|
||||
@@ -137,9 +137,9 @@ mod tests {
|
||||
|
||||
db.write_virtual_file(path, "%timeit a = b");
|
||||
|
||||
let file = db.files().add_virtual_file(&db, path).unwrap();
|
||||
let virtual_file = db.files().virtual_file(&db, path);
|
||||
|
||||
let parsed = parsed_module(&db, file);
|
||||
let parsed = parsed_module(&db, virtual_file.file());
|
||||
|
||||
assert!(parsed.is_valid());
|
||||
|
||||
|
||||
@@ -63,9 +63,6 @@ pub trait System: Debug {
|
||||
/// representation fall-back to deserializing the notebook from a string.
|
||||
fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result<Notebook, NotebookError>;
|
||||
|
||||
/// Reads the metadata of the virtual file at `path`.
|
||||
fn virtual_path_metadata(&self, path: &SystemVirtualPath) -> Result<Metadata>;
|
||||
|
||||
/// Reads the content of the virtual file at `path` into a [`String`].
|
||||
fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String>;
|
||||
|
||||
|
||||
@@ -136,22 +136,6 @@ impl MemoryFileSystem {
|
||||
ruff_notebook::Notebook::from_source_code(&content)
|
||||
}
|
||||
|
||||
pub(crate) fn virtual_path_metadata(
|
||||
&self,
|
||||
path: impl AsRef<SystemVirtualPath>,
|
||||
) -> Result<Metadata> {
|
||||
let virtual_files = self.inner.virtual_files.read().unwrap();
|
||||
let file = virtual_files
|
||||
.get(&path.as_ref().to_path_buf())
|
||||
.ok_or_else(not_found)?;
|
||||
|
||||
Ok(Metadata {
|
||||
revision: file.last_modified.into(),
|
||||
permissions: Some(MemoryFileSystem::PERMISSION),
|
||||
file_type: FileType::File,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn read_virtual_path_to_string(
|
||||
&self,
|
||||
path: impl AsRef<SystemVirtualPath>,
|
||||
|
||||
@@ -77,10 +77,6 @@ impl System for OsSystem {
|
||||
Notebook::from_path(path.as_std_path())
|
||||
}
|
||||
|
||||
fn virtual_path_metadata(&self, _path: &SystemVirtualPath) -> Result<Metadata> {
|
||||
Err(not_found())
|
||||
}
|
||||
|
||||
fn read_virtual_path_to_string(&self, _path: &SystemVirtualPath) -> Result<String> {
|
||||
Err(not_found())
|
||||
}
|
||||
|
||||
@@ -69,13 +69,6 @@ impl System for TestSystem {
|
||||
}
|
||||
}
|
||||
|
||||
fn virtual_path_metadata(&self, path: &SystemVirtualPath) -> Result<Metadata> {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.virtual_path_metadata(path),
|
||||
TestSystemInner::System(system) => system.virtual_path_metadata(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String> {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.read_virtual_path_to_string(path),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -17,7 +17,7 @@ app = FastAPI()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Errors
|
||||
# Fixable errors
|
||||
|
||||
@app.get("/items/")
|
||||
def get_items(
|
||||
@@ -40,6 +40,34 @@ def do_stuff(
|
||||
# do stuff
|
||||
pass
|
||||
|
||||
@app.get("/users/")
|
||||
def get_users(
|
||||
skip: int,
|
||||
limit: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
pass
|
||||
|
||||
@app.get("/users/")
|
||||
def get_users(
|
||||
current_user: User = Depends(get_current_user),
|
||||
skip: int = 0,
|
||||
limit: int = 10,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
# Non fixable errors
|
||||
|
||||
@app.get("/users/")
|
||||
def get_users(
|
||||
skip: int = 0,
|
||||
limit: int = 10,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
# Unchanged
|
||||
|
||||
|
||||
@@ -79,3 +79,15 @@ _ = f"a {f"first"
|
||||
+ f"second"} d"
|
||||
_ = f"a {f"first {f"middle"}"
|
||||
+ f"second"} d"
|
||||
|
||||
# See https://github.com/astral-sh/ruff/issues/12936
|
||||
_ = "\12""0" # fix should be "\0120"
|
||||
_ = "\\12""0" # fix should be "\\120"
|
||||
_ = "\\\12""0" # fix should be "\\\0120"
|
||||
_ = "\12 0""0" # fix should be "\12 00"
|
||||
_ = r"\12"r"0" # fix should be r"\120"
|
||||
_ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
_ = "\8""0" # fix should be "\80"
|
||||
_ = "\12""8" # fix should be "\128"
|
||||
_ = "\12""foo" # fix should be "\12foo"
|
||||
_ = "\12" "" # fix should be "\12"
|
||||
|
||||
@@ -47,7 +47,6 @@ with contextlib.ExitStack() as exit_stack:
|
||||
open("filename", "w").close()
|
||||
pathlib.Path("filename").open("w").close()
|
||||
|
||||
|
||||
# OK (custom context manager)
|
||||
class MyFile:
|
||||
def __init__(self, filename: str):
|
||||
@@ -58,3 +57,202 @@ class MyFile:
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.file.close()
|
||||
|
||||
|
||||
import tempfile
|
||||
import tarfile
|
||||
from tarfile import TarFile
|
||||
import zipfile
|
||||
import io
|
||||
import codecs
|
||||
import bz2
|
||||
import gzip
|
||||
import dbm
|
||||
import dbm.gnu
|
||||
import dbm.ndbm
|
||||
import dbm.dumb
|
||||
import lzma
|
||||
import shelve
|
||||
import tokenize
|
||||
import wave
|
||||
import fileinput
|
||||
|
||||
f = tempfile.NamedTemporaryFile()
|
||||
f = tempfile.TemporaryFile()
|
||||
f = tempfile.SpooledTemporaryFile()
|
||||
f = tarfile.open("foo.tar")
|
||||
f = TarFile("foo.tar").open()
|
||||
f = tarfile.TarFile("foo.tar").open()
|
||||
f = tarfile.TarFile().open()
|
||||
f = zipfile.ZipFile("foo.zip").open("foo.txt")
|
||||
f = io.open("foo.txt")
|
||||
f = io.open_code("foo.txt")
|
||||
f = codecs.open("foo.txt")
|
||||
f = bz2.open("foo.txt")
|
||||
f = gzip.open("foo.txt")
|
||||
f = dbm.open("foo.db")
|
||||
f = dbm.gnu.open("foo.db")
|
||||
f = dbm.ndbm.open("foo.db")
|
||||
f = dbm.dumb.open("foo.db")
|
||||
f = lzma.open("foo.xz")
|
||||
f = lzma.LZMAFile("foo.xz")
|
||||
f = shelve.open("foo.db")
|
||||
f = tokenize.open("foo.py")
|
||||
f = wave.open("foo.wav")
|
||||
f = tarfile.TarFile.taropen("foo.tar")
|
||||
f = fileinput.input("foo.txt")
|
||||
f = fileinput.FileInput("foo.txt")
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
# The following line is for example's sake.
|
||||
# For some f's above, this would raise an error (since it'd be f.readline() etc.)
|
||||
data = f.read()
|
||||
|
||||
f.close()
|
||||
|
||||
# OK
|
||||
with tempfile.TemporaryFile() as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with tarfile.open("foo.tar") as f:
|
||||
data = f.add("foo.txt")
|
||||
|
||||
# OK
|
||||
with tarfile.TarFile("foo.tar") as f:
|
||||
data = f.add("foo.txt")
|
||||
|
||||
# OK
|
||||
with tarfile.TarFile("foo.tar").open() as f:
|
||||
data = f.add("foo.txt")
|
||||
|
||||
# OK
|
||||
with zipfile.ZipFile("foo.zip") as f:
|
||||
data = f.read("foo.txt")
|
||||
|
||||
# OK
|
||||
with zipfile.ZipFile("foo.zip").open("foo.txt") as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with zipfile.ZipFile("foo.zip") as zf:
|
||||
with zf.open("foo.txt") as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with io.open("foo.txt") as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with io.open_code("foo.txt") as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with codecs.open("foo.txt") as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with bz2.open("foo.txt") as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with gzip.open("foo.txt") as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with dbm.open("foo.db") as f:
|
||||
data = f.get("foo")
|
||||
|
||||
# OK
|
||||
with dbm.gnu.open("foo.db") as f:
|
||||
data = f.get("foo")
|
||||
|
||||
# OK
|
||||
with dbm.ndbm.open("foo.db") as f:
|
||||
data = f.get("foo")
|
||||
|
||||
# OK
|
||||
with dbm.dumb.open("foo.db") as f:
|
||||
data = f.get("foo")
|
||||
|
||||
# OK
|
||||
with lzma.open("foo.xz") as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with lzma.LZMAFile("foo.xz") as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with shelve.open("foo.db") as f:
|
||||
data = f["foo"]
|
||||
|
||||
# OK
|
||||
with tokenize.open("foo.py") as f:
|
||||
data = f.read()
|
||||
|
||||
# OK
|
||||
with wave.open("foo.wav") as f:
|
||||
data = f.readframes(1024)
|
||||
|
||||
# OK
|
||||
with tarfile.TarFile.taropen("foo.tar") as f:
|
||||
data = f.add("foo.txt")
|
||||
|
||||
# OK
|
||||
with fileinput.input("foo.txt") as f:
|
||||
data = f.readline()
|
||||
|
||||
# OK
|
||||
with fileinput.FileInput("foo.txt") as f:
|
||||
data = f.readline()
|
||||
|
||||
# OK (quick one-liner to clear file contents)
|
||||
tempfile.NamedTemporaryFile().close()
|
||||
tempfile.TemporaryFile().close()
|
||||
tempfile.SpooledTemporaryFile().close()
|
||||
tarfile.open("foo.tar").close()
|
||||
tarfile.TarFile("foo.tar").close()
|
||||
tarfile.TarFile("foo.tar").open().close()
|
||||
tarfile.TarFile.open("foo.tar").close()
|
||||
zipfile.ZipFile("foo.zip").close()
|
||||
zipfile.ZipFile("foo.zip").open("foo.txt").close()
|
||||
io.open("foo.txt").close()
|
||||
io.open_code("foo.txt").close()
|
||||
codecs.open("foo.txt").close()
|
||||
bz2.open("foo.txt").close()
|
||||
gzip.open("foo.txt").close()
|
||||
dbm.open("foo.db").close()
|
||||
dbm.gnu.open("foo.db").close()
|
||||
dbm.ndbm.open("foo.db").close()
|
||||
dbm.dumb.open("foo.db").close()
|
||||
lzma.open("foo.xz").close()
|
||||
lzma.LZMAFile("foo.xz").close()
|
||||
shelve.open("foo.db").close()
|
||||
tokenize.open("foo.py").close()
|
||||
wave.open("foo.wav").close()
|
||||
tarfile.TarFile.taropen("foo.tar").close()
|
||||
fileinput.input("foo.txt").close()
|
||||
fileinput.FileInput("foo.txt").close()
|
||||
|
||||
def aliased():
|
||||
from shelve import open as open_shelf
|
||||
x = open_shelf("foo.dbm")
|
||||
x.close()
|
||||
|
||||
from tarfile import TarFile as TF
|
||||
f = TF("foo").open()
|
||||
f.close()
|
||||
|
||||
import dbm.sqlite3
|
||||
|
||||
# OK
|
||||
with dbm.sqlite3.open("foo.db") as f:
|
||||
print(f.keys())
|
||||
|
||||
# OK
|
||||
dbm.sqlite3.open("foo.db").close()
|
||||
|
||||
# SIM115
|
||||
f = dbm.sqlite3.open("foo.db")
|
||||
f.close()
|
||||
|
||||
75
crates/ruff_linter/resources/test/fixtures/pycodestyle/E741.pyi
vendored
Normal file
75
crates/ruff_linter/resources/test/fixtures/pycodestyle/E741.pyi
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
l = 0
|
||||
I = 0
|
||||
O = 0
|
||||
l: int = 0
|
||||
|
||||
a, l = 0, 1
|
||||
[a, l] = 0, 1
|
||||
a, *l = 0, 1, 2
|
||||
a = l = 0
|
||||
|
||||
o = 0
|
||||
i = 0
|
||||
|
||||
for l in range(3):
|
||||
pass
|
||||
|
||||
|
||||
for a, l in zip(range(3), range(3)):
|
||||
pass
|
||||
|
||||
|
||||
def f1():
|
||||
global l
|
||||
l = 0
|
||||
|
||||
|
||||
def f2():
|
||||
l = 0
|
||||
|
||||
def f3():
|
||||
nonlocal l
|
||||
l = 1
|
||||
|
||||
f3()
|
||||
return l
|
||||
|
||||
|
||||
def f4(l, /, I):
|
||||
return l, I, O
|
||||
|
||||
|
||||
def f5(l=0, *, I=1):
|
||||
return l, I
|
||||
|
||||
|
||||
def f6(*l, **I):
|
||||
return l, I
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ctx1():
|
||||
yield 0
|
||||
|
||||
|
||||
with ctx1() as l:
|
||||
pass
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ctx2():
|
||||
yield 0, 1
|
||||
|
||||
|
||||
with ctx2() as (a, l):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except ValueError as l:
|
||||
pass
|
||||
|
||||
if (l := 5) > 0:
|
||||
pass
|
||||
@@ -119,3 +119,91 @@ class A(metaclass=abc.abcmeta):
|
||||
def f(self):
|
||||
"""Lorem ipsum."""
|
||||
return True
|
||||
|
||||
|
||||
# OK - implicit None early return
|
||||
def foo(obj: object) -> None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Args:
|
||||
obj (object): An object.
|
||||
"""
|
||||
if obj is None:
|
||||
return
|
||||
print(obj)
|
||||
|
||||
|
||||
# OK - explicit None early return
|
||||
def foo(obj: object) -> None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Args:
|
||||
obj (object): An object.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
print(obj)
|
||||
|
||||
|
||||
# OK - explicit None early return w/o useful type annotations
|
||||
def foo(obj):
|
||||
"""A very helpful docstring.
|
||||
|
||||
Args:
|
||||
obj (object): An object.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
print(obj)
|
||||
|
||||
|
||||
# OK - multiple explicit None early returns
|
||||
def foo(obj: object) -> None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Args:
|
||||
obj (object): An object.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
if obj == "None":
|
||||
return
|
||||
if obj == 0:
|
||||
return None
|
||||
print(obj)
|
||||
|
||||
|
||||
# DOC201 - non-early return explicit None
|
||||
def foo(x: int) -> int | None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Args:
|
||||
x (int): An interger.
|
||||
"""
|
||||
if x < 0:
|
||||
return None
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
# DOC201 - non-early return explicit None w/o useful type annotations
|
||||
def foo(x):
|
||||
"""A very helpful docstring.
|
||||
|
||||
Args:
|
||||
x (int): An interger.
|
||||
"""
|
||||
if x < 0:
|
||||
return None
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
# DOC201 - only returns None, but return annotation is not None
|
||||
def foo(s: str) -> str | None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Args:
|
||||
s (str): A string.
|
||||
"""
|
||||
return None
|
||||
|
||||
@@ -85,3 +85,105 @@ class A(metaclass=abc.abcmeta):
|
||||
def f(self):
|
||||
"""Lorem ipsum."""
|
||||
return True
|
||||
|
||||
|
||||
# OK - implicit None early return
|
||||
def foo(obj: object) -> None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : object
|
||||
An object.
|
||||
"""
|
||||
if obj is None:
|
||||
return
|
||||
print(obj)
|
||||
|
||||
|
||||
# OK - explicit None early return
|
||||
def foo(obj: object) -> None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : object
|
||||
An object.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
print(obj)
|
||||
|
||||
|
||||
# OK - explicit None early return w/o useful type annotations
|
||||
def foo(obj):
|
||||
"""A very helpful docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : object
|
||||
An object.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
print(obj)
|
||||
|
||||
|
||||
# OK - multiple explicit None early returns
|
||||
def foo(obj: object) -> None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : object
|
||||
An object.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
if obj == "None":
|
||||
return
|
||||
if obj == 0:
|
||||
return None
|
||||
print(obj)
|
||||
|
||||
|
||||
# DOC201 - non-early return explicit None
|
||||
def foo(x: int) -> int | None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : int
|
||||
An interger.
|
||||
"""
|
||||
if x < 0:
|
||||
return None
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
# DOC201 - non-early return explicit None w/o useful type annotations
|
||||
def foo(x):
|
||||
"""A very helpful docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : int
|
||||
An interger.
|
||||
"""
|
||||
if x < 0:
|
||||
return None
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
# DOC201 - only returns None, but return annotation is not None
|
||||
def foo(s: str) -> str | None:
|
||||
"""A very helpful docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : str
|
||||
A string.
|
||||
"""
|
||||
return None
|
||||
|
||||
@@ -42,3 +42,16 @@ max(1, max(*a))
|
||||
|
||||
import builtins
|
||||
builtins.min(1, min(2, 3))
|
||||
|
||||
|
||||
# PLW3301
|
||||
max_word_len = max(
|
||||
max(len(word) for word in "blah blah blah".split(" ")),
|
||||
len("Done!"),
|
||||
)
|
||||
|
||||
# OK
|
||||
max_word_len = max(
|
||||
*(len(word) for word in "blah blah blah".split(" ")),
|
||||
len("Done!"),
|
||||
)
|
||||
|
||||
@@ -9,3 +9,13 @@ dictionary = {
|
||||
|
||||
|
||||
#import os # noqa: E501
|
||||
|
||||
def f():
|
||||
data = 1
|
||||
# line below should autofix to `return data # fmt: skip`
|
||||
return data # noqa: RET504 # fmt: skip
|
||||
|
||||
def f():
|
||||
data = 1
|
||||
# line below should autofix to `return data`
|
||||
return data # noqa: RET504 - intentional incorrect noqa, will be removed
|
||||
|
||||
@@ -70,11 +70,11 @@ pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &mut Check
|
||||
}
|
||||
if let Some(name) = name {
|
||||
if checker.enabled(Rule::AmbiguousVariableName) {
|
||||
if let Some(diagnostic) =
|
||||
pycodestyle::rules::ambiguous_variable_name(name.as_str(), name.range())
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
pycodestyle::rules::ambiguous_variable_name(
|
||||
checker,
|
||||
name.as_str(),
|
||||
name.range(),
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::BuiltinVariableShadowing) {
|
||||
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
|
||||
|
||||
@@ -259,11 +259,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::AmbiguousVariableName) {
|
||||
if let Some(diagnostic) =
|
||||
pycodestyle::rules::ambiguous_variable_name(id, expr.range())
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
pycodestyle::rules::ambiguous_variable_name(checker, id, expr.range());
|
||||
}
|
||||
if !checker.semantic.current_scope().kind.is_class() {
|
||||
if checker.enabled(Rule::BuiltinVariableShadowing) {
|
||||
@@ -883,7 +879,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
flake8_simplify::rules::use_capital_environment_variables(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::OpenFileWithContextHandler) {
|
||||
flake8_simplify::rules::open_file_with_context_handler(checker, func);
|
||||
flake8_simplify::rules::open_file_with_context_handler(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::DictGetWithNoneDefault) {
|
||||
flake8_simplify::rules::dict_get_with_none_default(checker, expr);
|
||||
|
||||
@@ -8,11 +8,11 @@ use crate::rules::{flake8_builtins, pep8_naming, pycodestyle};
|
||||
/// Run lint rules over a [`Parameter`] syntax node.
|
||||
pub(crate) fn parameter(parameter: &Parameter, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::AmbiguousVariableName) {
|
||||
if let Some(diagnostic) =
|
||||
pycodestyle::rules::ambiguous_variable_name(¶meter.name, parameter.name.range())
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
pycodestyle::rules::ambiguous_variable_name(
|
||||
checker,
|
||||
¶meter.name,
|
||||
parameter.name.range(),
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::InvalidArgumentName) {
|
||||
if let Some(diagnostic) = pep8_naming::rules::invalid_argument_name(
|
||||
|
||||
@@ -24,16 +24,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
pylint::rules::global_at_module_level(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::AmbiguousVariableName) {
|
||||
checker.diagnostics.extend(names.iter().filter_map(|name| {
|
||||
pycodestyle::rules::ambiguous_variable_name(name, name.range())
|
||||
}));
|
||||
for name in names {
|
||||
pycodestyle::rules::ambiguous_variable_name(checker, name, name.range());
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::Nonlocal(nonlocal @ ast::StmtNonlocal { names, range: _ }) => {
|
||||
if checker.enabled(Rule::AmbiguousVariableName) {
|
||||
checker.diagnostics.extend(names.iter().filter_map(|name| {
|
||||
pycodestyle::rules::ambiguous_variable_name(name, name.range())
|
||||
}));
|
||||
for name in names {
|
||||
pycodestyle::rules::ambiguous_variable_name(checker, name, name.range());
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::NonlocalWithoutBinding) {
|
||||
if !checker.semantic.scope_id.is_global() {
|
||||
|
||||
@@ -118,10 +118,10 @@ pub(crate) fn check_noqa(
|
||||
match &line.directive {
|
||||
Directive::All(directive) => {
|
||||
if line.matches.is_empty() {
|
||||
let edit = delete_comment(directive.range(), locator);
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
|
||||
diagnostic
|
||||
.set_fix(Fix::safe_edit(delete_comment(directive.range(), locator)));
|
||||
diagnostic.set_fix(Fix::safe_edit(edit));
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -172,6 +172,14 @@ pub(crate) fn check_noqa(
|
||||
&& unknown_codes.is_empty()
|
||||
&& unmatched_codes.is_empty())
|
||||
{
|
||||
let edit = if valid_codes.is_empty() {
|
||||
delete_comment(directive.range(), locator)
|
||||
} else {
|
||||
Edit::range_replacement(
|
||||
format!("# noqa: {}", valid_codes.join(", ")),
|
||||
directive.range(),
|
||||
)
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnusedNOQA {
|
||||
codes: Some(UnusedCodes {
|
||||
@@ -195,17 +203,7 @@ pub(crate) fn check_noqa(
|
||||
},
|
||||
directive.range(),
|
||||
);
|
||||
if valid_codes.is_empty() {
|
||||
diagnostic.set_fix(Fix::safe_edit(delete_comment(
|
||||
directive.range(),
|
||||
locator,
|
||||
)));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
format!("# noqa: {}", valid_codes.join(", ")),
|
||||
directive.range(),
|
||||
)));
|
||||
}
|
||||
diagnostic.set_fix(Fix::safe_edit(edit));
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,11 +99,8 @@ pub(crate) fn delete_comment(range: TextRange, locator: &Locator) -> Edit {
|
||||
}
|
||||
// Ex) `x = 1 # noqa here`
|
||||
else {
|
||||
// Replace `# noqa here` with `# here`.
|
||||
Edit::range_replacement(
|
||||
"# ".to_string(),
|
||||
TextRange::new(range.start(), range.end() + trailing_space_len),
|
||||
)
|
||||
// Remove `# noqa here` and whitespace
|
||||
Edit::deletion(range.start() - leading_space_len, line_range.end())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::helpers::map_callable;
|
||||
@@ -59,14 +59,16 @@ use crate::settings::types::PythonVersion;
|
||||
#[violation]
|
||||
pub struct FastApiNonAnnotatedDependency;
|
||||
|
||||
impl AlwaysFixableViolation for FastApiNonAnnotatedDependency {
|
||||
impl Violation for FastApiNonAnnotatedDependency {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("FastAPI dependency without `Annotated`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Replace with `Annotated`".to_string()
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Annotated`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,64 +77,95 @@ pub(crate) fn fastapi_non_annotated_dependency(
|
||||
checker: &mut Checker,
|
||||
function_def: &ast::StmtFunctionDef,
|
||||
) {
|
||||
if !checker.semantic().seen_module(Modules::FASTAPI) {
|
||||
return;
|
||||
}
|
||||
if !is_fastapi_route(function_def, checker.semantic()) {
|
||||
if !checker.semantic().seen_module(Modules::FASTAPI)
|
||||
|| !is_fastapi_route(function_def, checker.semantic())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut updatable_count = 0;
|
||||
let mut has_non_updatable_default = false;
|
||||
let total_params = function_def.parameters.args.len();
|
||||
|
||||
for parameter in &function_def.parameters.args {
|
||||
if let (Some(annotation), Some(default)) =
|
||||
(¶meter.parameter.annotation, ¶meter.default)
|
||||
{
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(map_callable(default))
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
[
|
||||
"fastapi",
|
||||
"Query"
|
||||
| "Path"
|
||||
| "Body"
|
||||
| "Cookie"
|
||||
| "Header"
|
||||
| "File"
|
||||
| "Form"
|
||||
| "Depends"
|
||||
| "Security"
|
||||
]
|
||||
)
|
||||
})
|
||||
{
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(FastApiNonAnnotatedDependency, parameter.range);
|
||||
let needs_update = matches!(
|
||||
(¶meter.parameter.annotation, ¶meter.default),
|
||||
(Some(_annotation), Some(default)) if is_fastapi_dependency(checker, default)
|
||||
);
|
||||
|
||||
diagnostic.try_set_fix(|| {
|
||||
let module = if checker.settings.target_version >= PythonVersion::Py39 {
|
||||
"typing"
|
||||
} else {
|
||||
"typing_extensions"
|
||||
};
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from(module, "Annotated"),
|
||||
function_def.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let content = format!(
|
||||
"{}: {}[{}, {}]",
|
||||
parameter.parameter.name.id,
|
||||
binding,
|
||||
checker.locator().slice(annotation.range()),
|
||||
checker.locator().slice(default.range())
|
||||
);
|
||||
let parameter_edit = Edit::range_replacement(content, parameter.range());
|
||||
Ok(Fix::unsafe_edits(import_edit, [parameter_edit]))
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
if needs_update {
|
||||
updatable_count += 1;
|
||||
// Determine if it's safe to update this parameter:
|
||||
// - if all parameters are updatable its safe.
|
||||
// - if we've encountered a non-updatable parameter with a default value, it's no longer
|
||||
// safe. (https://github.com/astral-sh/ruff/issues/12982)
|
||||
let safe_to_update = updatable_count == total_params || !has_non_updatable_default;
|
||||
create_diagnostic(checker, parameter, safe_to_update);
|
||||
} else if parameter.default.is_some() {
|
||||
has_non_updatable_default = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_fastapi_dependency(checker: &Checker, expr: &ast::Expr) -> bool {
|
||||
checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(map_callable(expr))
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
[
|
||||
"fastapi",
|
||||
"Query"
|
||||
| "Path"
|
||||
| "Body"
|
||||
| "Cookie"
|
||||
| "Header"
|
||||
| "File"
|
||||
| "Form"
|
||||
| "Depends"
|
||||
| "Security"
|
||||
]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn create_diagnostic(
|
||||
checker: &mut Checker,
|
||||
parameter: &ast::ParameterWithDefault,
|
||||
safe_to_update: bool,
|
||||
) {
|
||||
let mut diagnostic = Diagnostic::new(FastApiNonAnnotatedDependency, parameter.range);
|
||||
|
||||
if safe_to_update {
|
||||
if let (Some(annotation), Some(default)) =
|
||||
(¶meter.parameter.annotation, ¶meter.default)
|
||||
{
|
||||
diagnostic.try_set_fix(|| {
|
||||
let module = if checker.settings.target_version >= PythonVersion::Py39 {
|
||||
"typing"
|
||||
} else {
|
||||
"typing_extensions"
|
||||
};
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from(module, "Annotated"),
|
||||
parameter.range.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let content = format!(
|
||||
"{}: {}[{}, {}]",
|
||||
parameter.parameter.name.id,
|
||||
binding,
|
||||
checker.locator().slice(annotation.range()),
|
||||
checker.locator().slice(default.range())
|
||||
);
|
||||
let parameter_edit = Edit::range_replacement(content, parameter.range);
|
||||
Ok(Fix::unsafe_edits(import_edit, [parameter_edit]))
|
||||
});
|
||||
}
|
||||
} else {
|
||||
diagnostic.fix = None;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -261,3 +261,72 @@ FAST002.py:38:5: FAST002 [*] FastAPI dependency without `Annotated`
|
||||
39 40 | ):
|
||||
40 41 | # do stuff
|
||||
41 42 | pass
|
||||
|
||||
FAST002.py:47:5: FAST002 [*] FastAPI dependency without `Annotated`
|
||||
|
|
||||
45 | skip: int,
|
||||
46 | limit: int,
|
||||
47 | current_user: User = Depends(get_current_user),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
|
||||
48 | ):
|
||||
49 | pass
|
||||
|
|
||||
= help: Replace with `Annotated`
|
||||
|
||||
ℹ Unsafe fix
|
||||
12 12 | Security,
|
||||
13 13 | )
|
||||
14 14 | from pydantic import BaseModel
|
||||
15 |+from typing import Annotated
|
||||
15 16 |
|
||||
16 17 | app = FastAPI()
|
||||
17 18 | router = APIRouter()
|
||||
--------------------------------------------------------------------------------
|
||||
44 45 | def get_users(
|
||||
45 46 | skip: int,
|
||||
46 47 | limit: int,
|
||||
47 |- current_user: User = Depends(get_current_user),
|
||||
48 |+ current_user: Annotated[User, Depends(get_current_user)],
|
||||
48 49 | ):
|
||||
49 50 | pass
|
||||
50 51 |
|
||||
|
||||
FAST002.py:53:5: FAST002 [*] FastAPI dependency without `Annotated`
|
||||
|
|
||||
51 | @app.get("/users/")
|
||||
52 | def get_users(
|
||||
53 | current_user: User = Depends(get_current_user),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
|
||||
54 | skip: int = 0,
|
||||
55 | limit: int = 10,
|
||||
|
|
||||
= help: Replace with `Annotated`
|
||||
|
||||
ℹ Unsafe fix
|
||||
12 12 | Security,
|
||||
13 13 | )
|
||||
14 14 | from pydantic import BaseModel
|
||||
15 |+from typing import Annotated
|
||||
15 16 |
|
||||
16 17 | app = FastAPI()
|
||||
17 18 | router = APIRouter()
|
||||
--------------------------------------------------------------------------------
|
||||
50 51 |
|
||||
51 52 | @app.get("/users/")
|
||||
52 53 | def get_users(
|
||||
53 |- current_user: User = Depends(get_current_user),
|
||||
54 |+ current_user: Annotated[User, Depends(get_current_user)],
|
||||
54 55 | skip: int = 0,
|
||||
55 56 | limit: int = 10,
|
||||
56 57 | ):
|
||||
|
||||
FAST002.py:67:5: FAST002 FastAPI dependency without `Annotated`
|
||||
|
|
||||
65 | skip: int = 0,
|
||||
66 | limit: int = 10,
|
||||
67 | current_user: User = Depends(get_current_user),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
|
||||
68 | ):
|
||||
69 | pass
|
||||
|
|
||||
= help: Replace with `Annotated`
|
||||
|
||||
@@ -11,6 +11,7 @@ mod tests {
|
||||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::types::PythonVersion;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::test::test_path;
|
||||
|
||||
@@ -36,4 +37,18 @@ mod tests {
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("ASYNC109_0.py"); "asyncio")]
|
||||
#[test_case(Path::new("ASYNC109_1.py"); "trio")]
|
||||
fn async109_python_310_or_older(path: &Path) -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_async").join(path),
|
||||
&LinterSettings {
|
||||
target_version: PythonVersion::Py310,
|
||||
..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(path.file_name().unwrap().to_str().unwrap(), diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_async::helpers::AsyncModule;
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `async` functions with a `timeout` argument.
|
||||
@@ -86,6 +87,11 @@ pub(crate) fn async_function_with_timeout(
|
||||
AsyncModule::AsyncIo
|
||||
};
|
||||
|
||||
// asyncio.timeout feature was first introduced in Python 3.11
|
||||
if module == AsyncModule::AsyncIo && checker.settings.target_version < PythonVersion::Py311 {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
AsyncFunctionWithTimeout { module },
|
||||
timeout.range(),
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC109_0.py:8:16: ASYNC109 Async function definition with a `timeout` parameter
|
||||
|
|
||||
8 | async def func(timeout):
|
||||
| ^^^^^^^ ASYNC109
|
||||
9 | ...
|
||||
|
|
||||
= help: Use `trio.fail_after` instead
|
||||
|
||||
ASYNC109_0.py:12:16: ASYNC109 Async function definition with a `timeout` parameter
|
||||
|
|
||||
12 | async def func(timeout=10):
|
||||
| ^^^^^^^^^^ ASYNC109
|
||||
13 | ...
|
||||
|
|
||||
= help: Use `trio.fail_after` instead
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
@@ -172,9 +174,16 @@ fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator
|
||||
return None;
|
||||
}
|
||||
|
||||
let a_body = &a_text[a_leading_quote.len()..a_text.len() - a_trailing_quote.len()];
|
||||
let mut a_body =
|
||||
Cow::Borrowed(&a_text[a_leading_quote.len()..a_text.len() - a_trailing_quote.len()]);
|
||||
let b_body = &b_text[b_leading_quote.len()..b_text.len() - b_trailing_quote.len()];
|
||||
|
||||
if a_leading_quote.find(['r', 'R']).is_none()
|
||||
&& matches!(b_body.bytes().next(), Some(b'0'..=b'7'))
|
||||
{
|
||||
normalize_ending_octal(&mut a_body);
|
||||
}
|
||||
|
||||
let concatenation = format!("{a_leading_quote}{a_body}{b_body}{a_trailing_quote}");
|
||||
let range = TextRange::new(a_range.start(), b_range.end());
|
||||
|
||||
@@ -183,3 +192,39 @@ fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator
|
||||
range,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Pads an octal at the end of the string
|
||||
/// to three digits, if necessary.
|
||||
fn normalize_ending_octal(text: &mut Cow<'_, str>) {
|
||||
// Early return for short strings
|
||||
if text.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut rev_bytes = text.bytes().rev();
|
||||
if let Some(last_byte @ b'0'..=b'7') = rev_bytes.next() {
|
||||
// "\y" -> "\00y"
|
||||
if has_odd_consecutive_backslashes(&mut rev_bytes.clone()) {
|
||||
let prefix = &text[..text.len() - 2];
|
||||
*text = Cow::Owned(format!("{prefix}\\00{}", last_byte as char));
|
||||
}
|
||||
// "\xy" -> "\0xy"
|
||||
else if let Some(penultimate_byte @ b'0'..=b'7') = rev_bytes.next() {
|
||||
if has_odd_consecutive_backslashes(&mut rev_bytes.clone()) {
|
||||
let prefix = &text[..text.len() - 3];
|
||||
*text = Cow::Owned(format!(
|
||||
"{prefix}\\0{}{}",
|
||||
penultimate_byte as char, last_byte as char
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_odd_consecutive_backslashes(mut itr: impl Iterator<Item = u8>) -> bool {
|
||||
let mut odd_backslashes = false;
|
||||
while let Some(b'\\') = itr.next() {
|
||||
odd_backslashes = !odd_backslashes;
|
||||
}
|
||||
odd_backslashes
|
||||
}
|
||||
|
||||
@@ -296,4 +296,202 @@ ISC.py:73:20: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
75 75 | f"def"} g"
|
||||
76 76 |
|
||||
|
||||
ISC.py:84:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 | _ = "\12""0" # fix should be "\0120"
|
||||
| ^^^^^^^^ ISC001
|
||||
85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
81 81 | + f"second"} d"
|
||||
82 82 |
|
||||
83 83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 |-_ = "\12""0" # fix should be "\0120"
|
||||
84 |+_ = "\0120" # fix should be "\0120"
|
||||
85 85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
|
||||
ISC.py:85:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 | _ = "\12""0" # fix should be "\0120"
|
||||
85 | _ = "\\12""0" # fix should be "\\120"
|
||||
| ^^^^^^^^^ ISC001
|
||||
86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
82 82 |
|
||||
83 83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 84 | _ = "\12""0" # fix should be "\0120"
|
||||
85 |-_ = "\\12""0" # fix should be "\\120"
|
||||
85 |+_ = "\\120" # fix should be "\\120"
|
||||
86 86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
|
||||
ISC.py:86:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
84 | _ = "\12""0" # fix should be "\0120"
|
||||
85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
| ^^^^^^^^^^ ISC001
|
||||
87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
83 83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 84 | _ = "\12""0" # fix should be "\0120"
|
||||
85 85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 |-_ = "\\\12""0" # fix should be "\\\0120"
|
||||
86 |+_ = "\\\0120" # fix should be "\\\0120"
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
|
||||
ISC.py:87:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
| ^^^^^^^^^^ ISC001
|
||||
88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
84 84 | _ = "\12""0" # fix should be "\0120"
|
||||
85 85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 |-_ = "\12 0""0" # fix should be "\12 00"
|
||||
87 |+_ = "\12 00" # fix should be "\12 00"
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
|
||||
ISC.py:88:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
| ^^^^^^^^^^ ISC001
|
||||
89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 | _ = "\8""0" # fix should be "\80"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
85 85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 |-_ = r"\12"r"0" # fix should be r"\120"
|
||||
88 |+_ = r"\120" # fix should be r"\120"
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
91 91 | _ = "\12""8" # fix should be "\128"
|
||||
|
||||
ISC.py:89:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
| ^^^^^^^^^^^^^^^^^ ISC001
|
||||
90 | _ = "\8""0" # fix should be "\80"
|
||||
91 | _ = "\12""8" # fix should be "\128"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
86 86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 |-_ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
89 |+_ = "\12 and more0" # fix should be "\12 and more0"
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
91 91 | _ = "\12""8" # fix should be "\128"
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
|
||||
ISC.py:90:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 | _ = "\8""0" # fix should be "\80"
|
||||
| ^^^^^^^ ISC001
|
||||
91 | _ = "\12""8" # fix should be "\128"
|
||||
92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 |-_ = "\8""0" # fix should be "\80"
|
||||
90 |+_ = "\80" # fix should be "\80"
|
||||
91 91 | _ = "\12""8" # fix should be "\128"
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 93 | _ = "\12" "" # fix should be "\12"
|
||||
|
||||
ISC.py:91:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 | _ = "\8""0" # fix should be "\80"
|
||||
91 | _ = "\12""8" # fix should be "\128"
|
||||
| ^^^^^^^^ ISC001
|
||||
92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 | _ = "\12" "" # fix should be "\12"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
91 |-_ = "\12""8" # fix should be "\128"
|
||||
91 |+_ = "\128" # fix should be "\128"
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 93 | _ = "\12" "" # fix should be "\12"
|
||||
|
||||
ISC.py:92:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
90 | _ = "\8""0" # fix should be "\80"
|
||||
91 | _ = "\12""8" # fix should be "\128"
|
||||
92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
| ^^^^^^^^^^ ISC001
|
||||
93 | _ = "\12" "" # fix should be "\12"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
91 91 | _ = "\12""8" # fix should be "\128"
|
||||
92 |-_ = "\12""foo" # fix should be "\12foo"
|
||||
92 |+_ = "\12foo" # fix should be "\12foo"
|
||||
93 93 | _ = "\12" "" # fix should be "\12"
|
||||
|
||||
ISC.py:93:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
91 | _ = "\12""8" # fix should be "\128"
|
||||
92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 | _ = "\12" "" # fix should be "\12"
|
||||
| ^^^^^^^^ ISC001
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
91 91 | _ = "\12""8" # fix should be "\128"
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 |-_ = "\12" "" # fix should be "\12"
|
||||
93 |+_ = "\12" # fix should be "\12"
|
||||
|
||||
@@ -50,6 +50,6 @@ ISC.py:80:10: ISC003 Explicitly concatenated string should be implicitly concate
|
||||
| __________^
|
||||
81 | | + f"second"} d"
|
||||
| |_______________^ ISC003
|
||||
82 |
|
||||
83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -296,4 +296,202 @@ ISC.py:73:20: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
75 75 | f"def"} g"
|
||||
76 76 |
|
||||
|
||||
ISC.py:84:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 | _ = "\12""0" # fix should be "\0120"
|
||||
| ^^^^^^^^ ISC001
|
||||
85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
81 81 | + f"second"} d"
|
||||
82 82 |
|
||||
83 83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 |-_ = "\12""0" # fix should be "\0120"
|
||||
84 |+_ = "\0120" # fix should be "\0120"
|
||||
85 85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
|
||||
ISC.py:85:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 | _ = "\12""0" # fix should be "\0120"
|
||||
85 | _ = "\\12""0" # fix should be "\\120"
|
||||
| ^^^^^^^^^ ISC001
|
||||
86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
82 82 |
|
||||
83 83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 84 | _ = "\12""0" # fix should be "\0120"
|
||||
85 |-_ = "\\12""0" # fix should be "\\120"
|
||||
85 |+_ = "\\120" # fix should be "\\120"
|
||||
86 86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
|
||||
ISC.py:86:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
84 | _ = "\12""0" # fix should be "\0120"
|
||||
85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
| ^^^^^^^^^^ ISC001
|
||||
87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
83 83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 84 | _ = "\12""0" # fix should be "\0120"
|
||||
85 85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 |-_ = "\\\12""0" # fix should be "\\\0120"
|
||||
86 |+_ = "\\\0120" # fix should be "\\\0120"
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
|
||||
ISC.py:87:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
| ^^^^^^^^^^ ISC001
|
||||
88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
84 84 | _ = "\12""0" # fix should be "\0120"
|
||||
85 85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 |-_ = "\12 0""0" # fix should be "\12 00"
|
||||
87 |+_ = "\12 00" # fix should be "\12 00"
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
|
||||
ISC.py:88:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
| ^^^^^^^^^^ ISC001
|
||||
89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 | _ = "\8""0" # fix should be "\80"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
85 85 | _ = "\\12""0" # fix should be "\\120"
|
||||
86 86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 |-_ = r"\12"r"0" # fix should be r"\120"
|
||||
88 |+_ = r"\120" # fix should be r"\120"
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
91 91 | _ = "\12""8" # fix should be "\128"
|
||||
|
||||
ISC.py:89:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
| ^^^^^^^^^^^^^^^^^ ISC001
|
||||
90 | _ = "\8""0" # fix should be "\80"
|
||||
91 | _ = "\12""8" # fix should be "\128"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
86 86 | _ = "\\\12""0" # fix should be "\\\0120"
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 |-_ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
89 |+_ = "\12 and more0" # fix should be "\12 and more0"
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
91 91 | _ = "\12""8" # fix should be "\128"
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
|
||||
ISC.py:90:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 | _ = "\8""0" # fix should be "\80"
|
||||
| ^^^^^^^ ISC001
|
||||
91 | _ = "\12""8" # fix should be "\128"
|
||||
92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
87 87 | _ = "\12 0""0" # fix should be "\12 00"
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 |-_ = "\8""0" # fix should be "\80"
|
||||
90 |+_ = "\80" # fix should be "\80"
|
||||
91 91 | _ = "\12""8" # fix should be "\128"
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 93 | _ = "\12" "" # fix should be "\12"
|
||||
|
||||
ISC.py:91:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 | _ = "\8""0" # fix should be "\80"
|
||||
91 | _ = "\12""8" # fix should be "\128"
|
||||
| ^^^^^^^^ ISC001
|
||||
92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 | _ = "\12" "" # fix should be "\12"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
88 88 | _ = r"\12"r"0" # fix should be r"\120"
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
91 |-_ = "\12""8" # fix should be "\128"
|
||||
91 |+_ = "\128" # fix should be "\128"
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 93 | _ = "\12" "" # fix should be "\12"
|
||||
|
||||
ISC.py:92:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
90 | _ = "\8""0" # fix should be "\80"
|
||||
91 | _ = "\12""8" # fix should be "\128"
|
||||
92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
| ^^^^^^^^^^ ISC001
|
||||
93 | _ = "\12" "" # fix should be "\12"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
89 89 | _ = "\12 and more""0" # fix should be "\12 and more0"
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
91 91 | _ = "\12""8" # fix should be "\128"
|
||||
92 |-_ = "\12""foo" # fix should be "\12foo"
|
||||
92 |+_ = "\12foo" # fix should be "\12foo"
|
||||
93 93 | _ = "\12" "" # fix should be "\12"
|
||||
|
||||
ISC.py:93:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
91 | _ = "\12""8" # fix should be "\128"
|
||||
92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 | _ = "\12" "" # fix should be "\12"
|
||||
| ^^^^^^^^ ISC001
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Safe fix
|
||||
90 90 | _ = "\8""0" # fix should be "\80"
|
||||
91 91 | _ = "\12""8" # fix should be "\128"
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 |-_ = "\12" "" # fix should be "\12"
|
||||
93 |+_ = "\12" # fix should be "\12"
|
||||
|
||||
@@ -27,14 +27,14 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// class Foo:
|
||||
/// def __eq__(self, obj: typing.Any) -> bool: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// class Foo:
|
||||
/// def __eq__(self, obj: object) -> bool: ...
|
||||
/// ```
|
||||
|
||||
@@ -34,19 +34,17 @@ use crate::registry::Rule;
|
||||
/// ```
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// import sys
|
||||
///
|
||||
/// if sys.version_info > (3, 8):
|
||||
/// ...
|
||||
/// if sys.version_info > (3, 8): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// import sys
|
||||
///
|
||||
/// if sys.version_info >= (3, 9):
|
||||
/// ...
|
||||
/// if sys.version_info >= (3, 9): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct BadVersionInfoComparison;
|
||||
@@ -70,27 +68,23 @@ impl Violation for BadVersionInfoComparison {
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// import sys
|
||||
///
|
||||
/// if sys.version_info < (3, 10):
|
||||
///
|
||||
/// def read_data(x, *, preserve_order=True): ...
|
||||
///
|
||||
/// else:
|
||||
///
|
||||
/// def read_data(x): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// if sys.version_info >= (3, 10):
|
||||
///
|
||||
/// def read_data(x): ...
|
||||
///
|
||||
/// else:
|
||||
///
|
||||
/// def read_data(x, *, preserve_order=True): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
|
||||
@@ -21,17 +21,16 @@ use crate::checkers::ast::Checker;
|
||||
/// precisely.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from collections import namedtuple
|
||||
///
|
||||
/// person = namedtuple("Person", ["name", "age"])
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import NamedTuple
|
||||
///
|
||||
///
|
||||
/// class Person(NamedTuple):
|
||||
/// name: str
|
||||
/// age: int
|
||||
|
||||
@@ -20,27 +20,24 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import TypeAlias
|
||||
///
|
||||
/// a = b = int
|
||||
///
|
||||
///
|
||||
/// class Klass: ...
|
||||
///
|
||||
///
|
||||
/// Klass.X: TypeAlias = int
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import TypeAlias
|
||||
///
|
||||
/// a: TypeAlias = int
|
||||
/// b: TypeAlias = int
|
||||
///
|
||||
///
|
||||
/// class Klass:
|
||||
/// X: TypeAlias = int
|
||||
/// ```
|
||||
|
||||
@@ -16,19 +16,17 @@ use crate::checkers::ast::Checker;
|
||||
/// analyze your code.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// import sys
|
||||
///
|
||||
/// if (3, 10) <= sys.version_info < (3, 12):
|
||||
/// ...
|
||||
/// if (3, 10) <= sys.version_info < (3, 12): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// import sys
|
||||
///
|
||||
/// if sys.version_info >= (3, 10) and sys.version_info < (3, 12):
|
||||
/// ...
|
||||
/// if sys.version_info >= (3, 10) and sys.version_info < (3, 12): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -25,27 +25,22 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// class Foo:
|
||||
/// def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ...
|
||||
///
|
||||
/// def foo(self: _S, arg: bytes) -> _S: ...
|
||||
///
|
||||
/// @classmethod
|
||||
/// def bar(cls: type[_S], arg: int) -> _S: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import Self
|
||||
///
|
||||
///
|
||||
/// class Foo:
|
||||
/// def __new__(cls, *args: str, **kwargs: int) -> Self: ...
|
||||
///
|
||||
/// def foo(self, arg: bytes) -> Self: ...
|
||||
///
|
||||
/// @classmethod
|
||||
/// def bar(cls, arg: int) -> Self: ...
|
||||
/// ```
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def func(param: int) -> str:
|
||||
/// """This is a docstring."""
|
||||
/// ...
|
||||
@@ -23,7 +23,7 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def func(param: int) -> str: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
|
||||
@@ -15,14 +15,14 @@ use crate::fix;
|
||||
/// is redundant.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// class Foo:
|
||||
/// ...
|
||||
/// value: int
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// class Foo:
|
||||
/// value: int
|
||||
/// ```
|
||||
|
||||
@@ -24,10 +24,9 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from types import TracebackType
|
||||
///
|
||||
///
|
||||
/// class Foo:
|
||||
/// def __exit__(
|
||||
/// self, typ: BaseException, exc: BaseException, tb: TracebackType
|
||||
@@ -36,10 +35,9 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from types import TracebackType
|
||||
///
|
||||
///
|
||||
/// class Foo:
|
||||
/// def __exit__(
|
||||
/// self,
|
||||
|
||||
@@ -22,14 +22,14 @@ use crate::settings::types::PythonVersion::Py311;
|
||||
/// members).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import NoReturn
|
||||
///
|
||||
/// def foo(x: NoReturn): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import Never
|
||||
///
|
||||
/// def foo(x: Never): ...
|
||||
|
||||
@@ -15,13 +15,13 @@ use crate::checkers::ast::Checker;
|
||||
/// for this purpose.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def double(x: int) -> int:
|
||||
/// return x * 2
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def double(x: int) -> int: ...
|
||||
/// ```
|
||||
///
|
||||
|
||||
@@ -51,30 +51,23 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// class Foo:
|
||||
/// def __new__(cls, *args: Any, **kwargs: Any) -> Foo: ...
|
||||
///
|
||||
/// def __enter__(self) -> Foo: ...
|
||||
///
|
||||
/// async def __aenter__(self) -> Foo: ...
|
||||
///
|
||||
/// def __iadd__(self, other: Foo) -> Foo: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing_extensions import Self
|
||||
///
|
||||
///
|
||||
/// class Foo:
|
||||
/// def __new__(cls, *args: Any, **kwargs: Any) -> Self: ...
|
||||
///
|
||||
/// def __enter__(self) -> Self: ...
|
||||
///
|
||||
/// async def __aenter__(self) -> Self: ...
|
||||
///
|
||||
/// def __iadd__(self, other: Foo) -> Self: ...
|
||||
/// ```
|
||||
/// ## References
|
||||
|
||||
@@ -20,13 +20,13 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(arg: int = 693568516352839939918568862861217771399698285293568) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(arg: int = ...) -> None: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
|
||||
@@ -15,14 +15,14 @@ use crate::fix;
|
||||
/// stubs.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// class MyClass:
|
||||
/// x: int
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// class MyClass:
|
||||
/// x: int
|
||||
/// ```
|
||||
|
||||
@@ -13,12 +13,12 @@ use crate::checkers::ast::Checker;
|
||||
/// in stub files.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(bar: int) -> list[int]: pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(bar: int) -> list[int]: ...
|
||||
/// ```
|
||||
///
|
||||
|
||||
@@ -17,13 +17,13 @@ use crate::settings::types::PythonVersion;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(__x: int) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(x: int, /) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
|
||||
@@ -33,14 +33,14 @@ impl fmt::Display for VarKind {
|
||||
/// internal to the stub.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import TypeVar
|
||||
///
|
||||
/// T = TypeVar("T")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import TypeVar
|
||||
///
|
||||
/// _T = TypeVar("_T")
|
||||
|
||||
@@ -16,13 +16,13 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def function() -> "int": ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def function() -> int: ...
|
||||
/// ```
|
||||
///
|
||||
|
||||
@@ -16,13 +16,13 @@ use crate::fix::snippet::SourceCodeSnippet;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// x: Final[Literal[42]]
|
||||
/// y: Final[Literal[42]] = 42
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// x: Final = 42
|
||||
/// y: Final = 42
|
||||
/// ```
|
||||
|
||||
@@ -24,14 +24,14 @@ use crate::fix::snippet::SourceCodeSnippet;
|
||||
/// supertypes of `"A"` and `1` respectively.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import Literal
|
||||
///
|
||||
/// x: Literal["A", b"B"] | str
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import Literal
|
||||
///
|
||||
/// x: Literal[b"B"] | str
|
||||
|
||||
@@ -27,13 +27,13 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(x: float | int | str) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(x: float | str) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
|
||||
@@ -31,13 +31,13 @@ use crate::settings::types::PythonVersion;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(arg: list[int] = list(range(10_000))) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(arg: list[int] = ...) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
@@ -77,13 +77,13 @@ impl AlwaysFixableViolation for TypedArgumentDefaultInStub {
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(arg=[]) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(arg=...) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
@@ -122,12 +122,12 @@ impl AlwaysFixableViolation for ArgumentDefaultInStub {
|
||||
/// or varies according to the current platform or Python version.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// foo: str = "..."
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// foo: str = ...
|
||||
/// ```
|
||||
///
|
||||
@@ -176,12 +176,12 @@ impl Violation for UnannotatedAssignmentInStub {
|
||||
/// runtime counterparts.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// __all__: list[str]
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// __all__: list[str] = ["foo", "bar"]
|
||||
/// ```
|
||||
#[violation]
|
||||
@@ -210,12 +210,12 @@ impl Violation for UnassignedSpecialVariableInStub {
|
||||
/// to a normal variable assignment.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// Vector = list[float]
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// from typing import TypeAlias
|
||||
///
|
||||
/// Vector: TypeAlias = list[float]
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::fix::edits::delete_stmt;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// class Foo:
|
||||
/// def __repr__(self) -> str: ...
|
||||
/// ```
|
||||
|
||||
@@ -23,13 +23,13 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(arg: str = "51 character stringgggggggggggggggggggggggggggggggg") -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def foo(arg: str = ...) -> None: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def function():
|
||||
/// x = 1
|
||||
/// y = 2
|
||||
@@ -25,7 +25,7 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// ```pyi
|
||||
/// def function(): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user