Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
e935bc5578 [ty] enum.Flag 2025-07-29 16:52:42 +02:00
450 changed files with 4620 additions and 18896 deletions

6
.github/CODEOWNERS vendored
View File

@@ -19,10 +19,6 @@
# ty
/crates/ty* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ruff_db/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty_project/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty_server/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty_wasm/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/scripts/ty_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ty_python_semantic @carljm @AlexWaygood @sharkdp @dcreager

View File

@@ -63,7 +63,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.RUFF_BASE_IMG }}
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
@@ -123,7 +123,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.RUFF_BASE_IMG }}
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
@@ -219,7 +219,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
# ghcr.io prefers index level annotations
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
@@ -266,7 +266,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
with:

View File

@@ -240,11 +240,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-insta
- name: ty mdtests (GitHub annotations)
@@ -298,11 +298,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-insta
- name: "Run tests"
@@ -325,7 +325,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-nextest
- name: "Run tests"
@@ -429,7 +429,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
uses: cargo-bins/cargo-binstall@808dcb1b503398677d089d3216c51ac7cc11e7ab # v1.14.2
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -682,7 +682,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
- uses: cargo-bins/cargo-binstall@808dcb1b503398677d089d3216c51ac7cc11e7ab # v1.14.2
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -903,7 +903,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-codspeed
@@ -936,7 +936,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
with:
tool: cargo-codspeed

View File

@@ -83,7 +83,7 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: "ruff"

View File

@@ -24,7 +24,6 @@ env:
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
CONFORMANCE_SUITE_COMMIT: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
jobs:
typing_conformance:
@@ -41,10 +40,13 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: python/typing
ref: ${{ env.CONFORMANCE_SUITE_COMMIT }}
ref: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
path: typing
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
workspaces: "ruff"
@@ -62,13 +64,14 @@ jobs:
cd ruff
echo "new commit"
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
git checkout -b new_commit "${{ github.event.pull_request.head.sha }}"
git rev-list --format=%s --max-count=1 new_commit
cargo build --release --bin ty
mv target/release/ty ty-new
echo "old commit (merge base)"
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b old_commit "$MERGE_BASE"
echo "old commit (merge base)"
git rev-list --format=%s --max-count=1 old_commit
cargo build --release --bin ty
mv target/release/ty ty-old
@@ -92,7 +95,6 @@ jobs:
fi
echo ${{ github.event.number }} > pr-number
echo "${CONFORMANCE_SUITE_COMMIT}" > conformance-suite-commit
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
@@ -105,9 +107,3 @@ jobs:
with:
name: pr-number
path: pr-number
- name: Upload conformance suite commit
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: conformance-suite-commit
path: conformance-suite-commit

View File

@@ -32,14 +32,6 @@ jobs:
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download typing conformance suite commit
with:
name: conformance-suite-commit
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
if_no_artifact_found: ignore
allow_forks: true
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: "Download typing_conformance results"
id: download-typing_conformance_diff
@@ -69,14 +61,7 @@ jobs:
# subsequent runs
echo '<!-- generated-comment typing_conformance_diagnostics_diff -->' >> comment.txt
if [[ -f conformance-suite-commit ]]
then
echo "## Diagnostic diff on [typing conformance tests](https://github.com/python/typing/tree/$(<conformance-suite-commit)/conformance)" >> comment.txt
else
echo "conformance-suite-commit file not found"
echo "## Diagnostic diff on typing conformance tests" >> comment.txt
fi
echo '## Diagnostic diff on typing conformance tests' >> comment.txt
if [ -s "pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff" ]; then
echo '<details>' >> comment.txt
echo '<summary>Changes were detected when running ty on typing conformance tests</summary>' >> comment.txt

View File

@@ -81,7 +81,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.7
rev: v0.12.5
hooks:
- id: ruff-format
- id: ruff-check

View File

