Compare commits

...

22 Commits

Author SHA1 Message Date
Brent Westbrook
5bc81f26c8 Bump 0.12.3 (#19279) 2025-07-11 09:07:50 -04:00
Brent Westbrook
6908e2682f Filter ruff_linter::VERSION out of SARIF output tests (#19280)
Summary
--

Fixes the test failures in #19279. This is the same variable used to
construct the SARIF output:


350d563c88/crates/ruff_linter/src/message/sarif.rs (L39-L44)

Test Plan
--

Existing tests with the modified filter
2025-07-11 08:55:51 -04:00
Dhruv Manilawala
25c4295564 [ty] Avoid stale diagnostics for open files diagnostic mode (#19273)
## Summary

This PR fixes a bug where in `openFilesOnly` diagnostic mode, VS Code
wouldn't clean up the diagnostics even though the server asked it to by
sending an empty publish diagnostics.

This is not the long-term solution but a quick fix. Ideally, the server
would dynamically register for workspace diagnostics but that requires
listening for `didChangeConfiguration` notification which I'm going to
be working on with https://github.com/astral-sh/ty/issues/82.

## Test Plan

### Before

This uses the latest stable version of ty.


https://github.com/user-attachments/assets/0cc6c513-ccad-4955-a1b6-a0ee242119d6

### After

This uses the debug build of ty from this PR.


https://github.com/user-attachments/assets/e539d569-d852-46a9-bbfc-d54375127c62
2025-07-11 16:29:16 +05:30
Micha Reiser
426fa4bb12 [ty] Add signature help provider to playground (#19276) 2025-07-11 09:58:14 +02:00
UnboundVariable
b0b65c24ff [ty] Initial implementation of signature help provider (#19194)
This PR includes:
* Implemented core signature help logic
* Added new docstring method on Definition that returns a docstring for
function and class definitions
* Modified the display code for Signature that allows a signature string
to be broken into text ranges that correspond to each parameter in the
signature
* Augmented Signature struct so it can track the Definition for a
signature when available; this allows us to find the docstring
associated with the signature
* Added utility functions for parsing parameter documentation from three
popular docstring formats (Google, NumPy and reST)
* Implemented tests for all of the above

"Signature help" is displayed by an editor when you are typing a
function call expression. It is typically triggered when you type an
open parenthesis. The language server provides information about the
target function's signature (or multiple signatures), documentation, and
parameters.

Here is how this appears:


![image](https://github.com/user-attachments/assets/40dce616-ed74-4810-be62-42a5b5e4b334)

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-07-10 19:32:00 -07:00
Brent Westbrook
08bc6d2589 Add simple integration tests for all output formats (#19265)
Summary
--

I spun this off from #19133 to be sure to get an accurate baseline
before modifying any of the formats. I picked the code snippet to
include a lint diagnostic with a fix, one without a fix, and one syntax
error. I'm happy to expand it if there are any other kinds we want to
test.

I initially passed `CONTENT` on stdin, but I was a bit surprised to
notice that some of our output formats include an absolute path to the
file. I switched to a `TempDir` to use the `tempdir_filter`.

Test Plan
--

New CLI tests
2025-07-10 17:57:48 -04:00
Victor Hugo Gomes
f2ae12bab3 [flake8-return] Fix false-positive for variables used inside nested functions in RET504 (#18433)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
This PR is the same as #17656.

I accidentally deleted the branch of that PR, so I'm creating a new one.

Fixes #14052

## Test Plan

Add regression tests
<!-- How was it tested? -->
2025-07-10 16:10:22 -04:00
Zanie Blue
965f415212 [ty] Add a --quiet mode (#19233)
Adds a `--quiet` flag which silences diagnostic, warning logs, and
messages like "all checks passed" while retaining summary messages that
indicate problems, e.g., the number of diagnostics.

I'm a bit on the fence regarding filtering out warning logs, because it
can omit important details, e.g., the message that a fatal diagnostic
was encountered. Let's discuss that in
https://github.com/astral-sh/ruff/pull/19233#discussion_r2195408693

The implementation recycles the `Printer` abstraction used in uv, which
is intended to replace all direct usage of `std::io::stdout`. See
https://github.com/astral-sh/ruff/pull/19233#discussion_r2195140197

I ended up futzing with the progress bar more than I probably should
have to ensure it was also using the printer, but it doesn't seem like a
big deal. See
https://github.com/astral-sh/ruff/pull/19233#discussion_r2195330467

Closes https://github.com/astral-sh/ty/issues/772
2025-07-10 09:40:47 -05:00
frank
83b5bbf004 Treat form feed as valid whitespace before a line continuation (#19220)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-07-10 14:09:34 +00:00
Micha Reiser
87f6f08ef5 [ty] Make check_file a salsa query (#19255)
## Summary
We noticed that all files get reparsed when workspace diagnostics are
enabled.

I realised that this is because `check_file_impl` access the parsed
module but itself isn't a salsa query.
This pr makes `check_file_impl` a salsa query, so that we only access
the `parsed_module` when the file actually changed. I decided to remove
the salsa query from `check_types` because most functions it calls are
salsa queries itself and having both `check_types` and `check_file` as
salsa querise has the downside that we double cache the diagnostics.

## Test Plan

**Before**

```
2025-07-10 12:54:16.620766000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0c))}: File `/Users/micha/astral/test/yaml/yaml-stubs/__init__.pyi` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.621942000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c13))}: File `/Users/micha/astral/test/ignore2 2/nested-repository/main.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.622107000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c09))}: File `/Users/micha/astral/test/notebook.ipynb` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.622357000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c04))}: File `/Users/micha/astral/test/no-trailing.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.622634000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c02))}: File `/Users/micha/astral/test/simple.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.623056000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c07))}: File `/Users/micha/astral/test/open/more.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.623254000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c11))}: File `/Users/micha/astral/test/ignore-bug/backend/src/subdir/log/some_logging_lib.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.623450000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0f))}: File `/Users/micha/astral/test/yaml/tomllib/__init__.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.624599000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c05))}: File `/Users/micha/astral/test/create.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.624784000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c00))}: File `/Users/micha/astral/test/lib.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.624911000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0a))}: File `/Users/micha/astral/test/sub/test.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625032000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c12))}: File `/Users/micha/astral/test/ignore2/nested-repository/main.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625101000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c08))}: File `/Users/micha/astral/test/open/test.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625227000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c03))}: File `/Users/micha/astral/test/pseudocode_with_bom.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625353000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0b))}: File `/Users/micha/astral/test/yaml/yaml-stubs/loader.pyi` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625543000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c01))}: File `/Users/micha/astral/test/test_trailing.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625616000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0d))}: File `/Users/micha/astral/test/yaml/tomllib/_re.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625667000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c06))}: File `/Users/micha/astral/test/yaml/main.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625779000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c10))}: File `/Users/micha/astral/test/yaml/tomllib/_types.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.627526000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0e))}: File `/Users/micha/astral/test/yaml/tomllib/_parser.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.627959000 DEBUG request{id=19 method="workspace/diagnostic"}:Project::check: Checking all files took 0.007s
```

Now, no more logs regarding reparsing
2025-07-10 18:46:56 +05:30
Alex Waygood
59114d0301 [ty] Consolidate submodule resolving code between types.rs and ide_support.rs (#19256) 2025-07-10 13:10:09 +00:00
Micha Reiser
492f5bf2aa [ty] Remove countme from salsa-structs (#19257) 2025-07-10 11:45:09 +00:00
Alex Waygood
934aaa23f3 [ty] Improve and document equivalence for module-literal types (#19243) 2025-07-10 09:11:10 +00:00
Alex Waygood
59aa869724 [ty] Optimize protocol subtyping by removing expensive and unnecessary equivalence check from the top of Type::has_relation_to() (#19230) 2025-07-10 09:42:27 +01:00
David Peter
edaffa6c4f [ty] Ecosystem analyzer: parallelize, fix race condition (#19252)
## Summary

Pulls in two fixes and a performance optimization:

- Fix a bug with the Markdown table formatting.
- Combine the two `analyze` commands into a single `diff` command. This
means we only need to set up the projects once, which is faster and also
avoids a race condition where projects could change between the two
`analyze` runs.
2025-07-10 10:25:24 +02:00
Micha Reiser
5fb2fb916b [ty] Add completion kind to playground (#19251) 2025-07-10 07:41:59 +00:00
David Peter
801f69a7b4 [ty] Deploy ecosystem diff to Cloudflare pages (#19234)
## Summary

Changes the ecosystem-analyzer workflow to deploy the diff to Cloudflare
pages and post a link in the PR. Also adds a summary statistics to that
PR comment.

## Test Plan

The comment below:
https://github.com/astral-sh/ruff/pull/19234#issuecomment-3053205937. I
previously had some dummy changes on this PR to see a non-zero diff. And
I didn't reapply the label after I reverted that change, such that it's
still visible for reviewers.
2025-07-10 09:03:42 +02:00
Micha Reiser
3926dd8424 [ty] Add semantic token provider to playground (#19232) 2025-07-10 07:50:28 +02:00
Faisal
563268ce53 [docs] add capital one to who's using ruff (#19248)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Add Capital One to Who's Using Ruff (README)
Also thanks for the fantastic project!
2025-07-09 23:50:27 +00:00
Dan Parizher
221edcba5c [pyupgrade] Keyword arguments in super should suppress the UP008 fix (#19131)
## Summary

Fixes #19096
2025-07-09 15:13:22 -04:00
chiri
beb98dae7c [flake8-use-pathlib] Add autofixes for PTH100, PTH106, PTH107, PTH108, PTH110, PTH111, PTH112, PTH113, PTH114, PTH115, PTH117, PTH119, PTH120 (#19213)
## Summary

Part of #2331

## Test Plan

update snapshots for preview mode
2025-07-09 14:54:33 -04:00
InSync
05b1b788a0 [ty] Do not run mypy_primer.yaml when all changed files are Markdown files (#19244) 2025-07-09 19:40:43 +01:00
115 changed files with 7509 additions and 1164 deletions

View File

@@ -12,6 +12,7 @@ on:
- ".github/workflows/mypy_primer.yaml"
- ".github/workflows/mypy_primer_comment.yaml"
- "Cargo.lock"
- "!**.md"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}

View File

@@ -17,6 +17,7 @@ env:
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
REF_NAME: ${{ github.ref_name }}
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
jobs:
ty-ecosystem-analyzer:
@@ -63,32 +64,75 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@9c34dc514ee9aef6735db1dfebb80f63acbc3440"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda"
ecosystem-analyzer \
--repository ruff \
analyze \
--projects ruff/projects_old.txt \
--commit old_commit \
--output diagnostics_old.json
diff \
--projects-old ruff/projects_old.txt \
--projects-new ruff/projects_new.txt \
--old old_commit \
--new new_commit \
--output-old diagnostics-old.json \
--output-new diagnostics-new.json
ecosystem-analyzer \
--repository ruff \
analyze \
--projects ruff/projects_new.txt \
--commit new_commit \
--output diagnostics_new.json
mkdir dist
ecosystem-analyzer \
generate-diff \
diagnostics_old.json \
diagnostics_new.json \
diagnostics-old.json \
diagnostics-new.json \
--old-name "main (merge base)" \
--new-name "$REF_NAME" \
--output-html diff.html
--output-html dist/diff.html
- name: Upload HTML diff report
ecosystem-analyzer \
generate-diff-statistics \
diagnostics-old.json \
diagnostics-new.json \
--old-name "main (merge base)" \
--new-name "$REF_NAME" \
--output diff-statistics.md
echo '## `ecosystem-analyzer` results' > comment.md
echo >> comment.md
cat diff-statistics.md >> comment.md
cat diff-statistics.md >> "$GITHUB_STEP_SUMMARY"
echo ${{ github.event.number }} > pr-number
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
id: deploy
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages deploy dist --project-name=ty-ecosystem --branch ${{ github.head_ref }} --commit-hash ${GITHUB_SHA}
- name: "Append deployment URL"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
run: |
echo >> comment.md
echo "**[Full report with detailed diff]($DEPLOYMENT_URL/diff)**" >> comment.md
- name: Upload comment
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: comment.md
path: comment.md
- name: Upload pr-number
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pr-number
path: pr-number
- name: Upload diagnostics diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: diff.html
path: diff.html
path: dist/diff.html

1
.github/zizmor.yml vendored
View File

@@ -10,6 +10,7 @@ rules:
ignore:
- build-docker.yml
- publish-playground.yml
- ty-ecosystem-analyzer.yaml
excessive-permissions:
# it's hard to test what the impact of removing these ignores would be
# without actually running the release workflow...

View File

@@ -1,5 +1,33 @@
# Changelog
## 0.12.3
### Preview features
- \[`flake8-bugbear`\] Support non-context-manager calls in `B017` ([#19063](https://github.com/astral-sh/ruff/pull/19063))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH100`, `PTH106`, `PTH107`, `PTH108`, `PTH110`, `PTH111`, `PTH112`, `PTH113`, `PTH114`, `PTH115`, `PTH117`, `PTH119`, `PTH120` ([#19213](https://github.com/astral-sh/ruff/pull/19213))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH203`, `PTH204`, `PTH205` ([#18922](https://github.com/astral-sh/ruff/pull/18922))
### Bug fixes
- \[`flake8-return`\] Fix false-positive for variables used inside nested functions in `RET504` ([#18433](https://github.com/astral-sh/ruff/pull/18433))
- Treat form feed as valid whitespace before a line continuation ([#19220](https://github.com/astral-sh/ruff/pull/19220))
- \[`flake8-type-checking`\] Fix syntax error introduced by fix (`TC008`) ([#19150](https://github.com/astral-sh/ruff/pull/19150))
- \[`pyupgrade`\] Keyword arguments in `super` should suppress the `UP008` fix ([#19131](https://github.com/astral-sh/ruff/pull/19131))
### Documentation
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI007`, `PYI008`) ([#19103](https://github.com/astral-sh/ruff/pull/19103))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM116`) ([#19111](https://github.com/astral-sh/ruff/pull/19111))
- \[`flake8-type-checking`\] Make example error out-of-the-box (`TC001`) ([#19151](https://github.com/astral-sh/ruff/pull/19151))
- \[`flake8-use-pathlib`\] Make example error out-of-the-box (`PTH210`) ([#19189](https://github.com/astral-sh/ruff/pull/19189))
- \[`pycodestyle`\] Make example error out-of-the-box (`E272`) ([#19191](https://github.com/astral-sh/ruff/pull/19191))
- \[`pycodestyle`\] Make example not raise unnecessary `SyntaxError` (`E114`) ([#19190](https://github.com/astral-sh/ruff/pull/19190))
- \[`pydoclint`\] Make example error out-of-the-box (`DOC501`) ([#19218](https://github.com/astral-sh/ruff/pull/19218))
- \[`pylint`, `pyupgrade`\] Fix syntax errors in examples (`PLW1501`, `UP028`) ([#19127](https://github.com/astral-sh/ruff/pull/19127))
- \[`pylint`\] Update `missing-maxsplit-arg` docs and error to suggest proper usage (`PLC0207`) ([#18949](https://github.com/astral-sh/ruff/pull/18949))
- \[`flake8-bandit`\] Make example error out-of-the-box (`S412`) ([#19241](https://github.com/astral-sh/ruff/pull/19241))
## 0.12.2
### Preview features

73
Cargo.lock generated
View File

@@ -680,11 +680,6 @@ name = "countme"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
dependencies = [
"dashmap 5.5.3",
"once_cell",
"rustc-hash 1.1.0",
]
[[package]]
name = "cpufeatures"
@@ -852,19 +847,6 @@ dependencies = [
"syn",
]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "dashmap"
version = "6.1.0"
@@ -2262,7 +2244,7 @@ dependencies = [
"once_cell",
"pep440_rs",
"regex",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"smallvec",
"thiserror 1.0.69",
@@ -2729,7 +2711,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.2"
version = "0.12.3"
dependencies = [
"anyhow",
"argfile",
@@ -2772,7 +2754,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"shellexpand",
@@ -2818,7 +2800,7 @@ dependencies = [
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"tikv-jemallocator",
@@ -2847,7 +2829,7 @@ dependencies = [
"arc-swap",
"camino",
"countme",
"dashmap 6.1.0",
"dashmap",
"dunce",
"etcetera",
"filetime",
@@ -2866,7 +2848,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -2940,7 +2922,7 @@ dependencies = [
"ruff_cache",
"ruff_macros",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"schemars",
"serde",
"static_assertions",
@@ -2979,7 +2961,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.2"
version = "0.12.3"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3022,7 +3004,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"schemars",
"serde",
"serde_json",
@@ -3094,7 +3076,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -3144,7 +3126,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -3193,7 +3175,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"static_assertions",
@@ -3217,7 +3199,7 @@ dependencies = [
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"schemars",
"serde",
"smallvec",
@@ -3278,7 +3260,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"shellexpand",
@@ -3312,7 +3294,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.2"
version = "0.12.3"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3367,7 +3349,7 @@ dependencies = [
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_source_file",
"rustc-hash 2.1.1",
"rustc-hash",
"schemars",
"serde",
"shellexpand",
@@ -3386,12 +3368,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -3446,7 +3422,7 @@ dependencies = [
"parking_lot",
"portable-atomic",
"rayon",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa-macro-rules",
"salsa-macros",
"smallvec",
@@ -4144,7 +4120,6 @@ dependencies = [
"clap",
"clap_complete_command",
"colored 3.0.0",
"countme",
"crossbeam",
"ctrlc",
"dunce",
@@ -4177,11 +4152,14 @@ version = "0.0.0"
dependencies = [
"bitflags 2.9.1",
"insta",
"regex",
"ruff_db",
"ruff_python_ast",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"smallvec",
"tracing",
@@ -4213,7 +4191,7 @@ dependencies = [
"ruff_python_ast",
"ruff_python_formatter",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -4234,7 +4212,6 @@ dependencies = [
"camino",
"colored 3.0.0",
"compact_str",
"countme",
"dir-test",
"drop_bomb",
"get-size2",
@@ -4258,7 +4235,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -4290,7 +4267,7 @@ dependencies = [
"ruff_notebook",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"serde",
"serde_json",
@@ -4328,7 +4305,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"rustc-stable-hash",
"salsa",
"serde",

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.12.2/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.2/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.3/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.2
rev: v0.12.3
hooks:
# Run the linter.
- id: ruff-check
@@ -430,6 +430,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Babel](https://github.com/python-babel/babel)
- Benchling ([Refac](https://github.com/benchling/refac))
- [Bokeh](https://github.com/bokeh/bokeh)
- Capital One ([datacompy](https://github.com/capitalone/datacompy))
- CrowdCent ([NumerBlox](https://github.com/crowdcent/numerblox)) <!-- typos: ignore -->
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- CERN ([Indico](https://getindico.io/))

View File

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

View File

@@ -5692,3 +5692,57 @@ class Foo:
"
);
}
#[test_case::test_case("concise")]
#[test_case::test_case("full")]
#[test_case::test_case("json")]
#[test_case::test_case("json-lines")]
#[test_case::test_case("junit")]
#[test_case::test_case("grouped")]
#[test_case::test_case("github")]
#[test_case::test_case("gitlab")]
#[test_case::test_case("pylint")]
#[test_case::test_case("rdjson")]
#[test_case::test_case("azure")]
#[test_case::test_case("sarif")]
fn output_format(output_format: &str) -> Result<()> {
const CONTENT: &str = "\
import os # F401
x = y # F821
match 42: # invalid-syntax
case _: ...
";
let tempdir = TempDir::new()?;
let input = tempdir.path().join("input.py");
fs::write(&input, CONTENT)?;
let snapshot = format!("output_format_{output_format}");
insta::with_settings!({
filters => vec![
(tempdir_filter(&tempdir).as_str(), "[TMP]/"),
(r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#),
(ruff_linter::VERSION, "[VERSION]"),
]
}, {
assert_cmd_snapshot!(
snapshot,
Command::new(get_cargo_bin(BIN_NAME))
.args([
"check",
"--no-cache",
"--output-format",
output_format,
"--select",
"F401,F821",
"--target-version",
"py39",
"input.py",
])
.current_dir(&tempdir),
);
});
Ok(())
}

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- azure
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=1;columnnumber=8;code=F401;]`os` imported but unused
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=2;columnnumber=5;code=F821;]Undefined name `y`
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=3;columnnumber=1;]SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -0,0 +1,25 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- concise
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:1:8: F401 [*] `os` imported but unused
input.py:2:5: F821 Undefined name `y`
input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 3 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----

View File

@@ -0,0 +1,49 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- full
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:1:8: F401 [*] `os` imported but unused
|
1 | import os # F401
| ^^ F401
2 | x = y # F821
3 | match 42: # invalid-syntax
|
= help: Remove unused import: `os`
input.py:2:5: F821 Undefined name `y`
|
1 | import os # F401
2 | x = y # F821
| ^ F821
3 | match 42: # invalid-syntax
4 | case _: ...
|
input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
1 | import os # F401
2 | x = y # F821
3 | match 42: # invalid-syntax
| ^^^^^
4 | case _: ...
|
Found 3 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- github
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
::error title=Ruff (F401),file=[TMP]/input.py,line=1,col=8,endLine=1,endColumn=10::input.py:1:8: F401 `os` imported but unused
::error title=Ruff (F821),file=[TMP]/input.py,line=2,col=5,endLine=2,endColumn=6::input.py:2:5: F821 Undefined name `y`
::error title=Ruff,file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -0,0 +1,60 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- gitlab
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
[
{
"check_name": "F401",
"description": "`os` imported but unused",
"fingerprint": "4dbad37161e65c72",
"location": {
"lines": {
"begin": 1,
"end": 1
},
"path": "input.py"
},
"severity": "major"
},
{
"check_name": "F821",
"description": "Undefined name `y`",
"fingerprint": "7af59862a085230",
"location": {
"lines": {
"begin": 2,
"end": 2
},
"path": "input.py"
},
"severity": "major"
},
{
"check_name": "syntax-error",
"description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"fingerprint": "e558cec859bb66e8",
"location": {
"lines": {
"begin": 3,
"end": 3
},
"path": "input.py"
},
"severity": "major"
}
]
----- stderr -----

View File

@@ -0,0 +1,27 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- grouped
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:
1:8 F401 [*] `os` imported but unused
2:5 F821 Undefined name `y`
3:1 SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 3 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- json-lines
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
{"cell":null,"code":"F401","end_location":{"column":10,"row":1},"filename":"[TMP]/input.py","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
{"cell":null,"code":"F821","end_location":{"column":6,"row":2},"filename":"[TMP]/input.py","fix":null,"location":{"column":5,"row":2},"message":"Undefined name `y`","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/undefined-name"}
{"cell":null,"code":null,"end_location":{"column":6,"row":3},"filename":"[TMP]/input.py","fix":null,"location":{"column":1,"row":3},"message":"SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)","noqa_row":null,"url":null}
----- stderr -----

View File

@@ -0,0 +1,88 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- json
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
[
{
"cell": null,
"code": "F401",
"end_location": {
"column": 10,
"row": 1
},
"filename": "[TMP]/input.py",
"fix": {
"applicability": "safe",
"edits": [
{
"content": "",
"end_location": {
"column": 1,
"row": 2
},
"location": {
"column": 1,
"row": 1
}
}
],
"message": "Remove unused import: `os`"
},
"location": {
"column": 8,
"row": 1
},
"message": "`os` imported but unused",
"noqa_row": 1,
"url": "https://docs.astral.sh/ruff/rules/unused-import"
},
{
"cell": null,
"code": "F821",
"end_location": {
"column": 6,
"row": 2
},
"filename": "[TMP]/input.py",
"fix": null,
"location": {
"column": 5,
"row": 2
},
"message": "Undefined name `y`",
"noqa_row": 2,
"url": "https://docs.astral.sh/ruff/rules/undefined-name"
},
{
"cell": null,
"code": null,
"end_location": {
"column": 6,
"row": 3
},
"filename": "[TMP]/input.py",
"fix": null,
"location": {
"column": 1,
"row": 3
},
"message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"noqa_row": null,
"url": null
}
]
----- stderr -----

View File

@@ -0,0 +1,34 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- junit
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="ruff" tests="3" failures="3" errors="0">
<testsuite name="[TMP]/input.py" tests="3" disabled="0" errors="0" failures="3" package="org.ruff">
<testcase name="org.ruff.F401" classname="[TMP]/input" line="1" column="8">
<failure message="`os` imported but unused">line 1, col 8, `os` imported but unused</failure>
</testcase>
<testcase name="org.ruff.F821" classname="[TMP]/input" line="2" column="5">
<failure message="Undefined name `y`">line 2, col 5, Undefined name `y`</failure>
</testcase>
<testcase name="org.ruff" classname="[TMP]/input" line="3" column="1">
<failure message="SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)">line 3, col 1, SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)</failure>
</testcase>
</testsuite>
</testsuites>
----- stderr -----

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- pylint
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:1: [F401] `os` imported but unused
input.py:2: [F821] Undefined name `y`
input.py:3: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -0,0 +1,103 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- rdjson
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
{
"diagnostics": [
{
"code": {
"url": "https://docs.astral.sh/ruff/rules/unused-import",
"value": "F401"
},
"location": {
"path": "[TMP]/input.py",
"range": {
"end": {
"column": 10,
"line": 1
},
"start": {
"column": 8,
"line": 1
}
}
},
"message": "`os` imported but unused",
"suggestions": [
{
"range": {
"end": {
"column": 1,
"line": 2
},
"start": {
"column": 1,
"line": 1
}
},
"text": ""
}
]
},
{
"code": {
"url": "https://docs.astral.sh/ruff/rules/undefined-name",
"value": "F821"
},
"location": {
"path": "[TMP]/input.py",
"range": {
"end": {
"column": 6,
"line": 2
},
"start": {
"column": 5,
"line": 2
}
}
},
"message": "Undefined name `y`"
},
{
"code": {
"url": null,
"value": null
},
"location": {
"path": "[TMP]/input.py",
"range": {
"end": {
"column": 6,
"line": 3
},
"start": {
"column": 1,
"line": 3
}
}
},
"message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
}
],
"severity": "warning",
"source": {
"name": "ruff",
"url": "https://docs.astral.sh/ruff"
}
}
----- stderr -----

View File

@@ -0,0 +1,142 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- sarif
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"runs": [
{
"results": [
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"region": {
"endColumn": 10,
"endLine": 1,
"startColumn": 8,
"startLine": 1
}
}
}
],
"message": {
"text": "`os` imported but unused"
},
"ruleId": "F401"
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"region": {
"endColumn": 6,
"endLine": 2,
"startColumn": 5,
"startLine": 2
}
}
}
],
"message": {
"text": "Undefined name `y`"
},
"ruleId": "F821"
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"region": {
"endColumn": 6,
"endLine": 3,
"startColumn": 1,
"startLine": 3
}
}
}
],
"message": {
"text": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
},
"ruleId": null
}
],
"tool": {
"driver": {
"informationUri": "https://github.com/astral-sh/ruff",
"name": "ruff",
"rules": [
{
"fullDescription": {
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
},
"help": {
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
},
"helpUri": "https://docs.astral.sh/ruff/rules/unused-import",
"id": "F401",
"properties": {
"id": "F401",
"kind": "Pyflakes",
"name": "unused-import",
"problem.severity": "error"
},
"shortDescription": {
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
}
},
{
"fullDescription": {
"text": "## What it does\nChecks for uses of undefined names.\n\n## Why is this bad?\nAn undefined name is likely to raise `NameError` at runtime.\n\n## Example\n```python\ndef double():\n return n * 2 # raises `NameError` if `n` is undefined when `double` is called\n```\n\nUse instead:\n```python\ndef double(n):\n return n * 2\n```\n\n## Options\n- [`target-version`]: Can be used to configure which symbols Ruff will understand\n as being available in the `builtins` namespace.\n\n## References\n- [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)\n"
},
"help": {
"text": "Undefined name `{name}`. {tip}"
},
"helpUri": "https://docs.astral.sh/ruff/rules/undefined-name",
"id": "F821",
"properties": {
"id": "F821",
"kind": "Pyflakes",
"name": "undefined-name",
"problem.severity": "error"
},
"shortDescription": {
"text": "Undefined name `{name}`. {tip}"
}
}
],
"version": "[VERSION]"
}
}
}
],
"version": "2.1.0"
}
----- stderr -----

View File

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

View File

@@ -422,6 +422,35 @@ def func(a: dict[str, int]) -> list[dict[str, int]]:
services = a["services"]
return services
# See: https://github.com/astral-sh/ruff/issues/14052
def outer() -> list[object]:
@register
async def inner() -> None:
print(layout)
layout = [...]
return layout
def outer() -> list[object]:
with open("") as f:
async def inner() -> None:
print(layout)
layout = [...]
return layout
def outer() -> list[object]:
def inner():
with open("") as f:
async def inner_inner() -> None:
print(layout)
layout = [...]
return layout
# See: https://github.com/astral-sh/ruff/issues/18411
def f():
(#=

View File

@@ -0,0 +1,6 @@
# Regression test for: https://github.com/astral-sh/ruff/issues/19175
# there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash
from typing import TYPE_CHECKING \
if TYPE_CHECKING: import builtins
builtins.print("!")

View File

@@ -125,3 +125,19 @@ class ClassForCommentEnthusiasts(BaseClass):
self
# also a comment
).f()
# Issue #19096: super calls with keyword arguments should emit diagnostic but not be fixed
class Ord(int):
def __len__(self):
return super(Ord, self, uhoh=True, **{"error": True}).bit_length()
class ExampleWithKeywords:
def method1(self):
super(ExampleWithKeywords, self, invalid=True).some_method() # Should emit diagnostic but NOT be fixed
def method2(self):
super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
def method3(self):
super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords

View File

@@ -4,8 +4,8 @@ use crate::Fix;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_type_checking, pyflakes,
pylint, pyupgrade, refurb, ruff,
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_return,
flake8_type_checking, pyflakes, pylint, pyupgrade, refurb, ruff,
};
/// Run lint rules over the [`Binding`]s.
@@ -25,11 +25,20 @@ pub(crate) fn bindings(checker: &Checker) {
Rule::ForLoopWrites,
Rule::CustomTypeVarForSelf,
Rule::PrivateTypeParameter,
Rule::UnnecessaryAssign,
]) {
return;
}
for (binding_id, binding) in checker.semantic.bindings.iter_enumerated() {
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
if binding.kind.is_function_definition() {
flake8_return::rules::unnecessary_assign(
checker,
binding.statement(checker.semantic()).unwrap(),
);
}
}
if checker.is_rule_enabled(Rule::UnusedVariable) {
if binding.kind.is_bound_exception()
&& binding.is_unused()

View File

@@ -1039,27 +1039,14 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
}
if checker.any_rule_enabled(&[
Rule::OsPathAbspath,
Rule::OsChmod,
Rule::OsMkdir,
Rule::OsMakedirs,
Rule::OsRename,
Rule::OsReplace,
Rule::OsRmdir,
Rule::OsRemove,
Rule::OsUnlink,
Rule::OsGetcwd,
Rule::OsPathExists,
Rule::OsPathExpanduser,
Rule::OsPathIsdir,
Rule::OsPathIsfile,
Rule::OsPathIslink,
Rule::OsReadlink,
Rule::OsStat,
Rule::OsPathIsabs,
Rule::OsPathJoin,
Rule::OsPathBasename,
Rule::OsPathDirname,
Rule::OsPathSamefile,
Rule::OsPathSplitext,
Rule::BuiltinOpen,
@@ -1070,21 +1057,66 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
]) {
flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetsize) {
flake8_use_pathlib::rules::os_path_getsize(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetatime) {
flake8_use_pathlib::rules::os_path_getatime(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetctime) {
flake8_use_pathlib::rules::os_path_getctime(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetmtime) {
flake8_use_pathlib::rules::os_path_getmtime(checker, call);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(checker, call);
if let Some(qualified_name) = checker.semantic().resolve_qualified_name(&call.func) {
let segments = qualified_name.segments();
if checker.is_rule_enabled(Rule::OsPathGetsize) {
flake8_use_pathlib::rules::os_path_getsize(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathGetatime) {
flake8_use_pathlib::rules::os_path_getatime(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathGetctime) {
flake8_use_pathlib::rules::os_path_getctime(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathGetmtime) {
flake8_use_pathlib::rules::os_path_getmtime(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathAbspath) {
flake8_use_pathlib::rules::os_path_abspath(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsRmdir) {
flake8_use_pathlib::rules::os_rmdir(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsRemove) {
flake8_use_pathlib::rules::os_remove(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsUnlink) {
flake8_use_pathlib::rules::os_unlink(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathExists) {
flake8_use_pathlib::rules::os_path_exists(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathExpanduser) {
flake8_use_pathlib::rules::os_path_expanduser(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathBasename) {
flake8_use_pathlib::rules::os_path_basename(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathDirname) {
flake8_use_pathlib::rules::os_path_dirname(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathIsabs) {
flake8_use_pathlib::rules::os_path_isabs(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathIsdir) {
flake8_use_pathlib::rules::os_path_isdir(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathIsfile) {
flake8_use_pathlib::rules::os_path_isfile(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathIslink) {
flake8_use_pathlib::rules::os_path_islink(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsReadlink) {
flake8_use_pathlib::rules::os_readlink(checker, call, segments);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(
checker, call, segments,
);
}
}
if checker.is_rule_enabled(Rule::OsSepSplit) {
flake8_use_pathlib::rules::os_sep_split(checker, call);
}

View File

@@ -207,7 +207,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
Rule::UnnecessaryReturnNone,
Rule::ImplicitReturnValue,
Rule::ImplicitReturn,
Rule::UnnecessaryAssign,
Rule::SuperfluousElseReturn,
Rule::SuperfluousElseRaise,
Rule::SuperfluousElseContinue,

View File

@@ -919,27 +919,27 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Tryceratops, "401") => (RuleGroup::Stable, rules::tryceratops::rules::VerboseLogMessage),
// flake8-use-pathlib
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathAbspath),
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath),
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsChmod),
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir),
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs),
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRename),
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReplace),
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRmdir),
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRemove),
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsUnlink),
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir),
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove),
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink),
(Flake8UsePathlib, "109") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsGetcwd),
(Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathExists),
(Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathExpanduser),
(Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsdir),
(Flake8UsePathlib, "113") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsfile),
(Flake8UsePathlib, "114") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIslink),
(Flake8UsePathlib, "115") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReadlink),
(Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExists),
(Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExpanduser),
(Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsdir),
(Flake8UsePathlib, "113") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsfile),
(Flake8UsePathlib, "114") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIslink),
(Flake8UsePathlib, "115") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReadlink),
(Flake8UsePathlib, "116") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsStat),
(Flake8UsePathlib, "117") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsabs),
(Flake8UsePathlib, "117") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsabs),
(Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin),
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathBasename),
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathDirname),
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename),
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname),
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSamefile),
(Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext),
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen),

View File

@@ -5,6 +5,7 @@ use ruff_python_ast::Stmt;
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_codegen::Stylist;
use ruff_python_parser::{TokenKind, Tokens};
use ruff_python_trivia::is_python_whitespace;
use ruff_python_trivia::{PythonWhitespace, textwrap::indent};
use ruff_source_file::{LineRanges, UniversalNewlineIterator};
use ruff_text_size::{Ranged, TextSize};
@@ -306,7 +307,7 @@ fn match_semicolon(s: &str) -> Option<TextSize> {
fn match_continuation(s: &str) -> Option<TextSize> {
for (offset, c) in s.char_indices() {
match c {
' ' | '\t' => continue,
_ if is_python_whitespace(c) => continue,
'\\' => return Some(TextSize::try_from(offset).unwrap()),
_ => break,
}

View File

@@ -69,6 +69,71 @@ pub(crate) const fn is_fix_os_path_getctime_enabled(settings: &LinterSettings) -
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_abspath_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_rmdir_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_unlink_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_remove_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_exists_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_expanduser_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_isdir_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_isfile_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_islink_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_isabs_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_readlink_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_basename_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/11436
// https://github.com/astral-sh/ruff/pull/11168
pub(crate) const fn is_dunder_init_fix_unused_import_enabled(settings: &LinterSettings) -> bool {

View File

@@ -539,7 +539,21 @@ fn implicit_return(checker: &Checker, function_def: &ast::StmtFunctionDef, stmt:
}
/// RET504
fn unnecessary_assign(checker: &Checker, stack: &Stack) {
pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) {
let Stmt::FunctionDef(function_def) = function_stmt else {
return;
};
let Some(stack) = create_stack(checker, function_def) else {
return;
};
if !result_exists(&stack.returns) {
return;
}
let Some(function_scope) = checker.semantic().function_scope(function_def) else {
return;
};
for (assign, return_, stmt) in &stack.assignment_return {
// Identify, e.g., `return x`.
let Some(value) = return_.value.as_ref() else {
@@ -583,6 +597,22 @@ fn unnecessary_assign(checker: &Checker, stack: &Stack) {
continue;
}
let Some(assigned_binding) = function_scope
.get(assigned_id)
.map(|binding_id| checker.semantic().binding(binding_id))
else {
continue;
};
// Check if there's any reference made to `assigned_binding` in another scope, e.g, nested
// functions. If there is, ignore them.
if assigned_binding
.references()
.map(|reference_id| checker.semantic().reference(reference_id))
.any(|reference| reference.scope_id() != assigned_binding.scope)
{
continue;
}
let mut diagnostic = checker.report_diagnostic(
UnnecessaryAssign {
name: assigned_id.to_string(),
@@ -665,24 +695,21 @@ fn superfluous_elif_else(checker: &Checker, stack: &Stack) {
}
}
/// Run all checks from the `flake8-return` plugin.
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
let ast::StmtFunctionDef {
decorator_list,
returns,
body,
..
} = function_def;
fn create_stack<'a>(
checker: &'a Checker,
function_def: &'a ast::StmtFunctionDef,
) -> Option<Stack<'a>> {
let ast::StmtFunctionDef { body, .. } = function_def;
// Find the last statement in the function.
let Some(last_stmt) = body.last() else {
// Skip empty functions.
return;
return None;
};
// Skip functions that consist of a single return statement.
if body.len() == 1 && matches!(last_stmt, Stmt::Return(_)) {
return;
return None;
}
// Traverse the function body, to collect the stack.
@@ -696,9 +723,29 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
// Avoid false positives for generators.
if stack.is_generator {
return;
return None;
}
Some(stack)
}
/// Run all checks from the `flake8-return` plugin, but `RET504` which is ran
/// after the semantic model is fully built.
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
let ast::StmtFunctionDef {
decorator_list,
returns,
body,
..
} = function_def;
let Some(stack) = create_stack(checker, function_def) else {
return;
};
let Some(last_stmt) = body.last() else {
return;
};
if checker.any_rule_enabled(&[
Rule::SuperfluousElseReturn,
Rule::SuperfluousElseRaise,
@@ -721,10 +768,6 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
if checker.is_rule_enabled(Rule::ImplicitReturn) {
implicit_return(checker, function_def, last_stmt);
}
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
unnecessary_assign(checker, &stack);
}
} else {
if checker.is_rule_enabled(Rule::UnnecessaryReturnNone) {
// Skip functions that have a return annotation that is not `None`.

View File

@@ -247,8 +247,6 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
422 | services = a["services"]
423 | return services
| ^^^^^^^^ RET504
424 |
425 | # See: https://github.com/astral-sh/ruff/issues/18411
|
= help: Remove unnecessary assignment
@@ -260,46 +258,46 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
423 |- return services
422 |+ return a["services"]
424 423 |
425 424 | # See: https://github.com/astral-sh/ruff/issues/18411
426 425 | def f():
425 424 |
426 425 | # See: https://github.com/astral-sh/ruff/issues/14052
RET504.py:429:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
RET504.py:458:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
427 | (#=
428 | x) = 1
429 | return x
456 | (#=
457 | x) = 1
458 | return x
| ^ RET504
430 |
431 | def f():
459 |
460 | def f():
|
= help: Remove unnecessary assignment
Unsafe fix
424 424 |
425 425 | # See: https://github.com/astral-sh/ruff/issues/18411
426 426 | def f():
427 |- (#=
428 |- x) = 1
429 |- return x
427 |+ return 1
430 428 |
431 429 | def f():
432 430 | x = (1
453 453 |
454 454 | # See: https://github.com/astral-sh/ruff/issues/18411
455 455 | def f():
456 |- (#=
457 |- x) = 1
458 |- return x
456 |+ return 1
459 457 |
460 458 | def f():
461 459 | x = (1
RET504.py:434:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
RET504.py:463:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
432 | x = (1
433 | )
434 | return x
461 | x = (1
462 | )
463 | return x
| ^ RET504
|
= help: Remove unnecessary assignment
Unsafe fix
429 429 | return x
430 430 |
431 431 | def f():
432 |- x = (1
432 |+ return (1
433 433 | )
434 |- return x
458 458 | return x
459 459 |
460 460 | def f():
461 |- x = (1
461 |+ return (1
462 462 | )
463 |- return x

View File

@@ -36,6 +36,7 @@ mod tests {
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_8.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_9.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("whitespace.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TC010_1.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TC010_2.py"))]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TC001.py"))]

View File

@@ -0,0 +1,22 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
whitespace.py:5:26: TC004 [*] Move import `builtins` out of type-checking block. Import is used for more than type hinting.
|
3 | from typing import TYPE_CHECKING \
4 |
5 | if TYPE_CHECKING: import builtins
| ^^^^^^^^ TC004
6 | builtins.print("!")
|
= help: Move out of type-checking block
Unsafe fix
1 1 | # Regression test for: https://github.com/astral-sh/ruff/issues/19175
2 2 | # there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash
3 |-from typing import TYPE_CHECKING \
3 |+from typing import TYPE_CHECKING; import builtins \
4 4 |
5 |-if TYPE_CHECKING: import builtins
5 |+if TYPE_CHECKING: pass
6 6 | builtins.print("!")

View File

@@ -1,10 +1,17 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation};
use ruff_python_ast::{self as ast};
use ruff_python_ast::{Expr, ExprCall};
use ruff_text_size::Ranged;
pub(crate) fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
arguments
.find_keyword(name)
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
}
pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool {
expr.as_call_expr().is_some_and(|expr_call| {
checker
.semantic()
@@ -13,27 +20,22 @@ pub(crate) fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
})
}
pub(crate) fn check_os_path_get_calls(
/// We check functions that take only 1 argument, this does not apply to functions
/// with `dir_fd` argument, because `dir_fd` is not supported by pathlib,
/// so check if it's set to non-default values
pub(crate) fn check_os_pathlib_single_arg_calls(
checker: &Checker,
call: &ExprCall,
fn_name: &str,
attr: &str,
fn_argument: &str,
fix_enabled: bool,
violation: impl Violation,
) {
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_none_or(|qualified_name| qualified_name.segments() != ["os", "path", fn_name])
{
return;
}
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument_value("filename", 0) else {
let Some(arg) = call.arguments.find_argument_value(fn_argument, 0) else {
return;
};
@@ -56,10 +58,10 @@ pub(crate) fn check_os_path_get_calls(
Applicability::Safe
};
let replacement = if is_path_call(checker, arg) {
format!("{arg_code}.stat().{attr}")
let replacement = if is_pathlib_path_call(checker, arg) {
format!("{arg_code}.{attr}")
} else {
format!("{binding}({arg_code}).stat().{attr}")
format!("{binding}({arg_code}).{attr}")
};
Ok(Fix::applicable_edits(

View File

@@ -80,6 +80,48 @@ mod tests {
Ok(())
}
#[test_case(Path::new("full_name.py"))]
#[test_case(Path::new("import_as.py"))]
#[test_case(Path::new("import_from_as.py"))]
#[test_case(Path::new("import_from.py"))]
fn preview_rules(path: &Path) -> Result<()> {
let snapshot = format!("preview_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_use_pathlib").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rules(vec![
Rule::OsPathAbspath,
Rule::OsChmod,
Rule::OsMkdir,
Rule::OsMakedirs,
Rule::OsRename,
Rule::OsReplace,
Rule::OsRmdir,
Rule::OsRemove,
Rule::OsUnlink,
Rule::OsGetcwd,
Rule::OsPathExists,
Rule::OsPathExpanduser,
Rule::OsPathIsdir,
Rule::OsPathIsfile,
Rule::OsPathIslink,
Rule::OsReadlink,
Rule::OsStat,
Rule::OsPathIsabs,
Rule::OsPathJoin,
Rule::OsPathBasename,
Rule::OsPathDirname,
Rule::OsPathSamefile,
Rule::OsPathSplitext,
Rule::BuiltinOpen,
])
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]

View File

@@ -1,19 +1,45 @@
pub(crate) use glob_rule::*;
pub(crate) use invalid_pathlib_with_suffix::*;
pub(crate) use os_path_abspath::*;
pub(crate) use os_path_basename::*;
pub(crate) use os_path_dirname::*;
pub(crate) use os_path_exists::*;
pub(crate) use os_path_expanduser::*;
pub(crate) use os_path_getatime::*;
pub(crate) use os_path_getctime::*;
pub(crate) use os_path_getmtime::*;
pub(crate) use os_path_getsize::*;
pub(crate) use os_path_isabs::*;
pub(crate) use os_path_isdir::*;
pub(crate) use os_path_isfile::*;
pub(crate) use os_path_islink::*;
pub(crate) use os_readlink::*;
pub(crate) use os_remove::*;
pub(crate) use os_rmdir::*;
pub(crate) use os_sep_split::*;
pub(crate) use os_unlink::*;
pub(crate) use path_constructor_current_directory::*;
pub(crate) use replaceable_by_pathlib::*;
mod glob_rule;
mod invalid_pathlib_with_suffix;
mod os_path_abspath;
mod os_path_basename;
mod os_path_dirname;
mod os_path_exists;
mod os_path_expanduser;
mod os_path_getatime;
mod os_path_getctime;
mod os_path_getmtime;
mod os_path_getsize;
mod os_path_isabs;
mod os_path_isdir;
mod os_path_isfile;
mod os_path_islink;
mod os_readlink;
mod os_remove;
mod os_rmdir;
mod os_sep_split;
mod os_unlink;
mod path_constructor_current_directory;
mod replaceable_by_pathlib;

View File

@@ -0,0 +1,74 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_abspath_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.abspath`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.resolve()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.abspath()`).
///
/// ## Examples
/// ```python
/// import os
///
/// file_path = os.path.abspath("../path/to/file")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// file_path = Path("../path/to/file").resolve()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)
/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathAbspath;
impl Violation for OsPathAbspath {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.abspath()` should be replaced by `Path.resolve()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).resolve()`".to_string())
}
}
/// PTH100
pub(crate) fn os_path_abspath(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "abspath"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"resolve()",
"path",
is_fix_os_path_abspath_enabled(checker.settings()),
OsPathAbspath,
);
}

View File

@@ -0,0 +1,73 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_basename_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.basename`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.name` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.basename()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.basename(__file__)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(__file__).name
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)
/// - [Python documentation: `os.path.basename`](https://docs.python.org/3/library/os.path.html#os.path.basename)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathBasename;
impl Violation for OsPathBasename {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.basename()` should be replaced by `Path.name`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).name`".to_string())
}
}
/// PTH119
pub(crate) fn os_path_basename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "basename"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"name",
"p",
is_fix_os_path_basename_enabled(checker.settings()),
OsPathBasename,
);
}

View File

@@ -0,0 +1,73 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_dirname_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.dirname`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.parent` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.dirname()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.dirname(__file__)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(__file__).parent
/// ```
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `PurePath.parent`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent)
/// - [Python documentation: `os.path.dirname`](https://docs.python.org/3/library/os.path.html#os.path.dirname)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathDirname;
impl Violation for OsPathDirname {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.dirname()` should be replaced by `Path.parent`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).parent`".to_string())
}
}
/// PTH120
pub(crate) fn os_path_dirname(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "dirname"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"parent",
"p",
is_fix_os_path_dirname_enabled(checker.settings()),
OsPathDirname,
);
}

View File

@@ -0,0 +1,73 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_exists_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.exists`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.exists()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.exists()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.exists("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").exists()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.exists`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.exists)
/// - [Python documentation: `os.path.exists`](https://docs.python.org/3/library/os.path.html#os.path.exists)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathExists;
impl Violation for OsPathExists {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.exists()` should be replaced by `Path.exists()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).exists()`".to_string())
}
}
/// PTH110
pub(crate) fn os_path_exists(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "exists"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"exists()",
"path",
is_fix_os_path_exists_enabled(checker.settings()),
OsPathExists,
);
}

View File

@@ -0,0 +1,73 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_expanduser_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.expanduser`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.expanduser()` can improve readability over the `os.path`
/// module's counterparts (e.g., as `os.path.expanduser()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.expanduser("~/films/Monty Python")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("~/films/Monty Python").expanduser()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser)
/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathExpanduser;
impl Violation for OsPathExpanduser {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.expanduser()` should be replaced by `Path.expanduser()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).expanduser()`".to_string())
}
}
/// PTH111
pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "expanduser"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"expanduser()",
"path",
is_fix_os_path_expanduser_enabled(checker.settings()),
OsPathExpanduser,
);
}

View File

@@ -1,6 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getatime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -61,12 +61,15 @@ impl Violation for OsPathGetatime {
}
/// PTH203
pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "getatime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"getatime",
"st_atime",
"stat().st_atime",
"filename",
is_fix_os_path_getatime_enabled(checker.settings()),
OsPathGetatime,
);

View File

@@ -1,6 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getctime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -62,12 +62,15 @@ impl Violation for OsPathGetctime {
}
/// PTH205
pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "getctime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"getctime",
"st_ctime",
"stat().st_ctime",
"filename",
is_fix_os_path_getctime_enabled(checker.settings()),
OsPathGetctime,
);

View File

@@ -1,6 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getmtime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -62,12 +62,15 @@ impl Violation for OsPathGetmtime {
}
/// PTH204
pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "getmtime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"getmtime",
"st_mtime",
"stat().st_mtime",
"filename",
is_fix_os_path_getmtime_enabled(checker.settings()),
OsPathGetmtime,
);

View File

@@ -1,6 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getsize_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -62,12 +62,15 @@ impl Violation for OsPathGetsize {
}
/// PTH202
pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "getsize"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"getsize",
"st_size",
"stat().st_size",
"filename",
is_fix_os_path_getsize_enabled(checker.settings()),
OsPathGetsize,
);

View File

@@ -0,0 +1,72 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isabs_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isabs`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_absolute()` can improve readability over the `os.path`
/// module's counterparts (e.g., as `os.path.isabs()`).
///
/// ## Examples
/// ```python
/// import os
///
/// if os.path.isabs(file_name):
/// print("Absolute path!")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// if Path(file_name).is_absolute():
/// print("Absolute path!")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `PurePath.is_absolute`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_absolute)
/// - [Python documentation: `os.path.isabs`](https://docs.python.org/3/library/os.path.html#os.path.isabs)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsabs;
impl Violation for OsPathIsabs {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isabs()` should be replaced by `Path.is_absolute()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).is_absolute()`".to_string())
}
}
/// PTH117
pub(crate) fn os_path_isabs(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "isabs"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"is_absolute()",
"s",
is_fix_os_path_isabs_enabled(checker.settings()),
OsPathIsabs,
);
}

View File

@@ -0,0 +1,75 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isdir_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isdir`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_dir()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.isdir()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.isdir("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_dir()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.is_dir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir)
/// - [Python documentation: `os.path.isdir`](https://docs.python.org/3/library/os.path.html#os.path.isdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsdir;
impl Violation for OsPathIsdir {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isdir()` should be replaced by `Path.is_dir()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).is_dir()`".to_string())
}
}
/// PTH112
pub(crate) fn os_path_isdir(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "isdir"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"is_dir()",
"s",
is_fix_os_path_isdir_enabled(checker.settings()),
OsPathIsdir,
);
}

View File

@@ -0,0 +1,75 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isfile_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isfile`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_file()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.isfile()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.isfile("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_file()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.is_file`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_file)
/// - [Python documentation: `os.path.isfile`](https://docs.python.org/3/library/os.path.html#os.path.isfile)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsfile;
impl Violation for OsPathIsfile {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isfile()` should be replaced by `Path.is_file()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).is_file()`".to_string())
}
}
/// PTH113
pub(crate) fn os_path_isfile(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "isfile"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"is_file()",
"path",
is_fix_os_path_isfile_enabled(checker.settings()),
OsPathIsfile,
);
}

View File

@@ -0,0 +1,75 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_islink_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.islink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_symlink()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.islink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.islink("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_symlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink)
/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIslink;
impl Violation for OsPathIslink {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.islink()` should be replaced by `Path.is_symlink()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).is_symlink()`".to_string())
}
}
/// PTH114
pub(crate) fn os_path_islink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "islink"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"is_symlink()",
"path",
is_fix_os_path_islink_enabled(checker.settings()),
OsPathIslink,
);
}

View File

@@ -0,0 +1,91 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_readlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ExprCall, PythonVersion};
/// ## What it does
/// Checks for uses of `os.readlink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.readlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.readlink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.readlink(file_name)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(file_name).readlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline)
/// - [Python documentation: `os.readlink`](https://docs.python.org/3/library/os.html#os.readlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsReadlink;
impl Violation for OsReadlink {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.readlink()` should be replaced by `Path.readlink()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).readlink()`".to_string())
}
}
/// PTH115
pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// Python 3.9+
if checker.target_version() < PythonVersion::PY39 {
return;
}
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.readlink)
// ```text
// 0 1
// os.readlink(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
if segments != ["os", "readlink"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"readlink()",
"path",
is_fix_os_readlink_enabled(checker.settings()),
OsReadlink,
);
}

View File

@@ -0,0 +1,86 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_remove_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.remove`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.unlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.remove()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.remove("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").unlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.remove`](https://docs.python.org/3/library/os.html#os.remove)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsRemove;
impl Violation for OsRemove {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.remove()` should be replaced by `Path.unlink()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).unlink()`".to_string())
}
}
/// PTH107
pub(crate) fn os_remove(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.remove)
// ```text
// 0 1
// os.remove(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
if segments != ["os", "remove"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"unlink()",
"path",
is_fix_os_remove_enabled(checker.settings()),
OsRemove,
);
}

View File

@@ -0,0 +1,86 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_rmdir_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.rmdir`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.rmdir()` can improve readability over the `os`
/// module's counterparts (e.g., `os.rmdir()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.rmdir("folder/")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("folder/").rmdir()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.rmdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir)
/// - [Python documentation: `os.rmdir`](https://docs.python.org/3/library/os.html#os.rmdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsRmdir;
impl Violation for OsRmdir {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.rmdir()` should be replaced by `Path.rmdir()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).rmdir()`".to_string())
}
}
/// PTH106
pub(crate) fn os_rmdir(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rmdir)
// ```text
// 0 1
// os.rmdir(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
if segments != ["os", "rmdir"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"rmdir()",
"path",
is_fix_os_rmdir_enabled(checker.settings()),
OsRmdir,
);
}

View File

@@ -0,0 +1,86 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_unlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.unlink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.unlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.unlink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.unlink("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").unlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.unlink`](https://docs.python.org/3/library/os.html#os.unlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsUnlink;
impl Violation for OsUnlink {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.unlink()` should be replaced by `Path.unlink()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).unlink()`".to_string())
}
}
/// PTH108
pub(crate) fn os_unlink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.unlink)
// ```text
// 0 1
// os.unlink(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
if segments != ["os", "unlink"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"unlink()",
"path",
is_fix_os_unlink_enabled(checker.settings()),
OsUnlink,
);
}

View File

@@ -54,7 +54,11 @@ impl AlwaysFixableViolation for PathConstructorCurrentDirectory {
}
/// PTH201
pub(crate) fn path_constructor_current_directory(checker: &Checker, call: &ExprCall) {
pub(crate) fn path_constructor_current_directory(
checker: &Checker,
call: &ExprCall,
segments: &[&str],
) {
let applicability = |range| {
if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
@@ -63,15 +67,9 @@ pub(crate) fn path_constructor_current_directory(checker: &Checker, call: &ExprC
}
};
let (func, arguments) = (&call.func, &call.arguments);
let arguments = &call.arguments;
if !checker
.semantic()
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["pathlib", "Path" | "PurePath"])
})
{
if !matches!(segments, ["pathlib", "Path" | "PurePath"]) {
return;
}

View File

@@ -4,14 +4,12 @@ use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_use_pathlib::helpers::is_keyword_only_argument_non_default;
use crate::rules::flake8_use_pathlib::rules::Glob;
use crate::rules::flake8_use_pathlib::violations::{
BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathAbspath,
OsPathBasename, OsPathDirname, OsPathExists, OsPathExpanduser, OsPathIsabs, OsPathIsdir,
OsPathIsfile, OsPathIslink, OsPathJoin, OsPathSamefile, OsPathSplitext, OsReadlink, OsRemove,
OsRename, OsReplace, OsRmdir, OsStat, OsSymlink, OsUnlink, PyPath,
BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathJoin,
OsPathSamefile, OsPathSplitext, OsRename, OsReplace, OsStat, OsSymlink, PyPath,
};
use ruff_python_ast::PythonVersion;
pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
let Some(qualified_name) = checker.semantic().resolve_qualified_name(&call.func) else {
@@ -20,8 +18,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
let range = call.func.range();
match qualified_name.segments() {
// PTH100
["os", "path", "abspath"] => checker.report_diagnostic_if_enabled(OsPathAbspath, range),
// PTH101
["os", "chmod"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
@@ -87,60 +83,10 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
}
checker.report_diagnostic_if_enabled(OsReplace, range)
}
// PTH106
["os", "rmdir"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rmdir)
// ```text
// 0 1
// os.rmdir(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
checker.report_diagnostic_if_enabled(OsRmdir, range)
}
// PTH107
["os", "remove"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.remove)
// ```text
// 0 1
// os.remove(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
checker.report_diagnostic_if_enabled(OsRemove, range)
}
// PTH108
["os", "unlink"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.unlink)
// ```text
// 0 1
// os.unlink(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
checker.report_diagnostic_if_enabled(OsUnlink, range)
}
// PTH109
["os", "getcwd"] => checker.report_diagnostic_if_enabled(OsGetcwd, range),
["os", "getcwdb"] => checker.report_diagnostic_if_enabled(OsGetcwd, range),
// PTH110
["os", "path", "exists"] => checker.report_diagnostic_if_enabled(OsPathExists, range),
// PTH111
["os", "path", "expanduser"] => {
checker.report_diagnostic_if_enabled(OsPathExpanduser, range)
}
// PTH112
["os", "path", "isdir"] => checker.report_diagnostic_if_enabled(OsPathIsdir, range),
// PTH113
["os", "path", "isfile"] => checker.report_diagnostic_if_enabled(OsPathIsfile, range),
// PTH114
["os", "path", "islink"] => checker.report_diagnostic_if_enabled(OsPathIslink, range),
// PTH116
["os", "stat"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
@@ -159,8 +105,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
}
checker.report_diagnostic_if_enabled(OsStat, range)
}
// PTH117
["os", "path", "isabs"] => checker.report_diagnostic_if_enabled(OsPathIsabs, range),
// PTH118
["os", "path", "join"] => checker.report_diagnostic_if_enabled(
OsPathJoin {
@@ -184,10 +128,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
},
range,
),
// PTH119
["os", "path", "basename"] => checker.report_diagnostic_if_enabled(OsPathBasename, range),
// PTH120
["os", "path", "dirname"] => checker.report_diagnostic_if_enabled(OsPathDirname, range),
// PTH121
["os", "path", "samefile"] => checker.report_diagnostic_if_enabled(OsPathSamefile, range),
// PTH122
@@ -208,7 +148,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
// PTH123
["" | "builtins", "open"] => {
// `closefd` and `opener` are not supported by pathlib, so check if they are
// `closefd` and `opener` are not supported by pathlib, so check if they
// are set to non-default values.
// https://github.com/astral-sh/ruff/issues/7620
// Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open):
@@ -282,20 +222,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
range,
)
}
// PTH115
// Python 3.9+
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.readlink)
// ```text
// 0 1
// os.readlink(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
checker.report_diagnostic_if_enabled(OsReadlink, range)
}
// PTH208
["os", "listdir"] => {
if call
@@ -338,7 +264,7 @@ fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
match expr {
Expr::Name(name) => Some(name),
Expr::Call(ast::ExprCall { func, .. }) => get_name_expr(func),
Expr::Call(ExprCall { func, .. }) => get_name_expr(func),
_ => None,
}
}
@@ -349,9 +275,3 @@ fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usi
.find_argument_value(name, position)
.is_some_and(|expr| !expr.is_none_literal_expr())
}
fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
arguments
.find_keyword(name)
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
}

View File

@@ -10,6 +10,7 @@ full_name.py:7:5: PTH100 `os.path.abspath()` should be replaced by `Path.resolve
8 | aa = os.chmod(p)
9 | aaa = os.mkdir(p)
|
= help: Replace with `Path(...).resolve()`
full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
@@ -69,6 +70,7 @@ full_name.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
14 | os.remove(p)
15 | os.unlink(p)
|
= help: Replace with `Path(...).rmdir()`
full_name.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
|
@@ -79,6 +81,7 @@ full_name.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
15 | os.unlink(p)
16 | os.getcwd(p)
|
= help: Replace with `Path(...).unlink()`
full_name.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
|
@@ -89,6 +92,7 @@ full_name.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
16 | os.getcwd(p)
17 | b = os.path.exists(p)
|
= help: Replace with `Path(...).unlink()`
full_name.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
@@ -109,6 +113,7 @@ full_name.py:17:5: PTH110 `os.path.exists()` should be replaced by `Path.exists(
18 | bb = os.path.expanduser(p)
19 | bbb = os.path.isdir(p)
|
= help: Replace with `Path(...).exists()`
full_name.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
@@ -119,6 +124,7 @@ full_name.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.exp
19 | bbb = os.path.isdir(p)
20 | bbbb = os.path.isfile(p)
|
= help: Replace with `Path(...).expanduser()`
full_name.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()`
|
@@ -129,6 +135,7 @@ full_name.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()
20 | bbbb = os.path.isfile(p)
21 | bbbbb = os.path.islink(p)
|
= help: Replace with `Path(...).is_dir()`
full_name.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()`
|
@@ -139,6 +146,7 @@ full_name.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file
21 | bbbbb = os.path.islink(p)
22 | os.readlink(p)
|
= help: Replace with `Path(...).is_file()`
full_name.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()`
|
@@ -149,6 +157,7 @@ full_name.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_syml
22 | os.readlink(p)
23 | os.stat(p)
|
= help: Replace with `Path(...).is_symlink()`
full_name.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()`
|
@@ -159,6 +168,7 @@ full_name.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()
23 | os.stat(p)
24 | os.path.isabs(p)
|
= help: Replace with `Path(...).readlink()`
full_name.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
@@ -179,6 +189,7 @@ full_name.py:24:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_absol
25 | os.path.join(p, q)
26 | os.sep.join([p, q])
|
= help: Replace with `Path(...).is_absolute()`
full_name.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
@@ -219,6 +230,7 @@ full_name.py:28:1: PTH119 `os.path.basename()` should be replaced by `Path.name`
29 | os.path.dirname(p)
30 | os.path.samefile(p)
|
= help: Replace with `Path(...).name`
full_name.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent`
|
@@ -229,6 +241,7 @@ full_name.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent
30 | os.path.samefile(p)
31 | os.path.splitext(p)
|
= help: Replace with `Path(...).parent`
full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|

View File

@@ -10,6 +10,7 @@ import_as.py:7:5: PTH100 `os.path.abspath()` should be replaced by `Path.resolve
8 | aa = foo.chmod(p)
9 | aaa = foo.mkdir(p)
|
= help: Replace with `Path(...).resolve()`
import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
@@ -69,6 +70,7 @@ import_as.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
14 | foo.remove(p)
15 | foo.unlink(p)
|
= help: Replace with `Path(...).rmdir()`
import_as.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
|
@@ -79,6 +81,7 @@ import_as.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
15 | foo.unlink(p)
16 | foo.getcwd(p)
|
= help: Replace with `Path(...).unlink()`
import_as.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
|
@@ -89,6 +92,7 @@ import_as.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
16 | foo.getcwd(p)
17 | b = foo_p.exists(p)
|
= help: Replace with `Path(...).unlink()`
import_as.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
@@ -109,6 +113,7 @@ import_as.py:17:5: PTH110 `os.path.exists()` should be replaced by `Path.exists(
18 | bb = foo_p.expanduser(p)
19 | bbb = foo_p.isdir(p)
|
= help: Replace with `Path(...).exists()`
import_as.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
@@ -119,6 +124,7 @@ import_as.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.exp
19 | bbb = foo_p.isdir(p)
20 | bbbb = foo_p.isfile(p)
|
= help: Replace with `Path(...).expanduser()`
import_as.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()`
|
@@ -129,6 +135,7 @@ import_as.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()
20 | bbbb = foo_p.isfile(p)
21 | bbbbb = foo_p.islink(p)
|
= help: Replace with `Path(...).is_dir()`
import_as.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()`
|
@@ -139,6 +146,7 @@ import_as.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file
21 | bbbbb = foo_p.islink(p)
22 | foo.readlink(p)
|
= help: Replace with `Path(...).is_file()`
import_as.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()`
|
@@ -149,6 +157,7 @@ import_as.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_syml
22 | foo.readlink(p)
23 | foo.stat(p)
|
= help: Replace with `Path(...).is_symlink()`
import_as.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()`
|
@@ -159,6 +168,7 @@ import_as.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()
23 | foo.stat(p)
24 | foo_p.isabs(p)
|
= help: Replace with `Path(...).readlink()`
import_as.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
@@ -179,6 +189,7 @@ import_as.py:24:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_absol
25 | foo_p.join(p, q)
26 | foo.sep.join([p, q])
|
= help: Replace with `Path(...).is_absolute()`
import_as.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
@@ -219,6 +230,7 @@ import_as.py:28:1: PTH119 `os.path.basename()` should be replaced by `Path.name`
29 | foo_p.dirname(p)
30 | foo_p.samefile(p)
|
= help: Replace with `Path(...).name`
import_as.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent`
|
@@ -229,6 +241,7 @@ import_as.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent
30 | foo_p.samefile(p)
31 | foo_p.splitext(p)
|
= help: Replace with `Path(...).parent`
import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|

View File

@@ -10,6 +10,7 @@ import_from.py:9:5: PTH100 `os.path.abspath()` should be replaced by `Path.resol
10 | aa = chmod(p)
11 | aaa = mkdir(p)
|
= help: Replace with `Path(...).resolve()`
import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
@@ -69,6 +70,7 @@ import_from.py:15:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
16 | remove(p)
17 | unlink(p)
|
= help: Replace with `Path(...).rmdir()`
import_from.py:16:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
|
@@ -79,6 +81,7 @@ import_from.py:16:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
17 | unlink(p)
18 | getcwd(p)
|
= help: Replace with `Path(...).unlink()`
import_from.py:17:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
|
@@ -89,6 +92,7 @@ import_from.py:17:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
18 | getcwd(p)
19 | b = exists(p)
|
= help: Replace with `Path(...).unlink()`
import_from.py:18:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
@@ -109,6 +113,7 @@ import_from.py:19:5: PTH110 `os.path.exists()` should be replaced by `Path.exist
20 | bb = expanduser(p)
21 | bbb = isdir(p)
|
= help: Replace with `Path(...).exists()`
import_from.py:20:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
@@ -119,6 +124,7 @@ import_from.py:20:6: PTH111 `os.path.expanduser()` should be replaced by `Path.e
21 | bbb = isdir(p)
22 | bbbb = isfile(p)
|
= help: Replace with `Path(...).expanduser()`
import_from.py:21:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()`
|
@@ -129,6 +135,7 @@ import_from.py:21:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir
22 | bbbb = isfile(p)
23 | bbbbb = islink(p)
|
= help: Replace with `Path(...).is_dir()`
import_from.py:22:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()`
|
@@ -139,6 +146,7 @@ import_from.py:22:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_fi
23 | bbbbb = islink(p)
24 | readlink(p)
|
= help: Replace with `Path(...).is_file()`
import_from.py:23:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()`
|
@@ -149,6 +157,7 @@ import_from.py:23:9: PTH114 `os.path.islink()` should be replaced by `Path.is_sy
24 | readlink(p)
25 | stat(p)
|
= help: Replace with `Path(...).is_symlink()`
import_from.py:24:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()`
|
@@ -159,6 +168,7 @@ import_from.py:24:1: PTH115 `os.readlink()` should be replaced by `Path.readlink
25 | stat(p)
26 | isabs(p)
|
= help: Replace with `Path(...).readlink()`
import_from.py:25:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
@@ -179,6 +189,7 @@ import_from.py:26:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_abs
27 | join(p, q)
28 | sep.join((p, q))
|
= help: Replace with `Path(...).is_absolute()`
import_from.py:27:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
@@ -219,6 +230,7 @@ import_from.py:30:1: PTH119 `os.path.basename()` should be replaced by `Path.nam
31 | dirname(p)
32 | samefile(p)
|
= help: Replace with `Path(...).name`
import_from.py:31:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent`
|
@@ -229,6 +241,7 @@ import_from.py:31:1: PTH120 `os.path.dirname()` should be replaced by `Path.pare
32 | samefile(p)
33 | splitext(p)
|
= help: Replace with `Path(...).parent`
import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|

View File

@@ -10,6 +10,7 @@ import_from_as.py:14:5: PTH100 `os.path.abspath()` should be replaced by `Path.r
15 | aa = xchmod(p)
16 | aaa = xmkdir(p)
|
= help: Replace with `Path(...).resolve()`
import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
@@ -69,6 +70,7 @@ import_from_as.py:20:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
21 | xremove(p)
22 | xunlink(p)
|
= help: Replace with `Path(...).rmdir()`
import_from_as.py:21:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
|
@@ -79,6 +81,7 @@ import_from_as.py:21:1: PTH107 `os.remove()` should be replaced by `Path.unlink(
22 | xunlink(p)
23 | xgetcwd(p)
|
= help: Replace with `Path(...).unlink()`
import_from_as.py:22:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
|
@@ -89,6 +92,7 @@ import_from_as.py:22:1: PTH108 `os.unlink()` should be replaced by `Path.unlink(
23 | xgetcwd(p)
24 | b = xexists(p)
|
= help: Replace with `Path(...).unlink()`
import_from_as.py:23:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
@@ -109,6 +113,7 @@ import_from_as.py:24:5: PTH110 `os.path.exists()` should be replaced by `Path.ex
25 | bb = xexpanduser(p)
26 | bbb = xisdir(p)
|
= help: Replace with `Path(...).exists()`
import_from_as.py:25:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
@@ -119,6 +124,7 @@ import_from_as.py:25:6: PTH111 `os.path.expanduser()` should be replaced by `Pat
26 | bbb = xisdir(p)
27 | bbbb = xisfile(p)
|
= help: Replace with `Path(...).expanduser()`
import_from_as.py:26:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()`
|
@@ -129,6 +135,7 @@ import_from_as.py:26:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_
27 | bbbb = xisfile(p)
28 | bbbbb = xislink(p)
|
= help: Replace with `Path(...).is_dir()`
import_from_as.py:27:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()`
|
@@ -139,6 +146,7 @@ import_from_as.py:27:8: PTH113 `os.path.isfile()` should be replaced by `Path.is
28 | bbbbb = xislink(p)
29 | xreadlink(p)
|
= help: Replace with `Path(...).is_file()`
import_from_as.py:28:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()`
|
@@ -149,6 +157,7 @@ import_from_as.py:28:9: PTH114 `os.path.islink()` should be replaced by `Path.is
29 | xreadlink(p)
30 | xstat(p)
|
= help: Replace with `Path(...).is_symlink()`
import_from_as.py:29:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()`
|
@@ -159,6 +168,7 @@ import_from_as.py:29:1: PTH115 `os.readlink()` should be replaced by `Path.readl
30 | xstat(p)
31 | xisabs(p)
|
= help: Replace with `Path(...).readlink()`
import_from_as.py:30:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
@@ -179,6 +189,7 @@ import_from_as.py:31:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_
32 | xjoin(p, q)
33 | s.join((p, q))
|
= help: Replace with `Path(...).is_absolute()`
import_from_as.py:32:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
@@ -219,6 +230,7 @@ import_from_as.py:35:1: PTH119 `os.path.basename()` should be replaced by `Path.
36 | xdirname(p)
37 | xsamefile(p)
|
= help: Replace with `Path(...).name`
import_from_as.py:36:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent`
|
@@ -229,6 +241,7 @@ import_from_as.py:36:1: PTH120 `os.path.dirname()` should be replaced by `Path.p
37 | xsamefile(p)
38 | xsplitext(p)
|
= help: Replace with `Path(...).parent`
import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|

View File

@@ -0,0 +1,580 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
full_name.py:7:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
5 | q = "bar"
6 |
7 | a = os.path.abspath(p)
| ^^^^^^^^^^^^^^^ PTH100
8 | aa = os.chmod(p)
9 | aaa = os.mkdir(p)
|
= help: Replace with `Path(...).resolve()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
6 7 |
7 |-a = os.path.abspath(p)
8 |+a = pathlib.Path(p).resolve()
8 9 | aa = os.chmod(p)
9 10 | aaa = os.mkdir(p)
10 11 | os.makedirs(p)
full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
7 | a = os.path.abspath(p)
8 | aa = os.chmod(p)
| ^^^^^^^^ PTH101
9 | aaa = os.mkdir(p)
10 | os.makedirs(p)
|
full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
7 | a = os.path.abspath(p)
8 | aa = os.chmod(p)
9 | aaa = os.mkdir(p)
| ^^^^^^^^ PTH102
10 | os.makedirs(p)
11 | os.rename(p)
|
full_name.py:10:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
8 | aa = os.chmod(p)
9 | aaa = os.mkdir(p)
10 | os.makedirs(p)
| ^^^^^^^^^^^ PTH103
11 | os.rename(p)
12 | os.replace(p)
|
full_name.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
9 | aaa = os.mkdir(p)
10 | os.makedirs(p)
11 | os.rename(p)
| ^^^^^^^^^ PTH104
12 | os.replace(p)
13 | os.rmdir(p)
|
full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
10 | os.makedirs(p)
11 | os.rename(p)
12 | os.replace(p)
| ^^^^^^^^^^ PTH105
13 | os.rmdir(p)
14 | os.remove(p)
|
full_name.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
11 | os.rename(p)
12 | os.replace(p)
13 | os.rmdir(p)
| ^^^^^^^^ PTH106
14 | os.remove(p)
15 | os.unlink(p)
|
= help: Replace with `Path(...).rmdir()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
10 11 | os.makedirs(p)
11 12 | os.rename(p)
12 13 | os.replace(p)
13 |-os.rmdir(p)
14 |+pathlib.Path(p).rmdir()
14 15 | os.remove(p)
15 16 | os.unlink(p)
16 17 | os.getcwd(p)
full_name.py:14:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()`
|
12 | os.replace(p)
13 | os.rmdir(p)
14 | os.remove(p)
| ^^^^^^^^^ PTH107
15 | os.unlink(p)
16 | os.getcwd(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
11 12 | os.rename(p)
12 13 | os.replace(p)
13 14 | os.rmdir(p)
14 |-os.remove(p)
15 |+pathlib.Path(p).unlink()
15 16 | os.unlink(p)
16 17 | os.getcwd(p)
17 18 | b = os.path.exists(p)
full_name.py:15:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()`
|
13 | os.rmdir(p)
14 | os.remove(p)
15 | os.unlink(p)
| ^^^^^^^^^ PTH108
16 | os.getcwd(p)
17 | b = os.path.exists(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
12 13 | os.replace(p)
13 14 | os.rmdir(p)
14 15 | os.remove(p)
15 |-os.unlink(p)
16 |+pathlib.Path(p).unlink()
16 17 | os.getcwd(p)
17 18 | b = os.path.exists(p)
18 19 | bb = os.path.expanduser(p)
full_name.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
14 | os.remove(p)
15 | os.unlink(p)
16 | os.getcwd(p)
| ^^^^^^^^^ PTH109
17 | b = os.path.exists(p)
18 | bb = os.path.expanduser(p)
|
full_name.py:17:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
15 | os.unlink(p)
16 | os.getcwd(p)
17 | b = os.path.exists(p)
| ^^^^^^^^^^^^^^ PTH110
18 | bb = os.path.expanduser(p)
19 | bbb = os.path.isdir(p)
|
= help: Replace with `Path(...).exists()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
14 15 | os.remove(p)
15 16 | os.unlink(p)
16 17 | os.getcwd(p)
17 |-b = os.path.exists(p)
18 |+b = pathlib.Path(p).exists()
18 19 | bb = os.path.expanduser(p)
19 20 | bbb = os.path.isdir(p)
20 21 | bbbb = os.path.isfile(p)
full_name.py:18:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
16 | os.getcwd(p)
17 | b = os.path.exists(p)
18 | bb = os.path.expanduser(p)
| ^^^^^^^^^^^^^^^^^^ PTH111
19 | bbb = os.path.isdir(p)
20 | bbbb = os.path.isfile(p)
|
= help: Replace with `Path(...).expanduser()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
15 16 | os.unlink(p)
16 17 | os.getcwd(p)
17 18 | b = os.path.exists(p)
18 |-bb = os.path.expanduser(p)
19 |+bb = pathlib.Path(p).expanduser()
19 20 | bbb = os.path.isdir(p)
20 21 | bbbb = os.path.isfile(p)
21 22 | bbbbb = os.path.islink(p)
full_name.py:19:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
|
17 | b = os.path.exists(p)
18 | bb = os.path.expanduser(p)
19 | bbb = os.path.isdir(p)
| ^^^^^^^^^^^^^ PTH112
20 | bbbb = os.path.isfile(p)
21 | bbbbb = os.path.islink(p)
|
= help: Replace with `Path(...).is_dir()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
16 17 | os.getcwd(p)
17 18 | b = os.path.exists(p)
18 19 | bb = os.path.expanduser(p)
19 |-bbb = os.path.isdir(p)
20 |+bbb = pathlib.Path(p).is_dir()
20 21 | bbbb = os.path.isfile(p)
21 22 | bbbbb = os.path.islink(p)
22 23 | os.readlink(p)
full_name.py:20:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()`
|
18 | bb = os.path.expanduser(p)
19 | bbb = os.path.isdir(p)
20 | bbbb = os.path.isfile(p)
| ^^^^^^^^^^^^^^ PTH113
21 | bbbbb = os.path.islink(p)
22 | os.readlink(p)
|
= help: Replace with `Path(...).is_file()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
17 18 | b = os.path.exists(p)
18 19 | bb = os.path.expanduser(p)
19 20 | bbb = os.path.isdir(p)
20 |-bbbb = os.path.isfile(p)
21 |+bbbb = pathlib.Path(p).is_file()
21 22 | bbbbb = os.path.islink(p)
22 23 | os.readlink(p)
23 24 | os.stat(p)
full_name.py:21:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()`
|
19 | bbb = os.path.isdir(p)
20 | bbbb = os.path.isfile(p)
21 | bbbbb = os.path.islink(p)
| ^^^^^^^^^^^^^^ PTH114
22 | os.readlink(p)
23 | os.stat(p)
|
= help: Replace with `Path(...).is_symlink()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
18 19 | bb = os.path.expanduser(p)
19 20 | bbb = os.path.isdir(p)
20 21 | bbbb = os.path.isfile(p)
21 |-bbbbb = os.path.islink(p)
22 |+bbbbb = pathlib.Path(p).is_symlink()
22 23 | os.readlink(p)
23 24 | os.stat(p)
24 25 | os.path.isabs(p)
full_name.py:22:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()`
|
20 | bbbb = os.path.isfile(p)
21 | bbbbb = os.path.islink(p)
22 | os.readlink(p)
| ^^^^^^^^^^^ PTH115
23 | os.stat(p)
24 | os.path.isabs(p)
|
= help: Replace with `Path(...).readlink()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
19 20 | bbb = os.path.isdir(p)
20 21 | bbbb = os.path.isfile(p)
21 22 | bbbbb = os.path.islink(p)
22 |-os.readlink(p)
23 |+pathlib.Path(p).readlink()
23 24 | os.stat(p)
24 25 | os.path.isabs(p)
25 26 | os.path.join(p, q)
full_name.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
21 | bbbbb = os.path.islink(p)
22 | os.readlink(p)
23 | os.stat(p)
| ^^^^^^^ PTH116
24 | os.path.isabs(p)
25 | os.path.join(p, q)
|
full_name.py:24:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()`
|
22 | os.readlink(p)
23 | os.stat(p)
24 | os.path.isabs(p)
| ^^^^^^^^^^^^^ PTH117
25 | os.path.join(p, q)
26 | os.sep.join([p, q])
|
= help: Replace with `Path(...).is_absolute()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
21 22 | bbbbb = os.path.islink(p)
22 23 | os.readlink(p)
23 24 | os.stat(p)
24 |-os.path.isabs(p)
25 |+pathlib.Path(p).is_absolute()
25 26 | os.path.join(p, q)
26 27 | os.sep.join([p, q])
27 28 | os.sep.join((p, q))
full_name.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
23 | os.stat(p)
24 | os.path.isabs(p)
25 | os.path.join(p, q)
| ^^^^^^^^^^^^ PTH118
26 | os.sep.join([p, q])
27 | os.sep.join((p, q))
|
full_name.py:26:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
24 | os.path.isabs(p)
25 | os.path.join(p, q)
26 | os.sep.join([p, q])
| ^^^^^^^^^^^ PTH118
27 | os.sep.join((p, q))
28 | os.path.basename(p)
|
full_name.py:27:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
25 | os.path.join(p, q)
26 | os.sep.join([p, q])
27 | os.sep.join((p, q))
| ^^^^^^^^^^^ PTH118
28 | os.path.basename(p)
29 | os.path.dirname(p)
|
full_name.py:28:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name`
|
26 | os.sep.join([p, q])
27 | os.sep.join((p, q))
28 | os.path.basename(p)
| ^^^^^^^^^^^^^^^^ PTH119
29 | os.path.dirname(p)
30 | os.path.samefile(p)
|
= help: Replace with `Path(...).name`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
25 26 | os.path.join(p, q)
26 27 | os.sep.join([p, q])
27 28 | os.sep.join((p, q))
28 |-os.path.basename(p)
29 |+pathlib.Path(p).name
29 30 | os.path.dirname(p)
30 31 | os.path.samefile(p)
31 32 | os.path.splitext(p)
full_name.py:29:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
|
27 | os.sep.join((p, q))
28 | os.path.basename(p)
29 | os.path.dirname(p)
| ^^^^^^^^^^^^^^^ PTH120
30 | os.path.samefile(p)
31 | os.path.splitext(p)
|
= help: Replace with `Path(...).parent`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
26 27 | os.sep.join([p, q])
27 28 | os.sep.join((p, q))
28 29 | os.path.basename(p)
29 |-os.path.dirname(p)
30 |+pathlib.Path(p).parent
30 31 | os.path.samefile(p)
31 32 | os.path.splitext(p)
32 33 | with open(p) as fp:
full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
28 | os.path.basename(p)
29 | os.path.dirname(p)
30 | os.path.samefile(p)
| ^^^^^^^^^^^^^^^^ PTH121
31 | os.path.splitext(p)
32 | with open(p) as fp:
|
full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
29 | os.path.dirname(p)
30 | os.path.samefile(p)
31 | os.path.splitext(p)
| ^^^^^^^^^^^^^^^^ PTH122
32 | with open(p) as fp:
33 | fp.read()
|
full_name.py:32:6: PTH123 `open()` should be replaced by `Path.open()`
|
30 | os.path.samefile(p)
31 | os.path.splitext(p)
32 | with open(p) as fp:
| ^^^^ PTH123
33 | fp.read()
34 | open(p).close()
|
full_name.py:34:1: PTH123 `open()` should be replaced by `Path.open()`
|
32 | with open(p) as fp:
33 | fp.read()
34 | open(p).close()
| ^^^^ PTH123
35 | os.getcwdb(p)
36 | os.path.join(p, *q)
|
full_name.py:35:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
33 | fp.read()
34 | open(p).close()
35 | os.getcwdb(p)
| ^^^^^^^^^^ PTH109
36 | os.path.join(p, *q)
37 | os.sep.join(p, *q)
|
full_name.py:36:1: PTH118 `os.path.join()` should be replaced by `Path.joinpath()`
|
34 | open(p).close()
35 | os.getcwdb(p)
36 | os.path.join(p, *q)
| ^^^^^^^^^^^^ PTH118
37 | os.sep.join(p, *q)
|
full_name.py:37:1: PTH118 `os.sep.join()` should be replaced by `Path.joinpath()`
|
35 | os.getcwdb(p)
36 | os.path.join(p, *q)
37 | os.sep.join(p, *q)
| ^^^^^^^^^^^ PTH118
38 |
39 | # https://github.com/astral-sh/ruff/issues/7620
|
full_name.py:46:1: PTH123 `open()` should be replaced by `Path.open()`
|
44 | open(p, closefd=False)
45 | open(p, opener=opener)
46 | open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
| ^^^^ PTH123
47 | open(p, 'r', - 1, None, None, None, True, None)
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
full_name.py:47:1: PTH123 `open()` should be replaced by `Path.open()`
|
45 | open(p, opener=opener)
46 | open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
47 | open(p, 'r', - 1, None, None, None, True, None)
| ^^^^ PTH123
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
full_name.py:65:1: PTH123 `open()` should be replaced by `Path.open()`
|
63 | open(f())
64 |
65 | open(b"foo")
| ^^^^ PTH123
66 | byte_str = b"bar"
67 | open(byte_str)
|
full_name.py:67:1: PTH123 `open()` should be replaced by `Path.open()`
|
65 | open(b"foo")
66 | byte_str = b"bar"
67 | open(byte_str)
| ^^^^ PTH123
68 |
69 | def bytes_str_func() -> bytes:
|
full_name.py:71:1: PTH123 `open()` should be replaced by `Path.open()`
|
69 | def bytes_str_func() -> bytes:
70 | return b"foo"
71 | open(bytes_str_func())
| ^^^^ PTH123
72 |
73 | # https://github.com/astral-sh/ruff/issues/17693
|

View File

@@ -0,0 +1,478 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
import_as.py:7:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
5 | q = "bar"
6 |
7 | a = foo_p.abspath(p)
| ^^^^^^^^^^^^^ PTH100
8 | aa = foo.chmod(p)
9 | aaa = foo.mkdir(p)
|
= help: Replace with `Path(...).resolve()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
6 7 |
7 |-a = foo_p.abspath(p)
8 |+a = pathlib.Path(p).resolve()
8 9 | aa = foo.chmod(p)
9 10 | aaa = foo.mkdir(p)
10 11 | foo.makedirs(p)
import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
7 | a = foo_p.abspath(p)
8 | aa = foo.chmod(p)
| ^^^^^^^^^ PTH101
9 | aaa = foo.mkdir(p)
10 | foo.makedirs(p)
|
import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
7 | a = foo_p.abspath(p)
8 | aa = foo.chmod(p)
9 | aaa = foo.mkdir(p)
| ^^^^^^^^^ PTH102
10 | foo.makedirs(p)
11 | foo.rename(p)
|
import_as.py:10:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
8 | aa = foo.chmod(p)
9 | aaa = foo.mkdir(p)
10 | foo.makedirs(p)
| ^^^^^^^^^^^^ PTH103
11 | foo.rename(p)
12 | foo.replace(p)
|
import_as.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
9 | aaa = foo.mkdir(p)
10 | foo.makedirs(p)
11 | foo.rename(p)
| ^^^^^^^^^^ PTH104
12 | foo.replace(p)
13 | foo.rmdir(p)
|
import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
10 | foo.makedirs(p)
11 | foo.rename(p)
12 | foo.replace(p)
| ^^^^^^^^^^^ PTH105
13 | foo.rmdir(p)
14 | foo.remove(p)
|
import_as.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
11 | foo.rename(p)
12 | foo.replace(p)
13 | foo.rmdir(p)
| ^^^^^^^^^ PTH106
14 | foo.remove(p)
15 | foo.unlink(p)
|
= help: Replace with `Path(...).rmdir()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
10 11 | foo.makedirs(p)
11 12 | foo.rename(p)
12 13 | foo.replace(p)
13 |-foo.rmdir(p)
14 |+pathlib.Path(p).rmdir()
14 15 | foo.remove(p)
15 16 | foo.unlink(p)
16 17 | foo.getcwd(p)
import_as.py:14:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()`
|
12 | foo.replace(p)
13 | foo.rmdir(p)
14 | foo.remove(p)
| ^^^^^^^^^^ PTH107
15 | foo.unlink(p)
16 | foo.getcwd(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
11 12 | foo.rename(p)
12 13 | foo.replace(p)
13 14 | foo.rmdir(p)
14 |-foo.remove(p)
15 |+pathlib.Path(p).unlink()
15 16 | foo.unlink(p)
16 17 | foo.getcwd(p)
17 18 | b = foo_p.exists(p)
import_as.py:15:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()`
|
13 | foo.rmdir(p)
14 | foo.remove(p)
15 | foo.unlink(p)
| ^^^^^^^^^^ PTH108
16 | foo.getcwd(p)
17 | b = foo_p.exists(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
12 13 | foo.replace(p)
13 14 | foo.rmdir(p)
14 15 | foo.remove(p)
15 |-foo.unlink(p)
16 |+pathlib.Path(p).unlink()
16 17 | foo.getcwd(p)
17 18 | b = foo_p.exists(p)
18 19 | bb = foo_p.expanduser(p)
import_as.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
14 | foo.remove(p)
15 | foo.unlink(p)
16 | foo.getcwd(p)
| ^^^^^^^^^^ PTH109
17 | b = foo_p.exists(p)
18 | bb = foo_p.expanduser(p)
|
import_as.py:17:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
15 | foo.unlink(p)
16 | foo.getcwd(p)
17 | b = foo_p.exists(p)
| ^^^^^^^^^^^^ PTH110
18 | bb = foo_p.expanduser(p)
19 | bbb = foo_p.isdir(p)
|
= help: Replace with `Path(...).exists()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
14 15 | foo.remove(p)
15 16 | foo.unlink(p)
16 17 | foo.getcwd(p)
17 |-b = foo_p.exists(p)
18 |+b = pathlib.Path(p).exists()
18 19 | bb = foo_p.expanduser(p)
19 20 | bbb = foo_p.isdir(p)
20 21 | bbbb = foo_p.isfile(p)
import_as.py:18:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
16 | foo.getcwd(p)
17 | b = foo_p.exists(p)
18 | bb = foo_p.expanduser(p)
| ^^^^^^^^^^^^^^^^ PTH111
19 | bbb = foo_p.isdir(p)
20 | bbbb = foo_p.isfile(p)
|
= help: Replace with `Path(...).expanduser()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
15 16 | foo.unlink(p)
16 17 | foo.getcwd(p)
17 18 | b = foo_p.exists(p)
18 |-bb = foo_p.expanduser(p)
19 |+bb = pathlib.Path(p).expanduser()
19 20 | bbb = foo_p.isdir(p)
20 21 | bbbb = foo_p.isfile(p)
21 22 | bbbbb = foo_p.islink(p)
import_as.py:19:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
|
17 | b = foo_p.exists(p)
18 | bb = foo_p.expanduser(p)
19 | bbb = foo_p.isdir(p)
| ^^^^^^^^^^^ PTH112
20 | bbbb = foo_p.isfile(p)
21 | bbbbb = foo_p.islink(p)
|
= help: Replace with `Path(...).is_dir()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
16 17 | foo.getcwd(p)
17 18 | b = foo_p.exists(p)
18 19 | bb = foo_p.expanduser(p)
19 |-bbb = foo_p.isdir(p)
20 |+bbb = pathlib.Path(p).is_dir()
20 21 | bbbb = foo_p.isfile(p)
21 22 | bbbbb = foo_p.islink(p)
22 23 | foo.readlink(p)
import_as.py:20:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()`
|
18 | bb = foo_p.expanduser(p)
19 | bbb = foo_p.isdir(p)
20 | bbbb = foo_p.isfile(p)
| ^^^^^^^^^^^^ PTH113
21 | bbbbb = foo_p.islink(p)
22 | foo.readlink(p)
|
= help: Replace with `Path(...).is_file()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
17 18 | b = foo_p.exists(p)
18 19 | bb = foo_p.expanduser(p)
19 20 | bbb = foo_p.isdir(p)
20 |-bbbb = foo_p.isfile(p)
21 |+bbbb = pathlib.Path(p).is_file()
21 22 | bbbbb = foo_p.islink(p)
22 23 | foo.readlink(p)
23 24 | foo.stat(p)
import_as.py:21:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()`
|
19 | bbb = foo_p.isdir(p)
20 | bbbb = foo_p.isfile(p)
21 | bbbbb = foo_p.islink(p)
| ^^^^^^^^^^^^ PTH114
22 | foo.readlink(p)
23 | foo.stat(p)
|
= help: Replace with `Path(...).is_symlink()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
18 19 | bb = foo_p.expanduser(p)
19 20 | bbb = foo_p.isdir(p)
20 21 | bbbb = foo_p.isfile(p)
21 |-bbbbb = foo_p.islink(p)
22 |+bbbbb = pathlib.Path(p).is_symlink()
22 23 | foo.readlink(p)
23 24 | foo.stat(p)
24 25 | foo_p.isabs(p)
import_as.py:22:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()`
|
20 | bbbb = foo_p.isfile(p)
21 | bbbbb = foo_p.islink(p)
22 | foo.readlink(p)
| ^^^^^^^^^^^^ PTH115
23 | foo.stat(p)
24 | foo_p.isabs(p)
|
= help: Replace with `Path(...).readlink()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
19 20 | bbb = foo_p.isdir(p)
20 21 | bbbb = foo_p.isfile(p)
21 22 | bbbbb = foo_p.islink(p)
22 |-foo.readlink(p)
23 |+pathlib.Path(p).readlink()
23 24 | foo.stat(p)
24 25 | foo_p.isabs(p)
25 26 | foo_p.join(p, q)
import_as.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
21 | bbbbb = foo_p.islink(p)
22 | foo.readlink(p)
23 | foo.stat(p)
| ^^^^^^^^ PTH116
24 | foo_p.isabs(p)
25 | foo_p.join(p, q)
|
import_as.py:24:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()`
|
22 | foo.readlink(p)
23 | foo.stat(p)
24 | foo_p.isabs(p)
| ^^^^^^^^^^^ PTH117
25 | foo_p.join(p, q)
26 | foo.sep.join([p, q])
|
= help: Replace with `Path(...).is_absolute()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
21 22 | bbbbb = foo_p.islink(p)
22 23 | foo.readlink(p)
23 24 | foo.stat(p)
24 |-foo_p.isabs(p)
25 |+pathlib.Path(p).is_absolute()
25 26 | foo_p.join(p, q)
26 27 | foo.sep.join([p, q])
27 28 | foo.sep.join((p, q))
import_as.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
23 | foo.stat(p)
24 | foo_p.isabs(p)
25 | foo_p.join(p, q)
| ^^^^^^^^^^ PTH118
26 | foo.sep.join([p, q])
27 | foo.sep.join((p, q))
|
import_as.py:26:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
24 | foo_p.isabs(p)
25 | foo_p.join(p, q)
26 | foo.sep.join([p, q])
| ^^^^^^^^^^^^ PTH118
27 | foo.sep.join((p, q))
28 | foo_p.basename(p)
|
import_as.py:27:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
25 | foo_p.join(p, q)
26 | foo.sep.join([p, q])
27 | foo.sep.join((p, q))
| ^^^^^^^^^^^^ PTH118
28 | foo_p.basename(p)
29 | foo_p.dirname(p)
|
import_as.py:28:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name`
|
26 | foo.sep.join([p, q])
27 | foo.sep.join((p, q))
28 | foo_p.basename(p)
| ^^^^^^^^^^^^^^ PTH119
29 | foo_p.dirname(p)
30 | foo_p.samefile(p)
|
= help: Replace with `Path(...).name`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
25 26 | foo_p.join(p, q)
26 27 | foo.sep.join([p, q])
27 28 | foo.sep.join((p, q))
28 |-foo_p.basename(p)
29 |+pathlib.Path(p).name
29 30 | foo_p.dirname(p)
30 31 | foo_p.samefile(p)
31 32 | foo_p.splitext(p)
import_as.py:29:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
|
27 | foo.sep.join((p, q))
28 | foo_p.basename(p)
29 | foo_p.dirname(p)
| ^^^^^^^^^^^^^ PTH120
30 | foo_p.samefile(p)
31 | foo_p.splitext(p)
|
= help: Replace with `Path(...).parent`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
26 27 | foo.sep.join([p, q])
27 28 | foo.sep.join((p, q))
28 29 | foo_p.basename(p)
29 |-foo_p.dirname(p)
30 |+pathlib.Path(p).parent
30 31 | foo_p.samefile(p)
31 32 | foo_p.splitext(p)
import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
28 | foo_p.basename(p)
29 | foo_p.dirname(p)
30 | foo_p.samefile(p)
| ^^^^^^^^^^^^^^ PTH121
31 | foo_p.splitext(p)
|
import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
29 | foo_p.dirname(p)
30 | foo_p.samefile(p)
31 | foo_p.splitext(p)
| ^^^^^^^^^^^^^^ PTH122
|

View File

@@ -0,0 +1,521 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
import_from.py:9:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
7 | q = "bar"
8 |
9 | a = abspath(p)
| ^^^^^^^ PTH100
10 | aa = chmod(p)
11 | aaa = mkdir(p)
|
= help: Replace with `Path(...).resolve()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
8 9 |
9 |-a = abspath(p)
10 |+a = pathlib.Path(p).resolve()
10 11 | aa = chmod(p)
11 12 | aaa = mkdir(p)
12 13 | makedirs(p)
import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
9 | a = abspath(p)
10 | aa = chmod(p)
| ^^^^^ PTH101
11 | aaa = mkdir(p)
12 | makedirs(p)
|
import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
9 | a = abspath(p)
10 | aa = chmod(p)
11 | aaa = mkdir(p)
| ^^^^^ PTH102
12 | makedirs(p)
13 | rename(p)
|
import_from.py:12:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
10 | aa = chmod(p)
11 | aaa = mkdir(p)
12 | makedirs(p)
| ^^^^^^^^ PTH103
13 | rename(p)
14 | replace(p)
|
import_from.py:13:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
11 | aaa = mkdir(p)
12 | makedirs(p)
13 | rename(p)
| ^^^^^^ PTH104
14 | replace(p)
15 | rmdir(p)
|
import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
12 | makedirs(p)
13 | rename(p)
14 | replace(p)
| ^^^^^^^ PTH105
15 | rmdir(p)
16 | remove(p)
|
import_from.py:15:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
13 | rename(p)
14 | replace(p)
15 | rmdir(p)
| ^^^^^ PTH106
16 | remove(p)
17 | unlink(p)
|
= help: Replace with `Path(...).rmdir()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
12 13 | makedirs(p)
13 14 | rename(p)
14 15 | replace(p)
15 |-rmdir(p)
16 |+pathlib.Path(p).rmdir()
16 17 | remove(p)
17 18 | unlink(p)
18 19 | getcwd(p)
import_from.py:16:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()`
|
14 | replace(p)
15 | rmdir(p)
16 | remove(p)
| ^^^^^^ PTH107
17 | unlink(p)
18 | getcwd(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
13 14 | rename(p)
14 15 | replace(p)
15 16 | rmdir(p)
16 |-remove(p)
17 |+pathlib.Path(p).unlink()
17 18 | unlink(p)
18 19 | getcwd(p)
19 20 | b = exists(p)
import_from.py:17:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()`
|
15 | rmdir(p)
16 | remove(p)
17 | unlink(p)
| ^^^^^^ PTH108
18 | getcwd(p)
19 | b = exists(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
14 15 | replace(p)
15 16 | rmdir(p)
16 17 | remove(p)
17 |-unlink(p)
18 |+pathlib.Path(p).unlink()
18 19 | getcwd(p)
19 20 | b = exists(p)
20 21 | bb = expanduser(p)
import_from.py:18:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
16 | remove(p)
17 | unlink(p)
18 | getcwd(p)
| ^^^^^^ PTH109
19 | b = exists(p)
20 | bb = expanduser(p)
|
import_from.py:19:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
17 | unlink(p)
18 | getcwd(p)
19 | b = exists(p)
| ^^^^^^ PTH110
20 | bb = expanduser(p)
21 | bbb = isdir(p)
|
= help: Replace with `Path(...).exists()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
16 17 | remove(p)
17 18 | unlink(p)
18 19 | getcwd(p)
19 |-b = exists(p)
20 |+b = pathlib.Path(p).exists()
20 21 | bb = expanduser(p)
21 22 | bbb = isdir(p)
22 23 | bbbb = isfile(p)
import_from.py:20:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
18 | getcwd(p)
19 | b = exists(p)
20 | bb = expanduser(p)
| ^^^^^^^^^^ PTH111
21 | bbb = isdir(p)
22 | bbbb = isfile(p)
|
= help: Replace with `Path(...).expanduser()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
17 18 | unlink(p)
18 19 | getcwd(p)
19 20 | b = exists(p)
20 |-bb = expanduser(p)
21 |+bb = pathlib.Path(p).expanduser()
21 22 | bbb = isdir(p)
22 23 | bbbb = isfile(p)
23 24 | bbbbb = islink(p)
import_from.py:21:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
|
19 | b = exists(p)
20 | bb = expanduser(p)
21 | bbb = isdir(p)
| ^^^^^ PTH112
22 | bbbb = isfile(p)
23 | bbbbb = islink(p)
|
= help: Replace with `Path(...).is_dir()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
18 19 | getcwd(p)
19 20 | b = exists(p)
20 21 | bb = expanduser(p)
21 |-bbb = isdir(p)
22 |+bbb = pathlib.Path(p).is_dir()
22 23 | bbbb = isfile(p)
23 24 | bbbbb = islink(p)
24 25 | readlink(p)
import_from.py:22:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()`
|
20 | bb = expanduser(p)
21 | bbb = isdir(p)
22 | bbbb = isfile(p)
| ^^^^^^ PTH113
23 | bbbbb = islink(p)
24 | readlink(p)
|
= help: Replace with `Path(...).is_file()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
19 20 | b = exists(p)
20 21 | bb = expanduser(p)
21 22 | bbb = isdir(p)
22 |-bbbb = isfile(p)
23 |+bbbb = pathlib.Path(p).is_file()
23 24 | bbbbb = islink(p)
24 25 | readlink(p)
25 26 | stat(p)
import_from.py:23:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()`
|
21 | bbb = isdir(p)
22 | bbbb = isfile(p)
23 | bbbbb = islink(p)
| ^^^^^^ PTH114
24 | readlink(p)
25 | stat(p)
|
= help: Replace with `Path(...).is_symlink()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
20 21 | bb = expanduser(p)
21 22 | bbb = isdir(p)
22 23 | bbbb = isfile(p)
23 |-bbbbb = islink(p)
24 |+bbbbb = pathlib.Path(p).is_symlink()
24 25 | readlink(p)
25 26 | stat(p)
26 27 | isabs(p)
import_from.py:24:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()`
|
22 | bbbb = isfile(p)
23 | bbbbb = islink(p)
24 | readlink(p)
| ^^^^^^^^ PTH115
25 | stat(p)
26 | isabs(p)
|
= help: Replace with `Path(...).readlink()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
21 22 | bbb = isdir(p)
22 23 | bbbb = isfile(p)
23 24 | bbbbb = islink(p)
24 |-readlink(p)
25 |+pathlib.Path(p).readlink()
25 26 | stat(p)
26 27 | isabs(p)
27 28 | join(p, q)
import_from.py:25:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
23 | bbbbb = islink(p)
24 | readlink(p)
25 | stat(p)
| ^^^^ PTH116
26 | isabs(p)
27 | join(p, q)
|
import_from.py:26:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()`
|
24 | readlink(p)
25 | stat(p)
26 | isabs(p)
| ^^^^^ PTH117
27 | join(p, q)
28 | sep.join((p, q))
|
= help: Replace with `Path(...).is_absolute()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
23 24 | bbbbb = islink(p)
24 25 | readlink(p)
25 26 | stat(p)
26 |-isabs(p)
27 |+pathlib.Path(p).is_absolute()
27 28 | join(p, q)
28 29 | sep.join((p, q))
29 30 | sep.join([p, q])
import_from.py:27:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
25 | stat(p)
26 | isabs(p)
27 | join(p, q)
| ^^^^ PTH118
28 | sep.join((p, q))
29 | sep.join([p, q])
|
import_from.py:28:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
26 | isabs(p)
27 | join(p, q)
28 | sep.join((p, q))
| ^^^^^^^^ PTH118
29 | sep.join([p, q])
30 | basename(p)
|
import_from.py:29:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
27 | join(p, q)
28 | sep.join((p, q))
29 | sep.join([p, q])
| ^^^^^^^^ PTH118
30 | basename(p)
31 | dirname(p)
|
import_from.py:30:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name`
|
28 | sep.join((p, q))
29 | sep.join([p, q])
30 | basename(p)
| ^^^^^^^^ PTH119
31 | dirname(p)
32 | samefile(p)
|
= help: Replace with `Path(...).name`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
27 28 | join(p, q)
28 29 | sep.join((p, q))
29 30 | sep.join([p, q])
30 |-basename(p)
31 |+pathlib.Path(p).name
31 32 | dirname(p)
32 33 | samefile(p)
33 34 | splitext(p)
import_from.py:31:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
|
29 | sep.join([p, q])
30 | basename(p)
31 | dirname(p)
| ^^^^^^^ PTH120
32 | samefile(p)
33 | splitext(p)
|
= help: Replace with `Path(...).parent`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
28 29 | sep.join((p, q))
29 30 | sep.join([p, q])
30 31 | basename(p)
31 |-dirname(p)
32 |+pathlib.Path(p).parent
32 33 | samefile(p)
33 34 | splitext(p)
34 35 | with open(p) as fp:
import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
30 | basename(p)
31 | dirname(p)
32 | samefile(p)
| ^^^^^^^^ PTH121
33 | splitext(p)
34 | with open(p) as fp:
|
import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
31 | dirname(p)
32 | samefile(p)
33 | splitext(p)
| ^^^^^^^^ PTH122
34 | with open(p) as fp:
35 | fp.read()
|
import_from.py:34:6: PTH123 `open()` should be replaced by `Path.open()`
|
32 | samefile(p)
33 | splitext(p)
34 | with open(p) as fp:
| ^^^^ PTH123
35 | fp.read()
36 | open(p).close()
|
import_from.py:36:1: PTH123 `open()` should be replaced by `Path.open()`
|
34 | with open(p) as fp:
35 | fp.read()
36 | open(p).close()
| ^^^^ PTH123
|
import_from.py:43:10: PTH123 `open()` should be replaced by `Path.open()`
|
41 | from builtins import open
42 |
43 | with open(p) as _: ... # Error
| ^^^^ PTH123
|

View File

@@ -0,0 +1,491 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
import_from_as.py:14:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
12 | q = "bar"
13 |
14 | a = xabspath(p)
| ^^^^^^^^ PTH100
15 | aa = xchmod(p)
16 | aaa = xmkdir(p)
|
= help: Replace with `Path(...).resolve()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
13 14 |
14 |-a = xabspath(p)
15 |+a = pathlib.Path(p).resolve()
15 16 | aa = xchmod(p)
16 17 | aaa = xmkdir(p)
17 18 | xmakedirs(p)
import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
14 | a = xabspath(p)
15 | aa = xchmod(p)
| ^^^^^^ PTH101
16 | aaa = xmkdir(p)
17 | xmakedirs(p)
|
import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
14 | a = xabspath(p)
15 | aa = xchmod(p)
16 | aaa = xmkdir(p)
| ^^^^^^ PTH102
17 | xmakedirs(p)
18 | xrename(p)
|
import_from_as.py:17:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
15 | aa = xchmod(p)
16 | aaa = xmkdir(p)
17 | xmakedirs(p)
| ^^^^^^^^^ PTH103
18 | xrename(p)
19 | xreplace(p)
|
import_from_as.py:18:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
16 | aaa = xmkdir(p)
17 | xmakedirs(p)
18 | xrename(p)
| ^^^^^^^ PTH104
19 | xreplace(p)
20 | xrmdir(p)
|
import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
17 | xmakedirs(p)
18 | xrename(p)
19 | xreplace(p)
| ^^^^^^^^ PTH105
20 | xrmdir(p)
21 | xremove(p)
|
import_from_as.py:20:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
18 | xrename(p)
19 | xreplace(p)
20 | xrmdir(p)
| ^^^^^^ PTH106
21 | xremove(p)
22 | xunlink(p)
|
= help: Replace with `Path(...).rmdir()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
17 18 | xmakedirs(p)
18 19 | xrename(p)
19 20 | xreplace(p)
20 |-xrmdir(p)
21 |+pathlib.Path(p).rmdir()
21 22 | xremove(p)
22 23 | xunlink(p)
23 24 | xgetcwd(p)
import_from_as.py:21:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()`
|
19 | xreplace(p)
20 | xrmdir(p)
21 | xremove(p)
| ^^^^^^^ PTH107
22 | xunlink(p)
23 | xgetcwd(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
18 19 | xrename(p)
19 20 | xreplace(p)
20 21 | xrmdir(p)
21 |-xremove(p)
22 |+pathlib.Path(p).unlink()
22 23 | xunlink(p)
23 24 | xgetcwd(p)
24 25 | b = xexists(p)
import_from_as.py:22:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()`
|
20 | xrmdir(p)
21 | xremove(p)
22 | xunlink(p)
| ^^^^^^^ PTH108
23 | xgetcwd(p)
24 | b = xexists(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
19 20 | xreplace(p)
20 21 | xrmdir(p)
21 22 | xremove(p)
22 |-xunlink(p)
23 |+pathlib.Path(p).unlink()
23 24 | xgetcwd(p)
24 25 | b = xexists(p)
25 26 | bb = xexpanduser(p)
import_from_as.py:23:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
21 | xremove(p)
22 | xunlink(p)
23 | xgetcwd(p)
| ^^^^^^^ PTH109
24 | b = xexists(p)
25 | bb = xexpanduser(p)
|
import_from_as.py:24:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
22 | xunlink(p)
23 | xgetcwd(p)
24 | b = xexists(p)
| ^^^^^^^ PTH110
25 | bb = xexpanduser(p)
26 | bbb = xisdir(p)
|
= help: Replace with `Path(...).exists()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
21 22 | xremove(p)
22 23 | xunlink(p)
23 24 | xgetcwd(p)
24 |-b = xexists(p)
25 |+b = pathlib.Path(p).exists()
25 26 | bb = xexpanduser(p)
26 27 | bbb = xisdir(p)
27 28 | bbbb = xisfile(p)
import_from_as.py:25:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
23 | xgetcwd(p)
24 | b = xexists(p)
25 | bb = xexpanduser(p)
| ^^^^^^^^^^^ PTH111
26 | bbb = xisdir(p)
27 | bbbb = xisfile(p)
|
= help: Replace with `Path(...).expanduser()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
22 23 | xunlink(p)
23 24 | xgetcwd(p)
24 25 | b = xexists(p)
25 |-bb = xexpanduser(p)
26 |+bb = pathlib.Path(p).expanduser()
26 27 | bbb = xisdir(p)
27 28 | bbbb = xisfile(p)
28 29 | bbbbb = xislink(p)
import_from_as.py:26:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
|
24 | b = xexists(p)
25 | bb = xexpanduser(p)
26 | bbb = xisdir(p)
| ^^^^^^ PTH112
27 | bbbb = xisfile(p)
28 | bbbbb = xislink(p)
|
= help: Replace with `Path(...).is_dir()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
23 24 | xgetcwd(p)
24 25 | b = xexists(p)
25 26 | bb = xexpanduser(p)
26 |-bbb = xisdir(p)
27 |+bbb = pathlib.Path(p).is_dir()
27 28 | bbbb = xisfile(p)
28 29 | bbbbb = xislink(p)
29 30 | xreadlink(p)
import_from_as.py:27:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()`
|
25 | bb = xexpanduser(p)
26 | bbb = xisdir(p)
27 | bbbb = xisfile(p)
| ^^^^^^^ PTH113
28 | bbbbb = xislink(p)
29 | xreadlink(p)
|
= help: Replace with `Path(...).is_file()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
24 25 | b = xexists(p)
25 26 | bb = xexpanduser(p)
26 27 | bbb = xisdir(p)
27 |-bbbb = xisfile(p)
28 |+bbbb = pathlib.Path(p).is_file()
28 29 | bbbbb = xislink(p)
29 30 | xreadlink(p)
30 31 | xstat(p)
import_from_as.py:28:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()`
|
26 | bbb = xisdir(p)
27 | bbbb = xisfile(p)
28 | bbbbb = xislink(p)
| ^^^^^^^ PTH114
29 | xreadlink(p)
30 | xstat(p)
|
= help: Replace with `Path(...).is_symlink()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
25 26 | bb = xexpanduser(p)
26 27 | bbb = xisdir(p)
27 28 | bbbb = xisfile(p)
28 |-bbbbb = xislink(p)
29 |+bbbbb = pathlib.Path(p).is_symlink()
29 30 | xreadlink(p)
30 31 | xstat(p)
31 32 | xisabs(p)
import_from_as.py:29:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()`
|
27 | bbbb = xisfile(p)
28 | bbbbb = xislink(p)
29 | xreadlink(p)
| ^^^^^^^^^ PTH115
30 | xstat(p)
31 | xisabs(p)
|
= help: Replace with `Path(...).readlink()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
26 27 | bbb = xisdir(p)
27 28 | bbbb = xisfile(p)
28 29 | bbbbb = xislink(p)
29 |-xreadlink(p)
30 |+pathlib.Path(p).readlink()
30 31 | xstat(p)
31 32 | xisabs(p)
32 33 | xjoin(p, q)
import_from_as.py:30:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
28 | bbbbb = xislink(p)
29 | xreadlink(p)
30 | xstat(p)
| ^^^^^ PTH116
31 | xisabs(p)
32 | xjoin(p, q)
|
import_from_as.py:31:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()`
|
29 | xreadlink(p)
30 | xstat(p)
31 | xisabs(p)
| ^^^^^^ PTH117
32 | xjoin(p, q)
33 | s.join((p, q))
|
= help: Replace with `Path(...).is_absolute()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
28 29 | bbbbb = xislink(p)
29 30 | xreadlink(p)
30 31 | xstat(p)
31 |-xisabs(p)
32 |+pathlib.Path(p).is_absolute()
32 33 | xjoin(p, q)
33 34 | s.join((p, q))
34 35 | s.join([p, q])
import_from_as.py:32:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
30 | xstat(p)
31 | xisabs(p)
32 | xjoin(p, q)
| ^^^^^ PTH118
33 | s.join((p, q))
34 | s.join([p, q])
|
import_from_as.py:33:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
31 | xisabs(p)
32 | xjoin(p, q)
33 | s.join((p, q))
| ^^^^^^ PTH118
34 | s.join([p, q])
35 | xbasename(p)
|
import_from_as.py:34:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
32 | xjoin(p, q)
33 | s.join((p, q))
34 | s.join([p, q])
| ^^^^^^ PTH118
35 | xbasename(p)
36 | xdirname(p)
|
import_from_as.py:35:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name`
|
33 | s.join((p, q))
34 | s.join([p, q])
35 | xbasename(p)
| ^^^^^^^^^ PTH119
36 | xdirname(p)
37 | xsamefile(p)
|
= help: Replace with `Path(...).name`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
32 33 | xjoin(p, q)
33 34 | s.join((p, q))
34 35 | s.join([p, q])
35 |-xbasename(p)
36 |+pathlib.Path(p).name
36 37 | xdirname(p)
37 38 | xsamefile(p)
38 39 | xsplitext(p)
import_from_as.py:36:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
|
34 | s.join([p, q])
35 | xbasename(p)
36 | xdirname(p)
| ^^^^^^^^ PTH120
37 | xsamefile(p)
38 | xsplitext(p)
|
= help: Replace with `Path(...).parent`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
33 34 | s.join((p, q))
34 35 | s.join([p, q])
35 36 | xbasename(p)
36 |-xdirname(p)
37 |+pathlib.Path(p).parent
37 38 | xsamefile(p)
38 39 | xsplitext(p)
import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
35 | xbasename(p)
36 | xdirname(p)
37 | xsamefile(p)
| ^^^^^^^^^ PTH121
38 | xsplitext(p)
|
import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
36 | xdirname(p)
37 | xsamefile(p)
38 | xsplitext(p)
| ^^^^^^^^^ PTH122
|

View File

@@ -2,51 +2,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
/// ## What it does
/// Checks for uses of `os.path.abspath`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.resolve()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.abspath()`).
///
/// ## Examples
/// ```python
/// import os
///
/// file_path = os.path.abspath("../path/to/file")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// file_path = Path("../path/to/file").resolve()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)
/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathAbspath;
impl Violation for OsPathAbspath {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.abspath()` should be replaced by `Path.resolve()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.chmod`.
///
@@ -275,141 +230,6 @@ impl Violation for OsReplace {
}
}
/// ## What it does
/// Checks for uses of `os.rmdir`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.rmdir()` can improve readability over the `os`
/// module's counterparts (e.g., `os.rmdir()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.rmdir("folder/")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("folder/").rmdir()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.rmdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir)
/// - [Python documentation: `os.rmdir`](https://docs.python.org/3/library/os.html#os.rmdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsRmdir;
impl Violation for OsRmdir {
#[derive_message_formats]
fn message(&self) -> String {
"`os.rmdir()` should be replaced by `Path.rmdir()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.remove`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.unlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.remove()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.remove("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").unlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.remove`](https://docs.python.org/3/library/os.html#os.remove)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsRemove;
impl Violation for OsRemove {
#[derive_message_formats]
fn message(&self) -> String {
"`os.remove()` should be replaced by `Path.unlink()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.unlink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.unlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.unlink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.unlink("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").unlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.unlink`](https://docs.python.org/3/library/os.html#os.unlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsUnlink;
impl Violation for OsUnlink {
#[derive_message_formats]
fn message(&self) -> String {
"`os.unlink()` should be replaced by `Path.unlink()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.getcwd` and `os.getcwdb`.
///
@@ -456,276 +276,6 @@ impl Violation for OsGetcwd {
}
}
/// ## What it does
/// Checks for uses of `os.path.exists`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.exists()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.exists()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.exists("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").exists()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.exists`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.exists)
/// - [Python documentation: `os.path.exists`](https://docs.python.org/3/library/os.path.html#os.path.exists)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathExists;
impl Violation for OsPathExists {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.exists()` should be replaced by `Path.exists()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.expanduser`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.expanduser()` can improve readability over the `os.path`
/// module's counterparts (e.g., as `os.path.expanduser()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.expanduser("~/films/Monty Python")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("~/films/Monty Python").expanduser()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser)
/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathExpanduser;
impl Violation for OsPathExpanduser {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.expanduser()` should be replaced by `Path.expanduser()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.isdir`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_dir()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.isdir()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.isdir("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_dir()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.is_dir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir)
/// - [Python documentation: `os.path.isdir`](https://docs.python.org/3/library/os.path.html#os.path.isdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsdir;
impl Violation for OsPathIsdir {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isdir()` should be replaced by `Path.is_dir()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.isfile`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_file()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.isfile()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.isfile("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_file()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.is_file`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_file)
/// - [Python documentation: `os.path.isfile`](https://docs.python.org/3/library/os.path.html#os.path.isfile)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsfile;
impl Violation for OsPathIsfile {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isfile()` should be replaced by `Path.is_file()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.islink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_symlink()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.islink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.islink("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_symlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink)
/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIslink;
impl Violation for OsPathIslink {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.islink()` should be replaced by `Path.is_symlink()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.readlink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.readlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.readlink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.readlink(file_name)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(file_name).readlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline)
/// - [Python documentation: `os.readlink`](https://docs.python.org/3/library/os.html#os.readlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsReadlink;
impl Violation for OsReadlink {
#[derive_message_formats]
fn message(&self) -> String {
"`os.readlink()` should be replaced by `Path.readlink()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.stat`.
///
@@ -781,53 +331,6 @@ impl Violation for OsStat {
}
}
/// ## What it does
/// Checks for uses of `os.path.isabs`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_absolute()` can improve readability over the `os.path`
/// module's counterparts (e.g., as `os.path.isabs()`).
///
/// ## Examples
/// ```python
/// import os
///
/// if os.path.isabs(file_name):
/// print("Absolute path!")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// if Path(file_name).is_absolute():
/// print("Absolute path!")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `PurePath.is_absolute`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_absolute)
/// - [Python documentation: `os.path.isabs`](https://docs.python.org/3/library/os.path.html#os.path.isabs)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsabs;
impl Violation for OsPathIsabs {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isabs()` should be replaced by `Path.is_absolute()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.join`.
///
@@ -890,96 +393,6 @@ pub(crate) enum Joiner {
Joinpath,
}
/// ## What it does
/// Checks for uses of `os.path.basename`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.name` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.basename()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.basename(__file__)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(__file__).name
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)
/// - [Python documentation: `os.path.basename`](https://docs.python.org/3/library/os.path.html#os.path.basename)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathBasename;
impl Violation for OsPathBasename {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.basename()` should be replaced by `Path.name`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.dirname`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.parent` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.dirname()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.dirname(__file__)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(__file__).parent
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `PurePath.parent`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent)
/// - [Python documentation: `os.path.dirname`](https://docs.python.org/3/library/os.path.html#os.path.dirname)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathDirname;
impl Violation for OsPathDirname {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.dirname()` should be replaced by `Path.parent`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.samefile`.
///

View File

@@ -5,7 +5,7 @@ use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::preview::is_safe_super_call_with_parameters_fix_enabled;
use crate::{AlwaysFixableViolation, Edit, Fix};
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for `super` calls that pass redundant arguments.
@@ -57,14 +57,16 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
#[derive(ViolationMetadata)]
pub(crate) struct SuperCallWithParameters;
impl AlwaysFixableViolation for SuperCallWithParameters {
impl Violation for SuperCallWithParameters {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"Use `super()` instead of `super(__class__, self)`".to_string()
}
fn fix_title(&self) -> String {
"Remove `super()` parameters".to_string()
fn fix_title(&self) -> Option<String> {
Some("Remove `super()` parameters".to_string())
}
}
@@ -165,22 +167,26 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
return;
}
let applicability = if !checker.comment_ranges().intersects(call.arguments.range())
&& is_safe_super_call_with_parameters_fix_enabled(checker.settings())
{
Applicability::Safe
} else {
Applicability::Unsafe
};
let mut diagnostic = checker.report_diagnostic(SuperCallWithParameters, call.arguments.range());
diagnostic.set_fix(Fix::applicable_edit(
Edit::deletion(
call.arguments.start() + TextSize::new(1),
call.arguments.end() - TextSize::new(1),
),
applicability,
));
// Only provide a fix if there are no keyword arguments, since super() doesn't accept keyword arguments
if call.arguments.keywords.is_empty() {
let applicability = if !checker.comment_ranges().intersects(call.arguments.range())
&& is_safe_super_call_with_parameters_fix_enabled(checker.settings())
{
Applicability::Safe
} else {
Applicability::Unsafe
};
diagnostic.set_fix(Fix::applicable_edit(
Edit::deletion(
call.arguments.start() + TextSize::new(1),
call.arguments.end() - TextSize::new(1),
),
applicability,
));
}
}
/// Returns `true` if a call is an argumented `super` invocation.

View File

@@ -249,3 +249,53 @@ UP008.py:123:14: UP008 [*] Use `super()` instead of `super(__class__, self)`
126 |- # also a comment
127 |- ).f()
123 |+ super().f()
128 124 |
129 125 |
130 126 | # Issue #19096: super calls with keyword arguments should emit diagnostic but not be fixed
UP008.py:133:21: UP008 Use `super()` instead of `super(__class__, self)`
|
131 | class Ord(int):
132 | def __len__(self):
133 | return super(Ord, self, uhoh=True, **{"error": True}).bit_length()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
134 |
135 | class ExampleWithKeywords:
|
= help: Remove `super()` parameters
UP008.py:137:14: UP008 Use `super()` instead of `super(__class__, self)`
|
135 | class ExampleWithKeywords:
136 | def method1(self):
137 | super(ExampleWithKeywords, self, invalid=True).some_method() # Should emit diagnostic but NOT be fixed
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
138 |
139 | def method2(self):
|
= help: Remove `super()` parameters
UP008.py:140:14: UP008 Use `super()` instead of `super(__class__, self)`
|
139 | def method2(self):
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
141 |
142 | def method3(self):
|
= help: Remove `super()` parameters
UP008.py:143:14: UP008 [*] Use `super()` instead of `super(__class__, self)`
|
142 | def method3(self):
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
|
= help: Remove `super()` parameters
Unsafe fix
140 140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
141 141 |
142 142 | def method3(self):
143 |- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
143 |+ super().some_method() # Should be fixed - no keywords

View File

@@ -249,3 +249,53 @@ UP008.py:123:14: UP008 [*] Use `super()` instead of `super(__class__, self)`
126 |- # also a comment
127 |- ).f()
123 |+ super().f()
128 124 |
129 125 |
130 126 | # Issue #19096: super calls with keyword arguments should emit diagnostic but not be fixed
UP008.py:133:21: UP008 Use `super()` instead of `super(__class__, self)`
|
131 | class Ord(int):
132 | def __len__(self):
133 | return super(Ord, self, uhoh=True, **{"error": True}).bit_length()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
134 |
135 | class ExampleWithKeywords:
|
= help: Remove `super()` parameters
UP008.py:137:14: UP008 Use `super()` instead of `super(__class__, self)`
|
135 | class ExampleWithKeywords:
136 | def method1(self):
137 | super(ExampleWithKeywords, self, invalid=True).some_method() # Should emit diagnostic but NOT be fixed
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
138 |
139 | def method2(self):
|
= help: Remove `super()` parameters
UP008.py:140:14: UP008 Use `super()` instead of `super(__class__, self)`
|
139 | def method2(self):
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
141 |
142 | def method3(self):
|
= help: Remove `super()` parameters
UP008.py:143:14: UP008 [*] Use `super()` instead of `super(__class__, self)`
|
142 | def method3(self):
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
|
= help: Remove `super()` parameters
Safe fix
140 140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
141 141 |
142 142 | def method3(self):
143 |- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
143 |+ super().some_method() # Should be fixed - no keywords

View File

@@ -2094,6 +2094,20 @@ impl<'a> SemanticModel<'a> {
None
})
}
/// Finds and returns the [`Scope`] corresponding to a given [`ast::StmtFunctionDef`].
///
/// This method searches all scopes created by a function definition, comparing the
/// [`TextRange`] of the provided `function_def` with the the range of the function
/// associated with the scope.
pub fn function_scope(&self, function_def: &ast::StmtFunctionDef) -> Option<&Scope> {
self.scopes.iter().find(|scope| {
let Some(function) = scope.kind.as_function() else {
return false;
};
function.range() == function_def.range()
})
}
}
pub struct ShadowedBinding {

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.12.2"
version = "0.12.3"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -26,7 +26,6 @@ argfile = { workspace = true }
clap = { workspace = true, features = ["wrap_help", "string", "env"] }
clap_complete_command = { workspace = true }
colored = { workspace = true }
countme = { workspace = true, features = ["enable"] }
crossbeam = { workspace = true }
ctrlc = { version = "3.4.4" }
indicatif = { workspace = true }

3
crates/ty/docs/cli.md generated
View File

@@ -84,7 +84,8 @@ over all configuration files.</p>
<li><code>3.11</code></li>
<li><code>3.12</code></li>
<li><code>3.13</code></li>
</ul></dd><dt id="ty-check--respect-ignore-files"><a href="#ty-check--respect-ignore-files"><code>--respect-ignore-files</code></a></dt><dd><p>Respect file exclusions via <code>.gitignore</code> and other standard ignore files. Use <code>--no-respect-gitignore</code> to disable</p>
</ul></dd><dt id="ty-check--quiet"><a href="#ty-check--quiet"><code>--quiet</code></a></dt><dd><p>Use quiet output</p>
</dd><dt id="ty-check--respect-ignore-files"><a href="#ty-check--respect-ignore-files"><code>--respect-ignore-files</code></a></dt><dd><p>Respect file exclusions via <code>.gitignore</code> and other standard ignore files. Use <code>--no-respect-gitignore</code> to disable</p>
</dd><dt id="ty-check--typeshed"><a href="#ty-check--typeshed"><code>--typeshed</code></a>, <code>--custom-typeshed-dir</code> <i>path</i></dt><dd><p>Custom directory to use for stdlib typeshed stubs</p>
</dd><dt id="ty-check--verbose"><a href="#ty-check--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output (or <code>-vv</code> and <code>-vvv</code> for more verbose output)</p>
</dd><dt id="ty-check--warn"><a href="#ty-check--warn"><code>--warn</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'warn'. Can be specified multiple times.</p>

View File

@@ -1,12 +1,13 @@
mod args;
mod logging;
mod printer;
mod python_version;
mod version;
pub use args::Cli;
use ty_static::EnvVars;
use std::io::{self, BufWriter, Write, stdout};
use std::fmt::Write;
use std::process::{ExitCode, Termination};
use anyhow::Result;
@@ -14,6 +15,7 @@ use std::sync::Mutex;
use crate::args::{CheckCommand, Command, TerminalColor};
use crate::logging::setup_tracing;
use crate::printer::Printer;
use anyhow::{Context, anyhow};
use clap::{CommandFactory, Parser};
use colored::Colorize;
@@ -25,7 +27,7 @@ use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
use ty_project::metadata::options::ProjectOptionsOverrides;
use ty_project::watch::ProjectWatcher;
use ty_project::{Db, DummyReporter, Reporter, watch};
use ty_project::{Db, watch};
use ty_project::{ProjectDatabase, ProjectMetadata};
use ty_server::run_server;
@@ -42,6 +44,8 @@ pub fn run() -> anyhow::Result<ExitStatus> {
Command::Check(check_args) => run_check(check_args),
Command::Version => version().map(|()| ExitStatus::Success),
Command::GenerateShellCompletion { shell } => {
use std::io::stdout;
shell.generate(&mut Cli::command(), &mut stdout());
Ok(ExitStatus::Success)
}
@@ -49,7 +53,7 @@ pub fn run() -> anyhow::Result<ExitStatus> {
}
pub(crate) fn version() -> Result<()> {
let mut stdout = BufWriter::new(io::stdout().lock());
let mut stdout = Printer::default().stream_for_requested_summary().lock();
let version_info = crate::version::version();
writeln!(stdout, "ty {}", &version_info)?;
Ok(())
@@ -59,9 +63,10 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
set_colored_override(args.color);
let verbosity = args.verbosity.level();
countme::enable(verbosity.is_trace());
let _guard = setup_tracing(verbosity, args.color.unwrap_or_default())?;
let printer = Printer::default().with_verbosity(verbosity);
tracing::warn!(
"ty is pre-release software and not ready for production use. \
Expect to encounter bugs, missing features, and fatal errors.",
@@ -126,7 +131,8 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
}
let project_options_overrides = ProjectOptionsOverrides::new(config_file, options);
let (main_loop, main_loop_cancellation_token) = MainLoop::new(project_options_overrides);
let (main_loop, main_loop_cancellation_token) =
MainLoop::new(project_options_overrides, printer);
// Listen to Ctrl+C and abort the watch mode.
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
@@ -144,7 +150,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
main_loop.run(&mut db)?
};
let mut stdout = stdout().lock();
let mut stdout = printer.stream_for_requested_summary().lock();
match std::env::var(EnvVars::TY_MEMORY_REPORT).as_deref() {
Ok("short") => write!(stdout, "{}", db.salsa_memory_dump().display_short())?,
Ok("mypy_primer") => write!(stdout, "{}", db.salsa_memory_dump().display_mypy_primer())?,
@@ -152,8 +158,6 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
_ => {}
}
tracing::trace!("Counts for entire CLI run:\n{}", countme::get_all());
std::mem::forget(db);
if exit_zero {
@@ -195,12 +199,16 @@ struct MainLoop {
/// The file system watcher, if running in watch mode.
watcher: Option<ProjectWatcher>,
/// Interface for displaying information to the user.
printer: Printer,
project_options_overrides: ProjectOptionsOverrides,
}
impl MainLoop {
fn new(
project_options_overrides: ProjectOptionsOverrides,
printer: Printer,
) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);
@@ -210,6 +218,7 @@ impl MainLoop {
receiver,
watcher: None,
project_options_overrides,
printer,
},
MainLoopCancellationToken { sender },
)
@@ -226,32 +235,24 @@ impl MainLoop {
// Do not show progress bars with `--watch`, indicatif does not seem to
// handle cancelling independent progress bars very well.
self.run_with_progress::<DummyReporter>(db)?;
// TODO(zanieb): We can probably use `MultiProgress` to handle this case in the future.
self.printer = self.printer.with_no_progress();
self.run(db)?;
Ok(ExitStatus::Success)
}
fn run(self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
self.run_with_progress::<IndicatifReporter>(db)
}
fn run_with_progress<R>(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus>
where
R: Reporter + Default + 'static,
{
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
let result = self.main_loop::<R>(db);
let result = self.main_loop(db);
tracing::debug!("Exiting main loop");
result
}
fn main_loop<R>(&mut self, db: &mut ProjectDatabase) -> Result<ExitStatus>
where
R: Reporter + Default + 'static,
{
fn main_loop(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
// Schedule the first check.
tracing::debug!("Starting main loop");
@@ -267,7 +268,7 @@ impl MainLoop {
// to prevent blocking the main loop here.
rayon::spawn(move || {
match salsa::Cancelled::catch(|| {
let mut reporter = R::default();
let mut reporter = IndicatifReporter::from(self.printer);
db.check_with_reporter(&mut reporter)
}) {
Ok(result) => {
@@ -302,10 +303,12 @@ impl MainLoop {
return Ok(ExitStatus::Success);
}
let mut stdout = stdout().lock();
if result.is_empty() {
writeln!(stdout, "{}", "All checks passed!".green().bold())?;
writeln!(
self.printer.stream_for_success_summary(),
"{}",
"All checks passed!".green().bold()
)?;
if self.watcher.is_none() {
return Ok(ExitStatus::Success);
@@ -314,14 +317,19 @@ impl MainLoop {
let mut max_severity = Severity::Info;
let diagnostics_count = result.len();
let mut stdout = self.printer.stream_for_details().lock();
for diagnostic in result {
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
// Only render diagnostics if they're going to be displayed, since doing
// so is expensive.
if stdout.is_enabled() {
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
}
max_severity = max_severity.max(diagnostic.severity());
}
writeln!(
stdout,
self.printer.stream_for_failure_summary(),
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
@@ -353,8 +361,6 @@ impl MainLoop {
"Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}"
);
}
tracing::trace!("Counts after last check:\n{}", countme::get_all());
}
MainLoopMessage::ApplyChanges(changes) => {
@@ -383,27 +389,53 @@ impl MainLoop {
}
/// A progress reporter for `ty check`.
#[derive(Default)]
struct IndicatifReporter(Option<indicatif::ProgressBar>);
enum IndicatifReporter {
/// A constructed reporter that is not yet ready, contains the target for the progress bar.
Pending(indicatif::ProgressDrawTarget),
/// A reporter that is ready, containing a progress bar to report to.
///
/// Initialization of the bar is deferred to [`ty_project::ProgressReporter::set_files`] so we
/// do not initialize the bar too early as it may take a while to collect the number of files to
/// process and we don't want to display an empty "0/0" bar.
Initialized(indicatif::ProgressBar),
}
impl ty_project::Reporter for IndicatifReporter {
impl From<Printer> for IndicatifReporter {
fn from(printer: Printer) -> Self {
Self::Pending(printer.progress_target())
}
}
impl ty_project::ProgressReporter for IndicatifReporter {
fn set_files(&mut self, files: usize) {
let progress = indicatif::ProgressBar::new(files as u64);
progress.set_style(
let target = match std::mem::replace(
self,
IndicatifReporter::Pending(indicatif::ProgressDrawTarget::hidden()),
) {
Self::Pending(target) => target,
Self::Initialized(_) => panic!("The progress reporter should only be initialized once"),
};
let bar = indicatif::ProgressBar::with_draw_target(Some(files as u64), target);
bar.set_style(
indicatif::ProgressStyle::with_template(
"{msg:8.dim} {bar:60.green/dim} {pos}/{len} files",
)
.unwrap()
.progress_chars("--"),
);
progress.set_message("Checking");
self.0 = Some(progress);
bar.set_message("Checking");
*self = Self::Initialized(bar);
}
fn report_file(&self, _file: &ruff_db::files::File) {
if let Some(ref progress_bar) = self.0 {
progress_bar.inc(1);
match self {
IndicatifReporter::Initialized(progress_bar) => {
progress_bar.inc(1);
}
IndicatifReporter::Pending(_) => {
panic!("`report_file` called before `set_files`")
}
}
}
}

View File

@@ -24,15 +24,32 @@ pub(crate) struct Verbosity {
help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)",
action = clap::ArgAction::Count,
global = true,
overrides_with = "quiet",
)]
verbose: u8,
#[arg(
long,
help = "Use quiet output",
action = clap::ArgAction::Count,
global = true,
overrides_with = "verbose",
)]
quiet: u8,
}
impl Verbosity {
/// Returns the verbosity level based on the number of `-v` flags.
/// Returns the verbosity level based on the number of `-v` and `-q` flags.
///
/// Returns `None` if the user did not specify any verbosity flags.
pub(crate) fn level(&self) -> VerbosityLevel {
// `--quiet` and `--verbose` are mutually exclusive in Clap, so we can just check one first.
match self.quiet {
0 => {}
_ => return VerbosityLevel::Quiet,
// TODO(zanieb): Add support for `-qq` with a "silent" mode
}
match self.verbose {
0 => VerbosityLevel::Default,
1 => VerbosityLevel::Verbose,
@@ -42,9 +59,14 @@ impl Verbosity {
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)]
pub(crate) enum VerbosityLevel {
/// Quiet output. Only shows Ruff and ty events up to the [`ERROR`](tracing::Level::ERROR).
/// Silences output except for summary information.
Quiet,
/// Default output level. Only shows Ruff and ty events up to the [`WARN`](tracing::Level::WARN).
#[default]
Default,
/// Enables verbose output. Emits Ruff and ty events up to the [`INFO`](tracing::Level::INFO).
@@ -62,6 +84,7 @@ pub(crate) enum VerbosityLevel {
impl VerbosityLevel {
const fn level_filter(self) -> LevelFilter {
match self {
VerbosityLevel::Quiet => LevelFilter::ERROR,
VerbosityLevel::Default => LevelFilter::WARN,
VerbosityLevel::Verbose => LevelFilter::INFO,
VerbosityLevel::ExtraVerbose => LevelFilter::DEBUG,

172
crates/ty/src/printer.rs Normal file
View File

@@ -0,0 +1,172 @@
use std::io::StdoutLock;
use indicatif::ProgressDrawTarget;
use crate::logging::VerbosityLevel;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Printer {
verbosity: VerbosityLevel,
no_progress: bool,
}
impl Printer {
#[must_use]
pub(crate) fn with_no_progress(self) -> Self {
Self {
verbosity: self.verbosity,
no_progress: true,
}
}
#[must_use]
pub(crate) fn with_verbosity(self, verbosity: VerbosityLevel) -> Self {
Self {
verbosity,
no_progress: self.no_progress,
}
}
/// Return the [`ProgressDrawTarget`] for this printer.
pub(crate) fn progress_target(self) -> ProgressDrawTarget {
if self.no_progress {
return ProgressDrawTarget::hidden();
}
match self.verbosity {
VerbosityLevel::Quiet => ProgressDrawTarget::hidden(),
VerbosityLevel::Default => ProgressDrawTarget::stderr(),
// Hide the progress bar when in verbose mode.
// Otherwise, it gets interleaved with log messages.
VerbosityLevel::Verbose => ProgressDrawTarget::hidden(),
VerbosityLevel::ExtraVerbose => ProgressDrawTarget::hidden(),
VerbosityLevel::Trace => ProgressDrawTarget::hidden(),
}
}
/// Return the [`Stdout`] stream for important messages.
///
/// Unlike [`Self::stdout_general`], the returned stream will be enabled when
/// [`VerbosityLevel::Quiet`] is used.
fn stdout_important(self) -> Stdout {
match self.verbosity {
VerbosityLevel::Quiet => Stdout::enabled(),
VerbosityLevel::Default => Stdout::enabled(),
VerbosityLevel::Verbose => Stdout::enabled(),
VerbosityLevel::ExtraVerbose => Stdout::enabled(),
VerbosityLevel::Trace => Stdout::enabled(),
}
}
/// Return the [`Stdout`] stream for general messages.
///
/// The returned stream will be disabled when [`VerbosityLevel::Quiet`] is used.
fn stdout_general(self) -> Stdout {
match self.verbosity {
VerbosityLevel::Quiet => Stdout::disabled(),
VerbosityLevel::Default => Stdout::enabled(),
VerbosityLevel::Verbose => Stdout::enabled(),
VerbosityLevel::ExtraVerbose => Stdout::enabled(),
VerbosityLevel::Trace => Stdout::enabled(),
}
}
/// Return the [`Stdout`] stream for a summary message that was explicitly requested by the
/// user.
///
/// For example, in `ty version` the user has requested the version information and we should
/// display it even if [`VerbosityLevel::Quiet`] is used. Or, in `ty check`, if the
/// `TY_MEMORY_REPORT` variable has been set, we should display the memory report because the
/// user has opted-in to display.
pub(crate) fn stream_for_requested_summary(self) -> Stdout {
self.stdout_important()
}
/// Return the [`Stdout`] stream for a summary message on failure.
///
/// For example, in `ty check`, this would be used for the message indicating the number of
/// diagnostics found. The failure summary should capture information that is not reflected in
/// the exit code.
pub(crate) fn stream_for_failure_summary(self) -> Stdout {
self.stdout_important()
}
/// Return the [`Stdout`] stream for a summary message on success.
///
/// For example, in `ty check`, this would be used for the message indicating that no diagnostic
/// were found. The success summary does not capture important information for users that have
/// opted-in to [`VerbosityLevel::Quiet`].
pub(crate) fn stream_for_success_summary(self) -> Stdout {
self.stdout_general()
}
/// Return the [`Stdout`] stream for detailed messages.
///
/// For example, in `ty check`, this would be used for the diagnostic output.
pub(crate) fn stream_for_details(self) -> Stdout {
self.stdout_general()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StreamStatus {
Enabled,
Disabled,
}
#[derive(Debug)]
pub(crate) struct Stdout {
status: StreamStatus,
lock: Option<StdoutLock<'static>>,
}
impl Stdout {
fn enabled() -> Self {
Self {
status: StreamStatus::Enabled,
lock: None,
}
}
fn disabled() -> Self {
Self {
status: StreamStatus::Disabled,
lock: None,
}
}
pub(crate) fn lock(mut self) -> Self {
match self.status {
StreamStatus::Enabled => {
// Drop the previous lock first, to avoid deadlocking
self.lock.take();
self.lock = Some(std::io::stdout().lock());
}
StreamStatus::Disabled => self.lock = None,
}
self
}
fn handle(&mut self) -> Box<dyn std::io::Write + '_> {
match self.lock.as_mut() {
Some(lock) => Box::new(lock),
None => Box::new(std::io::stdout()),
}
}
pub(crate) fn is_enabled(&self) -> bool {
matches!(self.status, StreamStatus::Enabled)
}
}
impl std::fmt::Write for Stdout {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
match self.status {
StreamStatus::Enabled => {
let _ = write!(self.handle(), "{s}");
Ok(())
}
StreamStatus::Disabled => Ok(()),
}
}
}

View File

@@ -14,6 +14,64 @@ use std::{
};
use tempfile::TempDir;
#[test]
fn test_quiet_output() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", "x: int = 1")?;
// By default, we emit an "all checks passed" message
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// With `quiet`, the message is not displayed
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let case = CliTest::with_file("test.py", "x: int = 'foo'")?;
// By default, we emit a diagnostic
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int`
--> test.py:1:1
|
1 | x: int = 'foo'
| ^
|
info: rule `invalid-assignment` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
// With `quiet`, the diagnostic is not displayed, just the summary message
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"
success: false
exit_code: 1
----- stdout -----
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn test_run_in_sub_directory() -> anyhow::Result<()> {
let case = CliTest::with_files([("test.py", "~"), ("subdir/nothing", "")])?;

View File

@@ -15,9 +15,12 @@ bitflags = { workspace = true }
ruff_db = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ty_python_semantic = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
smallvec = { workspace = true }

View File

@@ -0,0 +1,664 @@
//! Docstring parsing utilities for language server features.
//!
//! This module provides functionality for extracting structured information from
//! Python docstrings, including parameter documentation for signature help.
//! Supports Google-style, NumPy-style, and reST/Sphinx-style docstrings.
//! There are no formal specifications for any of these formats, so the parsing
//! logic needs to be tolerant of variations.
use regex::Regex;
use ruff_python_trivia::leading_indentation;
use ruff_source_file::UniversalNewlines;
use std::collections::HashMap;
use std::sync::LazyLock;
// Static regex instances to avoid recompilation
static GOOGLE_SECTION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)^\s*(Args|Arguments|Parameters)\s*:\s*$")
.expect("Google section regex should be valid")
});
static GOOGLE_PARAM_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:\s*(.+)")
.expect("Google parameter regex should be valid")
});
static NUMPY_SECTION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)^\s*Parameters\s*$").expect("NumPy section regex should be valid")
});
static NUMPY_UNDERLINE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\s*-+\s*$").expect("NumPy underline regex should be valid"));
static REST_PARAM_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^\s*:param\s+(?:(\w+)\s+)?(\w+)\s*:\s*(.+)")
.expect("reST parameter regex should be valid")
});
/// Extract parameter documentation from popular docstring formats.
/// Returns a map of parameter names to their documentation.
pub fn get_parameter_documentation(docstring: &str) -> HashMap<String, String> {
let mut param_docs = HashMap::new();
// Google-style docstrings
param_docs.extend(extract_google_style_params(docstring));
// NumPy-style docstrings
param_docs.extend(extract_numpy_style_params(docstring));
// reST/Sphinx-style docstrings
param_docs.extend(extract_rest_style_params(docstring));
param_docs
}
/// Extract parameter documentation from Google-style docstrings.
fn extract_google_style_params(docstring: &str) -> HashMap<String, String> {
let mut param_docs = HashMap::new();
let mut in_args_section = false;
let mut current_param: Option<String> = None;
let mut current_doc = String::new();
for line_obj in docstring.universal_newlines() {
let line = line_obj.as_str();
if GOOGLE_SECTION_REGEX.is_match(line) {
in_args_section = true;
continue;
}
if in_args_section {
// Check if we hit another section (starts with a word followed by colon at line start)
if !line.starts_with(' ') && !line.starts_with('\t') && line.contains(':') {
if let Some(colon_pos) = line.find(':') {
let section_name = line[..colon_pos].trim();
// If this looks like another section, stop processing args
if !section_name.is_empty()
&& section_name
.chars()
.all(|c| c.is_alphabetic() || c.is_whitespace())
{
// Check if this is a known section name
let known_sections = [
"Returns", "Return", "Raises", "Yields", "Yield", "Examples",
"Example", "Note", "Notes", "Warning", "Warnings",
];
if known_sections.contains(&section_name) {
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
in_args_section = false;
continue;
}
}
}
}
if let Some(captures) = GOOGLE_PARAM_REGEX.captures(line) {
// Save previous parameter if exists
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
// Start new parameter
if let (Some(param), Some(desc)) = (captures.get(1), captures.get(3)) {
current_param = Some(param.as_str().to_string());
current_doc = desc.as_str().to_string();
}
} else if line.starts_with(' ') || line.starts_with('\t') {
// This is a continuation of the current parameter documentation
if current_param.is_some() {
if !current_doc.is_empty() {
current_doc.push('\n');
}
current_doc.push_str(line.trim());
}
} else {
// This is a line that doesn't start with whitespace and isn't a parameter
// It might be a section or other content, so stop processing args
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
in_args_section = false;
}
}
}
// Don't forget the last parameter
if let Some(param_name) = current_param {
param_docs.insert(param_name, current_doc.trim().to_string());
}
param_docs
}
/// Calculate the indentation level of a line (number of leading whitespace characters)
fn get_indentation_level(line: &str) -> usize {
leading_indentation(line).len()
}
/// Extract parameter documentation from NumPy-style docstrings.
fn extract_numpy_style_params(docstring: &str) -> HashMap<String, String> {
let mut param_docs = HashMap::new();
let mut lines = docstring
.universal_newlines()
.map(|line| line.as_str())
.peekable();
let mut in_params_section = false;
let mut found_underline = false;
let mut current_param: Option<String> = None;
let mut current_doc = String::new();
let mut base_param_indent: Option<usize> = None;
let mut base_content_indent: Option<usize> = None;
while let Some(line) = lines.next() {
if NUMPY_SECTION_REGEX.is_match(line) {
// Check if the next line is an underline
if let Some(next_line) = lines.peek() {
if NUMPY_UNDERLINE_REGEX.is_match(next_line) {
in_params_section = true;
found_underline = false;
base_param_indent = None;
base_content_indent = None;
continue;
}
}
}
if in_params_section && !found_underline {
if NUMPY_UNDERLINE_REGEX.is_match(line) {
found_underline = true;
continue;
}
}
if in_params_section && found_underline {
let current_indent = get_indentation_level(line);
let trimmed = line.trim();
// Skip empty lines
if trimmed.is_empty() {
continue;
}
// Check if we hit another section
if current_indent == 0 {
if let Some(next_line) = lines.peek() {
if NUMPY_UNDERLINE_REGEX.is_match(next_line) {
// This is another section
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
in_params_section = false;
continue;
}
}
}
// Determine if this could be a parameter line
let could_be_param = if let Some(base_indent) = base_param_indent {
// We've seen parameters before - check if this matches the expected parameter indentation
current_indent == base_indent
} else {
// First potential parameter - check if it has reasonable indentation and content
current_indent > 0
&& (trimmed.contains(':')
|| trimmed.chars().all(|c| c.is_alphanumeric() || c == '_'))
};
if could_be_param {
// Check if this could be a section header by looking at the next line
if let Some(next_line) = lines.peek() {
if NUMPY_UNDERLINE_REGEX.is_match(next_line) {
// This is a section header, not a parameter
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
in_params_section = false;
continue;
}
}
// Set base indentation levels on first parameter
if base_param_indent.is_none() {
base_param_indent = Some(current_indent);
}
// Handle parameter with type annotation (param : type)
if trimmed.contains(':') {
// Save previous parameter if exists
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
// Extract parameter name and description
let parts: Vec<&str> = trimmed.splitn(2, ':').collect();
if parts.len() == 2 {
let param_name = parts[0].trim();
// Extract just the parameter name (before any type info)
let param_name = param_name.split_whitespace().next().unwrap_or(param_name);
current_param = Some(param_name.to_string());
current_doc.clear(); // Description comes on following lines, not on this line
}
} else {
// Handle parameter without type annotation
// Save previous parameter if exists
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
// This line is the parameter name
current_param = Some(trimmed.to_string());
current_doc.clear();
}
} else if current_param.is_some() {
// Determine if this is content for the current parameter
let is_content = if let Some(base_content) = base_content_indent {
// We've seen content before - check if this matches expected content indentation
current_indent >= base_content
} else {
// First potential content line - should be more indented than parameter
if let Some(base_param) = base_param_indent {
current_indent > base_param
} else {
// Fallback: any indented content
current_indent > 0
}
};
if is_content {
// Set base content indentation on first content line
if base_content_indent.is_none() {
base_content_indent = Some(current_indent);
}
// This is a continuation of the current parameter documentation
if !current_doc.is_empty() {
current_doc.push('\n');
}
current_doc.push_str(trimmed);
} else {
// This line doesn't match our expected indentation patterns
// Save current parameter and stop processing
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
in_params_section = false;
}
}
}
}
// Don't forget the last parameter
if let Some(param_name) = current_param {
param_docs.insert(param_name, current_doc.trim().to_string());
}
param_docs
}
/// Extract parameter documentation from reST/Sphinx-style docstrings.
fn extract_rest_style_params(docstring: &str) -> HashMap<String, String> {
let mut param_docs = HashMap::new();
let mut current_param: Option<String> = None;
let mut current_doc = String::new();
for line_obj in docstring.universal_newlines() {
let line = line_obj.as_str();
if let Some(captures) = REST_PARAM_REGEX.captures(line) {
// Save previous parameter if exists
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
// Extract parameter name and description
if let (Some(param_match), Some(desc_match)) = (captures.get(2), captures.get(3)) {
current_param = Some(param_match.as_str().to_string());
current_doc = desc_match.as_str().to_string();
}
} else if current_param.is_some() {
let trimmed = line.trim();
// Check if this is a new section - stop processing if we hit section headers
if trimmed == "Parameters" || trimmed == "Args" || trimmed == "Arguments" {
// Save current param and stop processing
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
break;
}
// Check if this is another directive line starting with ':'
if trimmed.starts_with(':') {
// This is a new directive, save current param
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
// Let the next iteration handle this directive
continue;
}
// Check if this is a continuation line (indented)
if line.starts_with(" ") && !trimmed.is_empty() {
// This is a continuation line
if !current_doc.is_empty() {
current_doc.push('\n');
}
current_doc.push_str(trimmed);
} else if !trimmed.is_empty() && !line.starts_with(' ') && !line.starts_with('\t') {
// This is a non-indented line - likely end of the current parameter
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
break;
}
}
}
// Don't forget the last parameter
if let Some(param_name) = current_param {
param_docs.insert(param_name, current_doc.trim().to_string());
}
param_docs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_google_style_parameter_documentation() {
let docstring = r#"
This is a function description.
Args:
param1 (str): The first parameter description
param2 (int): The second parameter description
This is a continuation of param2 description.
param3: A parameter without type annotation
Returns:
str: The return value description
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(&param_docs["param1"], "The first parameter description");
assert_eq!(
&param_docs["param2"],
"The second parameter description\nThis is a continuation of param2 description."
);
assert_eq!(&param_docs["param3"], "A parameter without type annotation");
}
#[test]
fn test_numpy_style_parameter_documentation() {
let docstring = r#"
This is a function description.
Parameters
----------
param1 : str
The first parameter description
param2 : int
The second parameter description
This is a continuation of param2 description.
param3
A parameter without type annotation
Returns
-------
str
The return value description
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"The first parameter description"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"The second parameter description\nThis is a continuation of param2 description."
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"A parameter without type annotation"
);
}
#[test]
fn test_no_parameter_documentation() {
let docstring = r#"
This is a simple function description without parameter documentation.
"#;
let param_docs = get_parameter_documentation(docstring);
assert!(param_docs.is_empty());
}
#[test]
fn test_mixed_style_parameter_documentation() {
let docstring = r#"
This is a function description.
Args:
param1 (str): Google-style parameter
param2 (int): Another Google-style parameter
Parameters
----------
param3 : bool
NumPy-style parameter
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"Google-style parameter"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"Another Google-style parameter"
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"NumPy-style parameter"
);
}
#[test]
fn test_rest_style_parameter_documentation() {
let docstring = r#"
This is a function description.
:param str param1: The first parameter description
:param int param2: The second parameter description
This is a continuation of param2 description.
:param param3: A parameter without type annotation
:returns: The return value description
:rtype: str
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"The first parameter description"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"The second parameter description\nThis is a continuation of param2 description."
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"A parameter without type annotation"
);
}
#[test]
fn test_mixed_style_with_rest_parameter_documentation() {
let docstring = r#"
This is a function description.
Args:
param1 (str): Google-style parameter
:param int param2: reST-style parameter
:param param3: Another reST-style parameter
Parameters
----------
param4 : bool
NumPy-style parameter
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 4);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"Google-style parameter"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"reST-style parameter"
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"Another reST-style parameter"
);
assert_eq!(
param_docs.get("param4").expect("param4 should exist"),
"NumPy-style parameter"
);
}
#[test]
fn test_numpy_style_with_different_indentation() {
let docstring = r#"
This is a function description.
Parameters
----------
param1 : str
The first parameter description
param2 : int
The second parameter description
This is a continuation of param2 description.
param3
A parameter without type annotation
Returns
-------
str
The return value description
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"The first parameter description"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"The second parameter description\nThis is a continuation of param2 description."
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"A parameter without type annotation"
);
}
#[test]
fn test_numpy_style_with_tabs_and_mixed_indentation() {
// Using raw strings to avoid tab/space conversion issues in the test
let docstring = "
This is a function description.
Parameters
----------
\tparam1 : str
\t\tThe first parameter description
\tparam2 : int
\t\tThe second parameter description
\t\tThis is a continuation of param2 description.
\tparam3
\t\tA parameter without type annotation
";
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"The first parameter description"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"The second parameter description\nThis is a continuation of param2 description."
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"A parameter without type annotation"
);
}
#[test]
fn test_universal_newlines() {
// Test with Windows-style line endings (\r\n)
let docstring_windows = "This is a function description.\r\n\r\nArgs:\r\n param1 (str): The first parameter\r\n param2 (int): The second parameter\r\n";
// Test with old Mac-style line endings (\r)
let docstring_mac = "This is a function description.\r\rArgs:\r param1 (str): The first parameter\r param2 (int): The second parameter\r";
// Test with Unix-style line endings (\n) - should work the same
let docstring_unix = "This is a function description.\n\nArgs:\n param1 (str): The first parameter\n param2 (int): The second parameter\n";
let param_docs_windows = get_parameter_documentation(docstring_windows);
let param_docs_mac = get_parameter_documentation(docstring_mac);
let param_docs_unix = get_parameter_documentation(docstring_unix);
// All should produce the same results
assert_eq!(param_docs_windows.len(), 2);
assert_eq!(param_docs_mac.len(), 2);
assert_eq!(param_docs_unix.len(), 2);
assert_eq!(
param_docs_windows.get("param1"),
Some(&"The first parameter".to_string())
);
assert_eq!(
param_docs_mac.get("param1"),
Some(&"The first parameter".to_string())
);
assert_eq!(
param_docs_unix.get("param1"),
Some(&"The first parameter".to_string())
);
}
}

View File

@@ -1,14 +1,17 @@
mod completion;
mod db;
mod docstring;
mod find_node;
mod goto;
mod hover;
mod inlay_hints;
mod markup;
mod semantic_tokens;
mod signature_help;
pub use completion::completion;
pub use db::Db;
pub use docstring::get_parameter_documentation;
pub use goto::goto_type_definition;
pub use hover::hover;
pub use inlay_hints::inlay_hints;
@@ -16,6 +19,7 @@ pub use markup::MarkupKind;
pub use semantic_tokens::{
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, semantic_tokens,
};
pub use signature_help::{ParameterDetails, SignatureDetails, SignatureHelpInfo, signature_help};
use ruff_db::files::{File, FileRange};
use ruff_text_size::{Ranged, TextRange};

View File

@@ -0,0 +1,687 @@
//! This module handles the "signature help" request in the language server
//! protocol. This request is typically issued by a client when the user types
//! an open parenthesis and starts to enter arguments for a function call.
//! The signature help provides information that the editor displays to the
//! user about the target function signature including parameter names,
//! types, and documentation. It supports multiple signatures for union types
//! and overloads.
use crate::{Db, docstring::get_parameter_documentation, find_node::covering_node};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::semantic_index::definition::Definition;
use ty_python_semantic::types::{CallSignatureDetails, call_signature_details};
// Limitations of the current implementation:
// TODO - If the target function is declared in a stub file but defined (implemented)
// in a source file, the documentation will not reflect the a docstring that appears
// only in the implementation. To do this, we'll need to map the function or
// method in the stub to the implementation and extract the docstring from there.
/// Information about a function parameter
#[derive(Debug, Clone)]
pub struct ParameterDetails {
/// The parameter name (e.g., "param1")
pub name: String,
/// The parameter label in the signature (e.g., "param1: str")
pub label: String,
/// Documentation specific to the parameter, typically extracted from the
/// function's docstring
pub documentation: Option<String>,
}
/// Information about a function signature
#[derive(Debug, Clone)]
pub struct SignatureDetails {
/// Text representation of the full signature (including input parameters and return type).
pub label: String,
/// Documentation for the signature, typically from the function's docstring.
pub documentation: Option<String>,
/// Information about each of the parameters in left-to-right order.
pub parameters: Vec<ParameterDetails>,
/// Index of the parameter that corresponds to the argument where the
/// user's cursor is currently positioned.
pub active_parameter: Option<usize>,
}
/// Signature help information for function calls
#[derive(Debug, Clone)]
pub struct SignatureHelpInfo {
/// Information about each of the signatures for the function call. We
/// need to handle multiple because of unions, overloads, and composite
/// calls like constructors (which invoke both __new__ and __init__).
pub signatures: Vec<SignatureDetails>,
/// Index of the "active signature" which is the first signature where
/// all arguments that are currently present in the code map to parameters.
pub active_signature: Option<usize>,
}
/// Signature help information for function calls at the given position
pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option<SignatureHelpInfo> {
let parsed = parsed_module(db, file).load(db);
// Get the call expression at the given position.
let (call_expr, current_arg_index) = get_call_expr(&parsed, offset)?;
// Get signature details from the semantic analyzer.
let signature_details: Vec<CallSignatureDetails<'_>> =
call_signature_details(db, file, call_expr);
if signature_details.is_empty() {
return None;
}
// Find the active signature - the first signature where all arguments map to parameters.
let active_signature_index = find_active_signature_from_details(&signature_details);
// Convert to SignatureDetails objects.
let signatures: Vec<SignatureDetails> = signature_details
.into_iter()
.map(|details| {
create_signature_details_from_call_signature_details(db, &details, current_arg_index)
})
.collect();
Some(SignatureHelpInfo {
signatures,
active_signature: active_signature_index,
})
}
/// Returns the innermost call expression that contains the specified offset
/// and the index of the argument that the offset maps to.
fn get_call_expr(
parsed: &ruff_db::parsed::ParsedModuleRef,
offset: TextSize,
) -> Option<(&ast::ExprCall, usize)> {
// Create a range from the offset for the covering_node function.
let range = TextRange::new(offset, offset);
// Find the covering node at the given position that is a function call.
let covering_node = covering_node(parsed.syntax().into(), range)
.find_first(|node| matches!(node, AnyNodeRef::ExprCall(_)))
.ok()?;
// Get the function call expression.
let AnyNodeRef::ExprCall(call_expr) = covering_node.node() else {
return None;
};
// Determine which argument corresponding to the current cursor location.
let current_arg_index = get_argument_index(call_expr, offset);
Some((call_expr, current_arg_index))
}
/// Determine which argument is associated with the specified offset.
/// Returns zero if not within any argument.
fn get_argument_index(call_expr: &ast::ExprCall, offset: TextSize) -> usize {
let mut current_arg = 0;
for (i, arg) in call_expr.arguments.arguments_source_order().enumerate() {
if offset <= arg.end() {
return i;
}
current_arg = i + 1;
}
current_arg
}
/// Create signature details from `CallSignatureDetails`.
fn create_signature_details_from_call_signature_details(
db: &dyn crate::Db,
details: &CallSignatureDetails,
current_arg_index: usize,
) -> SignatureDetails {
let signature_label = details.label.clone();
let documentation = get_callable_documentation(db, details.definition);
// Translate the argument index to parameter index using the mapping.
let active_parameter =
if details.argument_to_parameter_mapping.is_empty() && current_arg_index == 0 {
Some(0)
} else {
details
.argument_to_parameter_mapping
.get(current_arg_index)
.and_then(|&param_index| param_index)
.or({
// If we can't find a mapping for this argument, but we have a current
// argument index, use that as the active parameter if it's within bounds.
if current_arg_index < details.parameter_label_offsets.len() {
Some(current_arg_index)
} else {
None
}
})
};
SignatureDetails {
label: signature_label.clone(),
documentation: Some(documentation),
parameters: create_parameters_from_offsets(
&details.parameter_label_offsets,
&signature_label,
db,
details.definition,
&details.parameter_names,
),
active_parameter,
}
}
/// Determine appropriate documentation for a callable type based on its original type.
fn get_callable_documentation(db: &dyn crate::Db, definition: Option<Definition>) -> String {
// TODO: If the definition is located within a stub file and no docstring
// is present, try to map the symbol to an implementation file and extract
// the docstring from that location.
if let Some(definition) = definition {
definition.docstring(db).unwrap_or_default()
} else {
String::new()
}
}
/// Create `ParameterDetails` objects from parameter label offsets.
fn create_parameters_from_offsets(
parameter_offsets: &[TextRange],
signature_label: &str,
db: &dyn crate::Db,
definition: Option<Definition>,
parameter_names: &[String],
) -> Vec<ParameterDetails> {
// Extract parameter documentation from the function's docstring if available.
let param_docs = if let Some(definition) = definition {
let docstring = definition.docstring(db);
docstring
.map(|doc| get_parameter_documentation(&doc))
.unwrap_or_default()
} else {
std::collections::HashMap::new()
};
parameter_offsets
.iter()
.enumerate()
.map(|(i, offset)| {
// Extract the parameter label from the signature string.
let start = usize::from(offset.start());
let end = usize::from(offset.end());
let label = signature_label
.get(start..end)
.unwrap_or("unknown")
.to_string();
// Get the parameter name for documentation lookup.
let param_name = parameter_names.get(i).map(String::as_str).unwrap_or("");
ParameterDetails {
name: param_name.to_string(),
label,
documentation: param_docs.get(param_name).cloned(),
}
})
.collect()
}
/// Find the active signature index from `CallSignatureDetails`.
/// The active signature is the first signature where all arguments present in the call
/// have valid mappings to parameters (i.e., none of the mappings are None).
fn find_active_signature_from_details(signature_details: &[CallSignatureDetails]) -> Option<usize> {
let first = signature_details.first()?;
// If there are no arguments in the mapping, just return the first signature.
if first.argument_to_parameter_mapping.is_empty() {
return Some(0);
}
// First, try to find a signature where all arguments have valid parameter mappings.
let perfect_match = signature_details.iter().position(|details| {
// Check if all arguments have valid parameter mappings (i.e., are not None).
details
.argument_to_parameter_mapping
.iter()
.all(Option::is_some)
});
if let Some(index) = perfect_match {
return Some(index);
}
// If no perfect match, find the signature with the most valid argument mappings.
let (best_index, _) = signature_details
.iter()
.enumerate()
.max_by_key(|(_, details)| {
details
.argument_to_parameter_mapping
.iter()
.filter(|mapping| mapping.is_some())
.count()
})?;
Some(best_index)
}
#[cfg(test)]
mod tests {
use crate::signature_help::SignatureHelpInfo;
use crate::tests::{CursorTest, cursor_test};
#[test]
fn signature_help_basic_function_call() {
let test = cursor_test(
r#"
def example_function(param1: str, param2: int) -> str:
"""This is a docstring for the example function.
Args:
param1: The first parameter as a string
param2: The second parameter as an integer
Returns:
A formatted string combining both parameters
"""
return f"{param1}: {param2}"
result = example_function(<CURSOR>
"#,
);
// Test that signature help is provided
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert!(signature.label.contains("param1") && signature.label.contains("param2"));
// Verify that the docstring is extracted and included in the documentation
let expected_docstring = concat!(
"This is a docstring for the example function.\n",
" \n",
" Args:\n",
" param1: The first parameter as a string\n",
" param2: The second parameter as an integer\n",
" \n",
" Returns:\n",
" A formatted string combining both parameters\n",
" "
);
assert_eq!(
signature.documentation,
Some(expected_docstring.to_string())
);
assert_eq!(result.active_signature, Some(0));
assert_eq!(signature.active_parameter, Some(0));
}
#[test]
fn signature_help_method_call() {
let test = cursor_test(
r#"
class MyClass:
def my_method(self, arg1: str, arg2: bool) -> None:
pass
obj = MyClass()
obj.my_method(arg2=True, arg1=<CURSOR>
"#,
);
// Test that signature help is provided for method calls
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert!(signature.label.contains("arg1") && signature.label.contains("arg2"));
assert_eq!(result.active_signature, Some(0));
// Check the active parameter from the active signature
if let Some(active_sig_index) = result.active_signature {
let active_signature = &result.signatures[active_sig_index];
assert_eq!(active_signature.active_parameter, Some(0));
}
}
#[test]
fn signature_help_nested_function_calls() {
let test = cursor_test(
r#"
def outer(a: int) -> int:
return a * 2
def inner(b: str) -> str:
return b.upper()
result = outer(inner(<CURSOR>
"#,
);
// Test that signature help focuses on the innermost function call
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert!(signature.label.contains("str") || signature.label.contains("->"));
assert_eq!(result.active_signature, Some(0));
assert_eq!(signature.active_parameter, Some(0));
}
#[test]
fn signature_help_union_callable() {
let test = cursor_test(
r#"
import random
def func_a(x: int) -> int:
return x
def func_b(y: str) -> str:
return y
if random.random() > 0.5:
f = func_a
else:
f = func_b
f(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 2);
let signature = &result.signatures[0];
assert_eq!(signature.label, "(x: int) -> int");
assert_eq!(signature.parameters.len(), 1);
// Check parameter information
let param = &signature.parameters[0];
assert_eq!(param.label, "x: int");
assert_eq!(param.name, "x");
// Validate the second signature (from func_b)
let signature_b = &result.signatures[1];
assert_eq!(signature_b.label, "(y: str) -> str");
assert_eq!(signature_b.parameters.len(), 1);
// Check parameter information for the second signature
let param_b = &signature_b.parameters[0];
assert_eq!(param_b.label, "y: str");
assert_eq!(param_b.name, "y");
assert_eq!(result.active_signature, Some(0));
// Check the active parameter from the active signature
if let Some(active_sig_index) = result.active_signature {
let active_signature = &result.signatures[active_sig_index];
assert_eq!(active_signature.active_parameter, Some(0));
}
}
#[test]
fn signature_help_overloaded_function() {
let test = cursor_test(
r#"
from typing import overload
@overload
def process(value: int) -> str: ...
@overload
def process(value: str) -> int: ...
def process(value):
if isinstance(value, int):
return str(value)
else:
return len(value)
result = process(<CURSOR>
"#,
);
// Test that signature help is provided for overloaded functions
let result = test.signature_help().expect("Should have signature help");
// We should have signatures for the overloads
assert_eq!(result.signatures.len(), 2);
assert_eq!(result.active_signature, Some(0));
// Check the active parameter from the active signature
if let Some(active_sig_index) = result.active_signature {
let active_signature = &result.signatures[active_sig_index];
assert_eq!(active_signature.active_parameter, Some(0));
}
// Validate the first overload: process(value: int) -> str
let signature1 = &result.signatures[0];
assert_eq!(signature1.label, "(value: int) -> str");
assert_eq!(signature1.parameters.len(), 1);
let param1 = &signature1.parameters[0];
assert_eq!(param1.label, "value: int");
assert_eq!(param1.name, "value");
// Validate the second overload: process(value: str) -> int
let signature2 = &result.signatures[1];
assert_eq!(signature2.label, "(value: str) -> int");
assert_eq!(signature2.parameters.len(), 1);
let param2 = &signature2.parameters[0];
assert_eq!(param2.label, "value: str");
assert_eq!(param2.name, "value");
}
#[test]
fn signature_help_class_constructor() {
let test = cursor_test(
r#"
class Point:
"""A simple point class representing a 2D coordinate."""
def __init__(self, x: int, y: int):
"""Initialize a point with x and y coordinates.
Args:
x: The x-coordinate
y: The y-coordinate
"""
self.x = x
self.y = y
point = Point(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
// Should have exactly one signature for the constructor
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
// Validate the constructor signature
assert_eq!(signature.label, "(x: int, y: int) -> Point");
assert_eq!(signature.parameters.len(), 2);
// Validate the first parameter (x: int)
let param_x = &signature.parameters[0];
assert_eq!(param_x.label, "x: int");
assert_eq!(param_x.name, "x");
assert_eq!(param_x.documentation, Some("The x-coordinate".to_string()));
// Validate the second parameter (y: int)
let param_y = &signature.parameters[1];
assert_eq!(param_y.label, "y: int");
assert_eq!(param_y.name, "y");
assert_eq!(param_y.documentation, Some("The y-coordinate".to_string()));
// Should have the __init__ method docstring as documentation (not the class docstring)
let expected_docstring = "Initialize a point with x and y coordinates.\n \n Args:\n x: The x-coordinate\n y: The y-coordinate\n ";
assert_eq!(
signature.documentation,
Some(expected_docstring.to_string())
);
}
#[test]
fn signature_help_callable_object() {
let test = cursor_test(
r#"
class Multiplier:
def __call__(self, x: int) -> int:
return x * 2
multiplier = Multiplier()
result = multiplier(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
// Should have a signature for the callable object
assert!(!result.signatures.is_empty());
let signature = &result.signatures[0];
// Should provide signature help for the callable
assert!(signature.label.contains("int") || signature.label.contains("->"));
}
#[test]
fn signature_help_subclass_of_constructor() {
let test = cursor_test(
r#"
from typing import Type
def create_instance(cls: Type[list]) -> list:
return cls(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
// Should have a signature
assert!(!result.signatures.is_empty());
let signature = &result.signatures[0];
// Should have empty documentation for now
assert_eq!(signature.documentation, Some(String::new()));
}
#[test]
fn signature_help_parameter_label_offsets() {
let test = cursor_test(
r#"
def test_function(param1: str, param2: int, param3: bool) -> str:
return f"{param1}: {param2}, {param3}"
result = test_function(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert_eq!(signature.parameters.len(), 3);
// Check that we have parameter labels
for (i, param) in signature.parameters.iter().enumerate() {
let expected_param_spec = match i {
0 => "param1: str",
1 => "param2: int",
2 => "param3: bool",
_ => panic!("Unexpected parameter index"),
};
assert_eq!(param.label, expected_param_spec);
}
}
#[test]
fn signature_help_active_signature_selection() {
// This test verifies that the algorithm correctly selects the first signature
// where all arguments present in the call have valid parameter mappings.
let test = cursor_test(
r#"
from typing import overload
@overload
def process(value: int) -> str: ...
@overload
def process(value: str, flag: bool) -> int: ...
def process(value, flag=None):
if isinstance(value, int):
return str(value)
elif flag is not None:
return len(value) if flag else 0
else:
return len(value)
# Call with two arguments - should select the second overload
result = process("hello", True<CURSOR>)
"#,
);
let result = test.signature_help().expect("Should have signature help");
// Should have signatures for the overloads.
assert!(!result.signatures.is_empty());
// Check that we have an active signature and parameter
if let Some(active_sig_index) = result.active_signature {
let active_signature = &result.signatures[active_sig_index];
assert_eq!(active_signature.active_parameter, Some(1));
}
}
#[test]
fn signature_help_parameter_documentation() {
let test = cursor_test(
r#"
def documented_function(param1: str, param2: int) -> str:
"""This is a function with parameter documentation.
Args:
param1: The first parameter description
param2: The second parameter description
"""
return f"{param1}: {param2}"
result = documented_function(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert_eq!(signature.parameters.len(), 2);
// Check that parameter documentation is extracted
let param1 = &signature.parameters[0];
assert_eq!(
param1.documentation,
Some("The first parameter description".to_string())
);
let param2 = &signature.parameters[1];
assert_eq!(
param2.documentation,
Some("The second parameter description".to_string())
);
}
impl CursorTest {
fn signature_help(&self) -> Option<SignatureHelpInfo> {
crate::signature_help::signature_help(&self.db, self.cursor.file, self.cursor.offset)
}
}
}

View File

@@ -5,7 +5,7 @@ use std::{cmp, fmt};
use crate::metadata::settings::file_settings;
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
use crate::{Project, ProjectMetadata, Reporter};
use crate::{ProgressReporter, Project, ProjectMetadata};
use ruff_db::Db as SourceDb;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, Files};
@@ -87,7 +87,7 @@ impl ProjectDatabase {
}
/// Checks all open files in the project and its dependencies, using the given reporter.
pub fn check_with_reporter(&self, reporter: &mut dyn Reporter) -> Vec<Diagnostic> {
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
let reporter = AssertUnwindSafe(reporter);
self.project().check(self, CheckMode::OpenFiles, reporter)
}
@@ -95,7 +95,7 @@ impl ProjectDatabase {
/// Check the project with the given mode.
pub fn check_with_mode(&self, mode: CheckMode) -> Vec<Diagnostic> {
let mut reporter = DummyReporter;
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn Reporter);
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn ProgressReporter);
self.project().check(self, mode, reporter)
}

View File

@@ -113,7 +113,7 @@ pub struct Project {
}
/// A progress reporter.
pub trait Reporter: Send + Sync {
pub trait ProgressReporter: Send + Sync {
/// Initialize the reporter with the number of files.
fn set_files(&mut self, files: usize);
@@ -121,11 +121,11 @@ pub trait Reporter: Send + Sync {
fn report_file(&self, file: &File);
}
/// A no-op implementation of [`Reporter`].
/// A no-op implementation of [`ProgressReporter`].
#[derive(Default)]
pub struct DummyReporter;
impl Reporter for DummyReporter {
impl ProgressReporter for DummyReporter {
fn set_files(&mut self, _files: usize) {}
fn report_file(&self, _file: &File) {}
}
@@ -212,7 +212,7 @@ impl Project {
self,
db: &ProjectDatabase,
mode: CheckMode,
mut reporter: AssertUnwindSafe<&mut dyn Reporter>,
mut reporter: AssertUnwindSafe<&mut dyn ProgressReporter>,
) -> Vec<Diagnostic> {
let project_span = tracing::debug_span!("Project::check");
let _span = project_span.enter();
@@ -257,8 +257,11 @@ impl Project {
tracing::debug_span!(parent: project_span, "check_file", ?file);
let _entered = check_file_span.entered();
let result = self.check_file_impl(&db, file);
file_diagnostics.lock().unwrap().extend(result);
let result = check_file_impl(&db, file);
file_diagnostics
.lock()
.unwrap()
.extend(result.iter().map(Clone::clone));
reporter.report_file(&file);
});
@@ -285,7 +288,7 @@ impl Project {
return Vec::new();
}
self.check_file_impl(db, file)
check_file_impl(db, file).iter().map(Clone::clone).collect()
}
/// Opens a file in the project.
@@ -466,71 +469,73 @@ impl Project {
self.set_file_set(db).to(IndexedFiles::lazy());
}
}
}
fn check_file_impl(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)]
pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Box<[Diagnostic]> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
// Abort checking if there are IO errors.
let source = source_text(db, file);
// Abort checking if there are IO errors.
let source = source_text(db, file);
if let Some(read_error) = source.read_error() {
diagnostics.push(
IOErrorDiagnostic {
file: Some(file),
error: read_error.clone().into(),
}
.to_diagnostic(),
);
return diagnostics;
}
let parsed = parsed_module(db, file);
let parsed_ref = parsed.load(db);
diagnostics.extend(
parsed_ref
.errors()
.iter()
.map(|error| Diagnostic::invalid_syntax(file, &error.error, error)),
);
diagnostics.extend(parsed_ref.unsupported_syntax_errors().iter().map(|error| {
let mut error = Diagnostic::invalid_syntax(file, error, error);
add_inferred_python_version_hint_to_diagnostic(db, &mut error, "parsing syntax");
error
}));
{
let db = AssertUnwindSafe(db);
match catch(&**db, file, || check_types(*db, file)) {
Ok(Some(type_check_diagnostics)) => {
diagnostics.extend(type_check_diagnostics.into_iter().cloned());
}
Ok(None) => {}
Err(diagnostic) => diagnostics.push(diagnostic),
if let Some(read_error) = source.read_error() {
diagnostics.push(
IOErrorDiagnostic {
file: Some(file),
error: read_error.clone().into(),
}
}
if self
.open_fileset(db)
.is_none_or(|files| !files.contains(&file))
{
// Drop the AST now that we are done checking this file. It is not currently open,
// so it is unlikely to be accessed again soon. If any queries need to access the AST
// from across files, it will be re-parsed.
parsed.clear();
}
diagnostics.sort_unstable_by_key(|diagnostic| {
diagnostic
.primary_span()
.and_then(|span| span.range())
.unwrap_or_default()
.start()
});
diagnostics
.to_diagnostic(),
);
return diagnostics.into_boxed_slice();
}
let parsed = parsed_module(db, file);
let parsed_ref = parsed.load(db);
diagnostics.extend(
parsed_ref
.errors()
.iter()
.map(|error| Diagnostic::invalid_syntax(file, &error.error, error)),
);
diagnostics.extend(parsed_ref.unsupported_syntax_errors().iter().map(|error| {
let mut error = Diagnostic::invalid_syntax(file, error, error);
add_inferred_python_version_hint_to_diagnostic(db, &mut error, "parsing syntax");
error
}));
{
let db = AssertUnwindSafe(db);
match catch(&**db, file, || check_types(*db, file)) {
Ok(Some(type_check_diagnostics)) => {
diagnostics.extend(type_check_diagnostics);
}
Ok(None) => {}
Err(diagnostic) => diagnostics.push(diagnostic),
}
}
if db
.project()
.open_fileset(db)
.is_none_or(|files| !files.contains(&file))
{
// Drop the AST now that we are done checking this file. It is not currently open,
// so it is unlikely to be accessed again soon. If any queries need to access the AST
// from across files, it will be re-parsed.
parsed.clear();
}
diagnostics.sort_unstable_by_key(|diagnostic| {
diagnostic
.primary_span()
.and_then(|span| span.range())
.unwrap_or_default()
.start()
});
diagnostics.into_boxed_slice()
}
#[derive(Debug)]
@@ -701,8 +706,8 @@ where
#[cfg(test)]
mod tests {
use crate::Db;
use crate::ProjectMetadata;
use crate::check_file_impl;
use crate::db::tests::TestDb;
use ruff_db::Db as _;
use ruff_db::files::system_path_to_file;
@@ -741,9 +746,8 @@ mod tests {
assert_eq!(source_text(&db, file).as_str(), "");
assert_eq!(
db.project()
.check_file_impl(&db, file)
.into_iter()
check_file_impl(&db, file)
.iter()
.map(|diagnostic| diagnostic.primary_message().to_string())
.collect::<Vec<_>>(),
vec!["Failed to read file: No such file or directory".to_string()]
@@ -758,9 +762,8 @@ mod tests {
assert_eq!(source_text(&db, file).as_str(), "");
assert_eq!(
db.project()
.check_file_impl(&db, file)
.into_iter()
check_file_impl(&db, file)
.iter()
.map(|diagnostic| diagnostic.primary_message().to_string())
.collect::<Vec<_>>(),
vec![] as Vec<String>

View File

@@ -29,7 +29,6 @@ bitflags = { workspace = true }
camino = { workspace = true }
colored = { workspace = true }
compact_str = { workspace = true }
countme = { workspace = true }
drop_bomb = { workspace = true }
get-size2 = { workspace = true }
indexmap = { workspace = true }

View File

@@ -502,5 +502,55 @@ def f6(a, /): ...
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
```
## Module-literal types
Two "copies" of a single-file module are considered equivalent types, even if the different copies
were originally imported in different first-party modules:
`module.py`:
```py
import typing
```
`main.py`:
```py
import typing
from module import typing as other_typing
from ty_extensions import TypeOf, static_assert, is_equivalent_to
static_assert(is_equivalent_to(TypeOf[typing], TypeOf[other_typing]))
static_assert(is_equivalent_to(TypeOf[typing] | int | str, str | int | TypeOf[other_typing]))
```
We currently do not consider module-literal types to be equivalent if the underlying module is a
package and the different "copies" of the module were originally imported in different modules. This
is because we might consider submodules to be available as attributes on one copy but not on the
other, depending on whether those submodules were explicitly imported in the original importing
module:
`module2.py`:
```py
import importlib
import importlib.abc
```
`main2.py`:
```py
import importlib
from module2 import importlib as other_importlib
from ty_extensions import TypeOf, static_assert, is_equivalent_to
# error: [unresolved-attribute] "Type `<module 'importlib'>` has no attribute `abc`"
reveal_type(importlib.abc) # revealed: Unknown
reveal_type(other_importlib.abc) # revealed: <module 'importlib.abc'>
static_assert(not is_equivalent_to(TypeOf[importlib], TypeOf[other_importlib]))
```
[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent

View File

@@ -259,7 +259,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
.push(UseDefMapBuilder::new(is_class_scope));
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::default());
let scope_id = ScopeId::new(self.db, self.file, file_scope_id, countme::Count::default());
let scope_id = ScopeId::new(self.db, self.file, file_scope_id);
self.scope_ids_by_scope.push(scope_id);
let previous = self.scopes_by_node.insert(node.node_key(), file_scope_id);
@@ -495,7 +495,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
place,
kind,
is_reexported,
countme::Count::default(),
);
let num_definitions = {
@@ -731,7 +730,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
subject,
kind,
guard,
countme::Count::default(),
);
let predicate = PredicateOrLiteral::Predicate(Predicate {
node: PredicateNode::Pattern(pattern_predicate),
@@ -781,7 +779,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
AstNodeRef::new(self.module, expression_node),
assigned_to.map(|assigned_to| AstNodeRef::new(self.module, assigned_to)),
expression_kind,
countme::Count::default(),
);
self.expressions_by_node
.insert(expression_node.into(), expression);
@@ -986,7 +983,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
// Note `target` belongs to the `self.module` tree
AstNodeRef::new(self.module, target),
UnpackValue::new(unpackable.kind(), value),
countme::Count::default(),
));
Some(unpackable.as_current_assignment(unpack))
}

View File

@@ -1,7 +1,7 @@
use std::ops::Deref;
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::ParsedModuleRef;
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextRange};
@@ -40,8 +40,6 @@ pub struct Definition<'db> {
/// This is a dedicated field to avoid accessing `kind` to compute this value.
pub(crate) is_reexported: bool,
count: countme::Count<Definition<'static>>,
}
// The Salsa heap is tracked separately.
@@ -59,6 +57,45 @@ impl<'db> Definition<'db> {
pub fn focus_range(self, db: &'db dyn Db, module: &ParsedModuleRef) -> FileRange {
FileRange::new(self.file(db), self.kind(db).target_range(module))
}
/// Extract a docstring from this definition, if applicable.
/// This method returns a docstring for function and class definitions.
/// The docstring is extracted from the first statement in the body if it's a string literal.
pub fn docstring(self, db: &'db dyn Db) -> Option<String> {
let file = self.file(db);
let module = parsed_module(db, file).load(db);
let kind = self.kind(db);
match kind {
DefinitionKind::Function(function_def) => {
let function_node = function_def.node(&module);
docstring_from_body(&function_node.body)
.map(|docstring_expr| docstring_expr.value.to_str().to_owned())
}
DefinitionKind::Class(class_def) => {
let class_node = class_def.node(&module);
docstring_from_body(&class_node.body)
.map(|docstring_expr| docstring_expr.value.to_str().to_owned())
}
_ => None,
}
}
}
/// Extract a docstring from a function or class body.
fn docstring_from_body(body: &[ast::Stmt]) -> Option<&ast::ExprStringLiteral> {
let stmt = body.first()?;
// Require the docstring to be a standalone expression.
let ast::Stmt::Expr(ast::StmtExpr {
value,
range: _,
node_index: _,
}) = stmt
else {
return None;
};
// Only match string literals.
value.as_string_literal_expr()
}
/// One or more [`Definition`]s.

View File

@@ -58,8 +58,6 @@ pub(crate) struct Expression<'db> {
/// Should this expression be inferred as a normal expression or a type expression?
pub(crate) kind: ExpressionKind,
count: countme::Count<Expression<'static>>,
}
// The Salsa heap is tracked separately.

View File

@@ -441,8 +441,6 @@ pub struct ScopeId<'db> {
pub file: File,
pub file_scope_id: FileScopeId,
count: countme::Count<ScopeId<'static>>,
}
// The Salsa heap is tracked separately.

View File

@@ -138,8 +138,6 @@ pub(crate) struct PatternPredicate<'db> {
pub(crate) kind: PatternPredicateKind<'db>,
pub(crate) guard: Option<Expression<'db>>,
count: countme::Count<PatternPredicate<'static>>,
}
// The Salsa heap is tracked separately.

View File

@@ -1,5 +1,5 @@
use infer::nearest_enclosing_class;
use itertools::Either;
use itertools::{Either, Itertools};
use ruff_db::parsed::parsed_module;
use std::slice::Iter;
@@ -46,7 +46,9 @@ use crate::types::generics::{
GenericContext, PartialSpecialization, Specialization, walk_generic_context,
walk_partial_specialization, walk_specialization,
};
pub use crate::types::ide_support::{all_members, definition_kind_for_name};
pub use crate::types::ide_support::{
CallSignatureDetails, all_members, call_signature_details, definition_kind_for_name,
};
use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
@@ -88,8 +90,7 @@ mod definition;
#[cfg(test)]
mod property_tests;
#[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)]
pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
pub fn check_types(db: &dyn Db, file: File) -> Vec<Diagnostic> {
let _span = tracing::trace_span!("check_types", ?file).entered();
tracing::debug!("Checking file '{path}'", path = file.path(db));
@@ -111,7 +112,7 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
check_suppressions(db, file, &mut diagnostics);
diagnostics
diagnostics.into_vec()
}
/// Infer the type of a binding.
@@ -818,7 +819,11 @@ impl<'db> Type<'db> {
}
pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: &Module) -> Self {
Self::ModuleLiteral(ModuleLiteralType::new(db, importing_file, submodule))
Self::ModuleLiteral(ModuleLiteralType::new(
db,
submodule,
submodule.kind().is_package().then_some(importing_file),
))
}
pub const fn into_module_literal(self) -> Option<ModuleLiteralType<'db>> {
@@ -1215,10 +1220,11 @@ impl<'db> Type<'db> {
fn has_relation_to(self, db: &'db dyn Db, target: Type<'db>, relation: TypeRelation) -> bool {
// Subtyping implies assignability, so if subtyping is reflexive and the two types are
// equivalent, it is both a subtype and assignable. Assignability is always reflexive.
if (relation.is_assignability() || self.subtyping_is_always_reflexive())
&& self.is_equivalent_to(db, target)
{
// equal, it is both a subtype and assignable. Assignability is always reflexive.
//
// Note that we could do a full equivalence check here, but that would be both expensive
// and unnecessary. This early return is only an optimisation.
if (relation.is_assignability() || self.subtyping_is_always_reflexive()) && self == target {
return true;
}
@@ -1256,6 +1262,9 @@ impl<'db> Type<'db> {
// Two identical typevars must always solve to the same type, so they are always
// subtypes of each other and assignable to each other.
//
// Note that this is not handled by the early return at the beginning of this method,
// since subtyping between a TypeVar and an arbitrary other type cannot be guaranteed to be reflexive.
(Type::TypeVar(lhs_typevar), Type::TypeVar(rhs_typevar))
if lhs_typevar == rhs_typevar =>
{
@@ -7499,20 +7508,51 @@ pub enum WrapperDescriptorKind {
#[salsa::interned(debug)]
#[derive(PartialOrd, Ord)]
pub struct ModuleLiteralType<'db> {
/// The file in which this module was imported.
///
/// We need this in order to know which submodules should be attached to it as attributes
/// (because the submodules were also imported in this file).
pub importing_file: File,
/// The imported module.
pub module: Module,
/// The file in which this module was imported.
///
/// If the module is a module that could have submodules (a package),
/// we need this in order to know which submodules should be attached to it as attributes
/// (because the submodules were also imported in this file). For a package, this should
/// therefore always be `Some()`. If the module is not a package, however, this should
/// always be `None`: this helps reduce memory usage (the information is redundant for
/// single-file modules), and ensures that two module-literal types that both refer to
/// the same underlying single-file module are understood by ty as being equivalent types
/// in all situations.
_importing_file: Option<File>,
}
// The Salsa heap is tracked separately.
impl get_size2::GetSize for ModuleLiteralType<'_> {}
impl<'db> ModuleLiteralType<'db> {
fn importing_file(self, db: &'db dyn Db) -> Option<File> {
debug_assert_eq!(
self._importing_file(db).is_some(),
self.module(db).kind().is_package()
);
self._importing_file(db)
}
fn available_submodule_attributes(&self, db: &'db dyn Db) -> impl Iterator<Item = Name> {
self.importing_file(db)
.into_iter()
.flat_map(|file| imported_modules(db, file))
.filter_map(|submodule_name| submodule_name.relative_to(self.module(db).name()))
.filter_map(|relative_submodule| relative_submodule.components().next().map(Name::from))
}
fn resolve_submodule(self, db: &'db dyn Db, name: &str) -> Option<Type<'db>> {
let importing_file = self.importing_file(db)?;
let relative_submodule_name = ModuleName::new(name)?;
let mut absolute_submodule_name = self.module(db).name().clone();
absolute_submodule_name.extend(&relative_submodule_name);
let submodule = resolve_module(db, &absolute_submodule_name)?;
Some(Type::module_literal(db, importing_file, &submodule))
}
fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
// `__dict__` is a very special member that is never overridden by module globals;
// we should always look it up directly as an attribute on `types.ModuleType`,
@@ -7532,16 +7572,9 @@ impl<'db> ModuleLiteralType<'db> {
// the parent module's `__init__.py` file being evaluated. That said, we have
// chosen to always have the submodule take priority. (This matches pyright's
// current behavior, but is the opposite of mypy's current behavior.)
if let Some(submodule_name) = ModuleName::new(name) {
let importing_file = self.importing_file(db);
let imported_submodules = imported_modules(db, importing_file);
let mut full_submodule_name = self.module(db).name().clone();
full_submodule_name.extend(&submodule_name);
if imported_submodules.contains(&full_submodule_name) {
if let Some(submodule) = resolve_module(db, &full_submodule_name) {
return Place::bound(Type::module_literal(db, importing_file, &submodule))
.into();
}
if self.available_submodule_attributes(db).contains(name) {
if let Some(submodule) = self.resolve_submodule(db, name) {
return Place::bound(submodule).into();
}
}
@@ -7906,6 +7939,10 @@ impl<'db> UnionType<'db> {
/// Return `true` if `self` represents the exact same sets of possible runtime objects as `other`
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
if self == other {
return true;
}
let self_elements = self.elements(db);
let other_elements = other.elements(db);
@@ -7913,10 +7950,6 @@ impl<'db> UnionType<'db> {
return false;
}
if self == other {
return true;
}
let sorted_self = self.normalized(db);
if sorted_self == other {
@@ -7997,8 +8030,11 @@ impl<'db> IntersectionType<'db> {
/// Return `true` if `self` represents exactly the same set of possible runtime objects as `other`
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
let self_positive = self.positive(db);
if self == other {
return true;
}
let self_positive = self.positive(db);
let other_positive = other.positive(db);
if self_positive.len() != other_positive.len() {
@@ -8006,17 +8042,12 @@ impl<'db> IntersectionType<'db> {
}
let self_negative = self.negative(db);
let other_negative = other.negative(db);
if self_negative.len() != other_negative.len() {
return false;
}
if self == other {
return true;
}
let sorted_self = self.normalized(db);
if sorted_self == other {

View File

@@ -3,7 +3,7 @@ use super::{Signature, Type};
use crate::Db;
mod arguments;
mod bind;
pub(crate) mod bind;
pub(super) use arguments::{Argument, CallArgumentTypes, CallArguments};
pub(super) use bind::{Binding, Bindings, CallableBinding};

View File

@@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::ops::{Deref, DerefMut};
use itertools::{Either, Itertools};
use ruff_python_ast as ast;
use crate::Db;
use crate::types::KnownClass;
@@ -14,6 +15,26 @@ use super::Type;
pub(crate) struct CallArguments<'a>(Vec<Argument<'a>>);
impl<'a> CallArguments<'a> {
/// Create `CallArguments` from AST arguments
pub(crate) fn from_arguments(arguments: &'a ast::Arguments) -> Self {
arguments
.arguments_source_order()
.map(|arg_or_keyword| match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic,
_ => Argument::Positional,
},
ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => {
if let Some(arg) = arg {
Argument::Keyword(&arg.id)
} else {
Argument::Keywords
}
}
})
.collect()
}
/// Prepend an optional extra synthetic argument (for a `self` or `cls` parameter) to the front
/// of this argument list. (If `bound_self` is none, we return the argument list
/// unmodified.)

View File

@@ -2109,7 +2109,7 @@ impl<'db> Binding<'db> {
}
}
fn match_parameters(
pub(crate) fn match_parameters(
&mut self,
arguments: &CallArguments<'_>,
argument_forms: &mut [Option<ParameterForm>],
@@ -2267,6 +2267,12 @@ impl<'db> Binding<'db> {
self.parameter_tys = parameter_tys;
self.errors = errors;
}
/// Returns a vector where each index corresponds to an argument position,
/// and the value is the parameter index that argument maps to (if any).
pub(crate) fn argument_to_parameter_mapping(&self) -> &[Option<usize>] {
&self.argument_parameters
}
}
#[derive(Clone, Debug)]

View File

@@ -443,8 +443,13 @@ impl<'db> ClassType<'db> {
}
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
if self == other {
return true;
}
match (self, other) {
(ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other,
// A non-generic class is never equivalent to a generic class.
// Two non-generic classes are only equivalent if they are equal (handled above).
(ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => false,
(ClassType::Generic(this), ClassType::Generic(other)) => {
@@ -673,6 +678,7 @@ impl<'db> ClassType<'db> {
if let Some(signature) = signature {
let synthesized_signature = |signature: &Signature<'db>| {
Signature::new(signature.parameters().clone(), Some(correct_return_type))
.with_definition(signature.definition())
.bind_self()
};

View File

@@ -1598,6 +1598,10 @@ impl TypeCheckDiagnostics {
self.diagnostics.shrink_to_fit();
}
pub(crate) fn into_vec(self) -> Vec<Diagnostic> {
self.diagnostics
}
pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic> {
self.diagnostics.iter()
}

View File

@@ -5,6 +5,7 @@ use std::fmt::{self, Display, Formatter, Write};
use ruff_db::display::FormatterJoinExtension;
use ruff_python_ast::str::{Quote, TripleQuotes};
use ruff_python_literal::escape::AsciiEscape;
use ruff_text_size::{TextRange, TextSize};
use crate::types::class::{ClassLiteral, ClassType, GenericAlias};
use crate::types::function::{FunctionType, OverloadLiteral};
@@ -557,46 +558,193 @@ pub(crate) struct DisplaySignature<'db> {
db: &'db dyn Db,
}
impl Display for DisplaySignature<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_char('(')?;
impl DisplaySignature<'_> {
/// Get detailed display information including component ranges
pub(crate) fn to_string_parts(&self) -> SignatureDisplayDetails {
let mut writer = SignatureWriter::Details(SignatureDetailsWriter::new());
self.write_signature(&mut writer).unwrap();
match writer {
SignatureWriter::Details(details) => details.finish(),
SignatureWriter::Formatter(_) => unreachable!("Expected Details variant"),
}
}
/// Internal method to write signature with the signature writer
fn write_signature(&self, writer: &mut SignatureWriter) -> fmt::Result {
// Opening parenthesis
writer.write_char('(')?;
if self.parameters.is_gradual() {
// We represent gradual form as `...` in the signature, internally the parameters still
// contain `(*args, **kwargs)` parameters.
f.write_str("...")?;
writer.write_str("...")?;
} else {
let mut star_added = false;
let mut needs_slash = false;
let mut join = f.join(", ");
let mut first = true;
for parameter in self.parameters.as_slice() {
// Handle special separators
if !star_added && parameter.is_keyword_only() {
join.entry(&'*');
if !first {
writer.write_str(", ")?;
}
writer.write_char('*')?;
star_added = true;
first = false;
}
if parameter.is_positional_only() {
needs_slash = true;
} else if needs_slash {
join.entry(&'/');
if !first {
writer.write_str(", ")?;
}
writer.write_char('/')?;
needs_slash = false;
first = false;
}
join.entry(&parameter.display(self.db));
// Add comma before parameter if not first
if !first {
writer.write_str(", ")?;
}
// Write parameter with range tracking
let param_name = parameter.display_name();
writer.write_parameter(&parameter.display(self.db), param_name.as_deref())?;
first = false;
}
if needs_slash {
join.entry(&'/');
if !first {
writer.write_str(", ")?;
}
writer.write_char('/')?;
}
join.finish()?;
}
write!(
f,
") -> {}",
self.return_ty.unwrap_or(Type::unknown()).display(self.db)
)
// Closing parenthesis
writer.write_char(')')?;
// Return type
let return_ty = self.return_ty.unwrap_or_else(Type::unknown);
writer.write_return_type(&return_ty.display(self.db))?;
Ok(())
}
}
impl Display for DisplaySignature<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut writer = SignatureWriter::Formatter(f);
self.write_signature(&mut writer)
}
}
/// Writer for building signature strings with different output targets
enum SignatureWriter<'a, 'b> {
/// Write directly to a formatter (for Display trait)
Formatter(&'a mut Formatter<'b>),
/// Build a string with range tracking (for `to_string_parts`)
Details(SignatureDetailsWriter),
}
/// Writer that builds a string with range tracking
struct SignatureDetailsWriter {
label: String,
parameter_ranges: Vec<TextRange>,
parameter_names: Vec<String>,
}
impl SignatureDetailsWriter {
fn new() -> Self {
Self {
label: String::new(),
parameter_ranges: Vec::new(),
parameter_names: Vec::new(),
}
}
fn finish(self) -> SignatureDisplayDetails {
SignatureDisplayDetails {
label: self.label,
parameter_ranges: self.parameter_ranges,
parameter_names: self.parameter_names,
}
}
}
impl SignatureWriter<'_, '_> {
fn write_char(&mut self, c: char) -> fmt::Result {
match self {
SignatureWriter::Formatter(f) => f.write_char(c),
SignatureWriter::Details(details) => {
details.label.push(c);
Ok(())
}
}
}
fn write_str(&mut self, s: &str) -> fmt::Result {
match self {
SignatureWriter::Formatter(f) => f.write_str(s),
SignatureWriter::Details(details) => {
details.label.push_str(s);
Ok(())
}
}
}
fn write_parameter<T: Display>(&mut self, param: &T, param_name: Option<&str>) -> fmt::Result {
match self {
SignatureWriter::Formatter(f) => param.fmt(f),
SignatureWriter::Details(details) => {
let param_start = details.label.len();
let param_display = param.to_string();
details.label.push_str(&param_display);
// Use TextSize::try_from for safe conversion, falling back to empty range on overflow
let start = TextSize::try_from(param_start).unwrap_or_default();
let length = TextSize::try_from(param_display.len()).unwrap_or_default();
details.parameter_ranges.push(TextRange::at(start, length));
// Store the parameter name if available
if let Some(name) = param_name {
details.parameter_names.push(name.to_string());
} else {
details.parameter_names.push(String::new());
}
Ok(())
}
}
}
fn write_return_type<T: Display>(&mut self, return_ty: &T) -> fmt::Result {
match self {
SignatureWriter::Formatter(f) => write!(f, " -> {return_ty}"),
SignatureWriter::Details(details) => {
let return_display = format!(" -> {return_ty}");
details.label.push_str(&return_display);
Ok(())
}
}
}
}
/// Details about signature display components, including ranges for parameters and return type
#[derive(Debug, Clone)]
pub(crate) struct SignatureDisplayDetails {
/// The full signature string
pub label: String,
/// Ranges for each parameter within the label
pub parameter_ranges: Vec<TextRange>,
/// Names of the parameters in order
pub parameter_names: Vec<String>,
}
impl<'db> Parameter<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplayParameter<'db> {
DisplayParameter { param: self, db }

View File

@@ -1,17 +1,20 @@
use std::cmp::Ordering;
use crate::module_resolver::resolve_module;
use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::definition::DefinitionKind;
use crate::semantic_index::place::ScopeId;
use crate::semantic_index::{
attribute_scopes, global_scope, imported_modules, place_table, semantic_index, use_def_map,
attribute_scopes, global_scope, place_table, semantic_index, use_def_map,
};
use crate::types::call::CallArguments;
use crate::types::signatures::Signature;
use crate::types::{ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type};
use crate::{Db, NameKind};
use crate::{Db, HasType, NameKind, SemanticModel};
use ruff_db::files::File;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_text_size::TextRange;
use rustc_hash::FxHashSet;
pub(crate) fn all_declarations_and_bindings<'db>(
@@ -197,24 +200,14 @@ impl<'db> AllMembers<'db> {
});
}
let module_name = module.name();
self.members.extend(
imported_modules(db, literal.importing_file(db))
.iter()
.filter_map(|submodule_name| {
let module = resolve_module(db, submodule_name)?;
let ty = Type::module_literal(db, file, &module);
Some((submodule_name, ty))
})
.filter_map(|(submodule_name, ty)| {
let relative = submodule_name.relative_to(module_name)?;
Some((relative, ty))
})
.filter_map(|(relative_submodule_name, ty)| {
let name = Name::from(relative_submodule_name.components().next()?);
self.members
.extend(literal.available_submodule_attributes(db).filter_map(
|submodule_name| {
let ty = literal.resolve_submodule(db, &submodule_name)?;
let name = submodule_name.clone();
Some(Member { name, ty })
}),
);
},
));
}
}
}
@@ -364,3 +357,73 @@ pub fn definition_kind_for_name<'db>(
None
}
/// Details about a callable signature for IDE support.
#[derive(Debug, Clone)]
pub struct CallSignatureDetails<'db> {
/// The signature itself
pub signature: Signature<'db>,
/// The display label for this signature (e.g., "(param1: str, param2: int) -> str")
pub label: String,
/// Label offsets for each parameter in the signature string.
/// Each range specifies the start position and length of a parameter label
/// within the full signature string.
pub parameter_label_offsets: Vec<TextRange>,
/// The names of the parameters in the signature, in order.
/// This provides easy access to parameter names for documentation lookup.
pub parameter_names: Vec<String>,
/// The definition where this callable was originally defined (useful for
/// extracting docstrings).
pub definition: Option<Definition<'db>>,
/// Mapping from argument indices to parameter indices. This helps
/// determine which parameter corresponds to which argument position.
pub argument_to_parameter_mapping: Vec<Option<usize>>,
}
/// Extract signature details from a function call expression.
/// This function analyzes the callable being invoked and returns zero or more
/// `CallSignatureDetails` objects, each representing one possible signature
/// (in case of overloads or union types).
pub fn call_signature_details<'db>(
db: &'db dyn Db,
file: File,
call_expr: &ast::ExprCall,
) -> Vec<CallSignatureDetails<'db>> {
let model = SemanticModel::new(db, file);
let func_type = call_expr.func.inferred_type(&model);
// Use into_callable to handle all the complex type conversions
if let Some(callable_type) = func_type.into_callable(db) {
let call_arguments = CallArguments::from_arguments(&call_expr.arguments);
let bindings = callable_type.bindings(db).match_parameters(&call_arguments);
// Extract signature details from all callable bindings
bindings
.into_iter()
.flat_map(std::iter::IntoIterator::into_iter)
.map(|binding| {
let signature = &binding.signature;
let display_details = signature.display(db).to_string_parts();
let parameter_label_offsets = display_details.parameter_ranges.clone();
let parameter_names = display_details.parameter_names.clone();
CallSignatureDetails {
signature: signature.clone(),
label: display_details.label,
parameter_label_offsets,
parameter_names,
definition: signature.definition(),
argument_to_parameter_mapping: binding.argument_to_parameter_mapping().to_vec(),
}
})
.collect()
} else {
// Type is not callable, return empty signatures
vec![]
}
}

View File

@@ -84,9 +84,7 @@ use crate::semantic_index::place::{
use crate::semantic_index::{
ApplicableConstraints, EagerSnapshotResult, SemanticIndex, place_table, semantic_index,
};
use crate::types::call::{
Argument, Binding, Bindings, CallArgumentTypes, CallArguments, CallError,
};
use crate::types::call::{Binding, Bindings, CallArgumentTypes, CallArguments, CallError};
use crate::types::class::{CodeGeneratorKind, MetaclassErrorKind, SliceLiteral};
use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
@@ -1917,7 +1915,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_type_parameters(type_params);
if let Some(arguments) = class.arguments.as_deref() {
let call_arguments = Self::parse_arguments(arguments);
let call_arguments = CallArguments::from_arguments(arguments);
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
self.infer_argument_types(arguments, call_arguments, &argument_forms);
}
@@ -4626,29 +4624,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_expression(expression)
}
fn parse_arguments(arguments: &ast::Arguments) -> CallArguments<'_> {
arguments
.arguments_source_order()
.map(|arg_or_keyword| {
match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic,
// TODO diagnostic if after a keyword argument
_ => Argument::Positional,
},
ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => {
if let Some(arg) = arg {
Argument::Keyword(&arg.id)
} else {
// TODO diagnostic if not last
Argument::Keywords
}
}
}
})
.collect()
}
fn infer_argument_types<'a>(
&mut self,
ast_arguments: &ast::Arguments,
@@ -5362,7 +5337,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// We don't call `Type::try_call`, because we want to perform type inference on the
// arguments after matching them to parameters, but before checking that the argument types
// are assignable to any parameter annotations.
let call_arguments = Self::parse_arguments(arguments);
let call_arguments = CallArguments::from_arguments(arguments);
let callable_type = self.infer_maybe_standalone_expression(func);
@@ -10035,7 +10010,7 @@ mod tests {
}
#[track_caller]
fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) {
fn assert_diagnostic_messages(diagnostics: &[Diagnostic], expected: &[&str]) {
let messages: Vec<&str> = diagnostics
.iter()
.map(Diagnostic::primary_message)
@@ -10048,7 +10023,7 @@ mod tests {
let file = system_path_to_file(db, filename).unwrap();
let diagnostics = check_types(db, file);
assert_diagnostic_messages(diagnostics, expected);
assert_diagnostic_messages(&diagnostics, expected);
}
#[test]

View File

@@ -213,7 +213,7 @@ impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> {
}
/// The signature of one of the overloads of a callable.
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
#[derive(Clone, Debug, salsa::Update, get_size2::GetSize)]
pub struct Signature<'db> {
/// The generic context for this overload, if it is generic.
pub(crate) generic_context: Option<GenericContext<'db>>,
@@ -223,6 +223,10 @@ pub struct Signature<'db> {
/// to its own generic context.
pub(crate) inherited_generic_context: Option<GenericContext<'db>>,
/// The original definition associated with this function, if available.
/// This is useful for locating and extracting docstring information for the signature.
pub(crate) definition: Option<Definition<'db>>,
/// Parameters, in source order.
///
/// The ordering of parameters in a valid signature must be: first positional-only parameters,
@@ -265,6 +269,7 @@ impl<'db> Signature<'db> {
Self {
generic_context: None,
inherited_generic_context: None,
definition: None,
parameters,
return_ty,
}
@@ -278,6 +283,7 @@ impl<'db> Signature<'db> {
Self {
generic_context,
inherited_generic_context: None,
definition: None,
parameters,
return_ty,
}
@@ -288,6 +294,7 @@ impl<'db> Signature<'db> {
Signature {
generic_context: None,
inherited_generic_context: None,
definition: None,
parameters: Parameters::gradual_form(),
return_ty: Some(signature_type),
}
@@ -300,6 +307,7 @@ impl<'db> Signature<'db> {
Signature {
generic_context: None,
inherited_generic_context: None,
definition: None,
parameters: Parameters::todo(),
return_ty: Some(signature_type),
}
@@ -332,6 +340,7 @@ impl<'db> Signature<'db> {
Self {
generic_context: generic_context.or(legacy_generic_context),
inherited_generic_context,
definition: Some(definition),
parameters,
return_ty,
}
@@ -351,6 +360,7 @@ impl<'db> Signature<'db> {
Self {
generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context,
definition: self.definition,
// Parameters are at contravariant position, so the variance is flipped.
parameters: self.parameters.materialize(db, variance.flip()),
return_ty: Some(
@@ -373,6 +383,7 @@ impl<'db> Signature<'db> {
inherited_generic_context: self
.inherited_generic_context
.map(|ctx| ctx.normalized_impl(db, visitor)),
definition: self.definition,
parameters: self
.parameters
.iter()
@@ -392,6 +403,7 @@ impl<'db> Signature<'db> {
Self {
generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context,
definition: self.definition,
parameters: self.parameters.apply_type_mapping(db, type_mapping),
return_ty: self
.return_ty
@@ -422,10 +434,16 @@ impl<'db> Signature<'db> {
&self.parameters
}
/// Return the definition associated with this signature, if any.
pub(crate) fn definition(&self) -> Option<Definition<'db>> {
self.definition
}
pub(crate) fn bind_self(&self) -> Self {
Self {
generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context,
definition: self.definition,
parameters: Parameters::new(self.parameters().iter().skip(1).cloned()),
return_ty: self.return_ty,
}
@@ -899,6 +917,33 @@ impl<'db> Signature<'db> {
true
}
/// Create a new signature with the given definition.
pub(crate) fn with_definition(self, definition: Option<Definition<'db>>) -> Self {
Self { definition, ..self }
}
}
// Manual implementations of PartialEq, Eq, and Hash that exclude the definition field
// since the definition is not relevant for type equality/equivalence
impl PartialEq for Signature<'_> {
fn eq(&self, other: &Self) -> bool {
self.generic_context == other.generic_context
&& self.inherited_generic_context == other.inherited_generic_context
&& self.parameters == other.parameters
&& self.return_ty == other.return_ty
}
}
impl Eq for Signature<'_> {}
impl std::hash::Hash for Signature<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.generic_context.hash(state);
self.inherited_generic_context.hash(state);
self.parameters.hash(state);
self.return_ty.hash(state);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]

View File

@@ -44,8 +44,6 @@ pub(crate) struct Unpack<'db> {
/// The ingredient representing the value expression of the unpacking. For example, in
/// `(a, b) = (1, 2)`, the value expression is `(1, 2)`.
pub(crate) value: UnpackValue<'db>,
count: countme::Count<Unpack<'static>>,
}
impl<'db> Unpack<'db> {

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