Compare commits
22 Commits
jack/not_l
...
0.12.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bc81f26c8 | ||
|
|
6908e2682f | ||
|
|
25c4295564 | ||
|
|
426fa4bb12 | ||
|
|
b0b65c24ff | ||
|
|
08bc6d2589 | ||
|
|
f2ae12bab3 | ||
|
|
965f415212 | ||
|
|
83b5bbf004 | ||
|
|
87f6f08ef5 | ||
|
|
59114d0301 | ||
|
|
492f5bf2aa | ||
|
|
934aaa23f3 | ||
|
|
59aa869724 | ||
|
|
edaffa6c4f | ||
|
|
5fb2fb916b | ||
|
|
801f69a7b4 | ||
|
|
3926dd8424 | ||
|
|
563268ce53 | ||
|
|
221edcba5c | ||
|
|
beb98dae7c | ||
|
|
05b1b788a0 |
1
.github/workflows/mypy_primer.yaml
vendored
1
.github/workflows/mypy_primer.yaml
vendored
@@ -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 }}
|
||||
|
||||
76
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
76
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -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
1
.github/zizmor.yml
vendored
@@ -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...
|
||||
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -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
73
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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/))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
23
crates/ruff/tests/snapshots/lint__output_format_azure.snap
Normal file
23
crates/ruff/tests/snapshots/lint__output_format_azure.snap
Normal 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 -----
|
||||
25
crates/ruff/tests/snapshots/lint__output_format_concise.snap
Normal file
25
crates/ruff/tests/snapshots/lint__output_format_concise.snap
Normal 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 -----
|
||||
49
crates/ruff/tests/snapshots/lint__output_format_full.snap
Normal file
49
crates/ruff/tests/snapshots/lint__output_format_full.snap
Normal 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 -----
|
||||
23
crates/ruff/tests/snapshots/lint__output_format_github.snap
Normal file
23
crates/ruff/tests/snapshots/lint__output_format_github.snap
Normal 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 -----
|
||||
60
crates/ruff/tests/snapshots/lint__output_format_gitlab.snap
Normal file
60
crates/ruff/tests/snapshots/lint__output_format_gitlab.snap
Normal 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 -----
|
||||
27
crates/ruff/tests/snapshots/lint__output_format_grouped.snap
Normal file
27
crates/ruff/tests/snapshots/lint__output_format_grouped.snap
Normal 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 -----
|
||||
@@ -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 -----
|
||||
88
crates/ruff/tests/snapshots/lint__output_format_json.snap
Normal file
88
crates/ruff/tests/snapshots/lint__output_format_json.snap
Normal 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 -----
|
||||
34
crates/ruff/tests/snapshots/lint__output_format_junit.snap
Normal file
34
crates/ruff/tests/snapshots/lint__output_format_junit.snap
Normal 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 -----
|
||||
23
crates/ruff/tests/snapshots/lint__output_format_pylint.snap
Normal file
23
crates/ruff/tests/snapshots/lint__output_format_pylint.snap
Normal 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 -----
|
||||
103
crates/ruff/tests/snapshots/lint__output_format_rdjson.snap
Normal file
103
crates/ruff/tests/snapshots/lint__output_format_rdjson.snap
Normal 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 -----
|
||||
142
crates/ruff/tests/snapshots/lint__output_format_sarif.snap
Normal file
142
crates/ruff/tests/snapshots/lint__output_format_sarif.snap
Normal 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 -----
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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():
|
||||
(#=
|
||||
|
||||
6
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/whitespace.py
vendored
Normal file
6
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/whitespace.py
vendored
Normal 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("!")
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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("!")
|
||||
@@ -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(
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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()`
|
||||
|
|
||||
|
||||
@@ -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()`
|
||||
|
|
||||
|
||||
@@ -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()`
|
||||
|
|
||||
|
||||
@@ -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()`
|
||||
|
|
||||
|
||||
@@ -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
|
||||
|
|
||||
@@ -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
|
||||
|
|
||||
@@ -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
|
||||
|
|
||||
@@ -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
|
||||
|
|
||||
@@ -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`.
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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
3
crates/ty/docs/cli.md
generated
@@ -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>
|
||||
|
||||
@@ -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`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
172
crates/ty/src/printer.rs
Normal 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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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", "")])?;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
664
crates/ty_ide/src/docstring.rs
Normal file
664
crates/ty_ide/src/docstring.rs
Normal 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(§ion_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!(¶m_docs["param1"], "The first parameter description");
|
||||
assert_eq!(
|
||||
¶m_docs["param2"],
|
||||
"The second parameter description\nThis is a continuation of param2 description."
|
||||
);
|
||||
assert_eq!(¶m_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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
687
crates/ty_ide/src/signature_help.rs
Normal file
687
crates/ty_ide/src/signature_help.rs
Normal 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(|¶m_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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(¶meter.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(¶meter.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(¶m_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 }
|
||||
|
||||
@@ -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![]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user