@@ -1,69 +1,5 @@
# Changelog
## 0.12.8
### Preview features
- \[`flake8-use-pathlib`\] Expand `PTH201` to check all `PurePath` subclasses ([#19440](https://github.com/astral-sh/ruff/pull/19440))
### Bug fixes
- \[`flake8-blind-except`\] Change `BLE001` to correctly parse exception tuples ([#19747](https://github.com/astral-sh/ruff/pull/19747))
- \[`flake8-errmsg`\] Exclude `typing.cast` from `EM101` ([#19656](https://github.com/astral-sh/ruff/pull/19656))
- \[`flake8-simplify`\] Fix raw string handling in `SIM905` for embedded quotes ([#19591](https://github.com/astral-sh/ruff/pull/19591))
- \[`flake8-import-conventions`\] Avoid false positives for NFKC-normalized `__debug__` import aliases in `ICN001` ([#19411](https://github.com/astral-sh/ruff/pull/19411))
- \[`isort`\] Fix syntax error after docstring ending with backslash (`I002`) ([#19505](https://github.com/astral-sh/ruff/pull/19505))
- \[`pylint`\] Mark `PLC0207` fixes as unsafe when `*args` unpacking is present ([#19679](https://github.com/astral-sh/ruff/pull/19679))
- \[`pyupgrade`\] Prevent infinite loop with `I002` (`UP010`, `UP035`) ([#19413](https://github.com/astral-sh/ruff/pull/19413))
- \[`ruff`\] Parenthesize generator expressions in f-strings (`RUF010`) ([#19434](https://github.com/astral-sh/ruff/pull/19434))
### Rule changes
- \[`eradicate`\] Don't flag `pyrefly` pragmas as unused code (`ERA001`) ([#19731](https://github.com/astral-sh/ruff/pull/19731))
### Documentation
- Replace "associative" with "commutative" in docs for `RUF036` ([#19706](https://github.com/astral-sh/ruff/pull/19706))
- Fix copy and line separator colors in dark mode ([#19630](https://github.com/astral-sh/ruff/pull/19630))
- Fix link to `typing` documentation ([#19648](https://github.com/astral-sh/ruff/pull/19648))
- \[`refurb`\] Make more examples error out-of-the-box ([#19695](https://github.com/astral-sh/ruff/pull/19695),[#19673](https://github.com/astral-sh/ruff/pull/19673),[#19672](https://github.com/astral-sh/ruff/pull/19672))
### Other changes
- Include column numbers in GitLab output format ([#19708](https://github.com/astral-sh/ruff/pull/19708))
- Always expand tabs to four spaces in diagnostics ([#19618](https://github.com/astral-sh/ruff/pull/19618))
- Update pre-commit's `ruff` id ([#19654](https://github.com/astral-sh/ruff/pull/19654))
## 0.12.7
This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.
## 0.12.6
### Preview features
- \[`flake8-commas`\] Add support for trailing comma checks in type parameter lists (`COM812`, `COM819`) ([#19390](https://github.com/astral-sh/ruff/pull/19390))
- \[`pylint`\] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`) ([#19387](https://github.com/astral-sh/ruff/pull/19387))
- \[`ruff`\] Offer fixes for `RUF039` in more cases ([#19065](https://github.com/astral-sh/ruff/pull/19065))
### Bug fixes
- Support `.pyi` files in ruff analyze graph ([#19611](https://github.com/astral-sh/ruff/pull/19611))
- \[`flake8-pyi`\] Preserve inline comment in ellipsis removal (`PYI013`) ([#19399](https://github.com/astral-sh/ruff/pull/19399))
- \[`perflint`\] Ignore rule if target is `global` or `nonlocal` (`PERF401`) ([#19539](https://github.com/astral-sh/ruff/pull/19539))
- \[`pyupgrade`\] Fix `UP030` to avoid modifying double curly braces in format strings ([#19378](https://github.com/astral-sh/ruff/pull/19378))
- \[`refurb`\] Ignore decorated functions for `FURB118` ([#19339](https://github.com/astral-sh/ruff/pull/19339))
- \[`refurb`\] Mark `int` and `bool` cases for `Decimal.from_float` as safe fixes (`FURB164`) ([#19468](https://github.com/astral-sh/ruff/pull/19468))
- \[`ruff`\] Fix `RUF033` for named default expressions ([#19115](https://github.com/astral-sh/ruff/pull/19115))
### Rule changes
- \[`flake8-blind-except`\] Change `BLE001` to permit `logging.critical(..., exc_info=True)` ([#19520](https://github.com/astral-sh/ruff/pull/19520))
### Performance
- Add support for specifying minimum dots in detected string imports ([#19538](https://github.com/astral-sh/ruff/pull/19538))
## 0.12.5
### Preview features

405
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,6 @@ ruff_graph = { path = "crates/ruff_graph" }
ruff_index = { path = "crates/ruff_index" }
ruff_linter = { path = "crates/ruff_linter" }
ruff_macros = { path = "crates/ruff_macros" }
ruff_memory_usage = { path = "crates/ruff_memory_usage" }
ruff_notebook = { path = "crates/ruff_notebook" }
ruff_options_metadata = { path = "crates/ruff_options_metadata" }
ruff_python_ast = { path = "crates/ruff_python_ast" }
@@ -41,7 +40,6 @@ ruff_text_size = { path = "crates/ruff_text_size" }
ruff_workspace = { path = "crates/ruff_workspace" }
ty = { path = "crates/ty" }
ty_combine = { path = "crates/ty_combine" }
ty_ide = { path = "crates/ty_ide" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { path = "crates/ty_python_semantic" }
@@ -85,7 +83,7 @@ etcetera = { version = "0.10.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
get-size2 = { version = "0.6.2", features = [
get-size2 = { version = "0.6.0", features = [
"derive",
"smallvec",
"hashbrown",
@@ -143,12 +141,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "b121ee46c4483ba74c19e933a3522bd548eb7343", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
"inventory",
] }
salsa = { git = "https://github.com/salsa-rs/salsa", rev = "dba66f1a37acca014c2402f231ed5b361bd7d8fe" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.12.8/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.8/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.5/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.8
rev: v0.12.5
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

@@ -798,7 +798,7 @@ fn stdin_parse_error() {
success: false
exit_code: 1
----- stdout -----
-:1:16: invalid-syntax: Expected one or more symbol names after import
-:1:16: SyntaxError: Expected one or more symbol names after import
|
1 | from foo import
| ^
@@ -818,14 +818,14 @@ fn stdin_multiple_parse_error() {
success: false
exit_code: 1
----- stdout -----
-:1:16: invalid-syntax: Expected one or more symbol names after import
-:1:16: SyntaxError: Expected one or more symbol names after import
|
1 | from foo import
| ^
2 | bar =
|
-:2:6: invalid-syntax: Expected an expression
-:2:6: SyntaxError: Expected an expression
|
1 | from foo import
2 | bar =
@@ -847,7 +847,7 @@ fn parse_error_not_included() {
success: false
exit_code: 1
----- stdout -----
-:1:6: invalid-syntax: Expected an expression
-:1:6: SyntaxError: Expected an expression
|
1 | foo =
| ^

View File

@@ -4996,37 +4996,6 @@ fn flake8_import_convention_invalid_aliases_config_module_name() -> Result<()> {
Ok(())
}
#[test]
fn flake8_import_convention_nfkc_normalization() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint.flake8-import-conventions.aliases]
"test.module" = "_𝘥𝘦𝘣𝘶𝘨"
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
, @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Invalid alias for module 'test.module': alias normalizes to '__debug__', which is not allowed.
");});
Ok(())
}
#[test]
fn flake8_import_convention_unused_aliased_import() {
assert_cmd_snapshot!(
@@ -5420,7 +5389,7 @@ fn walrus_before_py38() {
success: false
exit_code: 1
----- stdout -----
test.py:1:2: invalid-syntax: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8)
test.py:1:2: SyntaxError: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8)
Found 1 error.
----- stderr -----
@@ -5466,15 +5435,15 @@ match 2:
print("it's one")
"#
),
@r"
@r###"
success: false
exit_code: 1
----- stdout -----
test.py:2:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
test.py:2:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 1 error.
----- stderr -----
"
"###
);
// syntax error on 3.9 with preview
@@ -5495,7 +5464,7 @@ match 2:
success: false
exit_code: 1
----- stdout -----
test.py:2:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
test.py:2:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 1 error.
----- stderr -----
@@ -5523,7 +5492,7 @@ fn cache_syntax_errors() -> Result<()> {
success: false
exit_code: 1
----- stdout -----
main.py:1:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
main.py:1:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----
"
@@ -5536,7 +5505,7 @@ fn cache_syntax_errors() -> Result<()> {
success: false
exit_code: 1
----- stdout -----
main.py:1:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
main.py:1:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----
"
@@ -5649,7 +5618,7 @@ fn semantic_syntax_errors() -> Result<()> {
success: false
exit_code: 1
----- stdout -----
main.py:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable
main.py:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
main.py:1:20: F821 Undefined name `foo`
----- stderr -----
@@ -5663,7 +5632,7 @@ fn semantic_syntax_errors() -> Result<()> {
success: false
exit_code: 1
----- stdout -----
main.py:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable
main.py:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
main.py:1:20: F821 Undefined name `foo`
----- stderr -----
@@ -5682,7 +5651,7 @@ fn semantic_syntax_errors() -> Result<()> {
success: false
exit_code: 1
----- stdout -----
-:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable
-:1:3: SyntaxError: assignment expression cannot rebind comprehension variable
Found 1 error.
----- stderr -----

View File

@@ -95,6 +95,6 @@ is stricter, which could affect the suggested fix. See [this FAQ section](https:
## References
- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)
- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)
----- stderr -----

View File

@@ -18,6 +18,6 @@ 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;code=invalid-syntax;]Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=3;columnnumber=1;]SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -18,7 +18,7 @@ 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: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
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.

View File

@@ -34,7 +34,7 @@ input.py:2:5: F821 Undefined name `y`
4 | case _: ...
|
input.py:3:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
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

View File

@@ -18,6 +18,6 @@ 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 (invalid-syntax),file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
::error title=Ruff,file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -22,17 +22,11 @@ exit_code: 1
"description": "`os` imported but unused",
"fingerprint": "4dbad37161e65c72",
"location": {
"path": "input.py",
"positions": {
"begin": {
"column": 8,
"line": 1
},
"end": {
"column": 10,
"line": 1
}
}
"lines": {
"begin": 1,
"end": 1
},
"path": "input.py"
},
"severity": "major"
},
@@ -41,17 +35,11 @@ exit_code: 1
"description": "Undefined name `y`",
"fingerprint": "7af59862a085230",
"location": {
"path": "input.py",
"positions": {
"begin": {
"column": 5,
"line": 2
},
"end": {
"column": 6,
"line": 2
}
}
"lines": {
"begin": 2,
"end": 2
},
"path": "input.py"
},
"severity": "major"
},
@@ -60,17 +48,11 @@ exit_code: 1
"description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"fingerprint": "e558cec859bb66e8",
"location": {
"path": "input.py",
"positions": {
"begin": {
"column": 1,
"line": 3
},
"end": {
"column": 6,
"line": 3
}
}
"lines": {
"begin": 3,
"end": 3
},
"path": "input.py"
},
"severity": "major"
}

View File

@@ -19,7 +19,7 @@ exit_code: 1
input.py:
1:8 F401 [*] `os` imported but unused
2:5 F821 Undefined name `y`
3:1 invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
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.

View File

@@ -18,6 +18,6 @@ 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":"invalid-syntax","end_location":{"column":6,"row":3},"filename":"[TMP]/input.py","fix":null,"location":{"column":1,"row":3},"message":"Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)","noqa_row":null,"url":null}
{"cell":null,"code":null,"end_location":{"column":6,"row":3},"filename":"[TMP]/input.py","fix":null,"location":{"column":1,"row":3},"message":"SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)","noqa_row":null,"url":null}
----- stderr -----

View File

@@ -69,7 +69,7 @@ exit_code: 1
},
{
"cell": null,
"code": "invalid-syntax",
"code": null,
"end_location": {
"column": 6,
"row": 3
@@ -80,7 +80,7 @@ exit_code: 1
"column": 1,
"row": 3
},
"message": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"noqa_row": null,
"url": null
}

View File

@@ -26,7 +26,7 @@ exit_code: 1
<failure message="Undefined name `y`">line 2, col 5, Undefined name `y`</failure>
</testcase>
<testcase name="org.ruff.invalid-syntax" classname="[TMP]/input" line="3" column="1">
<failure message="Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)">line 3, col 1, Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)</failure>
<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>

View File

@@ -18,6 +18,6 @@ exit_code: 1
----- stdout -----
input.py:1: [F401] `os` imported but unused
input.py:2: [F821] Undefined name `y`
input.py:3: [invalid-syntax] Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
input.py:3: [invalid-syntax] SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -90,7 +90,7 @@ exit_code: 1
}
}
},
"message": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
"message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
}
],
"severity": "WARNING",

View File

@@ -83,9 +83,9 @@ exit_code: 1
}
],
"message": {
"text": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
"text": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
},
"ruleId": "invalid-syntax"
"ruleId": null
}
],
"tool": {
@@ -95,7 +95,7 @@ exit_code: 1
"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/spec/distributing.html#library-interface-public-and-private-symbols)\n"
"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"

View File

@@ -1,5 +1,3 @@
#![expect(clippy::needless_doctest_main)]
//! A library for formatting of text or programming code snippets.
//!
//! It's primary purpose is to build an ASCII-graphical representation of the snippet

View File

@@ -193,14 +193,9 @@ impl DisplaySet<'_> {
stylesheet: &Stylesheet,
buffer: &mut StyledBuffer,
) -> fmt::Result {
let hide_severity = annotation.annotation_type.is_none();
let color = get_annotation_style(&annotation.annotation_type, stylesheet);
let formatted_len = if let Some(id) = &annotation.id {
if hide_severity {
id.len()
} else {
2 + id.len() + annotation_type_len(&annotation.annotation_type)
}
2 + id.len() + annotation_type_len(&annotation.annotation_type)
} else {
annotation_type_len(&annotation.annotation_type)
};
@@ -214,66 +209,18 @@ impl DisplaySet<'_> {
if formatted_len == 0 {
self.format_label(line_offset, &annotation.label, stylesheet, buffer)
} else {
// TODO(brent) All of this complicated checking of `hide_severity` should be reverted
// once we have real severities in Ruff. This code is trying to account for two
// different cases:
//
// - main diagnostic message
// - subdiagnostic message
//
// In the first case, signaled by `hide_severity = true`, we want to print the ID (the
// noqa code for a ruff lint diagnostic, e.g. `F401`, or `invalid-syntax` for a syntax
// error) without brackets. Instead, for subdiagnostics, we actually want to print the
// severity (usually `help`) regardless of the `hide_severity` setting. This is signaled
// by an ID of `None`.
//
// With real severities these should be reported more like in ty:
//
// ```
// error[F401]: `math` imported but unused
// error[invalid-syntax]: Cannot use `match` statement on Python 3.9...
// ```
//
// instead of the current versions intended to mimic the old Ruff output format:
//
// ```
// F401 `math` imported but unused
// invalid-syntax: Cannot use `match` statement on Python 3.9...
// ```
//
// Note that the `invalid-syntax` colon is added manually in `ruff_db`, not here. We
// could eventually add a colon to Ruff lint diagnostics (`F401:`) and then make the
// colon below unconditional again.
//
// This also applies to the hard-coded `stylesheet.error()` styling of the
// hidden-severity `id`. This should just be `*color` again later, but for now we don't
// want an unformatted `id`, which is what `get_annotation_style` returns for
// `DisplayAnnotationType::None`.
let annotation_type = annotation_type_str(&annotation.annotation_type);
if let Some(id) = annotation.id {
if hide_severity {
buffer.append(line_offset, &format!("{id} "), *stylesheet.error());
} else {
buffer.append(line_offset, &format!("{annotation_type}[{id}]"), *color);
}
} else {
buffer.append(line_offset, annotation_type, *color);
}
if annotation.is_fixable {
buffer.append(line_offset, "[", stylesheet.none);
buffer.append(line_offset, "*", stylesheet.help);
buffer.append(line_offset, "]", stylesheet.none);
// In the hide-severity case, we need a space instead of the colon and space below.
if hide_severity {
buffer.append(line_offset, " ", stylesheet.none);
}
}
let id = match &annotation.id {
Some(id) => format!("[{id}]"),
None => String::new(),
};
buffer.append(
line_offset,
&format!("{}{}", annotation_type_str(&annotation.annotation_type), id),
*color,
);
if !is_annotation_empty(annotation) {
if annotation.id.is_none() || !hide_severity {
buffer.append(line_offset, ": ", stylesheet.none);
}
buffer.append(line_offset, ": ", stylesheet.none);
self.format_label(line_offset, &annotation.label, stylesheet, buffer)?;
}
Ok(())
@@ -302,15 +249,11 @@ impl DisplaySet<'_> {
let lineno_color = stylesheet.line_no();
buffer.puts(line_offset, lineno_width, header_sigil, *lineno_color);
buffer.puts(line_offset, lineno_width + 4, path, stylesheet.none);
if let Some(Position { row, col, cell }) = pos {
if let Some(cell) = cell {
buffer.append(line_offset, ":", stylesheet.none);
buffer.append(line_offset, &format!("cell {cell}"), stylesheet.none);
}
buffer.append(line_offset, ":", stylesheet.none);
buffer.append(line_offset, row.to_string().as_str(), stylesheet.none);
if let Some((col, row)) = pos {
buffer.append(line_offset, ":", stylesheet.none);
buffer.append(line_offset, col.to_string().as_str(), stylesheet.none);
buffer.append(line_offset, ":", stylesheet.none);
buffer.append(line_offset, row.to_string().as_str(), stylesheet.none);
}
Ok(())
}
@@ -825,7 +768,6 @@ pub(crate) struct Annotation<'a> {
pub(crate) annotation_type: DisplayAnnotationType,
pub(crate) id: Option<&'a str>,
pub(crate) label: Vec<DisplayTextFragment<'a>>,
pub(crate) is_fixable: bool,
}
/// A single line used in `DisplayList`.
@@ -891,13 +833,6 @@ impl DisplaySourceAnnotation<'_> {
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct Position {
row: usize,
col: usize,
cell: Option<usize>,
}
/// Raw line - a line which does not have the `lineno` part and is not considered
/// a part of the snippet.
#[derive(Debug, PartialEq)]
@@ -906,7 +841,7 @@ pub(crate) enum DisplayRawLine<'a> {
/// slice in the project structure.
Origin {
path: &'a str,
pos: Option<Position>,
pos: Option<(usize, usize)>,
header_type: DisplayHeaderType,
},
@@ -985,13 +920,6 @@ pub(crate) enum DisplayAnnotationType {
Help,
}
impl DisplayAnnotationType {
#[inline]
const fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
impl From<snippet::Level> for DisplayAnnotationType {
fn from(at: snippet::Level) -> Self {
match at {
@@ -1087,12 +1015,11 @@ fn format_message<'m>(
title,
footer,
snippets,
is_fixable,
} = message;
let mut sets = vec![];
let body = if !snippets.is_empty() || primary {
vec![format_title(level, id, title, is_fixable)]
vec![format_title(level, id, title)]
} else {
format_footer(level, id, title)
};
@@ -1133,18 +1060,12 @@ fn format_message<'m>(
sets
}
fn format_title<'a>(
level: crate::Level,
id: Option<&'a str>,
label: &'a str,
is_fixable: bool,
) -> DisplayLine<'a> {
fn format_title<'a>(level: crate::Level, id: Option<&'a str>, label: &'a str) -> DisplayLine<'a> {
DisplayLine::Raw(DisplayRawLine::Annotation {
annotation: Annotation {
annotation_type: DisplayAnnotationType::from(level),
id,
label: format_label(Some(label), Some(DisplayTextStyle::Emphasis)),
is_fixable,
},
source_aligned: false,
continuation: false,
@@ -1163,7 +1084,6 @@ fn format_footer<'a>(
annotation_type: DisplayAnnotationType::from(level),
id,
label: format_label(Some(line), None),
is_fixable: false,
},
source_aligned: true,
continuation: i != 0,
@@ -1198,23 +1118,6 @@ fn format_snippet<'m>(
let main_range = snippet.annotations.first().map(|x| x.range.start);
let origin = snippet.origin;
let need_empty_header = origin.is_some() || is_first;
let is_file_level = snippet.annotations.iter().any(|ann| ann.is_file_level);
if is_file_level {
assert!(
snippet.source.is_empty(),
"Non-empty file-level snippet that won't be rendered: {:?}",
snippet.source
);
let header = format_header(origin, main_range, &[], is_first, snippet.cell_index);
return DisplaySet {
display_lines: header.map_or_else(Vec::new, |header| vec![header]),
margin: Margin::new(0, 0, 0, 0, term_width, 0),
};
}
let cell_index = snippet.cell_index;
let mut body = format_body(
snippet,
need_empty_header,
@@ -1223,13 +1126,7 @@ fn format_snippet<'m>(
anonymized_line_numbers,
cut_indicator,
);
let header = format_header(
origin,
main_range,
&body.display_lines,
is_first,
cell_index,
);
let header = format_header(origin, main_range, &body.display_lines, is_first);
if let Some(header) = header {
body.display_lines.insert(0, header);
@@ -1249,7 +1146,6 @@ fn format_header<'a>(
main_range: Option<usize>,
body: &[DisplayLine<'_>],
is_first: bool,
cell_index: Option<usize>,
) -> Option<DisplayLine<'a>> {
let display_header = if is_first {
DisplayHeaderType::Initial
@@ -1286,11 +1182,7 @@ fn format_header<'a>(
return Some(DisplayLine::Raw(DisplayRawLine::Origin {
path,
pos: Some(Position {
row: line_offset,
col,
cell: cell_index,
}),
pos: Some((line_offset, col)),
header_type: display_header,
}));
}
@@ -1580,7 +1472,6 @@ fn format_body<'m>(
annotation_type,
id: None,
label: format_label(annotation.label, None),
is_fixable: false,
},
range,
annotation_type: DisplayAnnotationType::from(annotation.level),
@@ -1620,7 +1511,6 @@ fn format_body<'m>(
annotation_type,
id: None,
label: vec![],
is_fixable: false,
},
range,
annotation_type: DisplayAnnotationType::from(annotation.level),
@@ -1690,7 +1580,6 @@ fn format_body<'m>(
annotation_type,
id: None,
label: format_label(annotation.label, None),
is_fixable: false,
},
range,
annotation_type: DisplayAnnotationType::from(annotation.level),

View File

@@ -22,7 +22,6 @@ pub struct Message<'a> {
pub(crate) title: &'a str,
pub(crate) snippets: Vec<Snippet<'a>>,
pub(crate) footer: Vec<Message<'a>>,
pub(crate) is_fixable: bool,
}
impl<'a> Message<'a> {
@@ -50,15 +49,6 @@ impl<'a> Message<'a> {
self.footer.extend(footer);
self
}
/// Whether or not the diagnostic for this message is fixable.
///
/// This is rendered as a `[*]` indicator after the `id` in an annotation header, if the
/// annotation also has `Level::None`.
pub fn is_fixable(mut self, yes: bool) -> Self {
self.is_fixable = yes;
self
}
}
/// Structure containing the slice of text to be annotated and
@@ -75,10 +65,6 @@ pub struct Snippet<'a> {
pub(crate) annotations: Vec<Annotation<'a>>,
pub(crate) fold: bool,
/// The optional cell index in a Jupyter notebook, used for reporting source locations along
/// with the ranges on `annotations`.
pub(crate) cell_index: Option<usize>,
}
impl<'a> Snippet<'a> {
@@ -89,7 +75,6 @@ impl<'a> Snippet<'a> {
source,
annotations: vec![],
fold: false,
cell_index: None,
}
}
@@ -118,12 +103,6 @@ impl<'a> Snippet<'a> {
self.fold = fold;
self
}
/// Attach a Jupyter notebook cell index.
pub fn cell_index(mut self, index: Option<usize>) -> Self {
self.cell_index = index;
self
}
}
/// An annotation for a [`Snippet`].
@@ -135,7 +114,6 @@ pub struct Annotation<'a> {
pub(crate) range: Range<usize>,
pub(crate) label: Option<&'a str>,
pub(crate) level: Level,
pub(crate) is_file_level: bool,
}
impl<'a> Annotation<'a> {
@@ -143,11 +121,6 @@ impl<'a> Annotation<'a> {
self.label = Some(label);
self
}
pub fn is_file_level(mut self, yes: bool) -> Self {
self.is_file_level = yes;
self
}
}
/// Types of annotations.
@@ -172,7 +145,6 @@ impl Level {
title,
snippets: vec![],
footer: vec![],
is_fixable: false,
}
}
@@ -182,7 +154,6 @@ impl Level {
range: span,
label: None,
level: self,
is_file_level: false,
}
}
}

View File

@@ -351,41 +351,6 @@ fn benchmark_many_tuple_assignments(criterion: &mut Criterion) {
});
}
fn benchmark_tuple_implicit_instance_attributes(criterion: &mut Criterion) {
setup_rayon();
criterion.bench_function("ty_micro[many_tuple_assignments]", |b| {
b.iter_batched_ref(
|| {
// This is a regression benchmark for a case that used to hang:
// https://github.com/astral-sh/ty/issues/765
setup_micro_case(
r#"
from typing import Any
class A:
foo: tuple[Any, ...]
class B(A):
def __init__(self, parent: "C", x: tuple[Any]):
self.foo = parent.foo + x
class C(A):
def __init__(self, parent: B, x: tuple[Any]):
self.foo = parent.foo + x
"#,
)
},
|case| {
let Case { db, .. } = case;
let result = db.check();
assert_eq!(result.len(), 0);
},
BatchSize::SmallInput,
);
});
}
fn benchmark_complex_constrained_attributes_1(criterion: &mut Criterion) {
setup_rayon();
@@ -665,7 +630,6 @@ criterion_group!(
micro,
benchmark_many_string_assignments,
benchmark_many_tuple_assignments,
benchmark_tuple_implicit_instance_attributes,
benchmark_complex_constrained_attributes_1,
benchmark_complex_constrained_attributes_2,
benchmark_many_enum_members,

View File

@@ -14,7 +14,6 @@ license = { workspace = true }
ruff_annotate_snippets = { workspace = true }
ruff_cache = { workspace = true, optional = true }
ruff_diagnostics = { workspace = true }
ruff_memory_usage = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true, features = ["get-size"] }
ruff_python_parser = { workspace = true }
@@ -43,6 +42,7 @@ serde_json = { workspace = true, optional = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, optional = true }
unicode-width = { workspace = true }
zip = { workspace = true }
[target.'cfg(target_arch="wasm32")'.dependencies]

View File

@@ -21,7 +21,7 @@ mod stylesheet;
/// characteristics in the inputs given to the tool. Typically, but not always,
/// a characteristic is a deficiency. An example of a characteristic that is
/// _not_ a deficiency is the `reveal_type` diagnostic for our type checker.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
pub struct Diagnostic {
/// The actual diagnostic.
///
@@ -212,7 +212,7 @@ impl Diagnostic {
/// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what
/// you want.
pub fn concise_message(&self) -> ConciseMessage<'_> {
pub fn concise_message(&self) -> ConciseMessage {
let main = self.inner.message.as_str();
let annotation = self
.primary_annotation()
@@ -366,16 +366,6 @@ impl Diagnostic {
self.inner.secondary_code.as_ref()
}
/// Returns the secondary code for the diagnostic if it exists, or the lint name otherwise.
///
/// This is a common pattern for Ruff diagnostics, which want to use the noqa code in general,
/// but fall back on the `invalid-syntax` identifier for syntax errors, which don't have
/// secondary codes.
pub fn secondary_code_or_id(&self) -> &str {
self.secondary_code()
.map_or_else(|| self.inner.id.as_str(), SecondaryCode::as_str)
}
/// Set the secondary code for this diagnostic.
pub fn set_secondary_code(&mut self, code: SecondaryCode) {
Arc::make_mut(&mut self.inner).secondary_code = Some(code);
@@ -489,7 +479,7 @@ impl Diagnostic {
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
struct DiagnosticInner {
id: DiagnosticId,
severity: Severity,
@@ -565,7 +555,7 @@ impl Eq for RenderingSortKey<'_> {}
/// Currently, the order in which sub-diagnostics are rendered relative to one
/// another (for a single parent diagnostic) is the order in which they were
/// attached to the diagnostic.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
pub struct SubDiagnostic {
/// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
/// pointer-sized.
@@ -654,7 +644,7 @@ impl SubDiagnostic {
/// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what
/// you want.
pub fn concise_message(&self) -> ConciseMessage<'_> {
pub fn concise_message(&self) -> ConciseMessage {
let main = self.inner.message.as_str();
let annotation = self
.primary_annotation()
@@ -669,7 +659,7 @@ impl SubDiagnostic {
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
struct SubDiagnosticInner {
severity: SubDiagnosticSeverity,
message: DiagnosticMessage,
@@ -697,7 +687,7 @@ struct SubDiagnosticInner {
///
/// Messages attached to annotations should also be as brief and specific as
/// possible. Long messages could negative impact the quality of rendering.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
pub struct Annotation {
/// The span of this annotation, corresponding to some subsequence of the
/// user's input that we want to highlight.
@@ -712,11 +702,6 @@ pub struct Annotation {
is_primary: bool,
/// The diagnostic tags associated with this annotation.
tags: Vec<DiagnosticTag>,
/// Whether this annotation is a file-level or full-file annotation.
///
/// When set, rendering will only include the file's name and (optional) range. Everything else
/// is omitted, including any file snippet or message.
is_file_level: bool,
}
impl Annotation {
@@ -735,7 +720,6 @@ impl Annotation {
message: None,
is_primary: true,
tags: Vec::new(),
is_file_level: false,
}
}
@@ -752,7 +736,6 @@ impl Annotation {
message: None,
is_primary: false,
tags: Vec::new(),
is_file_level: false,
}
}
@@ -818,28 +801,13 @@ impl Annotation {
pub fn push_tag(&mut self, tag: DiagnosticTag) {
self.tags.push(tag);
}
/// Set whether or not this annotation is file-level.
///
/// File-level annotations are only rendered with their file name and range, if available. This
/// is intended for backwards compatibility with Ruff diagnostics, which historically used
/// `TextRange::default` to indicate a file-level diagnostic. In the new diagnostic model, a
/// [`Span`] with a range of `None` should be used instead, as mentioned in the `Span`
/// documentation.
///
/// TODO(brent) update this usage in Ruff and remove `is_file_level` entirely. See
/// <https://github.com/astral-sh/ruff/issues/19688>, especially my first comment, for more
/// details.
pub fn set_file_level(&mut self, yes: bool) {
self.is_file_level = yes;
}
}
/// Tags that can be associated with an annotation.
///
/// These tags are used to provide additional information about the annotation.
/// and are passed through to the language server protocol.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
pub enum DiagnosticTag {
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
Unnecessary,
@@ -1048,7 +1016,7 @@ impl std::fmt::Display for DiagnosticId {
///
/// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and
/// emitting diagnostics from both ty and ruff.
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
pub enum UnifiedFile {
Ty(File),
Ruff(SourceFile),
@@ -1099,7 +1067,7 @@ enum DiagnosticSource {
impl DiagnosticSource {
/// Returns this input as a `SourceCode` for convenient querying.
fn as_source_code(&self) -> SourceCode<'_, '_> {
fn as_source_code(&self) -> SourceCode {
match self {
DiagnosticSource::Ty(input) => SourceCode::new(input.text.as_str(), &input.line_index),
DiagnosticSource::Ruff(source) => SourceCode::new(source.source_text(), source.index()),
@@ -1112,7 +1080,7 @@ impl DiagnosticSource {
/// It consists of a `File` and an optional range into that file. When the
/// range isn't present, it semantically implies that the diagnostic refers to
/// the entire file. For example, when the file should be executable but isn't.
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
pub struct Span {
file: UnifiedFile,
range: Option<TextRange>,
@@ -1190,7 +1158,7 @@ impl From<crate::files::FileRange> for Span {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
pub enum Severity {
Info,
Warning,
@@ -1225,7 +1193,7 @@ impl Severity {
/// This type only exists to add an additional `Help` severity that isn't present in `Severity` or
/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
/// deleted and the two combined again.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
pub enum SubDiagnosticSeverity {
Help,
Info,
@@ -1460,7 +1428,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
/// In most cases, callers shouldn't need to use this. Instead, there is
/// a blanket trait implementation for `IntoDiagnosticMessage` for
/// anything that implements `std::fmt::Display`.
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
pub struct DiagnosticMessage(Box<str>);
impl DiagnosticMessage {

View File

@@ -135,7 +135,7 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
.none(stylesheet.none);
for diag in self.diagnostics {
let resolved = Resolved::new(self.resolver, diag, self.config);
let resolved = Resolved::new(self.resolver, diag);
let renderable = resolved.to_renderable(self.config.context);
for diag in renderable.diagnostics.iter() {
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
@@ -191,13 +191,9 @@ struct Resolved<'a> {
impl<'a> Resolved<'a> {
/// Creates a new resolved set of diagnostics.
fn new(
resolver: &'a dyn FileResolver,
diag: &'a Diagnostic,
config: &DisplayDiagnosticConfig,
) -> Resolved<'a> {
fn new(resolver: &'a dyn FileResolver, diag: &'a Diagnostic) -> Resolved<'a> {
let mut diagnostics = vec![];
diagnostics.push(ResolvedDiagnostic::from_diagnostic(resolver, config, diag));
diagnostics.push(ResolvedDiagnostic::from_diagnostic(resolver, diag));
for sub in &diag.inner.subs {
diagnostics.push(ResolvedDiagnostic::from_sub_diagnostic(resolver, sub));
}
@@ -227,14 +223,12 @@ struct ResolvedDiagnostic<'a> {
id: Option<String>,
message: String,
annotations: Vec<ResolvedAnnotation<'a>>,
is_fixable: bool,
}
impl<'a> ResolvedDiagnostic<'a> {
/// Resolve a single diagnostic.
fn from_diagnostic(
resolver: &'a dyn FileResolver,
config: &DisplayDiagnosticConfig,
diag: &'a Diagnostic,
) -> ResolvedDiagnostic<'a> {
let annotations: Vec<_> = diag
@@ -244,38 +238,16 @@ impl<'a> ResolvedDiagnostic<'a> {
.filter_map(|ann| {
let path = ann.span.file.path(resolver);
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
ResolvedAnnotation::new(path, &diagnostic_source, ann)
})
.collect();
let id = if config.hide_severity {
// Either the rule code alone (e.g. `F401`), or the lint id with a colon (e.g.
// `invalid-syntax:`). When Ruff gets real severities, we should put the colon back in
// `DisplaySet::format_annotation` for both cases, but this is a small hack to improve
// the formatting of syntax errors for now. This should also be kept consistent with the
// concise formatting.
Some(diag.secondary_code().map_or_else(
|| format!("{id}:", id = diag.inner.id),
|code| code.to_string(),
))
} else {
Some(diag.inner.id.to_string())
};
let level = if config.hide_severity {
AnnotateLevel::None
} else {
diag.inner.severity.to_annotate()
};
let id = Some(diag.inner.id.to_string());
let message = diag.inner.message.as_str().to_string();
ResolvedDiagnostic {
level,
level: diag.inner.severity.to_annotate(),
id,
message: diag.inner.message.as_str().to_string(),
message,
annotations,
is_fixable: diag
.fix()
.is_some_and(|fix| fix.applies(config.fix_applicability)),
}
}
@@ -291,7 +263,7 @@ impl<'a> ResolvedDiagnostic<'a> {
.filter_map(|ann| {
let path = ann.span.file.path(resolver);
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
ResolvedAnnotation::new(path, &diagnostic_source, ann)
})
.collect();
ResolvedDiagnostic {
@@ -299,7 +271,6 @@ impl<'a> ResolvedDiagnostic<'a> {
id: None,
message: diag.inner.message.as_str().to_string(),
annotations,
is_fixable: false,
}
}
@@ -330,49 +301,20 @@ impl<'a> ResolvedDiagnostic<'a> {
&prev.diagnostic_source.as_source_code(),
context,
prev.line_end,
prev.notebook_index.as_ref(),
)
.get();
let this_context_begins = context_before(
&ann.diagnostic_source.as_source_code(),
context,
ann.line_start,
ann.notebook_index.as_ref(),
)
.get();
// For notebooks, check whether the end of the
// previous annotation and the start of the current
// annotation are in different cells.
let prev_cell_index = prev.notebook_index.as_ref().map(|notebook_index| {
let prev_end = prev
.diagnostic_source
.as_source_code()
.line_column(prev.range.end());
notebook_index.cell(prev_end.line).unwrap_or_default().get()
});
let this_cell_index = ann.notebook_index.as_ref().map(|notebook_index| {
let this_start = ann
.diagnostic_source
.as_source_code()
.line_column(ann.range.start());
notebook_index
.cell(this_start.line)
.unwrap_or_default()
.get()
});
let in_different_cells = prev_cell_index != this_cell_index;
// The boundary case here is when `prev_context_ends`
// is exactly one less than `this_context_begins`. In
// that case, the context windows are adjacent and we
// should fall through below to add this annotation to
// the existing snippet.
//
// For notebooks, also check that the context windows
// are in the same cell. Windows from different cells
// should never be considered adjacent.
if in_different_cells || this_context_begins.saturating_sub(prev_context_ends) > 1 {
if this_context_begins.saturating_sub(prev_context_ends) > 1 {
snippet_by_path
.entry(path)
.or_default()
@@ -396,7 +338,6 @@ impl<'a> ResolvedDiagnostic<'a> {
id: self.id.as_deref(),
message: &self.message,
snippets_by_input,
is_fixable: self.is_fixable,
}
}
}
@@ -416,8 +357,6 @@ struct ResolvedAnnotation<'a> {
line_end: OneIndexed,
message: Option<&'a str>,
is_primary: bool,
is_file_level: bool,
notebook_index: Option<NotebookIndex>,
}
impl<'a> ResolvedAnnotation<'a> {
@@ -430,7 +369,6 @@ impl<'a> ResolvedAnnotation<'a> {
path: &'a str,
diagnostic_source: &DiagnosticSource,
ann: &'a Annotation,
resolver: &'a dyn FileResolver,
) -> Option<ResolvedAnnotation<'a>> {
let source = diagnostic_source.as_source_code();
let (range, line_start, line_end) = match (ann.span.range(), ann.message.is_some()) {
@@ -464,8 +402,6 @@ impl<'a> ResolvedAnnotation<'a> {
line_end,
message: ann.get_message(),
is_primary: ann.is_primary,
is_file_level: ann.is_file_level,
notebook_index: resolver.notebook_index(&ann.span.file),
})
}
}
@@ -500,10 +436,6 @@ struct RenderableDiagnostic<'r> {
/// should be from the same file, and none of the snippets inside of a
/// collection should overlap with one another or be directly adjacent.
snippets_by_input: Vec<RenderableSnippets<'r>>,
/// Whether or not the diagnostic is fixable.
///
/// This is rendered as a `[*]` indicator after the diagnostic ID.
is_fixable: bool,
}
impl RenderableDiagnostic<'_> {
@@ -516,7 +448,7 @@ impl RenderableDiagnostic<'_> {
.iter()
.map(|snippet| snippet.to_annotate(path))
});
let mut message = self.level.title(self.message).is_fixable(self.is_fixable);
let mut message = self.level.title(self.message);
if let Some(id) = self.id {
message = message.id(id);
}
@@ -598,27 +530,17 @@ struct RenderableSnippet<'r> {
/// Whether this snippet contains at least one primary
/// annotation.
has_primary: bool,
/// The cell index in a Jupyter notebook, if this snippet refers to a notebook.
///
/// This is used for rendering annotations with offsets like `cell 1:2:3` instead of simple row
/// and column numbers.
cell_index: Option<usize>,
}
impl<'r> RenderableSnippet<'r> {
/// Creates a new snippet with one or more annotations that is ready to be
/// rendered.
/// renderer.
///
/// The first line of the snippet is the smallest line number on which one
/// of the annotations begins, minus the context window size. The last line
/// is the largest line number on which one of the annotations ends, plus
/// the context window size.
///
/// For Jupyter notebooks, the context window may also be truncated at cell
/// boundaries. If multiple annotations are present, and they point to
/// different cells, these will have already been split into separate
/// snippets by `ResolvedDiagnostic::to_renderable`.
///
/// Callers should guarantee that the `input` on every `ResolvedAnnotation`
/// given is identical.
///
@@ -635,19 +557,19 @@ impl<'r> RenderableSnippet<'r> {
"creating a renderable snippet requires a non-zero number of annotations",
);
let diagnostic_source = &anns[0].diagnostic_source;
let notebook_index = anns[0].notebook_index.as_ref();
let source = diagnostic_source.as_source_code();
let has_primary = anns.iter().any(|ann| ann.is_primary);
let content_start_index = anns.iter().map(|ann| ann.line_start).min().unwrap();
let line_start = context_before(&source, context, content_start_index, notebook_index);
let start = source.line_column(anns[0].range.start());
let cell_index = notebook_index
.map(|notebook_index| notebook_index.cell(start.line).unwrap_or_default().get());
let content_end_index = anns.iter().map(|ann| ann.line_end).max().unwrap();
let line_end = context_after(&source, context, content_end_index, notebook_index);
let line_start = context_before(
&source,
context,
anns.iter().map(|ann| ann.line_start).min().unwrap(),
);
let line_end = context_after(
&source,
context,
anns.iter().map(|ann| ann.line_end).max().unwrap(),
);
let snippet_start = source.line_start(line_start);
let snippet_end = source.line_end(line_end);
@@ -663,20 +585,14 @@ impl<'r> RenderableSnippet<'r> {
let EscapedSourceCode {
text: snippet,
annotations,
} = replace_unprintable(snippet, annotations).fix_up_empty_spans_after_line_terminator();
let line_start = notebook_index.map_or(line_start, |notebook_index| {
notebook_index
.cell_row(line_start)
.unwrap_or(OneIndexed::MIN)
});
} = replace_whitespace_and_unprintable(snippet, annotations)
.fix_up_empty_spans_after_line_terminator();
RenderableSnippet {
snippet,
line_start,
annotations,
has_primary,
cell_index,
}
}
@@ -690,7 +606,6 @@ impl<'r> RenderableSnippet<'r> {
.iter()
.map(RenderableAnnotation::to_annotate),
)
.cell_index(self.cell_index)
}
}
@@ -705,8 +620,6 @@ struct RenderableAnnotation<'r> {
message: Option<&'r str>,
/// Whether this annotation is considered "primary" or not.
is_primary: bool,
/// Whether this annotation applies to an entire file, rather than a snippet within it.
is_file_level: bool,
}
impl<'r> RenderableAnnotation<'r> {
@@ -724,7 +637,6 @@ impl<'r> RenderableAnnotation<'r> {
range,
message: ann.message,
is_primary: ann.is_primary,
is_file_level: ann.is_file_level,
}
}
@@ -750,7 +662,7 @@ impl<'r> RenderableAnnotation<'r> {
if let Some(message) = self.message {
ann = ann.label(message);
}
ann.is_file_level(self.is_file_level)
ann
}
}
@@ -877,15 +789,7 @@ pub struct Input {
///
/// The line number returned is guaranteed to be less than
/// or equal to `start`.
///
/// In Jupyter notebooks, lines outside the cell containing
/// `start` will be omitted.
fn context_before(
source: &SourceCode<'_, '_>,
len: usize,
start: OneIndexed,
notebook_index: Option<&NotebookIndex>,
) -> OneIndexed {
fn context_before(source: &SourceCode<'_, '_>, len: usize, start: OneIndexed) -> OneIndexed {
let mut line = start.saturating_sub(len);
// Trim leading empty lines.
while line < start {
@@ -894,17 +798,6 @@ fn context_before(
}
line = line.saturating_add(1);
}
if let Some(index) = notebook_index {
let content_start_cell = index.cell(start).unwrap_or(OneIndexed::MIN);
while line < start {
if index.cell(line).unwrap_or(OneIndexed::MIN) == content_start_cell {
break;
}
line = line.saturating_add(1);
}
}
line
}
@@ -914,15 +807,7 @@ fn context_before(
/// The line number returned is guaranteed to be greater
/// than or equal to `start` and no greater than the
/// number of lines in `source`.
///
/// In Jupyter notebooks, lines outside the cell containing
/// `start` will be omitted.
fn context_after(
source: &SourceCode<'_, '_>,
len: usize,
start: OneIndexed,
notebook_index: Option<&NotebookIndex>,
) -> OneIndexed {
fn context_after(source: &SourceCode<'_, '_>, len: usize, start: OneIndexed) -> OneIndexed {
let max_lines = OneIndexed::from_zero_indexed(source.line_count());
let mut line = start.saturating_add(len).min(max_lines);
// Trim trailing empty lines.
@@ -932,17 +817,6 @@ fn context_after(
}
line = line.saturating_sub(1);
}
if let Some(index) = notebook_index {
let content_end_cell = index.cell(start).unwrap_or(OneIndexed::MIN);
while line > start {
if index.cell(line).unwrap_or(OneIndexed::MIN) == content_end_cell {
break;
}
line = line.saturating_sub(1);
}
}
line
}
@@ -954,18 +828,13 @@ fn relativize_path<'p>(cwd: &SystemPath, path: &'p str) -> &'p str {
path
}
/// Given some source code and annotation ranges, this routine replaces
/// unprintable characters with printable representations of them.
/// Given some source code and annotation ranges, this routine replaces tabs
/// with ASCII whitespace, and unprintable characters with printable
/// representations of them.
///
/// The source code and annotations returned are updated to reflect changes made
/// to the source code (if any).
///
/// We don't need to normalize whitespace, such as converting tabs to spaces,
/// because `annotate-snippets` handles that internally. Similarly, it's safe to
/// modify the annotation ranges by inserting 3-byte Unicode replacements
/// because `annotate-snippets` will account for their actual width when
/// rendering and displaying the column to the user.
fn replace_unprintable<'r>(
fn replace_whitespace_and_unprintable<'r>(
source: &'r str,
mut annotations: Vec<RenderableAnnotation<'r>>,
) -> EscapedSourceCode<'r> {
@@ -997,17 +866,48 @@ fn replace_unprintable<'r>(
}
};
const TAB_SIZE: usize = 4;
let mut width = 0;
let mut column = 0;
let mut last_end = 0;
let mut result = String::new();
for (index, c) in source.char_indices() {
if let Some(printable) = unprintable_replacement(c) {
result.push_str(&source[last_end..index]);
let old_width = width;
match c {
'\n' | '\r' => {
width = 0;
column = 0;
}
'\t' => {
let tab_offset = TAB_SIZE - (column % TAB_SIZE);
width += tab_offset;
column += tab_offset;
let len = printable.text_len().to_u32();
update_ranges(result.text_len().to_usize(), len);
let tab_width =
u32::try_from(width - old_width).expect("small width because of tab size");
result.push_str(&source[last_end..index]);
result.push(printable);
last_end = index + 1;
update_ranges(result.text_len().to_usize(), tab_width);
for _ in 0..tab_width {
result.push(' ');
}
last_end = index + 1;
}
_ => {
width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
column += 1;
if let Some(printable) = unprintable_replacement(c) {
result.push_str(&source[last_end..index]);
let len = printable.text_len().to_u32();
update_ranges(result.text_len().to_usize(), len);
result.push(printable);
last_end = index + 1;
}
}
}
}
@@ -2644,12 +2544,7 @@ watermelon
/// of the corresponding line minus one. (The "minus one" is because
/// otherwise, the span will end where the next line begins, and this
/// confuses `ruff_annotate_snippets` as of 2025-03-13.)
pub(super) fn span(
&self,
path: &str,
line_offset_start: &str,
line_offset_end: &str,
) -> Span {
fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span {
let span = self.path(path);
let file = span.expect_ty_file();
@@ -2672,7 +2567,7 @@ watermelon
}
/// Like `span`, but only attaches a file path.
pub(super) fn path(&self, path: &str) -> Span {
fn path(&self, path: &str) -> Span {
let file = system_path_to_file(&self.db, path).unwrap();
Span::from(file)
}
@@ -2786,7 +2681,7 @@ watermelon
///
/// See the docs on `TestEnvironment::span` for the meaning of
/// `path`, `line_offset_start` and `line_offset_end`.
pub(super) fn secondary(
fn secondary(
mut self,
path: &str,
line_offset_start: &str,
@@ -2822,7 +2717,7 @@ watermelon
}
/// Adds a "help" sub-diagnostic with the given message.
pub(super) fn help(mut self, message: impl IntoDiagnosticMessage) -> DiagnosticBuilder<'e> {
fn help(mut self, message: impl IntoDiagnosticMessage) -> DiagnosticBuilder<'e> {
self.diag.help(message);
self
}
@@ -2982,10 +2877,10 @@ if call(foo
env.format(format);
let diagnostics = vec![
env.invalid_syntax("Expected one or more symbol names after import")
env.invalid_syntax("SyntaxError: Expected one or more symbol names after import")
.primary("syntax_errors.py", "1:14", "1:15", "")
.build(),
env.invalid_syntax("Expected ')', found newline")
env.invalid_syntax("SyntaxError: Expected ')', found newline")
.primary("syntax_errors.py", "3:11", "3:12", "")
.build(),
];
@@ -2993,8 +2888,7 @@ if call(foo
(env, diagnostics)
}
/// A Jupyter notebook for testing diagnostics.
///
/// Create Ruff-style diagnostics for testing the various output formats for a notebook.
///
/// The concatenated cells look like this:
///
@@ -3014,7 +2908,17 @@ if call(foo
/// The first diagnostic is on the unused `os` import with location cell 1, row 2, column 8
/// (`cell 1:2:8`). The second diagnostic is the unused `math` import at `cell 2:2:8`, and the
/// third diagnostic is an unfixable unused variable at `cell 3:4:5`.
pub(super) static NOTEBOOK: &str = r##"
#[allow(
dead_code,
reason = "This is currently only used for JSON but will be needed soon for other formats"
)]
pub(crate) fn create_notebook_diagnostics(
format: DiagnosticFormat,
) -> (TestEnvironment, Vec<Diagnostic>) {
let mut env = TestEnvironment::new();
env.add(
"notebook.ipynb",
r##"
{
"cells": [
{
@@ -3053,14 +2957,8 @@ if call(foo
"nbformat": 4,
"nbformat_minor": 5
}
"##;
/// Create Ruff-style diagnostics for testing the various output formats for a notebook.
pub(crate) fn create_notebook_diagnostics(
format: DiagnosticFormat,
) -> (TestEnvironment, Vec<Diagnostic>) {
let mut env = TestEnvironment::new();
env.add("notebook.ipynb", NOTEBOOK);
"##,
);
env.format(format);
let diagnostics = vec![

View File

@@ -50,8 +50,10 @@ impl AzureRenderer<'_> {
}
writeln!(
f,
"code={code};]{body}",
code = diag.secondary_code_or_id(),
"{code}]{body}",
code = diag
.secondary_code()
.map_or_else(String::new, |code| format!("code={code};")),
body = diag.body(),
)?;
}

View File

@@ -69,12 +69,6 @@ impl<'a> ConciseRenderer<'a> {
"{code} ",
code = fmt_styled(code, stylesheet.secondary_code)
)?;
} else {
write!(
f,
"{id}: ",
id = fmt_styled(diag.inner.id.as_str(), stylesheet.secondary_code)
)?;
}
if self.config.show_fix_status {
if let Some(fix) = diag.fix() {
@@ -162,8 +156,8 @@ mod tests {
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline
syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
syntax_errors.py:3:12: SyntaxError: Expected ')', found newline
");
}
@@ -171,8 +165,8 @@ mod tests {
fn syntax_errors() {
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Concise);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
syntax_errors.py:1:15: error[invalid-syntax] Expected one or more symbol names after import
syntax_errors.py:3:12: error[invalid-syntax] Expected ')', found newline
syntax_errors.py:1:15: error[invalid-syntax] SyntaxError: Expected one or more symbol names after import
syntax_errors.py:3:12: error[invalid-syntax] SyntaxError: Expected ')', found newline
");
}

View File

@@ -1,14 +1,8 @@
#[cfg(test)]
mod tests {
use ruff_diagnostics::Applicability;
use ruff_text_size::TextRange;
use crate::diagnostic::{
Annotation, DiagnosticFormat, Severity,
render::tests::{
NOTEBOOK, TestEnvironment, create_diagnostics, create_notebook_diagnostics,
create_syntax_error_diagnostics,
},
DiagnosticFormat, Severity,
render::tests::{TestEnvironment, create_diagnostics, create_syntax_error_diagnostics},
};
#[test]
@@ -48,7 +42,7 @@ mod tests {
fn syntax_errors() {
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Full);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[invalid-syntax]: Expected one or more symbol names after import
error[invalid-syntax]: SyntaxError: Expected one or more symbol names after import
--> syntax_errors.py:1:15
|
1 | from os import
@@ -57,71 +51,7 @@ mod tests {
3 | if call(foo
|
error[invalid-syntax]: Expected ')', found newline
--> syntax_errors.py:3:12
|
1 | from os import
2 |
3 | if call(foo
| ^
4 | def bar():
5 | pass
|
");
}
#[test]
fn hide_severity_output() {
let (mut env, diagnostics) = create_diagnostics(DiagnosticFormat::Full);
env.hide_severity(true);
env.fix_applicability(Applicability::DisplayOnly);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r#"
F401 [*] `os` imported but unused
--> fib.py:1:8
|
1 | import os
| ^^
|
help: Remove unused import: `os`
F841 [*] Local variable `x` is assigned to but never used
--> fib.py:6:5
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^
7 | if n == 0:
8 | return 0
|
help: Remove assignment to unused variable `x`
F821 Undefined name `a`
--> undef.py:1:4
|
1 | if a == 1: pass
| ^
|
"#);
}
#[test]
fn hide_severity_syntax_errors() {
let (mut env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Full);
env.hide_severity(true);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
invalid-syntax: Expected one or more symbol names after import
--> syntax_errors.py:1:15
|
1 | from os import
| ^
2 |
3 | if call(foo
|
invalid-syntax: Expected ')', found newline
error[invalid-syntax]: SyntaxError: Expected ')', found newline
--> syntax_errors.py:3:12
|
1 | from os import
@@ -247,157 +177,4 @@ print()
Ok(())
}
/// Ensure that the header column matches the column in the user's input, even if we've replaced
/// tabs with spaces for rendering purposes.
#[test]
fn tab_replacement() {
let mut env = TestEnvironment::new();
env.add("example.py", "def foo():\n\treturn 1");
env.format(DiagnosticFormat::Full);
let diagnostic = env.err().primary("example.py", "2:1", "2:9", "").build();
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
--> example.py:2:2
|
1 | def foo():
2 | return 1
| ^^^^^^^^
|
");
}
/// For file-level diagnostics, we expect to see the header line with the diagnostic information
/// and the `-->` line with the file information but no lines of source code.
#[test]
fn file_level() {
let mut env = TestEnvironment::new();
env.add("example.py", "");
env.format(DiagnosticFormat::Full);
let mut diagnostic = env.err().build();
let span = env.path("example.py").with_range(TextRange::default());
let mut annotation = Annotation::primary(span);
annotation.set_file_level(true);
diagnostic.annotate(annotation);
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
--> example.py:1:1
");
}
/// Check that ranges in notebooks are remapped relative to the cells.
#[test]
fn notebook_output() {
let (env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
error[unused-import][*]: `math` imported but unused
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^
3 |
4 | print('hello world')
|
help: Remove unused import: `math`
error[unused-variable]: Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| ^
|
help: Remove assignment to unused variable `x`
");
}
/// Check notebook handling for multiple annotations in a single diagnostic that span cells.
#[test]
fn notebook_output_multiple_annotations() {
let mut env = TestEnvironment::new();
env.add("notebook.ipynb", NOTEBOOK);
let diagnostics = vec![
// adjacent context windows
env.builder("unused-import", Severity::Error, "`os` imported but unused")
.primary("notebook.ipynb", "2:7", "2:9", "")
.secondary("notebook.ipynb", "4:7", "4:11", "second cell")
.help("Remove unused import: `os`")
.build(),
// non-adjacent context windows
env.builder("unused-import", Severity::Error, "`os` imported but unused")
.primary("notebook.ipynb", "2:7", "2:9", "")
.secondary("notebook.ipynb", "10:4", "10:5", "second cell")
.help("Remove unused import: `os`")
.build(),
// adjacent context windows in the same cell
env.err()
.primary("notebook.ipynb", "4:7", "4:11", "second cell")
.secondary("notebook.ipynb", "6:0", "6:5", "print statement")
.help("Remove `print` statement")
.build(),
];
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[unused-import]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
::: notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ---- second cell
3 |
4 | print('hello world')
|
help: Remove unused import: `os`
error[unused-import]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
::: notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| - second cell
|
help: Remove unused import: `os`
error[test-diagnostic]: main diagnostic message
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^ second cell
3 |
4 | print('hello world')
| ----- print statement
|
help: Remove `print` statement
");
}
}

View File

@@ -6,7 +6,7 @@ use ruff_notebook::NotebookIndex;
use ruff_source_file::{LineColumn, OneIndexed};
use ruff_text_size::Ranged;
use crate::diagnostic::{Diagnostic, DiagnosticSource, DisplayDiagnosticConfig};
use crate::diagnostic::{Diagnostic, DiagnosticSource, DisplayDiagnosticConfig, SecondaryCode};
use super::FileResolver;
@@ -99,7 +99,7 @@ pub(super) fn diagnostic_to_json<'a>(
// In preview, the locations and filename can be optional.
if config.preview {
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
code: diagnostic.secondary_code(),
url: diagnostic.to_ruff_url(),
message: diagnostic.body(),
fix,
@@ -111,7 +111,7 @@ pub(super) fn diagnostic_to_json<'a>(
}
} else {
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
code: diagnostic.secondary_code(),
url: diagnostic.to_ruff_url(),
message: diagnostic.body(),
fix,
@@ -221,7 +221,7 @@ impl Serialize for ExpandedEdits<'_> {
#[derive(Serialize)]
pub(crate) struct JsonDiagnostic<'a> {
cell: Option<OneIndexed>,
code: &'a str,
code: Option<&'a SecondaryCode>,
end_location: Option<JsonLocation>,
filename: Option<&'a str>,
fix: Option<JsonFix<'a>>,
@@ -302,7 +302,7 @@ mod tests {
[
{
"cell": null,
"code": "test-diagnostic",
"code": null,
"end_location": {
"column": 1,
"row": 1
@@ -336,7 +336,7 @@ mod tests {
[
{
"cell": null,
"code": "test-diagnostic",
"code": null,
"end_location": null,
"filename": null,
"fix": null,

View File

@@ -2,5 +2,5 @@
source: crates/ruff_db/src/diagnostic/render/azure.rs
expression: env.render_diagnostics(&diagnostics)
---
##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=1;columnnumber=15;code=invalid-syntax;]Expected one or more symbol names after import
##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=3;columnnumber=12;code=invalid-syntax;]Expected ')', found newline
##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=1;columnnumber=15;]SyntaxError: Expected one or more symbol names after import
##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=3;columnnumber=12;]SyntaxError: Expected ')', found newline

View File

@@ -5,7 +5,7 @@ expression: env.render_diagnostics(&diagnostics)
[
{
"cell": null,
"code": "invalid-syntax",
"code": null,
"end_location": {
"column": 1,
"row": 2
@@ -16,13 +16,13 @@ expression: env.render_diagnostics(&diagnostics)
"column": 15,
"row": 1
},
"message": "Expected one or more symbol names after import",
"message": "SyntaxError: Expected one or more symbol names after import",
"noqa_row": null,
"url": null
},
{
"cell": null,
"code": "invalid-syntax",
"code": null,
"end_location": {
"column": 1,
"row": 4
@@ -33,7 +33,7 @@ expression: env.render_diagnostics(&diagnostics)
"column": 12,
"row": 3
},
"message": "Expected ')', found newline",
"message": "SyntaxError: Expected ')', found newline",
"noqa_row": null,
"url": null
}

View File

@@ -2,5 +2,5 @@
source: crates/ruff_db/src/diagnostic/render/json_lines.rs
expression: env.render_diagnostics(&diagnostics)
---
{"cell":null,"code":"invalid-syntax","end_location":{"column":1,"row":2},"filename":"syntax_errors.py","fix":null,"location":{"column":15,"row":1},"message":"Expected one or more symbol names after import","noqa_row":null,"url":null}
{"cell":null,"code":"invalid-syntax","end_location":{"column":1,"row":4},"filename":"syntax_errors.py","fix":null,"location":{"column":12,"row":3},"message":"Expected ')', found newline","noqa_row":null,"url":null}
{"cell":null,"code":null,"end_location":{"column":1,"row":2},"filename":"syntax_errors.py","fix":null,"location":{"column":15,"row":1},"message":"SyntaxError: Expected one or more symbol names after import","noqa_row":null,"url":null}
{"cell":null,"code":null,"end_location":{"column":1,"row":4},"filename":"syntax_errors.py","fix":null,"location":{"column":12,"row":3},"message":"SyntaxError: Expected ')', found newline","noqa_row":null,"url":null}

View File

@@ -6,10 +6,10 @@ expression: env.render_diagnostics(&diagnostics)
<testsuites name="ruff" tests="2" failures="2" errors="0">
<testsuite name="syntax_errors.py" tests="2" disabled="0" errors="0" failures="2" package="org.ruff">
<testcase name="org.ruff.invalid-syntax" classname="syntax_errors" line="1" column="15">
<failure message="Expected one or more symbol names after import">line 1, col 15, Expected one or more symbol names after import</failure>
<failure message="SyntaxError: Expected one or more symbol names after import">line 1, col 15, SyntaxError: Expected one or more symbol names after import</failure>
</testcase>
<testcase name="org.ruff.invalid-syntax" classname="syntax_errors" line="3" column="12">
<failure message="Expected &apos;)&apos;, found newline">line 3, col 12, Expected &apos;)&apos;, found newline</failure>
<failure message="SyntaxError: Expected &apos;)&apos;, found newline">line 3, col 12, SyntaxError: Expected &apos;)&apos;, found newline</failure>
</testcase>
</testsuite>
</testsuites>

View File

@@ -2,5 +2,5 @@
source: crates/ruff_db/src/diagnostic/render/pylint.rs
expression: env.render_diagnostics(&diagnostics)
---
syntax_errors.py:1: [invalid-syntax] Expected one or more symbol names after import
syntax_errors.py:3: [invalid-syntax] Expected ')', found newline
syntax_errors.py:1: [invalid-syntax] SyntaxError: Expected one or more symbol names after import
syntax_errors.py:3: [invalid-syntax] SyntaxError: Expected ')', found newline

View File

@@ -21,7 +21,7 @@ expression: env.render_diagnostics(&diagnostics)
}
}
},
"message": "Expected one or more symbol names after import"
"message": "SyntaxError: Expected one or more symbol names after import"
},
{
"code": {
@@ -40,7 +40,7 @@ expression: env.render_diagnostics(&diagnostics)
}
}
},
"message": "Expected ')', found newline"
"message": "SyntaxError: Expected ')', found newline"
}
],
"severity": "WARNING",

View File

@@ -21,7 +21,7 @@ use crate::source::source_text;
/// reflected in the changed AST offsets.
/// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires
/// for determining if a query result is unchanged.
#[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size)]
#[salsa::tracked(returns(ref), no_eq, heap_size=get_size2::GetSize::get_heap_size)]
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
let _span = tracing::trace_span!("parsed_module", ?file).entered();

View File

@@ -9,7 +9,7 @@ use crate::Db;
use crate::files::{File, FilePath};
/// Reads the source text of a python text file (must be valid UTF8) or notebook.
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
pub fn source_text(db: &dyn Db, file: File) -> SourceText {
let path = file.path(db);
let _span = tracing::trace_span!("source_text", file = %path).entered();
@@ -69,21 +69,21 @@ impl SourceText {
pub fn as_str(&self) -> &str {
match &self.inner.kind {
SourceTextKind::Text(source) => source,
SourceTextKind::Notebook { notebook } => notebook.source_code(),
SourceTextKind::Notebook(notebook) => notebook.source_code(),
}
}
/// Returns the underlying notebook if this is a notebook file.
pub fn as_notebook(&self) -> Option<&Notebook> {
match &self.inner.kind {
SourceTextKind::Notebook { notebook } => Some(notebook),
SourceTextKind::Notebook(notebook) => Some(notebook),
SourceTextKind::Text(_) => None,
}
}
/// Returns `true` if this is a notebook source file.
pub fn is_notebook(&self) -> bool {
matches!(&self.inner.kind, SourceTextKind::Notebook { .. })
matches!(&self.inner.kind, SourceTextKind::Notebook(_))
}
/// Returns `true` if there was an error when reading the content of the file.
@@ -108,7 +108,7 @@ impl std::fmt::Debug for SourceText {
SourceTextKind::Text(text) => {
dbg.field(text);
}
SourceTextKind::Notebook { notebook } => {
SourceTextKind::Notebook(notebook) => {
dbg.field(notebook);
}
}
@@ -123,15 +123,23 @@ struct SourceTextInner {
read_error: Option<SourceTextError>,
}
#[derive(Eq, PartialEq, get_size2::GetSize)]
#[derive(Eq, PartialEq)]
enum SourceTextKind {
Text(String),
Notebook {
// Jupyter notebooks are not very relevant for memory profiling, and contain
// arbitrary JSON values that do not implement the `GetSize` trait.
#[get_size(ignore)]
notebook: Box<Notebook>,
},
Notebook(Box<Notebook>),
}
impl get_size2::GetSize for SourceTextKind {
fn get_heap_size(&self) -> usize {
match self {
SourceTextKind::Text(text) => text.get_heap_size(),
// TODO: The `get-size` derive does not support ignoring enum variants.
//
// Jupyter notebooks are not very relevant for memory profiling, and contain
// arbitrary JSON values that do not implement the `GetSize` trait.
SourceTextKind::Notebook(_) => 0,
}
}
}
impl From<String> for SourceTextKind {
@@ -142,9 +150,7 @@ impl From<String> for SourceTextKind {
impl From<Notebook> for SourceTextKind {
fn from(notebook: Notebook) -> Self {
SourceTextKind::Notebook {
notebook: Box::new(notebook),
}
SourceTextKind::Notebook(Box::new(notebook))
}
}
@@ -157,7 +163,7 @@ pub enum SourceTextError {
}
/// Computes the [`LineIndex`] for `file`.
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
pub fn line_index(db: &dyn Db, file: File) -> LineIndex {
let _span = tracing::trace_span!("line_index", ?file).entered();

View File

@@ -236,7 +236,7 @@ impl SystemPath {
///
/// [`CurDir`]: camino::Utf8Component::CurDir
#[inline]
pub fn components(&self) -> camino::Utf8Components<'_> {
pub fn components(&self) -> camino::Utf8Components {
self.0.components()
}

View File

@@ -195,7 +195,7 @@ impl VendoredFileSystem {
///
/// ## Panics:
/// If the current thread already holds the lock.
fn lock_archive(&self) -> LockedZipArchive<'_> {
fn lock_archive(&self) -> LockedZipArchive {
self.inner.lock().unwrap()
}
}
@@ -360,7 +360,7 @@ impl VendoredZipArchive {
Ok(Self(ZipArchive::new(io::Cursor::new(data))?))
}
fn lookup_path(&mut self, path: &NormalizedVendoredPath) -> Result<ZipFile<'_>> {
fn lookup_path(&mut self, path: &NormalizedVendoredPath) -> Result<ZipFile> {
Ok(self.0.by_name(path.as_str())?)
}

View File

@@ -37,7 +37,7 @@ impl VendoredPath {
self.0.as_std_path()
}
pub fn components(&self) -> Utf8Components<'_> {
pub fn components(&self) -> Utf8Components {
self.0.components()
}

View File

@@ -348,7 +348,7 @@ fn format_dev_multi_project(
debug!(parent: None, "Starting {}", project_path.display());
match format_dev_project(
std::slice::from_ref(&project_path),
&[project_path.clone()],
args.stability_check,
args.write,
args.preview,
@@ -628,7 +628,7 @@ struct CheckRepoResult {
}
impl CheckRepoResult {
fn display(&self, format: Format) -> DisplayCheckRepoResult<'_> {
fn display(&self, format: Format) -> DisplayCheckRepoResult {
DisplayCheckRepoResult {
result: self,
format,
@@ -665,7 +665,7 @@ struct Diagnostic {
}
impl Diagnostic {
fn display(&self, format: Format) -> DisplayDiagnostic<'_> {
fn display(&self, format: Format) -> DisplayDiagnostic {
DisplayDiagnostic {
diagnostic: self,
format,

View File

@@ -43,7 +43,7 @@ pub enum IsolationLevel {
}
/// A collection of [`Edit`] elements to be applied to a source file.
#[derive(Debug, PartialEq, Eq, Clone, Hash, get_size2::GetSize)]
#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Fix {
/// The [`Edit`] elements to be applied, sorted by [`Edit::start`] in ascending order.

View File

@@ -562,7 +562,7 @@ struct RemoveSoftLinebreaksSnapshot {
pub trait BufferExtensions: Buffer + Sized {
/// Returns a new buffer that calls the passed inspector for every element that gets written to the output
#[must_use]
fn inspect<F>(&mut self, inspector: F) -> Inspect<'_, Self::Context, F>
fn inspect<F>(&mut self, inspector: F) -> Inspect<Self::Context, F>
where
F: FnMut(&FormatElement),
{
@@ -607,7 +607,7 @@ pub trait BufferExtensions: Buffer + Sized {
/// # }
/// ```
#[must_use]
fn start_recording(&mut self) -> Recording<'_, Self> {
fn start_recording(&mut self) -> Recording<Self> {
Recording::new(self)
}

View File

@@ -340,7 +340,7 @@ impl<Context> Format<Context> for SourcePosition {
/// Creates a text from a dynamic string.
///
/// This is done by allocating a new string internally.
pub fn text(text: &str) -> Text<'_> {
pub fn text(text: &str) -> Text {
debug_assert_no_newlines(text);
Text { text }
@@ -459,10 +459,7 @@ fn debug_assert_no_newlines(text: &str) {
/// # }
/// ```
#[inline]
pub fn line_suffix<Content, Context>(
inner: &Content,
reserved_width: u32,
) -> LineSuffix<'_, Context>
pub fn line_suffix<Content, Context>(inner: &Content, reserved_width: u32) -> LineSuffix<Context>
where
Content: Format<Context>,
{
@@ -600,10 +597,7 @@ impl<Context> Format<Context> for LineSuffixBoundary {
/// Use `Memoized.inspect(f)?.has_label(LabelId::of::<SomeLabelId>()` if you need to know if some content breaks that should
/// only be written later.
#[inline]
pub fn labelled<Content, Context>(
label_id: LabelId,
content: &Content,
) -> FormatLabelled<'_, Context>
pub fn labelled<Content, Context>(label_id: LabelId, content: &Content) -> FormatLabelled<Context>
where
Content: Format<Context>,
{
@@ -706,7 +700,7 @@ impl<Context> Format<Context> for Space {
/// # }
/// ```
#[inline]
pub fn indent<Content, Context>(content: &Content) -> Indent<'_, Context>
pub fn indent<Content, Context>(content: &Content) -> Indent<Context>
where
Content: Format<Context>,
{
@@ -777,7 +771,7 @@ impl<Context> std::fmt::Debug for Indent<'_, Context> {
/// # }
/// ```
#[inline]
pub fn dedent<Content, Context>(content: &Content) -> Dedent<'_, Context>
pub fn dedent<Content, Context>(content: &Content) -> Dedent<Context>
where
Content: Format<Context>,
{
@@ -852,7 +846,7 @@ impl<Context> std::fmt::Debug for Dedent<'_, Context> {
///
/// This resembles the behaviour of Prettier's `align(Number.NEGATIVE_INFINITY, content)` IR element.
#[inline]
pub fn dedent_to_root<Content, Context>(content: &Content) -> Dedent<'_, Context>
pub fn dedent_to_root<Content, Context>(content: &Content) -> Dedent<Context>
where
Content: Format<Context>,
{
@@ -966,7 +960,7 @@ where
///
/// - tab indentation: Printer indents the expression with two tabs because the `align` increases the indentation level.
/// - space indentation: Printer indents the expression by 4 spaces (one indentation level) **and** 2 spaces for the align.
pub fn align<Content, Context>(count: u8, content: &Content) -> Align<'_, Context>
pub fn align<Content, Context>(count: u8, content: &Content) -> Align<Context>
where
Content: Format<Context>,
{
@@ -1036,7 +1030,7 @@ impl<Context> std::fmt::Debug for Align<'_, Context> {
/// # }
/// ```
#[inline]
pub fn block_indent<Context>(content: &impl Format<Context>) -> BlockIndent<'_, Context> {
pub fn block_indent<Context>(content: &impl Format<Context>) -> BlockIndent<Context> {
BlockIndent {
content: Argument::new(content),
mode: IndentMode::Block,
@@ -1107,7 +1101,7 @@ pub fn block_indent<Context>(content: &impl Format<Context>) -> BlockIndent<'_,
/// # }
/// ```
#[inline]
pub fn soft_block_indent<Context>(content: &impl Format<Context>) -> BlockIndent<'_, Context> {
pub fn soft_block_indent<Context>(content: &impl Format<Context>) -> BlockIndent<Context> {
BlockIndent {
content: Argument::new(content),
mode: IndentMode::Soft,
@@ -1181,9 +1175,7 @@ pub fn soft_block_indent<Context>(content: &impl Format<Context>) -> BlockIndent
/// # }
/// ```
#[inline]
pub fn soft_line_indent_or_space<Context>(
content: &impl Format<Context>,
) -> BlockIndent<'_, Context> {
pub fn soft_line_indent_or_space<Context>(content: &impl Format<Context>) -> BlockIndent<Context> {
BlockIndent {
content: Argument::new(content),
mode: IndentMode::SoftLineOrSpace,
@@ -1316,9 +1308,7 @@ impl<Context> std::fmt::Debug for BlockIndent<'_, Context> {
/// # Ok(())
/// # }
/// ```
pub fn soft_space_or_block_indent<Context>(
content: &impl Format<Context>,
) -> BlockIndent<'_, Context> {
pub fn soft_space_or_block_indent<Context>(content: &impl Format<Context>) -> BlockIndent<Context> {
BlockIndent {
content: Argument::new(content),
mode: IndentMode::SoftSpace,
@@ -1398,7 +1388,7 @@ pub fn soft_space_or_block_indent<Context>(
/// # }
/// ```
#[inline]
pub fn group<Context>(content: &impl Format<Context>) -> Group<'_, Context> {
pub fn group<Context>(content: &impl Format<Context>) -> Group<Context> {
Group {
content: Argument::new(content),
id: None,
@@ -1561,7 +1551,7 @@ impl<Context> std::fmt::Debug for Group<'_, Context> {
#[inline]
pub fn best_fit_parenthesize<Context>(
content: &impl Format<Context>,
) -> BestFitParenthesize<'_, Context> {
) -> BestFitParenthesize<Context> {
BestFitParenthesize {
content: Argument::new(content),
group_id: None,
@@ -1701,7 +1691,7 @@ impl<Context> std::fmt::Debug for BestFitParenthesize<'_, Context> {
pub fn conditional_group<Content, Context>(
content: &Content,
condition: Condition,
) -> ConditionalGroup<'_, Context>
) -> ConditionalGroup<Context>
where
Content: Format<Context>,
{
@@ -1862,7 +1852,7 @@ impl<Context> Format<Context> for ExpandParent {
/// # }
/// ```
#[inline]
pub fn if_group_breaks<Content, Context>(content: &Content) -> IfGroupBreaks<'_, Context>
pub fn if_group_breaks<Content, Context>(content: &Content) -> IfGroupBreaks<Context>
where
Content: Format<Context>,
{
@@ -1943,7 +1933,7 @@ where
/// # }
/// ```
#[inline]
pub fn if_group_fits_on_line<Content, Context>(flat_content: &Content) -> IfGroupBreaks<'_, Context>
pub fn if_group_fits_on_line<Content, Context>(flat_content: &Content) -> IfGroupBreaks<Context>
where
Content: Format<Context>,
{
@@ -2132,7 +2122,7 @@ impl<Context> std::fmt::Debug for IfGroupBreaks<'_, Context> {
pub fn indent_if_group_breaks<Content, Context>(
content: &Content,
group_id: GroupId,
) -> IndentIfGroupBreaks<'_, Context>
) -> IndentIfGroupBreaks<Context>
where
Content: Format<Context>,
{
@@ -2215,7 +2205,7 @@ impl<Context> std::fmt::Debug for IndentIfGroupBreaks<'_, Context> {
/// # Ok(())
/// # }
/// ```
pub fn fits_expanded<Content, Context>(content: &Content) -> FitsExpanded<'_, Context>
pub fn fits_expanded<Content, Context>(content: &Content) -> FitsExpanded<Context>
where
Content: Format<Context>,
{

View File

@@ -197,7 +197,7 @@ pub const LINE_TERMINATORS: [char; 3] = ['\r', LINE_SEPARATOR, PARAGRAPH_SEPARAT
/// Replace the line terminators matching the provided list with "\n"
/// since its the only line break type supported by the printer
pub fn normalize_newlines<const N: usize>(text: &str, terminators: [char; N]) -> Cow<'_, str> {
pub fn normalize_newlines<const N: usize>(text: &str, terminators: [char; N]) -> Cow<str> {
let mut result = String::new();
let mut last_end = 0;

View File

@@ -222,7 +222,7 @@ impl FormatContext for IrFormatContext<'_> {
&IrFormatOptions
}
fn source_code(&self) -> SourceCode<'_> {
fn source_code(&self) -> SourceCode {
self.source_code
}
}

View File

@@ -193,7 +193,7 @@ pub trait FormatContext {
fn options(&self) -> &Self::Options;
/// Returns the source code from the document that gets formatted.
fn source_code(&self) -> SourceCode<'_>;
fn source_code(&self) -> SourceCode;
}
/// Options customizing how the source code should be formatted.
@@ -239,7 +239,7 @@ impl FormatContext for SimpleFormatContext {
&self.options
}
fn source_code(&self) -> SourceCode<'_> {
fn source_code(&self) -> SourceCode {
SourceCode::new(&self.source_code)
}
}
@@ -326,7 +326,7 @@ where
printer.print_with_indent(&self.document, indent)
}
fn create_printer(&self) -> Printer<'_> {
fn create_printer(&self) -> Printer {
let source_code = self.context.source_code();
let print_options = self.context.options().as_print_options();

View File

@@ -69,7 +69,7 @@ impl<'a> Resolver<'a> {
}
/// Resolves a module name to a module.
pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}

View File

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

View File

@@ -89,14 +89,3 @@ print(1)
# ///
#
# Foobar
# Regression tests for https://github.com/astral-sh/ruff/issues/19713
# mypy: ignore-errors
# pyright: ignore-errors
# pyrefly: ignore-errors
# ty: ignore[unresolved-import]
# pyrefly: ignore[unused-import]
print(1)

View File

@@ -162,86 +162,3 @@ except Exception:
exception("An error occurred")
else:
exception("An error occurred")
# Test tuple exceptions
try:
pass
except (Exception,):
pass
try:
pass
except (Exception, ValueError):
pass
try:
pass
except (ValueError, Exception):
pass
try:
pass
except (ValueError, Exception) as e:
print(e)
try:
pass
except (BaseException, TypeError):
pass
try:
pass
except (TypeError, BaseException):
pass
try:
pass
except (Exception, BaseException):
pass
try:
pass
except (BaseException, Exception):
pass
# Test nested tuples
try:
pass
except ((Exception, ValueError), TypeError):
pass
try:
pass
except (ValueError, (BaseException, TypeError)):
pass
# Test valid tuple exceptions (should not trigger)
try:
pass
except (ValueError, TypeError):
pass
try:
pass
except (OSError, FileNotFoundError):
pass
try:
pass
except (OSError, FileNotFoundError) as e:
print(e)
try:
pass
except (Exception, ValueError):
critical("...", exc_info=True)
try:
pass
except (Exception, ValueError):
raise
try:
pass
except (Exception, ValueError) as e:
raise e

View File

@@ -88,25 +88,3 @@ def f_multi_line_string2():
example="example"
)
)
def raise_typing_cast_exception():
import typing
raise typing.cast("Exception", None)
def f_typing_cast_excluded():
from typing import cast
raise cast(RuntimeError, "This should not trigger EM101")
def f_typing_cast_excluded_import():
import typing
raise typing.cast(RuntimeError, "This should not trigger EM101")
def f_typing_cast_excluded_aliased():
from typing import cast as my_cast
raise my_cast(RuntimeError, "This should not trigger EM101")

View File

@@ -39,11 +39,6 @@ class NonEmptyWithInit:
pass
class NonEmptyChildWithInlineComment:
value: int
... # preserve me
class EmptyClass:
...

View File

@@ -38,10 +38,6 @@ class NonEmptyWithInit:
def __init__():
pass
class NonEmptyChildWithInlineComment:
value: int
... # preserve me
# Not violations
class EmptyClass: ...

View File

@@ -129,35 +129,4 @@ print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))
# https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
r"""simple@example.com
very.common@example.com
FirstName.LastName@EasierReading.org
x@example.com
long.email-address-with-hyphens@and.subdomains.example.com
user.name+tag+sorting@example.com
name/surname@example.com
xample@s.example
" "@example.org
"john..doe"@example.org
mailhost!username@example.org
"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
user%example.com@example.org
user-@example.org
I❤CHOCOLATE@example.com
this\ still\"not\\allowed@example.com
stellyamburrr985@example.com
Abc.123@example.com
user+mailbox/department=shipping@example.com
!#$%&'*+-/=?^_`.{|}~@example.com
"Abc@def"@example.com
"Fred\ Bloggs"@example.com
"Joe.\\Blow"@example.com""".split("\n")
r"""first
'no need' to escape
"swap" quote style
"use' ugly triple quotes""".split("\n")
print(" x ".rsplit(sep=None, maxsplit=0))

View File

@@ -1,4 +1,4 @@
from pathlib import Path, PurePath, PosixPath, PurePosixPath, WindowsPath, PureWindowsPath
from pathlib import Path, PurePath
from pathlib import Path as pth
@@ -68,11 +68,3 @@ Path(".", "folder")
PurePath(".", "folder")
Path()
from importlib.metadata import PackagePath
_ = PosixPath(".")
_ = PurePosixPath(".")
_ = WindowsPath(".")
_ = PureWindowsPath(".")
_ = PackagePath(".")

View File

@@ -1,3 +0,0 @@
"""Hello, world!"""\
x = 1; y = 2

View File

@@ -182,13 +182,3 @@ kwargs_with_maxsplit = {"maxsplit": 1}
"1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
"1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
## Test unpacked list literal args (starred expressions)
# Errors
"1,2,3".split(",", *[-1])[0]
## Test unpacked list variable args
# Errors
args_list = [-1]
"1,2,3".split(",", *args_list)[0]

View File

@@ -59,7 +59,3 @@ kwargs = {x: x for x in range(10)}
"{1}_{0}".format(1, 2, *args)
"{1}_{0}".format(1, 2)
r"\d{{1,2}} {0}".format(42)
"{{{0}}}".format(123)

View File

@@ -52,7 +52,3 @@ f"{repr(lambda: 1)}"
f"{repr(x := 2)}"
f"{str(object=3)}"
f"{str(x for x in [])}"
f"{str((x for x in []))}"

View File

@@ -315,7 +315,7 @@ impl<'a> Checker<'a> {
}
/// Create a [`Generator`] to generate source code based on the current AST state.
pub(crate) fn generator(&self) -> Generator<'_> {
pub(crate) fn generator(&self) -> Generator {
Generator::new(self.stylist.indentation(), self.stylist.line_ending())
}
@@ -590,16 +590,6 @@ impl<'a> Checker<'a> {
member,
})
}
/// Return the [`LintContext`] for the current analysis.
///
/// Note that you should always prefer calling methods like `settings`, `report_diagnostic`, or
/// `is_rule_enabled` directly on [`Checker`] when possible. This method exists only for the
/// rare cases where rules or helper functions need to be accessed by both a `Checker` and a
/// `LintContext` in different analysis phases.
pub(crate) const fn context(&self) -> &'a LintContext<'a> {
self.context
}
}
pub(crate) struct TypingImporter<'a, 'b> {

View File

@@ -8,14 +8,14 @@ use libcst_native::{
};
use ruff_python_codegen::Stylist;
pub(crate) fn match_module(module_text: &str) -> Result<Module<'_>> {
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) {
Ok(module) => Ok(module),
Err(_) => bail!("Failed to extract CST from source"),
}
}
pub(crate) fn match_statement(statement_text: &str) -> Result<Statement<'_>> {
pub(crate) fn match_statement(statement_text: &str) -> Result<Statement> {
match libcst_native::parse_statement(statement_text) {
Ok(statement) => Ok(statement),
Err(_) => bail!("Failed to extract statement from source"),
@@ -220,7 +220,7 @@ pub(crate) fn match_if<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a m
///
/// If the expression is not guaranteed to be valid as a standalone expression (e.g., if it may
/// span multiple lines and/or require parentheses), use [`transform_expression`] instead.
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression<'_>> {
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),

View File

@@ -13,7 +13,7 @@ use ruff_text_size::{Ranged, TextSize};
use crate::Locator;
/// Extract doc lines (standalone comments) from a token sequence.
pub(crate) fn doc_lines_from_tokens(tokens: &Tokens) -> DocLines<'_> {
pub(crate) fn doc_lines_from_tokens(tokens: &Tokens) -> DocLines {
DocLines::new(tokens)
}

View File

@@ -32,7 +32,7 @@ impl<'a> Docstring<'a> {
}
/// The contents of the docstring, excluding the opening and closing quotes.
pub(crate) fn body(&self) -> DocstringBody<'_> {
pub(crate) fn body(&self) -> DocstringBody {
DocstringBody { docstring: self }
}

View File

@@ -208,7 +208,7 @@ impl<'a> SectionContexts<'a> {
self.contexts.len()
}
pub(crate) fn iter(&self) -> SectionContextsIter<'_> {
pub(crate) fn iter(&self) -> SectionContextsIter {
SectionContextsIter {
docstring_body: self.docstring.body(),
inner: self.contexts.iter(),

View File

@@ -56,19 +56,13 @@ impl<'a> Insertion<'a> {
stylist: &Stylist,
) -> Insertion<'static> {
// Skip over any docstrings.
let mut location = if let Some(mut location) = match_docstring_end(body) {
let mut location = if let Some(location) = match_docstring_end(body) {
// If the first token after the docstring is a semicolon, insert after the semicolon as
// an inline statement.
if let Some(offset) = match_semicolon(locator.after(location)) {
return Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";");
}
// If the first token after the docstring is a continuation character (i.e. "\"), advance
// an additional row to prevent inserting in the same logical line.
if match_continuation(locator.after(location)).is_some() {
location = locator.full_line_end(location);
}
// Otherwise, advance to the next row.
locator.full_line_end(location)
} else {
@@ -329,7 +323,7 @@ mod tests {
#[test]
fn start_of_file() -> Result<()> {
fn insert(contents: &str) -> Result<Insertion<'_>> {
fn insert(contents: &str) -> Result<Insertion> {
let parsed = parse_module(contents)?;
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents());
@@ -369,16 +363,6 @@ mod tests {
Insertion::own_line("", TextSize::from(20), "\n")
);
let contents = r#"
"""Hello, world!"""\
"#
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(22), "\n")
);
let contents = r"
x = 1
"
@@ -450,7 +434,7 @@ x = 1
#[test]
fn start_of_block() {
fn insert(contents: &str, offset: TextSize) -> Insertion<'_> {
fn insert(contents: &str, offset: TextSize) -> Insertion {
let parsed = parse_module(contents).unwrap();
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents());

View File

@@ -49,7 +49,7 @@ impl<'a> Locator<'a> {
self.index.get()
}
pub fn to_source_code(&self) -> SourceCode<'_, '_> {
pub fn to_source_code(&self) -> SourceCode {
SourceCode::new(self.contents, self.to_index())
}

View File

@@ -33,8 +33,10 @@ impl Emitter for GithubEmitter {
write!(
writer,
"::error title=Ruff ({code}),file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
code = diagnostic.secondary_code_or_id(),
"::error title=Ruff{code},file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
code = diagnostic
.secondary_code()
.map_or_else(String::new, |code| format!(" ({code})")),
file = filename,
row = source_location.line,
column = source_location.column,
@@ -52,8 +54,6 @@ impl Emitter for GithubEmitter {
if let Some(code) = diagnostic.secondary_code() {
write!(writer, " {code}")?;
} else {
write!(writer, " {id}:", id = diagnostic.id())?;
}
writeln!(writer, " {}", diagnostic.body())?;

View File

@@ -61,17 +61,22 @@ impl Serialize for SerializedMessages<'_> {
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
for diagnostic in self.diagnostics {
let filename = diagnostic.expect_ruff_filename();
let start_location = diagnostic.expect_ruff_start_location();
let end_location = diagnostic.expect_ruff_end_location();
let (start_location, end_location) = if self.context.is_notebook(&filename) {
let filename = diagnostic.expect_ruff_filename();
let lines = if self.context.is_notebook(&filename) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
Default::default()
json!({
"begin": 1,
"end": 1
})
} else {
(
diagnostic.expect_ruff_start_location(),
diagnostic.expect_ruff_end_location(),
)
json!({
"begin": start_location.line,
"end": end_location.line
})
};
let path = self.project_dir.as_ref().map_or_else(
@@ -106,11 +111,8 @@ impl Serialize for SerializedMessages<'_> {
"fingerprint": format!("{:x}", message_fingerprint),
"location": {
"path": path,
"positions": {
"begin": start_location,
"end": end_location,
},
},
"lines": lines
}
});
s.serialize_element(&value)?;

View File

@@ -33,7 +33,8 @@ mod text;
/// Creates a `Diagnostic` from a syntax error, with the format expected by Ruff.
///
/// This is almost identical to `ruff_db::diagnostic::create_syntax_error_diagnostic`, except the
/// `message` is stored as the primary diagnostic message instead of on the primary annotation.
/// `message` is stored as the primary diagnostic message instead of on the primary annotation, and
/// `SyntaxError: ` is prepended to the message.
///
/// TODO(brent) These should be unified at some point, but we keep them separate for now to avoid a
/// ton of snapshot changes while combining ruff's diagnostic type with `Diagnostic`.
@@ -42,7 +43,11 @@ pub fn create_syntax_error_diagnostic(
message: impl std::fmt::Display,
range: impl Ranged,
) -> Diagnostic {
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
let mut diag = Diagnostic::new(
DiagnosticId::InvalidSyntax,
Severity::Error,
format_args!("SyntaxError: {message}"),
);
let span = span.into().with_range(range.range());
diag.annotate(Annotation::primary(span));
diag
@@ -70,15 +75,7 @@ where
);
let span = Span::from(file).with_range(range);
let mut annotation = Annotation::primary(span);
// The `0..0` range is used to highlight file-level diagnostics.
//
// TODO(brent) We should instead set this flag on annotations for individual lint rules that
// actually need it, but we need to be able to cache the new diagnostic model first. See
// https://github.com/astral-sh/ruff/issues/19688.
if range == TextRange::default() {
annotation.set_file_level(true);
}
let annotation = Annotation::primary(span);
diagnostic.annotate(annotation);
if let Some(suggestion) = suggestion {
@@ -149,7 +146,7 @@ impl Deref for MessageWithLocation<'_> {
fn group_diagnostics_by_filename(
diagnostics: &[Diagnostic],
) -> BTreeMap<String, Vec<MessageWithLocation<'_>>> {
) -> BTreeMap<String, Vec<MessageWithLocation>> {
let mut grouped_messages = BTreeMap::default();
for diagnostic in diagnostics {
grouped_messages

View File

@@ -27,10 +27,7 @@ impl Emitter for SarifEmitter {
.map(SarifResult::from_message)
.collect::<Result<Vec<_>>>()?;
let unique_rules: HashSet<_> = results
.iter()
.filter_map(|result| result.code.as_secondary_code())
.collect();
let unique_rules: HashSet<_> = results.iter().filter_map(|result| result.code).collect();
let mut rules: Vec<SarifRule> = unique_rules.into_iter().map(SarifRule::from).collect();
rules.sort_by(|a, b| a.code.cmp(b.code));
@@ -112,40 +109,9 @@ impl Serialize for SarifRule<'_> {
}
}
#[derive(Debug)]
enum RuleCode<'a> {
SecondaryCode(&'a SecondaryCode),
LintId(&'a str),
}
impl RuleCode<'_> {
fn as_secondary_code(&self) -> Option<&SecondaryCode> {
match self {
RuleCode::SecondaryCode(code) => Some(code),
RuleCode::LintId(_) => None,
}
}
fn as_str(&self) -> &str {
match self {
RuleCode::SecondaryCode(code) => code.as_str(),
RuleCode::LintId(id) => id,
}
}
}
impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
fn from(code: &'a Diagnostic) -> Self {
match code.secondary_code() {
Some(diagnostic) => Self::SecondaryCode(diagnostic),
None => Self::LintId(code.id().as_str()),
}
}
}
#[derive(Debug)]
struct SarifResult<'a> {
code: RuleCode<'a>,
code: Option<&'a SecondaryCode>,
level: String,
message: String,
uri: String,
@@ -162,7 +128,7 @@ impl<'a> SarifResult<'a> {
let end_location = message.expect_ruff_end_location();
let path = normalize_path(&*message.expect_ruff_filename());
Ok(Self {
code: RuleCode::from(message),
code: message.secondary_code(),
level: "error".to_string(),
message: message.body().to_string(),
uri: url::Url::from_file_path(&path)
@@ -182,7 +148,7 @@ impl<'a> SarifResult<'a> {
let end_location = message.expect_ruff_end_location();
let path = normalize_path(&*message.expect_ruff_filename());
Ok(Self {
code: RuleCode::from(message),
code: message.secondary_code(),
level: "error".to_string(),
message: message.body().to_string(),
uri: path.display().to_string(),
@@ -217,7 +183,7 @@ impl Serialize for SarifResult<'_> {
}
}
}],
"ruleId": self.code.as_str(),
"ruleId": self.code,
})
.serialize(serializer)
}

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_linter/src/message/github.rs
expression: content
snapshot_kind: text
---
::error title=Ruff (invalid-syntax),file=syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
::error title=Ruff (invalid-syntax),file=syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline
::error title=Ruff,file=syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
::error title=Ruff,file=syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: SyntaxError: Expected ')', found newline

View File

@@ -8,17 +8,11 @@ expression: redact_fingerprint(&content)
"description": "`os` imported but unused",
"fingerprint": "<redacted>",
"location": {
"path": "fib.py",
"positions": {
"begin": {
"column": 8,
"line": 1
},
"end": {
"column": 10,
"line": 1
}
}
"lines": {
"begin": 1,
"end": 1
},
"path": "fib.py"
},
"severity": "major"
},
@@ -27,17 +21,11 @@ expression: redact_fingerprint(&content)
"description": "Local variable `x` is assigned to but never used",
"fingerprint": "<redacted>",
"location": {
"path": "fib.py",
"positions": {
"begin": {
"column": 5,
"line": 6
},
"end": {
"column": 6,
"line": 6
}
}
"lines": {
"begin": 6,
"end": 6
},
"path": "fib.py"
},
"severity": "major"
},
@@ -46,17 +34,11 @@ expression: redact_fingerprint(&content)
"description": "Undefined name `a`",
"fingerprint": "<redacted>",
"location": {
"path": "undef.py",
"positions": {
"begin": {
"column": 4,
"line": 1
},
"end": {
"column": 5,
"line": 1
}
}
"lines": {
"begin": 1,
"end": 1
},
"path": "undef.py"
},
"severity": "major"
}

View File

@@ -8,17 +8,11 @@ expression: redact_fingerprint(&content)
"description": "Expected one or more symbol names after import",
"fingerprint": "<redacted>",
"location": {
"path": "syntax_errors.py",
"positions": {
"begin": {
"column": 15,
"line": 1
},
"end": {
"column": 1,
"line": 2
}
}
"lines": {
"begin": 1,
"end": 2
},
"path": "syntax_errors.py"
},
"severity": "major"
},
@@ -27,17 +21,11 @@ expression: redact_fingerprint(&content)
"description": "Expected ')', found newline",
"fingerprint": "<redacted>",
"location": {
"path": "syntax_errors.py",
"positions": {
"begin": {
"column": 12,
"line": 3
},
"end": {
"column": 1,
"line": 4
}
}
"lines": {
"begin": 3,
"end": 4
},
"path": "syntax_errors.py"
},
"severity": "major"
}

View File

@@ -1,7 +1,8 @@
---
source: crates/ruff_linter/src/message/grouped.rs
expression: content
snapshot_kind: text
---
syntax_errors.py:
1:15 invalid-syntax: Expected one or more symbol names after import
3:12 invalid-syntax: Expected ')', found newline
1:15 SyntaxError: Expected one or more symbol names after import
3:12 SyntaxError: Expected ')', found newline

View File

@@ -81,7 +81,7 @@ expression: value
"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/spec/distributing.html#library-interface-public-and-private-symbols)\n"
"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"

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/message/text.rs
expression: content
---
syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
|
1 | from os import
| ^
@@ -11,7 +11,7 @@ syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after i
4 | def bar():
|
syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline
syntax_errors.py:3:12: SyntaxError: Expected ')', found newline
|
1 | from os import
2 |

View File

@@ -13,6 +13,7 @@ use ruff_notebook::NotebookIndex;
use ruff_source_file::OneIndexed;
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::line_width::{IndentWidth, LineWidthBuilder};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext};
use crate::settings::types::UnsafeFixes;
@@ -154,12 +155,7 @@ impl Display for RuleCodeAndBody<'_> {
body = self.message.body(),
)
} else {
write!(
f,
"{code}: {body}",
code = self.message.id().as_str().red().bold(),
body = self.message.body(),
)
f.write_str(self.message.body())
}
}
}
@@ -233,7 +229,7 @@ impl Display for MessageCodeFrame<'_> {
let start_offset = source_code.line_start(start_index);
let end_offset = source_code.line_end(end_index);
let source = replace_unprintable(
let source = replace_whitespace_and_unprintable(
source_code.slice(TextRange::new(start_offset, end_offset)),
self.message.expect_range() - start_offset,
)
@@ -276,20 +272,16 @@ impl Display for MessageCodeFrame<'_> {
}
/// Given some source code and an annotation range, this routine replaces
/// unprintable characters with printable representations of them.
/// tabs with ASCII whitespace, and unprintable characters with printable
/// representations of them.
///
/// The source code returned has an annotation that is updated to reflect
/// changes made to the source code (if any).
///
/// We don't need to normalize whitespace, such as converting tabs to spaces,
/// because `annotate-snippets` handles that internally. Similarly, it's safe to
/// modify the annotation ranges by inserting 3-byte Unicode replacements
/// because `annotate-snippets` will account for their actual width when
/// rendering and displaying the column to the user.
fn replace_unprintable(source: &str, annotation_range: TextRange) -> SourceCode<'_> {
fn replace_whitespace_and_unprintable(source: &str, annotation_range: TextRange) -> SourceCode {
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut line_width = LineWidthBuilder::new(IndentWidth::default());
// Updates the range given by the caller whenever a single byte (at
// `index` in `source`) is replaced with `len` bytes.
@@ -318,7 +310,19 @@ fn replace_unprintable(source: &str, annotation_range: TextRange) -> SourceCode<
};
for (index, c) in source.char_indices() {
if let Some(printable) = unprintable_replacement(c) {
let old_width = line_width.get();
line_width = line_width.add_char(c);
if matches!(c, '\t') {
let tab_width = u32::try_from(line_width.get() - old_width)
.expect("small width because of tab size");
result.push_str(&source[last_end..index]);
for _ in 0..tab_width {
result.push(' ');
}
last_end = index + 1;
update_range(index, tab_width);
} else if let Some(printable) = unprintable_replacement(c) {
result.push_str(&source[last_end..index]);
result.push(printable);
last_end = index + 1;

View File

@@ -99,7 +99,7 @@ pub(crate) struct Codes<'a> {
impl Codes<'_> {
/// Returns an iterator over the [`Code`]s in the `noqa` directive.
pub(crate) fn iter(&self) -> std::slice::Iter<'_, Code<'_>> {
pub(crate) fn iter(&self) -> std::slice::Iter<Code> {
self.codes.iter()
}
@@ -306,7 +306,7 @@ impl<'a> FileNoqaDirectives<'a> {
Self(lines)
}
pub(crate) fn lines(&self) -> &[FileNoqaDirectiveLine<'_>] {
pub(crate) fn lines(&self) -> &[FileNoqaDirectiveLine] {
&self.0
}
@@ -1106,10 +1106,7 @@ impl<'a> NoqaDirectives<'a> {
Self { inner: directives }
}
pub(crate) fn find_line_with_directive(
&self,
offset: TextSize,
) -> Option<&NoqaDirectiveLine<'_>> {
pub(crate) fn find_line_with_directive(&self, offset: TextSize) -> Option<&NoqaDirectiveLine> {
self.find_line_index(offset).map(|index| &self.inner[index])
}
@@ -1142,7 +1139,7 @@ impl<'a> NoqaDirectives<'a> {
.ok()
}
pub(crate) fn lines(&self) -> &[NoqaDirectiveLine<'_>] {
pub(crate) fn lines(&self) -> &[NoqaDirectiveLine] {
&self.inner
}

View File

@@ -21,7 +21,6 @@ static ALLOWLIST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
(?:
# Case-sensitive
pyright
| pyrefly
| mypy:
| type:\s*ignore
| SPDX-License-Identifier:

View File

@@ -75,22 +75,6 @@ impl Violation for BlindExcept {
}
}
fn contains_blind_exception<'a>(
semantic: &'a SemanticModel,
expr: &'a Expr,
) -> Option<(&'a str, ruff_text_size::TextRange)> {
match expr {
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
.iter()
.find_map(|elt| contains_blind_exception(semantic, elt)),
_ => {
let builtin_exception_type = semantic.resolve_builtin_symbol(expr)?;
matches!(builtin_exception_type, "BaseException" | "Exception")
.then(|| (builtin_exception_type, expr.range()))
}
}
}
/// BLE001
pub(crate) fn blind_except(
checker: &Checker,
@@ -103,9 +87,12 @@ pub(crate) fn blind_except(
};
let semantic = checker.semantic();
let Some((builtin_exception_type, range)) = contains_blind_exception(semantic, type_) else {
let Some(builtin_exception_type) = semantic.resolve_builtin_symbol(type_) else {
return;
};
if !matches!(builtin_exception_type, "BaseException" | "Exception") {
return;
}
// If the exception is re-raised, don't flag an error.
let mut visitor = ReraiseVisitor::new(name);
@@ -123,9 +110,9 @@ pub(crate) fn blind_except(
checker.report_diagnostic(
BlindExcept {
name: builtin_exception_type.into(),
name: builtin_exception_type.to_string(),
},
range,
type_.range(),
);
}

View File

@@ -147,93 +147,3 @@ BLE.py:131:8: BLE001 Do not catch blind exception: `Exception`
| ^^^^^^^^^ BLE001
132 | critical("...", exc_info=None)
|
BLE.py:169:9: BLE001 Do not catch blind exception: `Exception`
|
167 | try:
168 | pass
169 | except (Exception,):
| ^^^^^^^^^ BLE001
170 | pass
|
BLE.py:174:9: BLE001 Do not catch blind exception: `Exception`
|
172 | try:
173 | pass
174 | except (Exception, ValueError):
| ^^^^^^^^^ BLE001
175 | pass
|
BLE.py:179:21: BLE001 Do not catch blind exception: `Exception`
|
177 | try:
178 | pass
179 | except (ValueError, Exception):
| ^^^^^^^^^ BLE001
180 | pass
|
BLE.py:184:21: BLE001 Do not catch blind exception: `Exception`
|
182 | try:
183 | pass
184 | except (ValueError, Exception) as e:
| ^^^^^^^^^ BLE001
185 | print(e)
|
BLE.py:189:9: BLE001 Do not catch blind exception: `BaseException`
|
187 | try:
188 | pass
189 | except (BaseException, TypeError):
| ^^^^^^^^^^^^^ BLE001
190 | pass
|
BLE.py:194:20: BLE001 Do not catch blind exception: `BaseException`
|
192 | try:
193 | pass
194 | except (TypeError, BaseException):
| ^^^^^^^^^^^^^ BLE001
195 | pass
|
BLE.py:199:9: BLE001 Do not catch blind exception: `Exception`
|
197 | try:
198 | pass
199 | except (Exception, BaseException):
| ^^^^^^^^^ BLE001
200 | pass
|
BLE.py:204:9: BLE001 Do not catch blind exception: `BaseException`
|
202 | try:
203 | pass
204 | except (BaseException, Exception):
| ^^^^^^^^^^^^^ BLE001
205 | pass
|
BLE.py:210:10: BLE001 Do not catch blind exception: `Exception`
|
208 | try:
209 | pass
210 | except ((Exception, ValueError), TypeError):
| ^^^^^^^^^ BLE001
211 | pass
|
BLE.py:215:22: BLE001 Do not catch blind exception: `BaseException`
|
213 | try:
214 | pass
215 | except (ValueError, (BaseException, TypeError)):
| ^^^^^^^^^^^^^ BLE001
216 | pass
|

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
---
COM81_syntax_error.py:3:5: invalid-syntax: Starred expression cannot be used here
COM81_syntax_error.py:3:5: SyntaxError: Starred expression cannot be used here
|
1 | # Check for `flake8-commas` violation for a file containing syntax errors.
2 | (
@@ -10,7 +10,7 @@ COM81_syntax_error.py:3:5: invalid-syntax: Starred expression cannot be used her
4 | )
|
COM81_syntax_error.py:6:9: invalid-syntax: Type parameter list cannot be empty
COM81_syntax_error.py:6:9: SyntaxError: Type parameter list cannot be empty
|
4 | )
5 |

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
---
COM81_syntax_error.py:3:5: invalid-syntax: Starred expression cannot be used here
COM81_syntax_error.py:3:5: SyntaxError: Starred expression cannot be used here
|
1 | # Check for `flake8-commas` violation for a file containing syntax errors.
2 | (
@@ -10,7 +10,7 @@ COM81_syntax_error.py:3:5: invalid-syntax: Starred expression cannot be used her
4 | )
|
COM81_syntax_error.py:6:9: invalid-syntax: Type parameter list cannot be empty
COM81_syntax_error.py:6:9: SyntaxError: Type parameter list cannot be empty
|
4 | )
5 |

View File

@@ -182,82 +182,19 @@ impl Violation for DotFormatInException {
/// EM101, EM102, EM103
pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
let Expr::Call(ast::ExprCall {
func,
if let Expr::Call(ast::ExprCall {
arguments: Arguments { args, .. },
..
}) = exc
else {
return;
};
if checker.semantic().match_typing_expr(func, "cast") {
return;
}
if let Some(first) = args.first() {
match first {
// Check for string literals.
Expr::StringLiteral(ast::ExprStringLiteral { value: string, .. }) => {
if checker.is_rule_enabled(Rule::RawStringInException) {
if string.len() >= checker.settings().flake8_errmsg.max_string_length {
let mut diagnostic =
checker.report_diagnostic(RawStringInException, first.range());
if let Some(indentation) = whitespace::indentation(checker.source(), stmt) {
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
}
}
}
// Check for byte string literals.
Expr::BytesLiteral(ast::ExprBytesLiteral { value: bytes, .. }) => {
if checker.settings().rules.enabled(Rule::RawStringInException) {
if bytes.len() >= checker.settings().flake8_errmsg.max_string_length
&& is_raise_exception_byte_string_enabled(checker.settings())
{
let mut diagnostic =
checker.report_diagnostic(RawStringInException, first.range());
if let Some(indentation) = whitespace::indentation(checker.source(), stmt) {
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
}
}
}
// Check for f-strings.
Expr::FString(_) => {
if checker.is_rule_enabled(Rule::FStringInException) {
let mut diagnostic =
checker.report_diagnostic(FStringInException, first.range());
if let Some(indentation) = whitespace::indentation(checker.source(), stmt) {
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
}
}
// Check for .format() calls.
Expr::Call(ast::ExprCall { func, .. }) => {
if checker.is_rule_enabled(Rule::DotFormatInException) {
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
if attr == "format" && value.is_literal_expr() {
{
if let Some(first) = args.first() {
match first {
// Check for string literals.
Expr::StringLiteral(ast::ExprStringLiteral { value: string, .. }) => {
if checker.is_rule_enabled(Rule::RawStringInException) {
if string.len() >= checker.settings().flake8_errmsg.max_string_length {
let mut diagnostic =
checker.report_diagnostic(DotFormatInException, first.range());
checker.report_diagnostic(RawStringInException, first.range());
if let Some(indentation) =
whitespace::indentation(checker.source(), stmt)
{
@@ -272,8 +209,70 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
}
}
}
// Check for byte string literals.
Expr::BytesLiteral(ast::ExprBytesLiteral { value: bytes, .. }) => {
if checker.settings().rules.enabled(Rule::RawStringInException) {
if bytes.len() >= checker.settings().flake8_errmsg.max_string_length
&& is_raise_exception_byte_string_enabled(checker.settings())
{
let mut diagnostic =
checker.report_diagnostic(RawStringInException, first.range());
if let Some(indentation) =
whitespace::indentation(checker.source(), stmt)
{
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
}
}
}
// Check for f-strings.
Expr::FString(_) => {
if checker.is_rule_enabled(Rule::FStringInException) {
let mut diagnostic =
checker.report_diagnostic(FStringInException, first.range());
if let Some(indentation) = whitespace::indentation(checker.source(), stmt) {
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
}
}
// Check for .format() calls.
Expr::Call(ast::ExprCall { func, .. }) => {
if checker.is_rule_enabled(Rule::DotFormatInException) {
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) =
func.as_ref()
{
if attr == "format" && value.is_literal_expr() {
let mut diagnostic =
checker.report_diagnostic(DotFormatInException, first.range());
if let Some(indentation) =
whitespace::indentation(checker.source(), stmt)
{
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
}
}
}
}
_ => {}
}
_ => {}
}
}
}

View File

@@ -278,6 +278,3 @@ EM.py:84:9: EM103 [*] Exception must not use a `.format()` string directly, assi
91 |+ raise RuntimeError(
92 |+ msg
93 |+ )
91 94 |
92 95 |
93 96 | def raise_typing_cast_exception():

View File

@@ -343,6 +343,3 @@ EM.py:84:9: EM103 [*] Exception must not use a `.format()` string directly, assi
91 |+ raise RuntimeError(
92 |+ msg
93 |+ )
91 94 |
92 95 |
93 96 | def raise_typing_cast_exception():

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
---
ISC_syntax_error.py:2:5: invalid-syntax: missing closing quote in string literal
ISC_syntax_error.py:2:5: SyntaxError: missing closing quote in string literal
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
@@ -10,7 +10,7 @@ ISC_syntax_error.py:2:5: invalid-syntax: missing closing quote in string literal
4 | "a" """b
|
ISC_syntax_error.py:2:7: invalid-syntax: Expected a statement
ISC_syntax_error.py:2:7: SyntaxError: Expected a statement
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
@@ -31,7 +31,7 @@ ISC_syntax_error.py:3:1: ISC001 Implicitly concatenated string literals on one l
|
= help: Combine string literals
ISC_syntax_error.py:3:9: invalid-syntax: missing closing quote in string literal
ISC_syntax_error.py:3:9: SyntaxError: missing closing quote in string literal
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
@@ -41,7 +41,7 @@ ISC_syntax_error.py:3:9: invalid-syntax: missing closing quote in string literal
5 | c""" "d
|
ISC_syntax_error.py:3:11: invalid-syntax: Expected a statement
ISC_syntax_error.py:3:11: SyntaxError: Expected a statement
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
@@ -63,7 +63,7 @@ ISC_syntax_error.py:4:1: ISC001 Implicitly concatenated string literals on one l
|
= help: Combine string literals
ISC_syntax_error.py:5:6: invalid-syntax: missing closing quote in string literal
ISC_syntax_error.py:5:6: SyntaxError: missing closing quote in string literal
|
3 | "a" "b" "c
4 | "a" """b
@@ -73,7 +73,7 @@ ISC_syntax_error.py:5:6: invalid-syntax: missing closing quote in string literal
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
ISC_syntax_error.py:5:8: invalid-syntax: Expected a statement
ISC_syntax_error.py:5:8: SyntaxError: Expected a statement
|
3 | "a" "b" "c
4 | "a" """b
@@ -84,7 +84,7 @@ ISC_syntax_error.py:5:8: invalid-syntax: Expected a statement
8 | # unterminated f-strings.
|
ISC_syntax_error.py:9:8: invalid-syntax: f-string: unterminated string
ISC_syntax_error.py:9:8: SyntaxError: f-string: unterminated string
|
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
@@ -94,7 +94,7 @@ ISC_syntax_error.py:9:8: invalid-syntax: f-string: unterminated string
11 | f"a" f"""b
|
ISC_syntax_error.py:9:9: invalid-syntax: Expected FStringEnd, found newline
ISC_syntax_error.py:9:9: SyntaxError: Expected FStringEnd, found newline
|
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
@@ -116,7 +116,7 @@ ISC_syntax_error.py:10:1: ISC001 Implicitly concatenated string literals on one
|
= help: Combine string literals
ISC_syntax_error.py:10:13: invalid-syntax: f-string: unterminated string
ISC_syntax_error.py:10:13: SyntaxError: f-string: unterminated string
|
8 | # unterminated f-strings.
9 | f"a" f"b
@@ -126,7 +126,7 @@ ISC_syntax_error.py:10:13: invalid-syntax: f-string: unterminated string
12 | c""" f"d {e
|
ISC_syntax_error.py:10:14: invalid-syntax: Expected FStringEnd, found newline
ISC_syntax_error.py:10:14: SyntaxError: Expected FStringEnd, found newline
|
8 | # unterminated f-strings.
9 | f"a" f"b
@@ -148,7 +148,7 @@ ISC_syntax_error.py:11:1: ISC001 Implicitly concatenated string literals on one
|
= help: Combine string literals
ISC_syntax_error.py:16:5: invalid-syntax: missing closing quote in string literal
ISC_syntax_error.py:16:5: SyntaxError: missing closing quote in string literal
|
14 | (
15 | "a"
@@ -158,7 +158,7 @@ ISC_syntax_error.py:16:5: invalid-syntax: missing closing quote in string litera
18 | "d"
|
ISC_syntax_error.py:26:9: invalid-syntax: f-string: unterminated triple-quoted string
ISC_syntax_error.py:26:9: SyntaxError: f-string: unterminated triple-quoted string
|
24 | (
25 | """abc"""
@@ -170,14 +170,14 @@ ISC_syntax_error.py:26:9: invalid-syntax: f-string: unterminated triple-quoted s
| |__^
|
ISC_syntax_error.py:30:1: invalid-syntax: unexpected EOF while parsing
ISC_syntax_error.py:30:1: SyntaxError: unexpected EOF while parsing
|
28 | "i" "j"
29 | )
| ^
|
ISC_syntax_error.py:30:1: invalid-syntax: f-string: unterminated string
ISC_syntax_error.py:30:1: SyntaxError: f-string: unterminated string
|
28 | "i" "j"
29 | )

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
---
ISC_syntax_error.py:2:5: invalid-syntax: missing closing quote in string literal
ISC_syntax_error.py:2:5: SyntaxError: missing closing quote in string literal
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
@@ -10,7 +10,7 @@ ISC_syntax_error.py:2:5: invalid-syntax: missing closing quote in string literal
4 | "a" """b
|
ISC_syntax_error.py:2:7: invalid-syntax: Expected a statement
ISC_syntax_error.py:2:7: SyntaxError: Expected a statement
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
@@ -20,7 +20,7 @@ ISC_syntax_error.py:2:7: invalid-syntax: Expected a statement
5 | c""" "d
|
ISC_syntax_error.py:3:9: invalid-syntax: missing closing quote in string literal
ISC_syntax_error.py:3:9: SyntaxError: missing closing quote in string literal
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
@@ -30,7 +30,7 @@ ISC_syntax_error.py:3:9: invalid-syntax: missing closing quote in string literal
5 | c""" "d
|
ISC_syntax_error.py:3:11: invalid-syntax: Expected a statement
ISC_syntax_error.py:3:11: SyntaxError: Expected a statement
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
@@ -40,7 +40,7 @@ ISC_syntax_error.py:3:11: invalid-syntax: Expected a statement
5 | c""" "d
|
ISC_syntax_error.py:5:6: invalid-syntax: missing closing quote in string literal
ISC_syntax_error.py:5:6: SyntaxError: missing closing quote in string literal
|
3 | "a" "b" "c
4 | "a" """b
@@ -50,7 +50,7 @@ ISC_syntax_error.py:5:6: invalid-syntax: missing closing quote in string literal
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
ISC_syntax_error.py:5:8: invalid-syntax: Expected a statement
ISC_syntax_error.py:5:8: SyntaxError: Expected a statement
|
3 | "a" "b" "c
4 | "a" """b
@@ -61,7 +61,7 @@ ISC_syntax_error.py:5:8: invalid-syntax: Expected a statement
8 | # unterminated f-strings.
|
ISC_syntax_error.py:9:8: invalid-syntax: f-string: unterminated string
ISC_syntax_error.py:9:8: SyntaxError: f-string: unterminated string
|
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
@@ -71,7 +71,7 @@ ISC_syntax_error.py:9:8: invalid-syntax: f-string: unterminated string
11 | f"a" f"""b
|
ISC_syntax_error.py:9:9: invalid-syntax: Expected FStringEnd, found newline
ISC_syntax_error.py:9:9: SyntaxError: Expected FStringEnd, found newline
|
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
@@ -82,7 +82,7 @@ ISC_syntax_error.py:9:9: invalid-syntax: Expected FStringEnd, found newline
12 | c""" f"d {e
|
ISC_syntax_error.py:10:13: invalid-syntax: f-string: unterminated string
ISC_syntax_error.py:10:13: SyntaxError: f-string: unterminated string
|
8 | # unterminated f-strings.
9 | f"a" f"b
@@ -92,7 +92,7 @@ ISC_syntax_error.py:10:13: invalid-syntax: f-string: unterminated string
12 | c""" f"d {e
|
ISC_syntax_error.py:10:14: invalid-syntax: Expected FStringEnd, found newline
ISC_syntax_error.py:10:14: SyntaxError: Expected FStringEnd, found newline
|
8 | # unterminated f-strings.
9 | f"a" f"b
@@ -102,7 +102,7 @@ ISC_syntax_error.py:10:14: invalid-syntax: Expected FStringEnd, found newline
12 | c""" f"d {e
|
ISC_syntax_error.py:16:5: invalid-syntax: missing closing quote in string literal
ISC_syntax_error.py:16:5: SyntaxError: missing closing quote in string literal
|
14 | (
15 | "a"
@@ -112,7 +112,7 @@ ISC_syntax_error.py:16:5: invalid-syntax: missing closing quote in string litera
18 | "d"
|
ISC_syntax_error.py:26:9: invalid-syntax: f-string: unterminated triple-quoted string
ISC_syntax_error.py:26:9: SyntaxError: f-string: unterminated triple-quoted string
|
24 | (
25 | """abc"""
@@ -124,14 +124,14 @@ ISC_syntax_error.py:26:9: invalid-syntax: f-string: unterminated triple-quoted s
| |__^
|
ISC_syntax_error.py:30:1: invalid-syntax: unexpected EOF while parsing
ISC_syntax_error.py:30:1: SyntaxError: unexpected EOF while parsing
|
28 | "i" "j"
29 | )
| ^
|
ISC_syntax_error.py:30:1: invalid-syntax: f-string: unterminated string
ISC_syntax_error.py:30:1: SyntaxError: f-string: unterminated string
|
28 | "i" "j"
29 | )

View File

@@ -1,11 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::whitespace::trailing_comment_start_offset;
use ruff_python_ast::{Stmt, StmtExpr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix;
use crate::{Edit, Fix, FixAvailability, Violation};
use crate::{Fix, FixAvailability, Violation};
/// ## What it does
/// Removes ellipses (`...`) in otherwise non-empty class bodies.
@@ -51,21 +50,15 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &Checker, body: &[Stmt])
}
for stmt in body {
let Stmt::Expr(StmtExpr { value, .. }) = stmt else {
let Stmt::Expr(StmtExpr { value, .. }) = &stmt else {
continue;
};
if value.is_ellipsis_literal_expr() {
let mut diagnostic =
checker.report_diagnostic(EllipsisInNonEmptyClassBody, stmt.range());
// Try to preserve trailing comment if it exists
let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.source()) {
Edit::range_deletion(stmt.range().add_end(index))
} else {
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer())
};
let edit =
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation(
checker.semantic().current_statement_id(),
)));

View File

@@ -145,22 +145,3 @@ PYI013.py:36:5: PYI013 [*] Non-empty class body must not contain `...`
37 36 |
38 37 | def __init__():
39 38 | pass
PYI013.py:44:5: PYI013 [*] Non-empty class body must not contain `...`
|
42 | class NonEmptyChildWithInlineComment:
43 | value: int
44 | ... # preserve me
| ^^^ PYI013
|
= help: Remove unnecessary `...`
Safe fix
41 41 |
42 42 | class NonEmptyChildWithInlineComment:
43 43 | value: int
44 |- ... # preserve me
44 |+ # preserve me
45 45 |
46 46 |
47 47 | class EmptyClass:

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