Compare commits

..

40 Commits

Author SHA1 Message Date
Douglas Creager
eb95e49f96 clean up some claude-isms 2025-10-09 10:06:42 -04:00
Douglas Creager
0817a367e1 spelling 2025-10-09 09:57:53 -04:00
Douglas Creager
34b463d0a1 reword some docs 2025-10-09 09:55:51 -04:00
Douglas Creager
b438631de2 clean up the diff 2025-10-09 08:22:07 -04:00
Douglas Creager
fbc211f344 update tests 2025-10-09 08:20:12 -04:00
Douglas Creager
1810b7c7a3 precommit 2025-10-09 08:09:50 -04:00
Douglas Creager
39353da108 remove finished plan 2025-10-09 08:09:07 -04:00
Douglas Creager
7b88440fc2 update tests 2025-10-09 08:08:57 -04:00
Douglas Creager
8426cc6915 use ty_ide 2025-10-08 16:09:42 -04:00
Douglas Creager
c08a2d6d65 [ty] Fix hover to prefer expression nodes over identifiers
When hovering on an attribute name like 'value' in 'instance.value',
the minimal covering node is the Identifier, but we need the Attribute
expression to get the type. Update find_covering_node to track both
the minimal node and minimal expression, preferring the expression.

This fixes hover on attribute accesses. All hover tests now pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 15:54:27 -04:00
Douglas Creager
6ddf729864 clean up mdtest 2025-10-08 15:51:12 -04:00
Douglas Creager
eaba8bc61e [ty] Add comprehensive hover.md mdtest (partial)
Add hover.md with examples of hover assertions across different
expression types. Some tests still need arrow alignment fixes, but
many sections are passing including basic literals, function definitions,
comprehensions, and the simple hover test file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 15:25:10 -04:00
Douglas Creager
b18d213869 [ty] Update PLAN.md with testing progress 2025-10-08 15:08:59 -04:00
Douglas Creager
847e5f0c68 [ty] Handle expression statements in hover type inference
When hovering, if we find a statement node (like StmtExpr), extract the
expression from within it. This allows hover assertions to work on
standalone expressions like variable references.

Also add a simple working mdtest to demonstrate hover assertions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 15:08:23 -04:00
Douglas Creager
41772466d5 [ty_test] Store CheckOutput references in SortedCheckOutputs
Update SortedCheckOutputs to store references to CheckOutput instead
of owned values, matching the design of the previous SortedDiagnostics
implementation. This avoids unnecessary cloning and makes the API more
consistent.

Changes:
- SortedCheckOutputs now stores Vec<&CheckOutput>
- new() takes IntoIterator<Item = &CheckOutput>
- LineCheckOutputs.outputs is now &[&CheckOutput]
- Implement Unmatched and UnmatchedWithColumn for &CheckOutput
- Update match_line to take &[&CheckOutput]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:57:33 -04:00
Douglas Creager
180d9de472 [ty_test] Fix clippy warnings in hover module
- Elide unnecessary explicit lifetimes in find_covering_node
- Add backticks around CheckOutputs in doc comment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:48:55 -04:00
Douglas Creager
adf58b6c19 [ty_test] Change match_line to take slice of CheckOutput
Update match_line signature to accept &[CheckOutput] instead of
&LineCheckOutputs for consistency with the previous API that took
&[Diagnostic].

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:47:45 -04:00
Douglas Creager
b683da8cde [ty_test] Store hover column as OneIndexed
Store the hover assertion column as OneIndexed since that's the type
returned by line_column() and required by SourceLocation. This
eliminates unnecessary conversions between zero-based and one-based
indexing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:41:34 -04:00
Douglas Creager
6d1c549c4e [ty_test] Simplify column calculation using line_column
Instead of manually calculating character offsets, use line_column()
which does all the work for us:

1. Find the byte offset of the arrow in the comment text
2. Add that to the comment's TextRange to get the arrow's absolute position
3. Call line_column() on that position to get the character column

This is much simpler and lets line_column handle all the UTF-8/UTF-32
conversion complexity.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:36:24 -04:00
Douglas Creager
c574dff6b0 [ty_test] Fix column units: use character offset not byte offset
The column field was being treated inconsistently - calculated as a
character offset but used as a byte offset. This would break on any
line with multi-byte UTF-8 characters before the hover position.

Fixes:
1. Use chars().position() instead of find() to get character offset
2. Use LineIndex::offset() with PositionEncoding::Utf32 to properly
   convert character offset to byte offset (TextSize)
3. Document that column is a UTF-32 character offset

This ensures hover assertions work correctly with Unicode text.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:33:29 -04:00
Douglas Creager
8c12fcb927 [ty_test] Use named fields for UnparsedAssertion::Hover
Convert the Hover variant from tuple-style to named fields for better
clarity and self-documentation.

Before: Hover(&'a str, &'a str, TextRange)
After: Hover { expected_type, full_comment, range }

This makes the code more readable and easier to maintain.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:26:28 -04:00
Douglas Creager
a2eaf7ce26 [ty_test] Calculate hover column at parse time
Move the column calculation from generate_hover_outputs into
HoverAssertion::from_str() by passing LineIndex and SourceText to
the parse() method.

This simplifies the code and centralizes the column calculation logic
in one place during parsing, rather than spreading it across multiple
locations. The HoverAssertion now directly stores the final column
position in the line, ready to use.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:23:07 -04:00
Douglas Creager
c3626a6d74 [ty_test] Fix hover column calculation to use line position
The previous implementation calculated the down arrow position within
the comment text, but then incorrectly used that as an offset into the
target line. This only worked if the comment started at column 0.

Now we properly calculate the column position by:
1. Finding the arrow offset within the comment text
2. Getting the comment's column position in its line
3. Adding these together to get the arrow's column in the line
4. Using that column to index into the target line

This fixes hover assertions when comments are indented or don't start
at the beginning of the line.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:21:03 -04:00
Douglas Creager
8221450cbc [ty_test] Store HoverAssertion.column as zero-based
Change the column field in HoverAssertion from OneIndexed to usize,
storing it as a zero-based index. This matches how it's used and
eliminates unnecessary conversions between zero-based and one-based
indexing.

The arrow position from find() is already zero-based, and TextSize
uses zero-based offsets, so this is more natural.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:18:03 -04:00
Douglas Creager
93db8833ef [ty_test] Add use statements to hover.rs
Import InlineFileAssertions, ParsedAssertion, and UnparsedAssertion
at the module level instead of using fully qualified crate::assertion
names throughout the code.

This makes the code cleaner and more idiomatic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:16:16 -04:00
Douglas Creager
51d5bc709d [ty_test] Calculate hover column at parse time
Move the column calculation from generate_hover_outputs to the
HoverAssertion parsing logic. This makes better use of the existing
column field in HoverAssertion.

Changes:
- UnparsedAssertion::Hover now stores both the expected type and the
  full comment text
- HoverAssertion::from_str() now takes both parameters and calculates
  the column from the down arrow position in the full comment
- generate_hover_outputs() now reads the column from the parsed
  assertion instead of recalculating it

This eliminates redundant calculations and makes the column field
actually useful.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:09:00 -04:00
Douglas Creager
0198857224 [ty_test] Simplify unmatched output handling
Implement UnmatchedWithColumn for CheckOutput and update the column()
method to work with CheckOutput instead of just Diagnostic.

This allows eliminating the match statement when formatting unmatched
outputs - we can now just call unmatched_with_column() on all outputs
uniformly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 13:57:58 -04:00
Douglas Creager
35b568dd6b [ty_test] Move HoverOutput to hover module
Move the HoverOutput type from check_output.rs to hover.rs where it
logically belongs. The check_output module should only contain the
CheckOutput enum and sorting infrastructure, while hover-specific
types belong in the hover module.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 13:54:59 -04:00
Douglas Creager
35a5fd767d [ty_test] Extract HoverOutput type from CheckOutput enum
Create a dedicated HoverOutput struct to hold hover result data,
replacing the inline fields in CheckOutput::Hover variant.

This allows implementing Unmatched and UnmatchedWithColumn traits
directly on HoverOutput, simplifying the CheckOutput implementations
to simple delegation.

Benefits:
- Better separation of concerns
- Cleaner trait implementations
- More consistent with Diagnostic handling
- Easier to extend HoverOutput in the future

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 13:52:58 -04:00
Douglas Creager
ac1b68c56d [ty_test] Refactor: rename diagnostic.rs to check_output.rs
SortedDiagnostics is now replaced by SortedCheckOutputs, which handles
both diagnostics and hover results. This refactoring:

- Renames diagnostic.rs to check_output.rs to better reflect its purpose
- Moves CheckOutput and SortedCheckOutputs definitions from matcher.rs
  to check_output.rs where they belong
- Removes the now-unused SortedDiagnostics infrastructure
- Ports the test to use SortedCheckOutputs instead of SortedDiagnostics
- Updates all imports throughout the codebase

The check_output module now serves as the central location for sorting
and grouping all types of check outputs (diagnostics and hover results)
by line number.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 13:20:18 -04:00
Douglas Creager
ab261360e4 [ty_test] Use let-else pattern in generate_hover_outputs
Replace nested if-let blocks with let-else + continue to reduce
indentation and improve readability.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 13:09:29 -04:00
Douglas Creager
19600ecd51 [ty_test] Fix hover assertion line number calculation
The previous implementation incorrectly assumed the target line was
always line_number + 1, which breaks when multiple assertion comments
are stacked on consecutive lines.

Now generate_hover_outputs accepts the parsed InlineFileAssertions,
which already correctly associates each assertion with its target line
number (accounting for stacked comments).

For the column position, we extract it directly from the down arrow
position in the UnparsedAssertion::Hover text, avoiding the need to
parse the assertion ourselves (parsing happens later in the matcher).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 13:05:07 -04:00
Douglas Creager
319f5be78c [ty_test] Simplify find_covering_node by comparing range lengths
Replace the leave_node() approach with a simpler strategy: just compare
the range lengths and keep the smallest node that covers the offset.

This is more direct and easier to understand than tracking when we
leave nodes. The minimal covering node is simply the one with the
shortest range that contains the offset.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 12:54:42 -04:00
Douglas Creager
6370aea644 [ty_test] Fix find_covering_node to correctly find minimal node
The previous implementation had a bug: it would overwrite `found` for
every matching node in source order, which could incorrectly select a
sibling node instead of the minimal covering node.

Now use the same approach as ty_ide's covering_node:
- Use leave_node() to detect when we've finished traversing a subtree
- Set a `found` flag when leaving the minimal node to prevent further
  updates
- This ensures we return the deepest (most specific) node that covers
  the offset, not just the last one visited in source order

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 12:53:06 -04:00
Douglas Creager
9d8e35b165 [ty_test] Simplify infer_type_at_position using as_expr_ref()
Replace the large match statement over all AnyNodeRef expression variants
with a simple call to as_expr_ref(). This helper method handles all
expression-related variants automatically, reducing the function from
~60 lines to just 6 lines.

Also removes statement-related variants (StmtFunctionDef, StmtClassDef,
StmtExpr) to focus only on expression nodes, which is the primary use
case for hover assertions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 12:50:28 -04:00
Douglas Creager
5c75e91abe [ty_test] Refactor hover logic into separate module
Move hover-related functions from lib.rs into a new hover.rs module
to improve code organization:
- find_covering_node() - locate AST nodes at specific offsets
- infer_type_at_position() - get inferred types using SemanticModel
- generate_hover_outputs() - scan for hover assertions and generate results

This keeps lib.rs focused on the main test execution flow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 12:47:15 -04:00
Douglas Creager
6e73b859ef Update PLAN.md - mark steps 4 & 5 complete
All core hover assertion functionality is now implemented and compiling!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 12:41:25 -04:00
Douglas Creager
11e5ecb91e Implement hover type inference and matching for mdtest
- Add find_covering_node() to locate AST nodes at specific positions
- Add infer_type_at_position() to get type information using ty_python_semantic
- Add generate_hover_outputs() to scan for hover assertions and generate results
- Integrate hover outputs into check flow in run_test()
- Implement hover matching logic in matcher.rs to compare types
- Avoid adding ty_ide dependency; instead use ty_python_semantic directly

The implementation extracts hover assertions from comments, computes the target
position from the down arrow location, infers the type at that position using
the AST and SemanticModel, and matches it against the expected type in the
assertion.

Hover assertions now work end-to-end in mdtest files!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 12:40:52 -04:00
Douglas Creager
be47fbe0f6 Add CheckOutput enum to support hover results in mdtest
- Create CheckOutput enum with Diagnostic and Hover variants
- Replace SortedDiagnostics with SortedCheckOutputs in matcher
- Update match_file to accept &[CheckOutput] instead of &[Diagnostic]
- Update matching logic to handle both diagnostics and hover results
- Implement Unmatched trait for CheckOutput
- Convert diagnostics to CheckOutput in lib.rs before matching

This infrastructure allows hover assertions to be matched against hover
results without polluting the DiagnosticId enum with test-specific IDs.
The hover matching logic will be implemented in the next commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 12:28:09 -04:00
Douglas Creager
37effea8fd Add hover assertion type to mdtest framework
- Add Hover variant to UnparsedAssertion and ParsedAssertion enums
- Create HoverAssertion struct with column and expected_type fields
- Add HoverAssertionParseError enum for validation errors
- Update from_comment() to recognize '# hover:' and '# ↓ hover:' patterns
- Simplified design: down arrow must appear immediately before 'hover' keyword
- Add placeholder matching logic in matcher.rs (to be completed)
- Add PLAN.md to track implementation progress

The hover assertion syntax uses a down arrow to indicate column position:
    # ↓ hover: expected_type
    expression_to_hover

This will enable testing hover functionality in mdtest files, similar to
how ty_ide tests work with <CURSOR> markers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 12:19:21 -04:00
577 changed files with 8474 additions and 22832 deletions

View File

@@ -10,7 +10,7 @@ indent_style = space
insert_final_newline = true
indent_size = 2
[*.{rs,py,pyi,toml}]
[*.{rs,py,pyi}]
indent_size = 4
[*.snap]
@@ -18,3 +18,6 @@ trim_trailing_whitespace = false
[*.md]
max_line_length = 100
[*.toml]
indent_size = 4

View File

@@ -142,7 +142,7 @@ jobs:
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- 'python/py-fuzzer/**' \
if git diff --quiet "${MERGE_BASE}...HEAD" -- 'python/py_fuzzer/**' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
@@ -361,37 +361,6 @@ jobs:
cargo nextest run --all-features --profile ci
cargo test --all-features --doc
cargo-test-macos:
name: "cargo test (macos)"
runs-on: macos-latest
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: |
cargo nextest run --all-features --profile ci
cargo test --all-features --doc
cargo-test-wasm:
name: "cargo test (wasm)"
runs-on: ubuntu-latest
@@ -422,6 +391,23 @@ jobs:
cd crates/ty_wasm
wasm-pack test --node
cargo-build-release:
name: "cargo build (release)"
runs-on: macos-latest
if: ${{ github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Build"
run: cargo build --release --locked
cargo-build-msrv:
name: "cargo build (msrv)"
runs-on: depot-ubuntu-latest-8
@@ -466,7 +452,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@a66119fbb1c952daba62640c2609111fe0803621 # v1.15.7
uses: cargo-bins/cargo-binstall@38e8f5e4c386b611d51e8aa997b9a06a3c8eb67a # v1.15.6
- name: "Install cargo-fuzz"
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
@@ -717,7 +703,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@a66119fbb1c952daba62640c2609111fe0803621 # v1.15.7
- uses: cargo-bins/cargo-binstall@38e8f5e4c386b611d51e8aa997b9a06a3c8eb67a # v1.15.6
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -735,7 +721,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Run ty completion evaluation"
run: cargo run --release --package ty_completion_eval -- all --threshold 0.4 --tasks /tmp/completion-evaluation-tasks.csv
run: cargo run --release --package ty_completion_eval -- all --threshold 0.1 --tasks /tmp/completion-evaluation-tasks.csv
- name: "Ensure there are no changes"
run: diff ./crates/ty_completion_eval/completion-evaluation-tasks.csv /tmp/completion-evaluation-tasks.csv
@@ -967,7 +953,7 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
uses: CodSpeedHQ/action@3959e9e296ef25296e93e32afcc97196f966e57f # v4.1.0
with:
mode: instrumentation
run: cargo codspeed run
@@ -1002,23 +988,19 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench ty
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
uses: CodSpeedHQ/action@3959e9e296ef25296e93e32afcc97196f966e57f # v4.1.0
with:
mode: instrumentation
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
benchmarks-walltime:
name: "benchmarks walltime (${{ matrix.benchmarks }})"
runs-on: codspeed-macro
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
strategy:
matrix:
benchmarks:
- "medium|multithreaded"
- "small|large"
env:
TY_LOG: ruff_benchmark=debug
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -1040,7 +1022,7 @@ jobs:
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
uses: CodSpeedHQ/action@3959e9e296ef25296e93e32afcc97196f966e57f # v4.1.0
env:
# enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't
# appear to provide much useful insight for our walltime benchmarks right now
@@ -1048,5 +1030,5 @@ jobs:
CODSPEED_PERF_ENABLED: false
with:
mode: walltime
run: cargo codspeed run --bench ty_walltime "${{ matrix.benchmarks }}"
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -16,10 +16,8 @@ name: Sync typeshed
# 3. Once the Windows worker is done, a MacOS worker:
# a. Checks out the branch created by the Linux worker
# b. Syncs all docstrings available on MacOS that are not available on Linux or Windows
# c. Attempts to update any snapshots that might have changed
# (this sub-step is allowed to fail)
# d. Commits the changes and pushes them to the same upstream branch
# e. Creates a PR against the `main` branch using the branch all three workers have pushed to
# c. Commits the changes and pushes them to the same upstream branch
# d. Creates a PR against the `main` branch using the branch all three workers have pushed to
# 4. If any of steps 1-3 failed, an issue is created in the `astral-sh/ruff` repository
on:
@@ -29,12 +27,7 @@ on:
- cron: "0 0 1,15 * *"
env:
# Don't set this flag globally for the workflow: it does strange things
# to the snapshots in the `cargo insta test --accept` step in the MacOS job.
#
# FORCE_COLOR: 1
CARGO_TERM_COLOR: always
FORCE_COLOR: 1
GH_TOKEN: ${{ github.token }}
# The name of the upstream branch that the first worker creates,
@@ -93,8 +86,6 @@ jobs:
git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)" --allow-empty
- name: Sync Linux docstrings
if: ${{ success() }}
env:
FORCE_COLOR: 1
run: |
cd ruff
./scripts/codemod_docstrings.sh
@@ -134,8 +125,6 @@ jobs:
- name: Sync Windows docstrings
id: docstrings
shell: bash
env:
FORCE_COLOR: 1
run: ./scripts/codemod_docstrings.sh
- name: Commit the changes
if: ${{ steps.docstrings.outcome == 'success' }}
@@ -172,63 +161,26 @@ jobs:
git config --global user.name typeshedbot
git config --global user.email '<>'
- name: Sync macOS docstrings
run: ./scripts/codemod_docstrings.sh
- name: Commit and push the changes
if: ${{ success() }}
env:
FORCE_COLOR: 1
run: |
./scripts/codemod_docstrings.sh
git commit -am "Sync macOS docstrings" --allow-empty
- name: Format the changes
if: ${{ success() }}
env:
FORCE_COLOR: 1
run: |
# Here we just reformat the codemodded stubs so that they are
# consistent with the other typeshed stubs around them.
# Typeshed formats code using black in their CI, so we just invoke
# black on the stubs the same way that typeshed does.
uvx black "${VENDORED_TYPESHED}/stdlib" --config "${VENDORED_TYPESHED}/pyproject.toml" || true
git commit -am "Format codemodded docstrings" --allow-empty
- name: Remove typeshed pyproject.toml file
if: ${{ success() }}
run: |
rm "${VENDORED_TYPESHED}/pyproject.toml"
git commit -am "Remove pyproject.toml file"
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
if: ${{ success() }}
run: rustup show
- name: "Install mold"
if: ${{ success() }}
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
if: ${{ success() }}
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install cargo insta"
if: ${{ success() }}
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-insta
- name: Update snapshots
if: ${{ success() }}
run: |
# The `cargo insta` docs indicate that `--unreferenced=delete` might be a good option,
# but from local testing it appears to just revert all changes made by `cargo insta test --accept`.
#
# If there were only snapshot-related failures, `cargo insta test --accept` will have exit code 0,
# but if there were also other mdtest failures (for example), it will return a nonzero exit code.
# We don't care about other tests failing here, we just want snapshots updated where possible,
# so we use `|| true` here to ignore the exit code.
cargo insta test --accept --color=always --all-features --test-runner=nextest || true
- name: Commit snapshot changes
if: ${{ success() }}
run: git commit -am "Update snapshots" || echo "No snapshot changes to commit"
- name: Push changes upstream and create a PR
if: ${{ success() }}
run: |
git push
- name: Create a PR
if: ${{ success() }}
run: |
gh pr list --repo "${GITHUB_REPOSITORY}" --head "${UPSTREAM_BRANCH}" --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty"

View File

@@ -64,12 +64,12 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@279f8a15b0e7f77213bf9096dbc2335a19ef89c5"
ecosystem-analyzer \
--repository ruff \
diff \
--profile=profiling \
--profile=release \
--projects-old ruff/projects_old.txt \
--projects-new ruff/projects_new.txt \
--old old_commit \

View File

@@ -49,13 +49,13 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@279f8a15b0e7f77213bf9096dbc2335a19ef89c5"
ecosystem-analyzer \
--verbose \
--repository ruff \
analyze \
--profile=profiling \
--profile=release \
--projects ruff/crates/ty_python_semantic/resources/primer/good.txt \
--output ecosystem-diagnostics.json

160
Cargo.lock generated
View File

@@ -50,9 +50,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.21"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -65,9 +65,9 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.13"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-lossy"
@@ -214,6 +214,15 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bincode"
version = "2.0.1"
@@ -234,26 +243,6 @@ dependencies = [
"virtue",
]
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.9.4",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -327,9 +316,9 @@ dependencies = [
[[package]]
name = "camino"
version = "1.2.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603"
dependencies = [
"serde_core",
]
@@ -361,15 +350,6 @@ dependencies = [
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.3"
@@ -420,17 +400,6 @@ dependencies = [
"half",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.48"
@@ -517,17 +486,16 @@ dependencies = [
[[package]]
name = "codspeed"
version = "4.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0f62ea8934802f8b374bf691eea524c3aa444d7014f604dd4182a3667b69510"
checksum = "35584c5fcba8059780748866387fb97c5a203bcfc563fc3d0790af406727a117"
dependencies = [
"anyhow",
"bindgen",
"cc",
"bincode 1.3.3",
"colored 2.2.0",
"glob",
"libc",
"nix 0.30.1",
"nix 0.29.0",
"serde",
"serde_json",
"statrs",
@@ -536,22 +504,20 @@ dependencies = [
[[package]]
name = "codspeed-criterion-compat"
version = "4.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87efbc015fc0ff1b2001cd87df01c442824de677e01a77230bf091534687abb"
checksum = "78f6c1c6bed5fd84d319e8b0889da051daa361c79b7709c9394dfe1a882bba67"
dependencies = [
"clap",
"codspeed",
"codspeed-criterion-compat-walltime",
"colored 2.2.0",
"regex",
]
[[package]]
name = "codspeed-criterion-compat-walltime"
version = "4.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae5713ace440123bb4f1f78dd068d46872cb8548bfe61f752e7b2ad2c06d7f00"
checksum = "c989289ce6b1cbde72ed560496cb8fbf5aa14d5ef5666f168e7f87751038352e"
dependencies = [
"anes",
"cast",
@@ -574,22 +540,20 @@ dependencies = [
[[package]]
name = "codspeed-divan-compat"
version = "4.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95b4214b974f8f5206497153e89db90274e623f06b00bf4b9143eeb7735d975d"
checksum = "adf64eda57508448d59efd940bad62ede7c50b0d451a150b8d6a0eca642792a6"
dependencies = [
"clap",
"codspeed",
"codspeed-divan-compat-macros",
"codspeed-divan-compat-walltime",
"regex",
]
[[package]]
name = "codspeed-divan-compat-macros"
version = "4.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a53f34a16cb70ce4fd9ad57e1db016f0718e434f34179ca652006443b9a39967"
checksum = "058167258e819b16a4ba601fdfe270349ef191154758dbce122c62a698f70ba8"
dependencies = [
"divan-macros",
"itertools 0.14.0",
@@ -601,9 +565,9 @@ dependencies = [
[[package]]
name = "codspeed-divan-compat-walltime"
version = "4.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a5099050c8948dce488b8eaa2e68dc5cf571cb8f9fce99aaaecbdddb940bcd"
checksum = "48f9866ee3a4ef9d2868823ea5811886763af244f2df584ca247f49281c43f1f"
dependencies = [
"cfg-if",
"clap",
@@ -1563,7 +1527,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"serde",
"serde_core",
]
@@ -1837,15 +1801,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.177"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libcst"
version = "1.8.5"
version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d56bcd52d9b5e5f43e7fba20eb1f423ccb18c84cdf1cb506b8c1b95776b0b49"
checksum = "052ef5d9fc958a51aeebdf3713573b36c6fd6eed0bf0e60e204d2c0f8cf19b9f"
dependencies = [
"annotate-snippets",
"libcst_derive",
@@ -1858,24 +1822,14 @@ dependencies = [
[[package]]
name = "libcst_derive"
version = "1.8.5"
version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fcf5a725c4db703660124fe0edb98285f1605d0b87b7ee8684b699764a4f01a"
checksum = "a91a751afee92cbdd59d4bc6754c7672712eec2d30a308f23de4e3287b2929cb"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link 0.2.0",
]
[[package]]
name = "libmimalloc-sys"
version = "0.1.44"
@@ -2017,9 +1971,9 @@ checksum = "2f926ade0c4e170215ae43342bf13b9310a437609c81f29f86c5df6657582ef9"
[[package]]
name = "memchr"
version = "2.7.6"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "memoffset"
@@ -2528,16 +2482,6 @@ dependencies = [
"yansi",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
@@ -2569,9 +2513,9 @@ dependencies = [
[[package]]
name = "pyproject-toml"
version = "0.13.7"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d755483ad14b49e76713b52285235461a5b4f73f17612353e11a5de36a5fd2"
checksum = "ec768e063102b426e8962989758115e8659485124de9207bc365fab524125d65"
dependencies = [
"indexmap",
"pep440_rs",
@@ -2769,9 +2713,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.3"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
@@ -2781,9 +2725,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.11"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
@@ -2820,7 +2764,7 @@ dependencies = [
"anyhow",
"argfile",
"assert_fs",
"bincode",
"bincode 2.0.1",
"bitflags 2.9.4",
"cachedir",
"clap",
@@ -3540,8 +3484,8 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
dependencies = [
"boxcar",
"compact_str",
@@ -3564,13 +3508,13 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
dependencies = [
"proc-macro2",
"quote",
@@ -4499,7 +4443,6 @@ dependencies = [
"colored 3.0.0",
"insta",
"memchr",
"path-slash",
"regex",
"ruff_db",
"ruff_index",
@@ -4517,6 +4460,7 @@ dependencies = [
"thiserror 2.0.16",
"toml",
"tracing",
"ty_ide",
"ty_python_semantic",
"ty_static",
"ty_vendored",

View File

@@ -71,8 +71,8 @@ clap = { version = "4.5.3", features = ["derive"] }
clap_complete_command = { version = "0.6.0" }
clearscreen = { version = "4.0.0" }
csv = { version = "1.3.1" }
divan = { package = "codspeed-divan-compat", version = "4.0.4" }
codspeed-criterion-compat = { version = "4.0.4", default-features = false }
divan = { package = "codspeed-divan-compat", version = "3.0.2" }
codspeed-criterion-compat = { version = "3.0.2", default-features = false }
colored = { version = "3.0.0" }
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
@@ -146,7 +146,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 = "ef9f9329be6923acd050c8dddd172e3bc93e8051", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "29ab321b45d00daa4315fa2a06f7207759a8c87e", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
@@ -268,7 +268,12 @@ large_stack_arrays = "allow"
[profile.release]
lto = "fat"
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times
# and runtime performance[1].
#
# [1]: https://github.com/astral-sh/ruff/pull/9031
lto = "thin"
codegen-units = 16
# Some crates don't change as much but benefit more from
@@ -278,8 +283,6 @@ codegen-units = 16
codegen-units = 1
[profile.release.package.ruff_python_ast]
codegen-units = 1
[profile.release.package.salsa]
codegen-units = 1
[profile.dev.package.insta]
opt-level = 3
@@ -295,30 +298,11 @@ opt-level = 3
[profile.dev.package.ruff_python_parser]
opt-level = 1
# This profile is meant to mimic the `release` profile as closely as
# possible, but using settings that are more beneficial for iterative
# development. That is, the `release` profile is intended for actually
# building the release, where as `profiling` is meant for building ty/ruff
# for running benchmarks.
#
# The main differences here are to avoid stripping debug information
# and disabling fat lto. This does result in a mismatch between our release
# configuration and our benchmarking configuration, which is unfortunate.
# But compile times with `lto = fat` are completely untenable.
#
# This setup does risk that we are measuring something in benchmarks
# that we aren't shipping, but in order to make those two the same, we'd
# either need to make compile times way worse for development, or take
# a hit to binary size and a slight hit to runtime performance in our
# release builds.
#
# Use the `--profile profiling` flag to show symbols in release mode.
# e.g. `cargo build --profile profiling`
[profile.profiling]
inherits = "release"
strip = false
debug = "full"
lto = false
debug = 1
# The profile that 'cargo dist' will build with.
[profile.dist]

View File

@@ -28,7 +28,7 @@ An extremely fast Python linter and code formatter, written in Rust.
- ⚡️ 10-100x faster than existing linters (like Flake8) and formatters (like Black)
- 🐍 Installable via `pip`
- 🛠️ `pyproject.toml` support
- 🤝 Python 3.14 compatibility
- 🤝 Python 3.13 compatibility
- ⚖️ Drop-in parity with [Flake8](https://docs.astral.sh/ruff/faq/#how-does-ruffs-linter-compare-to-flake8), isort, and [Black](https://docs.astral.sh/ruff/faq/#how-does-ruffs-formatter-compare-to-black)
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports)

View File

@@ -599,7 +599,7 @@ impl<'a> ProjectBenchmark<'a> {
self.project
.check_paths()
.iter()
.map(|path| SystemPathBuf::from(*path))
.map(|path| path.to_path_buf())
.collect(),
);
@@ -645,8 +645,8 @@ fn hydra(criterion: &mut Criterion) {
name: "hydra-zen",
repository: "https://github.com/mit-ll-responsible-ai/hydra-zen",
commit: "dd2b50a9614c6f8c46c5866f283c8f7e7a960aa8",
paths: &["src"],
dependencies: &["pydantic", "beartype", "hydra-core"],
paths: vec![SystemPath::new("src")],
dependencies: vec!["pydantic", "beartype", "hydra-core"],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
@@ -662,8 +662,8 @@ fn attrs(criterion: &mut Criterion) {
name: "attrs",
repository: "https://github.com/python-attrs/attrs",
commit: "a6ae894aad9bc09edc7cdad8c416898784ceec9b",
paths: &["src"],
dependencies: &[],
paths: vec![SystemPath::new("src")],
dependencies: vec![],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
@@ -679,8 +679,8 @@ fn anyio(criterion: &mut Criterion) {
name: "anyio",
repository: "https://github.com/agronholm/anyio",
commit: "561d81270a12f7c6bbafb5bc5fad99a2a13f96be",
paths: &["src"],
dependencies: &[],
paths: vec![SystemPath::new("src")],
dependencies: vec![],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
@@ -696,8 +696,8 @@ fn datetype(criterion: &mut Criterion) {
name: "DateType",
repository: "https://github.com/glyph/DateType",
commit: "57c9c93cf2468069f72945fc04bf27b64100dad8",
paths: &["src"],
dependencies: &[],
paths: vec![SystemPath::new("src")],
dependencies: vec![],
max_dep_date: "2025-07-04",
python_version: PythonVersion::PY313,
},

View File

@@ -1,6 +1,7 @@
use divan::{Bencher, bench};
use std::fmt::{Display, Formatter};
use divan::{Bencher, bench};
use rayon::ThreadPoolBuilder;
use ruff_benchmark::real_world_projects::{InstalledProject, RealWorldProject};
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
@@ -12,39 +13,29 @@ use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
struct Benchmark<'a> {
project: RealWorldProject<'a>,
installed_project: std::sync::OnceLock<InstalledProject<'a>>,
project: InstalledProject<'a>,
max_diagnostics: usize,
}
impl<'a> Benchmark<'a> {
const fn new(project: RealWorldProject<'a>, max_diagnostics: usize) -> Self {
fn new(project: RealWorldProject<'a>, max_diagnostics: usize) -> Self {
let setup_project = project.setup().expect("Failed to setup project");
Self {
project,
installed_project: std::sync::OnceLock::new(),
project: setup_project,
max_diagnostics,
}
}
fn installed_project(&self) -> &InstalledProject<'a> {
self.installed_project.get_or_init(|| {
self.project
.clone()
.setup()
.expect("Failed to setup project")
})
}
fn setup_iteration(&self) -> ProjectDatabase {
let installed_project = self.installed_project();
let root = SystemPathBuf::from_path_buf(installed_project.path.clone()).unwrap();
let root = SystemPathBuf::from_path_buf(self.project.path.clone()).unwrap();
let system = OsSystem::new(&root);
let mut metadata = ProjectMetadata::discover(&root, &system).unwrap();
metadata.apply_options(Options {
environment: Some(EnvironmentOptions {
python_version: Some(RangedValue::cli(installed_project.config.python_version)),
python_version: Some(RangedValue::cli(self.project.config.python_version)),
python: Some(RelativePathBuf::cli(SystemPath::new(".venv"))),
..EnvironmentOptions::default()
}),
@@ -55,7 +46,7 @@ impl<'a> Benchmark<'a> {
db.project().set_included_paths(
&mut db,
installed_project
self.project
.check_paths()
.iter()
.map(|path| SystemPath::absolute(path, &root))
@@ -67,7 +58,7 @@ impl<'a> Benchmark<'a> {
impl Display for Benchmark<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.project.name.fmt(f)
f.write_str(self.project.config.name)
}
}
@@ -84,150 +75,166 @@ fn check_project(db: &ProjectDatabase, max_diagnostics: usize) {
);
}
static ALTAIR: Benchmark = Benchmark::new(
RealWorldProject {
name: "altair",
repository: "https://github.com/vega/altair",
commit: "d1f4a1ef89006e5f6752ef1f6df4b7a509336fba",
paths: &["altair"],
dependencies: &[
"jinja2",
"narwhals",
"numpy",
"packaging",
"pandas-stubs",
"pyarrow-stubs",
"pytest",
"scipy-stubs",
"types-jsonschema",
],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
1000,
);
static ALTAIR: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new(|| {
Benchmark::new(
RealWorldProject {
name: "altair",
repository: "https://github.com/vega/altair",
commit: "d1f4a1ef89006e5f6752ef1f6df4b7a509336fba",
paths: vec![SystemPath::new("altair")],
dependencies: vec![
"jinja2",
"narwhals",
"numpy",
"packaging",
"pandas-stubs",
"pyarrow-stubs",
"pytest",
"scipy-stubs",
"types-jsonschema",
],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
1000,
)
});
static COLOUR_SCIENCE: Benchmark = Benchmark::new(
RealWorldProject {
name: "colour-science",
repository: "https://github.com/colour-science/colour",
commit: "a17e2335c29e7b6f08080aa4c93cfa9b61f84757",
paths: &["colour"],
dependencies: &[
"matplotlib",
"numpy",
"pandas-stubs",
"pytest",
"scipy-stubs",
],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY310,
},
600,
);
static COLOUR_SCIENCE: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new(|| {
Benchmark::new(
RealWorldProject {
name: "colour-science",
repository: "https://github.com/colour-science/colour",
commit: "a17e2335c29e7b6f08080aa4c93cfa9b61f84757",
paths: vec![SystemPath::new("colour")],
dependencies: vec![
"matplotlib",
"numpy",
"pandas-stubs",
"pytest",
"scipy-stubs",
],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY310,
},
600,
)
});
static FREQTRADE: Benchmark = Benchmark::new(
RealWorldProject {
name: "freqtrade",
repository: "https://github.com/freqtrade/freqtrade",
commit: "2d842ea129e56575852ee0c45383c8c3f706be19",
paths: &["freqtrade"],
dependencies: &[
"numpy",
"pandas-stubs",
"pydantic",
"sqlalchemy",
"types-cachetools",
"types-filelock",
"types-python-dateutil",
"types-requests",
"types-tabulate",
],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
400,
);
static FREQTRADE: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new(|| {
Benchmark::new(
RealWorldProject {
name: "freqtrade",
repository: "https://github.com/freqtrade/freqtrade",
commit: "2d842ea129e56575852ee0c45383c8c3f706be19",
paths: vec![SystemPath::new("freqtrade")],
dependencies: vec![
"numpy",
"pandas-stubs",
"pydantic",
"sqlalchemy",
"types-cachetools",
"types-filelock",
"types-python-dateutil",
"types-requests",
"types-tabulate",
],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
400,
)
});
static PANDAS: Benchmark = Benchmark::new(
RealWorldProject {
name: "pandas",
repository: "https://github.com/pandas-dev/pandas",
commit: "5909621e2267eb67943a95ef5e895e8484c53432",
paths: &["pandas"],
dependencies: &[
"numpy",
"types-python-dateutil",
"types-pytz",
"types-PyMySQL",
"types-setuptools",
"pytest",
],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
3000,
);
static PANDAS: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new(|| {
Benchmark::new(
RealWorldProject {
name: "pandas",
repository: "https://github.com/pandas-dev/pandas",
commit: "5909621e2267eb67943a95ef5e895e8484c53432",
paths: vec![SystemPath::new("pandas")],
dependencies: vec![
"numpy",
"types-python-dateutil",
"types-pytz",
"types-PyMySQL",
"types-setuptools",
"pytest",
],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
3000,
)
});
static PYDANTIC: Benchmark = Benchmark::new(
RealWorldProject {
name: "pydantic",
repository: "https://github.com/pydantic/pydantic",
commit: "0c4a22b64b23dfad27387750cf07487efc45eb05",
paths: &["pydantic"],
dependencies: &[
"annotated-types",
"pydantic-core",
"typing-extensions",
"typing-inspection",
],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY39,
},
1000,
);
static PYDANTIC: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new(|| {
Benchmark::new(
RealWorldProject {
name: "pydantic",
repository: "https://github.com/pydantic/pydantic",
commit: "0c4a22b64b23dfad27387750cf07487efc45eb05",
paths: vec![SystemPath::new("pydantic")],
dependencies: vec![
"annotated-types",
"pydantic-core",
"typing-extensions",
"typing-inspection",
],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY39,
},
1000,
)
});
static SYMPY: Benchmark = Benchmark::new(
RealWorldProject {
name: "sympy",
repository: "https://github.com/sympy/sympy",
commit: "22fc107a94eaabc4f6eb31470b39db65abb7a394",
paths: &["sympy"],
dependencies: &["mpmath"],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
13000,
);
static SYMPY: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new(|| {
Benchmark::new(
RealWorldProject {
name: "sympy",
repository: "https://github.com/sympy/sympy",
commit: "22fc107a94eaabc4f6eb31470b39db65abb7a394",
paths: vec![SystemPath::new("sympy")],
dependencies: vec!["mpmath"],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
13000,
)
});
static TANJUN: Benchmark = Benchmark::new(
RealWorldProject {
name: "tanjun",
repository: "https://github.com/FasterSpeeding/Tanjun",
commit: "69f40db188196bc59516b6c69849c2d85fbc2f4a",
paths: &["tanjun"],
dependencies: &["hikari", "alluka"],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
100,
);
static TANJUN: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new(|| {
Benchmark::new(
RealWorldProject {
name: "tanjun",
repository: "https://github.com/FasterSpeeding/Tanjun",
commit: "69f40db188196bc59516b6c69849c2d85fbc2f4a",
paths: vec![SystemPath::new("tanjun")],
dependencies: vec!["hikari", "alluka"],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
100,
)
});
static STATIC_FRAME: Benchmark = Benchmark::new(
RealWorldProject {
name: "static-frame",
repository: "https://github.com/static-frame/static-frame",
commit: "34962b41baca5e7f98f5a758d530bff02748a421",
paths: &["static_frame"],
// N.B. `arraykit` is installed as a dependency during mypy_primer runs,
// but it takes much longer to be installed in a Codspeed run than it does in a mypy_primer run
// (seems to be built from source on the Codspeed CI runners for some reason).
dependencies: &["numpy"],
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
630,
);
static STATIC_FRAME: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new(|| {
Benchmark::new(
RealWorldProject {
name: "static-frame",
repository: "https://github.com/static-frame/static-frame",
commit: "34962b41baca5e7f98f5a758d530bff02748a421",
paths: vec![SystemPath::new("static_frame")],
// N.B. `arraykit` is installed as a dependency during mypy_primer runs,
// but it takes much longer to be installed in a Codspeed run than it does in a mypy_primer run
// (seems to be built from source on the Codspeed CI runners for some reason).
dependencies: vec!["numpy"],
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
600,
)
});
#[track_caller]
fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) {
@@ -238,22 +245,22 @@ fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) {
});
}
#[bench(args=[&ALTAIR, &FREQTRADE, &PYDANTIC, &TANJUN], sample_size=2, sample_count=3)]
#[bench(args=[&*ALTAIR, &*FREQTRADE, &*PYDANTIC, &*TANJUN], sample_size=2, sample_count=3)]
fn small(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(args=[&COLOUR_SCIENCE, &PANDAS, &STATIC_FRAME], sample_size=1, sample_count=3)]
#[bench(args=[&*COLOUR_SCIENCE, &*PANDAS, &*STATIC_FRAME], sample_size=1, sample_count=3)]
fn medium(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(args=[&SYMPY], sample_size=1, sample_count=2)]
#[bench(args=[&*SYMPY], sample_size=1, sample_count=2)]
fn large(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(args=[&PYDANTIC], sample_size=3, sample_count=8)]
#[bench(args=[&*PYDANTIC], sample_size=3, sample_count=8)]
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
let thread_pool = ThreadPoolBuilder::new().build().unwrap();

View File

@@ -30,9 +30,9 @@ pub struct RealWorldProject<'a> {
/// Specific commit hash to checkout
pub commit: &'a str,
/// List of paths within the project to check (`ty check <paths>`)
pub paths: &'a [&'a str],
pub paths: Vec<&'a SystemPath>,
/// Dependencies to install via uv
pub dependencies: &'a [&'a str],
pub dependencies: Vec<&'a str>,
/// Limit candidate packages to those that were uploaded prior to a given point in time (ISO 8601 format).
/// Maps to uv's `exclude-newer`.
pub max_dep_date: &'a str,
@@ -125,9 +125,9 @@ impl<'a> InstalledProject<'a> {
&self.config
}
/// Get the benchmark paths
pub fn check_paths(&self) -> &[&str] {
self.config.paths
/// Get the benchmark paths as `SystemPathBuf`
pub fn check_paths(&self) -> &[&SystemPath] {
&self.config.paths
}
/// Get the virtual environment path
@@ -297,7 +297,7 @@ fn install_dependencies(checkout: &Checkout) -> Result<()> {
"--exclude-newer",
checkout.project().max_dep_date,
])
.args(checkout.project().dependencies);
.args(&checkout.project().dependencies);
let output = cmd
.output()

View File

@@ -7,8 +7,7 @@ use ruff_annotate_snippets::Level as AnnotateLevel;
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use self::render::{
DisplayDiagnostic, DisplayDiagnostics, DummyFileResolver, FileResolver, Input,
ceil_char_boundary,
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
github::{DisplayGithubDiagnostics, GithubRenderer},
};
use crate::{Db, files::File};

View File

@@ -1170,31 +1170,6 @@ pub fn ceil_char_boundary(text: &str, offset: TextSize) -> TextSize {
.unwrap_or_else(|| TextSize::from(upper_bound))
}
/// A stub implementation of [`FileResolver`] intended for testing.
pub struct DummyFileResolver;
impl FileResolver for DummyFileResolver {
fn path(&self, _file: File) -> &str {
unimplemented!()
}
fn input(&self, _file: File) -> Input {
unimplemented!()
}
fn notebook_index(&self, _file: &UnifiedFile) -> Option<NotebookIndex> {
None
}
fn is_notebook(&self, _file: &UnifiedFile) -> bool {
false
}
fn current_directory(&self) -> &Path {
Path::new(".")
}
}
#[cfg(test)]
mod tests {

View File

@@ -93,39 +93,14 @@ fn generate_markdown() -> String {
})
.join("\n");
let status_text = match lint.status() {
ty_python_semantic::lint::LintStatus::Stable { since } => {
format!(
r#"Added in <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>"#
)
}
ty_python_semantic::lint::LintStatus::Preview { since } => {
format!(
r#"Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
)
}
ty_python_semantic::lint::LintStatus::Deprecated { since, .. } => {
format!(
r#"Deprecated (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
)
}
ty_python_semantic::lint::LintStatus::Removed { since, .. } => {
format!(
r#"Removed (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
)
}
};
let _ = writeln!(
&mut output,
r#"<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of '{level}'."><code>{level}</code></a> ·
{status_text} ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/{file}#L{line}" target="_blank">View source</a>
Default level: [`{level}`](../rules.md#rule-levels "This lint has a default level of '{level}'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}) ·
[View source](https://github.com/astral-sh/ruff/blob/main/{file}#L{line})
</small>
{documentation}
"#,
level = lint.default_level(),

View File

@@ -98,10 +98,6 @@ impl Db for ModuleDb {
fn lint_registry(&self) -> &LintRegistry {
default_lint_registry()
}
fn verbose(&self) -> bool {
false
}
}
#[salsa::db]

View File

@@ -11,7 +11,7 @@ from airflow import (
)
from airflow.api_connexion.security import requires_access
from airflow.contrib.aws_athena_hook import AWSAthenaHook
from airflow.datasets import DatasetAliasEvent, DatasetEvent
from airflow.datasets import DatasetAliasEvent
from airflow.operators.postgres_operator import Mapping
from airflow.operators.subdag import SubDagOperator
from airflow.secrets.cache import SecretCache
@@ -48,7 +48,6 @@ AWSAthenaHook()
# airflow.datasets
DatasetAliasEvent()
DatasetEvent()
# airflow.operators.subdag.*

View File

@@ -227,32 +227,3 @@ async def read_thing(query: str):
@app.get("/things/{ thing_id : str }")
async def read_thing(query: str):
return {"query": query}
# https://github.com/astral-sh/ruff/issues/20680
# These should NOT trigger FAST003 because FastAPI doesn't recognize them as path parameters
# Non-ASCII characters in parameter name
@app.get("/f1/{用户身份}")
async def f1():
return locals()
# Space in parameter name
@app.get("/f2/{x: str}")
async def f2():
return locals()
# Non-ASCII converter
@app.get("/f3/{complex_number:}")
async def f3():
return locals()
# Mixed non-ASCII characters
@app.get("/f4/{用户_id}")
async def f4():
return locals()
# Space in parameter name with converter
@app.get("/f5/{param: int}")
async def f5():
return locals()

View File

@@ -33,10 +33,3 @@ class ShellConfig:
def run(self, username):
Popen("true", shell={**self.shell_defaults, **self.fetch_shell_config(username)})
# Additional truthiness cases for generator, lambda, and f-strings
Popen("true", shell=(i for i in ()))
Popen("true", shell=lambda: 0)
Popen("true", shell=f"{b''}")
x = 1
Popen("true", shell=f"{x=}")

View File

@@ -6,19 +6,3 @@ foo(shell=True)
foo(shell={**{}})
foo(shell={**{**{}}})
# Truthy non-bool values for `shell`
foo(shell=(i for i in ()))
foo(shell=lambda: 0)
# f-strings guaranteed non-empty
foo(shell=f"{b''}")
x = 1
foo(shell=f"{x=}")
# Additional truthiness cases for generator, lambda, and f-strings
foo(shell=(i for i in ()))
foo(shell=lambda: 0)
foo(shell=f"{b''}")
x = 1
foo(shell=f"{x=}")

View File

@@ -9,10 +9,3 @@ os.system("tar cf foo.tar bar/*")
subprocess.Popen(["chmod", "+w", "*.py"], shell={**{}})
subprocess.Popen(["chmod", "+w", "*.py"], shell={**{**{}}})
# Additional truthiness cases for generator, lambda, and f-strings
subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
subprocess.Popen("chmod +w foo*", shell=lambda: 0)
subprocess.Popen("chmod +w foo*", shell=f"{b''}")
x = 1
subprocess.Popen("chmod +w foo*", shell=f"{x=}")

View File

@@ -1,10 +1,10 @@
# The lexer emits a string token if it's unterminated
# The lexer doesn't emit a string token if it's unterminated
"a" "b
"a" "b" "c
"a" """b
c""" "d
# This is also true for
# For f-strings, the `FStringRanges` won't contain the range for
# unterminated f-strings.
f"a" f"b
f"a" f"b" f"c

View File

@@ -1,8 +0,0 @@
import logging
variablename = "value"
log = logging.getLogger(__name__)
log.info(f"a" f"b {variablename}")
log.info("a " f"b {variablename}")
log.info("prefix " f"middle {variablename}" f" suffix")

View File

@@ -78,11 +78,3 @@ b: None | Literal[None] | None
c: (None | Literal[None]) | None
d: None | (Literal[None] | None)
e: None | ((None | Literal[None]) | None) | None
# Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__

View File

@@ -197,10 +197,3 @@ for x in {**a, **b} or [None]:
# https://github.com/astral-sh/ruff/issues/7127
def f(a: "'b' or 'c'"): ...
# https://github.com/astral-sh/ruff/issues/20703
print(f"{b''}" or "bar") # SIM222
x = 1
print(f"{x=}" or "bar") # SIM222
(lambda: 1) or True # SIM222
(i for i in range(1)) or "bar" # SIM222

View File

@@ -18,20 +18,3 @@ def print_third_word(word: Hello.Text) -> None:
def print_fourth_word(word: Goodbye) -> None:
print(word)
import typing_extensions
import typing_extensions as TypingExt
from typing_extensions import Text as TextAlias
def print_fifth_word(word: typing_extensions.Text) -> None:
print(word)
def print_sixth_word(word: TypingExt.Text) -> None:
print(word)
def print_seventh_word(word: TextAlias) -> None:
print(word)

View File

@@ -43,7 +43,7 @@ class Foo:
T = typing.TypeVar(*args)
x: typing.TypeAlias = list[T]
# `default` was added in Python 3.13
# `default` should be skipped for now, added in Python 3.13
T = typing.TypeVar("T", default=Any)
x: typing.TypeAlias = list[T]
@@ -90,9 +90,9 @@ PositiveList = TypeAliasType(
"PositiveList2", list[Annotated[T, Gt(0)]], type_params=(T,)
)
# `default` was added in Python 3.13
# `default` should be skipped for now, added in Python 3.13
T = typing.TypeVar("T", default=Any)
AnyList = TypeAliasType("AnyList", list[T], type_params=(T,))
AnyList = TypeAliasType("AnyList", list[T], typep_params=(T,))
# unsafe fix if comments within the fix
T = TypeVar("T")
@@ -128,7 +128,3 @@ T: TypeAlias = ( # comment0
str # comment6
# comment7
) # comment8
# Test case for TypeVar with default - should be converted when preview mode is enabled
T_default = TypeVar("T_default", default=int)
DefaultList: TypeAlias = list[T_default]

View File

@@ -122,7 +122,7 @@ class MixedGenerics[U]:
return (u, t)
# default requires 3.13
# TODO(brent) default requires 3.13
V = TypeVar("V", default=Any, bound=str)
@@ -130,14 +130,6 @@ class DefaultTypeVar(Generic[V]): # -> [V: str = Any]
var: V
# Test case for TypeVar with default but no bound
W = TypeVar("W", default=int)
class DefaultOnlyTypeVar(Generic[W]): # -> [W = int]
var: W
# nested classes and functions are skipped
class Outer:
class Inner(Generic[T]):

View File

@@ -44,7 +44,9 @@ def any_str_param(s: AnyStr) -> AnyStr:
return s
# default requires 3.13
# these cases are not handled
# TODO(brent) default requires 3.13
V = TypeVar("V", default=Any, bound=str)
@@ -52,8 +54,6 @@ def default_var(v: V) -> V:
return v
# these cases are not handled
def outer():
def inner(t: T) -> T:
return t

View File

@@ -50,6 +50,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::nonlocal_and_global(checker, nonlocal);
}
}
Stmt::Break(_) => {
if checker.is_rule_enabled(Rule::BreakOutsideLoop) {
pyflakes::rules::break_outside_loop(
checker,
stmt,
&mut checker.semantic.current_statements().skip(1),
);
}
}
Stmt::Continue(_) => {
if checker.is_rule_enabled(Rule::ContinueOutsideLoop) {
pyflakes::rules::continue_outside_loop(
checker,
stmt,
&mut checker.semantic.current_statements().skip(1),
);
}
}
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
is_async,

View File

@@ -697,7 +697,6 @@ impl SemanticSyntaxContext for Checker<'_> {
}
}
SemanticSyntaxErrorKind::FutureFeatureNotDefined(name) => {
// F407
if self.is_rule_enabled(Rule::FutureFeatureNotDefined) {
self.report_diagnostic(
pyflakes::rules::FutureFeatureNotDefined { name },
@@ -705,18 +704,6 @@ impl SemanticSyntaxContext for Checker<'_> {
);
}
}
SemanticSyntaxErrorKind::BreakOutsideLoop => {
// F701
if self.is_rule_enabled(Rule::BreakOutsideLoop) {
self.report_diagnostic(pyflakes::rules::BreakOutsideLoop, error.range);
}
}
SemanticSyntaxErrorKind::ContinueOutsideLoop => {
// F702
if self.is_rule_enabled(Rule::ContinueOutsideLoop) {
self.report_diagnostic(pyflakes::rules::ContinueOutsideLoop, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
@@ -824,40 +811,19 @@ impl SemanticSyntaxContext for Checker<'_> {
}
)
}
fn in_loop_context(&self) -> bool {
let mut child = self.semantic.current_statement();
for parent in self.semantic.current_statements().skip(1) {
match parent {
Stmt::For(ast::StmtFor { orelse, .. })
| Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return true;
}
}
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {
break;
}
_ => {}
}
child = parent;
}
false
}
}
impl<'a> Visitor<'a> for Checker<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
// Step 0: Pre-processing
self.semantic.push_node(stmt);
// For functions, defer semantic syntax error checks until the body of the function is
// visited
if !stmt.is_function_def_stmt() {
self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context));
}
// Step 0: Pre-processing
self.semantic.push_node(stmt);
// For Jupyter Notebooks, we'll reset the `IMPORT_BOUNDARY` flag when
// we encounter a cell boundary.
if self.source_type.is_ipynb()

View File

@@ -11,7 +11,8 @@ use crate::settings::types::CompiledPerFileIgnoreList;
pub fn get_cwd() -> &'static Path {
#[cfg(target_arch = "wasm32")]
{
Path::new(".")
static CWD: std::sync::LazyLock<PathBuf> = std::sync::LazyLock::new(|| PathBuf::from("."));
&CWD
}
#[cfg(not(target_arch = "wasm32"))]
path_absolutize::path_dedot::CWD.as_path()

View File

@@ -242,11 +242,6 @@ pub(crate) const fn is_refined_submodule_import_match_enabled(settings: &LinterS
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20660
pub(crate) const fn is_type_var_default_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// github.com/astral-sh/ruff/issues/20004
pub(crate) const fn is_b006_check_guaranteed_mutable_expr_enabled(
settings: &LinterSettings,
@@ -270,7 +265,3 @@ pub(crate) const fn is_fix_read_whole_file_enabled(settings: &LinterSettings) ->
pub(crate) const fn is_fix_write_whole_file_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -655,11 +655,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
},
// airflow.datasets
["airflow", "datasets", "DatasetAliasEvent"] => Replacement::None,
["airflow", "datasets", "DatasetEvent"] => Replacement::Message(
"`DatasetEvent` has been made private in Airflow 3. \
Use `dict[str, Any]` for the time being. \
An `AssetEvent` type will be added to the apache-airflow-task-sdk in a future version.",
),
// airflow.hooks
["airflow", "hooks", "base_hook", "BaseHook"] => Replacement::Rename {

View File

@@ -104,49 +104,38 @@ AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
49 | # airflow.datasets
50 | DatasetAliasEvent()
| ^^^^^^^^^^^^^^^^^
51 | DatasetEvent()
|
AIR301 `airflow.datasets.DatasetEvent` is removed in Airflow 3.0
--> AIR301_names.py:51:1
|
49 | # airflow.datasets
50 | DatasetAliasEvent()
51 | DatasetEvent()
| ^^^^^^^^^^^^
|
help: `DatasetEvent` has been made private in Airflow 3. Use `dict[str, Any]` for the time being. An `AssetEvent` type will be added to the apache-airflow-task-sdk in a future version.
AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0
--> AIR301_names.py:55:1
--> AIR301_names.py:54:1
|
54 | # airflow.operators.subdag.*
55 | SubDagOperator()
53 | # airflow.operators.subdag.*
54 | SubDagOperator()
| ^^^^^^^^^^^^^^
56 |
57 | # airflow.operators.postgres_operator
55 |
56 | # airflow.operators.postgres_operator
|
help: The whole `airflow.subdag` module has been removed.
AIR301 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0
--> AIR301_names.py:58:1
--> AIR301_names.py:57:1
|
57 | # airflow.operators.postgres_operator
58 | Mapping()
56 | # airflow.operators.postgres_operator
57 | Mapping()
| ^^^^^^^
59 |
60 | # airflow.secrets
58 |
59 | # airflow.secrets
|
AIR301 [*] `airflow.secrets.cache.SecretCache` is removed in Airflow 3.0
--> AIR301_names.py:65:1
--> AIR301_names.py:64:1
|
64 | # airflow.secrets.cache
65 | SecretCache()
63 | # airflow.secrets.cache
64 | SecretCache()
| ^^^^^^^^^^^
|
help: Use `SecretCache` from `airflow.sdk` instead.
14 | from airflow.datasets import DatasetAliasEvent, DatasetEvent
14 | from airflow.datasets import DatasetAliasEvent
15 | from airflow.operators.postgres_operator import Mapping
16 | from airflow.operators.subdag import SubDagOperator
- from airflow.secrets.cache import SecretCache
@@ -164,211 +153,211 @@ help: Use `SecretCache` from `airflow.sdk` instead.
note: This is an unsafe fix and may change runtime behavior
AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
--> AIR301_names.py:69:1
--> AIR301_names.py:68:1
|
68 | # airflow.triggers.external_task
69 | TaskStateTrigger()
67 | # airflow.triggers.external_task
68 | TaskStateTrigger()
| ^^^^^^^^^^^^^^^^
70 |
71 | # airflow.utils.date
69 |
70 | # airflow.utils.date
|
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:72:1
--> AIR301_names.py:71:1
|
71 | # airflow.utils.date
72 | dates.date_range
70 | # airflow.utils.date
71 | dates.date_range
| ^^^^^^^^^^^^^^^^
73 | dates.days_ago
72 | dates.days_ago
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:73:1
--> AIR301_names.py:72:1
|
71 | # airflow.utils.date
72 | dates.date_range
73 | dates.days_ago
70 | # airflow.utils.date
71 | dates.date_range
72 | dates.days_ago
| ^^^^^^^^^^^^^^
74 |
75 | date_range
73 |
74 | date_range
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:75:1
--> AIR301_names.py:74:1
|
73 | dates.days_ago
74 |
75 | date_range
72 | dates.days_ago
73 |
74 | date_range
| ^^^^^^^^^^
76 | days_ago
77 | infer_time_unit
75 | days_ago
76 | infer_time_unit
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:76:1
--> AIR301_names.py:75:1
|
75 | date_range
76 | days_ago
74 | date_range
75 | days_ago
| ^^^^^^^^
77 | infer_time_unit
78 | parse_execution_date
76 | infer_time_unit
77 | parse_execution_date
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
--> AIR301_names.py:77:1
--> AIR301_names.py:76:1
|
75 | date_range
76 | days_ago
77 | infer_time_unit
74 | date_range
75 | days_ago
76 | infer_time_unit
| ^^^^^^^^^^^^^^^
78 | parse_execution_date
79 | round_time
77 | parse_execution_date
78 | round_time
|
AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
--> AIR301_names.py:78:1
--> AIR301_names.py:77:1
|
76 | days_ago
77 | infer_time_unit
78 | parse_execution_date
75 | days_ago
76 | infer_time_unit
77 | parse_execution_date
| ^^^^^^^^^^^^^^^^^^^^
79 | round_time
80 | scale_time_units
78 | round_time
79 | scale_time_units
|
AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0
--> AIR301_names.py:79:1
--> AIR301_names.py:78:1
|
77 | infer_time_unit
78 | parse_execution_date
79 | round_time
76 | infer_time_unit
77 | parse_execution_date
78 | round_time
| ^^^^^^^^^^
80 | scale_time_units
79 | scale_time_units
|
AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
--> AIR301_names.py:80:1
--> AIR301_names.py:79:1
|
78 | parse_execution_date
79 | round_time
80 | scale_time_units
77 | parse_execution_date
78 | round_time
79 | scale_time_units
| ^^^^^^^^^^^^^^^^
81 |
82 | # This one was not deprecated.
80 |
81 | # This one was not deprecated.
|
AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
--> AIR301_names.py:87:1
--> AIR301_names.py:86:1
|
86 | # airflow.utils.dag_cycle_tester
87 | test_cycle
85 | # airflow.utils.dag_cycle_tester
86 | test_cycle
| ^^^^^^^^^^
|
AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0
--> AIR301_names.py:91:1
--> AIR301_names.py:90:1
|
90 | # airflow.utils.db
91 | create_session
89 | # airflow.utils.db
90 | create_session
| ^^^^^^^^^^^^^^
92 |
93 | # airflow.utils.decorators
91 |
92 | # airflow.utils.decorators
|
AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0
--> AIR301_names.py:94:1
--> AIR301_names.py:93:1
|
93 | # airflow.utils.decorators
94 | apply_defaults
92 | # airflow.utils.decorators
93 | apply_defaults
| ^^^^^^^^^^^^^^
95 |
96 | # airflow.utils.file
94 |
95 | # airflow.utils.file
|
help: `apply_defaults` is now unconditionally done and can be safely removed.
AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0
--> AIR301_names.py:97:1
--> AIR301_names.py:96:1
|
96 | # airflow.utils.file
97 | mkdirs
95 | # airflow.utils.file
96 | mkdirs
| ^^^^^^
|
help: Use `pathlib.Path({path}).mkdir` instead
AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
--> AIR301_names.py:101:1
--> AIR301_names.py:100:1
|
100 | # airflow.utils.state
101 | SHUTDOWN
99 | # airflow.utils.state
100 | SHUTDOWN
| ^^^^^^^^
102 | terminating_states
101 | terminating_states
|
AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
--> AIR301_names.py:102:1
--> AIR301_names.py:101:1
|
100 | # airflow.utils.state
101 | SHUTDOWN
102 | terminating_states
99 | # airflow.utils.state
100 | SHUTDOWN
101 | terminating_states
| ^^^^^^^^^^^^^^^^^^
103 |
104 | # airflow.utils.trigger_rule
102 |
103 | # airflow.utils.trigger_rule
|
AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
--> AIR301_names.py:105:1
--> AIR301_names.py:104:1
|
104 | # airflow.utils.trigger_rule
105 | TriggerRule.DUMMY
103 | # airflow.utils.trigger_rule
104 | TriggerRule.DUMMY
| ^^^^^^^^^^^^^^^^^
106 | TriggerRule.NONE_FAILED_OR_SKIPPED
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
--> AIR301_names.py:106:1
--> AIR301_names.py:105:1
|
104 | # airflow.utils.trigger_rule
105 | TriggerRule.DUMMY
106 | TriggerRule.NONE_FAILED_OR_SKIPPED
103 | # airflow.utils.trigger_rule
104 | TriggerRule.DUMMY
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0
--> AIR301_names.py:110:1
--> AIR301_names.py:109:1
|
109 | # airflow.www.auth
110 | has_access
108 | # airflow.www.auth
109 | has_access
| ^^^^^^^^^^
111 | has_access_dataset
110 | has_access_dataset
|
AIR301 `airflow.www.auth.has_access_dataset` is removed in Airflow 3.0
--> AIR301_names.py:111:1
--> AIR301_names.py:110:1
|
109 | # airflow.www.auth
110 | has_access
111 | has_access_dataset
108 | # airflow.www.auth
109 | has_access
110 | has_access_dataset
| ^^^^^^^^^^^^^^^^^^
112 |
113 | # airflow.www.utils
111 |
112 | # airflow.www.utils
|
AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0
--> AIR301_names.py:114:1
--> AIR301_names.py:113:1
|
113 | # airflow.www.utils
114 | get_sensitive_variables_fields
112 | # airflow.www.utils
113 | get_sensitive_variables_fields
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
115 | should_hide_value_for_key
114 | should_hide_value_for_key
|
AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0
--> AIR301_names.py:115:1
--> AIR301_names.py:114:1
|
113 | # airflow.www.utils
114 | get_sensitive_variables_fields
115 | should_hide_value_for_key
112 | # airflow.www.utils
113 | get_sensitive_variables_fields
114 | should_hide_value_for_key
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|

View File

@@ -1,12 +1,12 @@
use std::iter::Peekable;
use std::ops::Range;
use std::sync::LazyLock;
use regex::{CaptureMatches, Regex};
use std::str::CharIndices;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::{Arguments, Expr, ExprCall, ExprSubscript, Parameter, ParameterWithDefault};
use ruff_python_semantic::{BindingKind, Modules, ScopeKind, SemanticModel};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_text_size::{Ranged, TextSize};
use crate::Fix;
@@ -165,6 +165,11 @@ pub(crate) fn fastapi_unused_path_parameter(
// Check if any of the path parameters are not in the function signature.
for (path_param, range) in path_params {
// Ignore invalid identifiers (e.g., `user-id`, as opposed to `user_id`)
if !is_identifier(path_param) {
continue;
}
// If the path parameter is already in the function or the dependency signature,
// we don't need to do anything.
if named_args.contains(&path_param) {
@@ -456,19 +461,15 @@ fn parameter_alias<'a>(parameter: &'a Parameter, semantic: &SemanticModel) -> Op
/// the parameter name. For example, `/{x}` is a valid parameter, but `/{ x }` is treated literally.
#[derive(Debug)]
struct PathParamIterator<'a> {
inner: CaptureMatches<'a, 'a>,
input: &'a str,
chars: Peekable<CharIndices<'a>>,
}
impl<'a> PathParamIterator<'a> {
fn new(input: &'a str) -> Self {
/// Matches the Starlette pattern for path parameters with optional converters from
/// <https://github.com/Kludex/starlette/blob/e18637c68e36d112b1983bc0c8b663681e6a4c50/starlette/routing.py#L121>
static FASTAPI_PATH_PARAM_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::[a-zA-Z_][a-zA-Z0-9_]*)?\}").unwrap()
});
Self {
inner: FASTAPI_PATH_PARAM_REGEX.captures_iter(input),
PathParamIterator {
input,
chars: input.char_indices().peekable(),
}
}
}
@@ -477,10 +478,19 @@ impl<'a> Iterator for PathParamIterator<'a> {
type Item = (&'a str, Range<usize>);
fn next(&mut self) -> Option<Self::Item> {
self.inner
.next()
// Extract the first capture group (the path parameter), but return the range of the
// whole match (everything in braces and including the braces themselves).
.and_then(|capture| Some((capture.get(1)?.as_str(), capture.get(0)?.range())))
while let Some((start, c)) = self.chars.next() {
if c == '{' {
if let Some((end, _)) = self.chars.by_ref().find(|&(_, ch)| ch == '}') {
let param_content = &self.input[start + 1..end];
// We ignore text after a colon, since those are path converters
// See also: https://fastapi.tiangolo.com/tutorial/path-params/?h=path#path-convertor
let param_name_end = param_content.find(':').unwrap_or(param_content.len());
let param_name = &param_content[..param_name_end];
return Some((param_name, start..end + 1));
}
}
}
None
}
}

View File

@@ -1091,12 +1091,9 @@ fn suspicious_function(
] => checker.report_diagnostic_if_enabled(SuspiciousInsecureCipherModeUsage, range),
// Mktemp
["tempfile", "mktemp"] => checker
.report_diagnostic_if_enabled(SuspiciousMktempUsage, range)
.map(|mut diagnostic| {
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
diagnostic
}),
["tempfile", "mktemp"] => {
checker.report_diagnostic_if_enabled(SuspiciousMktempUsage, range)
}
// Eval
["" | "builtins", "eval"] => {

View File

@@ -127,44 +127,3 @@ S602 `subprocess` call with `shell=True` identified, security issue
21 |
22 | # Check dict display with only double-starred expressions can be falsey.
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:38:1
|
37 | # Additional truthiness cases for generator, lambda, and f-strings
38 | Popen("true", shell=(i for i in ()))
| ^^^^^
39 | Popen("true", shell=lambda: 0)
40 | Popen("true", shell=f"{b''}")
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:39:1
|
37 | # Additional truthiness cases for generator, lambda, and f-strings
38 | Popen("true", shell=(i for i in ()))
39 | Popen("true", shell=lambda: 0)
| ^^^^^
40 | Popen("true", shell=f"{b''}")
41 | x = 1
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:40:1
|
38 | Popen("true", shell=(i for i in ()))
39 | Popen("true", shell=lambda: 0)
40 | Popen("true", shell=f"{b''}")
| ^^^^^
41 | x = 1
42 | Popen("true", shell=f"{x=}")
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:42:1
|
40 | Popen("true", shell=f"{b''}")
41 | x = 1
42 | Popen("true", shell=f"{x=}")
| ^^^^^
|

View File

@@ -9,85 +9,3 @@ S604 Function call with `shell=True` parameter identified, security issue
6 |
7 | foo(shell={**{}})
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:11:1
|
10 | # Truthy non-bool values for `shell`
11 | foo(shell=(i for i in ()))
| ^^^
12 | foo(shell=lambda: 0)
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:12:1
|
10 | # Truthy non-bool values for `shell`
11 | foo(shell=(i for i in ()))
12 | foo(shell=lambda: 0)
| ^^^
13 |
14 | # f-strings guaranteed non-empty
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:15:1
|
14 | # f-strings guaranteed non-empty
15 | foo(shell=f"{b''}")
| ^^^
16 | x = 1
17 | foo(shell=f"{x=}")
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:17:1
|
15 | foo(shell=f"{b''}")
16 | x = 1
17 | foo(shell=f"{x=}")
| ^^^
18 |
19 | # Additional truthiness cases for generator, lambda, and f-strings
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:20:1
|
19 | # Additional truthiness cases for generator, lambda, and f-strings
20 | foo(shell=(i for i in ()))
| ^^^
21 | foo(shell=lambda: 0)
22 | foo(shell=f"{b''}")
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:21:1
|
19 | # Additional truthiness cases for generator, lambda, and f-strings
20 | foo(shell=(i for i in ()))
21 | foo(shell=lambda: 0)
| ^^^
22 | foo(shell=f"{b''}")
23 | x = 1
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:22:1
|
20 | foo(shell=(i for i in ()))
21 | foo(shell=lambda: 0)
22 | foo(shell=f"{b''}")
| ^^^
23 | x = 1
24 | foo(shell=f"{x=}")
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:24:1
|
22 | foo(shell=f"{b''}")
23 | x = 1
24 | foo(shell=f"{x=}")
| ^^^
|

View File

@@ -43,44 +43,3 @@ S609 Possible wildcard injection in call due to `*` usage
9 |
10 | subprocess.Popen(["chmod", "+w", "*.py"], shell={**{}})
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:14:18
|
13 | # Additional truthiness cases for generator, lambda, and f-strings
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
| ^^^^^^^^^^^^^^^
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:15:18
|
13 | # Additional truthiness cases for generator, lambda, and f-strings
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
| ^^^^^^^^^^^^^^^
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
17 | x = 1
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:16:18
|
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
| ^^^^^^^^^^^^^^^
17 | x = 1
18 | subprocess.Popen("chmod +w foo*", shell=f"{x=}")
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:18:18
|
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
17 | x = 1
18 | subprocess.Popen("chmod +w foo*", shell=f"{x=}")
| ^^^^^^^^^^^^^^^
|

View File

@@ -188,10 +188,16 @@ fn move_initialization(
content.push_str(stylist.line_ending().as_str());
content.push_str(stylist.indentation());
if is_b006_unsafe_fix_preserve_assignment_expr_enabled(checker.settings()) {
let annotation = if let Some(ann) = parameter.annotation() {
format!(": {}", locator.slice(ann))
} else {
String::new()
};
let _ = write!(
&mut content,
"{} = {}",
"{}{} = {}",
parameter.parameter.name(),
annotation,
locator.slice(
parenthesized_range(
default.into(),

View File

@@ -16,7 +16,7 @@ help: Replace with `None`; initialize within function
5 + def import_module_wrong(value: dict[str, str] = None):
6 | import os
7 + if value is None:
8 + value = {}
8 + value: dict[str, str] = {}
9 |
10 |
11 | def import_module_with_values_wrong(value: dict[str, str] = {}):
@@ -38,7 +38,7 @@ help: Replace with `None`; initialize within function
10 | import os
11 |
12 + if value is None:
13 + value = {}
13 + value: dict[str, str] = {}
14 | return 2
15 |
16 |
@@ -62,7 +62,7 @@ help: Replace with `None`; initialize within function
17 | import sys
18 | import itertools
19 + if value is None:
20 + value = {}
20 + value: dict[str, str] = {}
21 |
22 |
23 | def from_import_module_wrong(value: dict[str, str] = {}):
@@ -83,7 +83,7 @@ help: Replace with `None`; initialize within function
21 + def from_import_module_wrong(value: dict[str, str] = None):
22 | from os import path
23 + if value is None:
24 + value = {}
24 + value: dict[str, str] = {}
25 |
26 |
27 | def from_imports_module_wrong(value: dict[str, str] = {}):
@@ -106,7 +106,7 @@ help: Replace with `None`; initialize within function
26 | from os import path
27 | from sys import version_info
28 + if value is None:
29 + value = {}
29 + value: dict[str, str] = {}
30 |
31 |
32 | def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
@@ -129,7 +129,7 @@ help: Replace with `None`; initialize within function
31 | import os
32 | from sys import version_info
33 + if value is None:
34 + value = {}
34 + value: dict[str, str] = {}
35 |
36 |
37 | def import_docstring_module_wrong(value: dict[str, str] = {}):
@@ -152,7 +152,7 @@ help: Replace with `None`; initialize within function
36 | """Docstring"""
37 | import os
38 + if value is None:
39 + value = {}
39 + value: dict[str, str] = {}
40 |
41 |
42 | def import_module_wrong(value: dict[str, str] = {}):
@@ -175,7 +175,7 @@ help: Replace with `None`; initialize within function
41 | """Docstring"""
42 | import os; import sys
43 + if value is None:
44 + value = {}
44 + value: dict[str, str] = {}
45 |
46 |
47 | def import_module_wrong(value: dict[str, str] = {}):
@@ -197,7 +197,7 @@ help: Replace with `None`; initialize within function
45 + def import_module_wrong(value: dict[str, str] = None):
46 | """Docstring"""
47 + if value is None:
48 + value = {}
48 + value: dict[str, str] = {}
49 | import os; import sys; x = 1
50 |
51 |
@@ -220,7 +220,7 @@ help: Replace with `None`; initialize within function
51 | """Docstring"""
52 | import os; import sys
53 + if value is None:
54 + value = {}
54 + value: dict[str, str] = {}
55 |
56 |
57 | def import_module_wrong(value: dict[str, str] = {}):
@@ -241,7 +241,7 @@ help: Replace with `None`; initialize within function
55 + def import_module_wrong(value: dict[str, str] = None):
56 | import os; import sys
57 + if value is None:
58 + value = {}
58 + value: dict[str, str] = {}
59 |
60 |
61 | def import_module_wrong(value: dict[str, str] = {}):
@@ -261,7 +261,7 @@ help: Replace with `None`; initialize within function
- def import_module_wrong(value: dict[str, str] = {}):
59 + def import_module_wrong(value: dict[str, str] = None):
60 + if value is None:
61 + value = {}
61 + value: dict[str, str] = {}
62 | import os; import sys; x = 1
63 |
64 |
@@ -282,7 +282,7 @@ help: Replace with `None`; initialize within function
63 + def import_module_wrong(value: dict[str, str] = None):
64 | import os; import sys
65 + if value is None:
66 + value = {}
66 + value: dict[str, str] = {}
67 |
68 |
69 | def import_module_wrong(value: dict[str, str] = {}): import os

View File

@@ -51,7 +51,7 @@ help: Replace with `None`; initialize within function
10 + def baz(a: list = None):
11 | """This one raises a different exception"""
12 + if a is None:
13 + a = []
13 + a: list = []
14 | raise IndexError()
15 |
16 |

View File

@@ -11,15 +11,15 @@ use crate::checkers::ast::Checker;
/// Checks for usage of `datetime.date.fromtimestamp()`.
///
/// ## Why is this bad?
/// Python date objects are naive, that is, not timezone-aware. While an aware
/// Python datetime objects can be naive or timezone-aware. While an aware
/// object represents a specific moment in time, a naive object does not
/// contain enough information to unambiguously locate itself relative to other
/// datetime objects. Since this can lead to errors, it is recommended to
/// always use timezone-aware objects.
///
/// `datetime.date.fromtimestamp(ts)` returns a naive date object.
/// Instead, use `datetime.datetime.fromtimestamp(ts, tz=...).date()` to
/// create a timezone-aware datetime object and retrieve its date component.
/// `datetime.date.fromtimestamp(ts)` returns a naive datetime object.
/// Instead, use `datetime.datetime.fromtimestamp(ts, tz=...)` to create a
/// timezone-aware object.
///
/// ## Example
/// ```python
@@ -32,14 +32,14 @@ use crate::checkers::ast::Checker;
/// ```python
/// import datetime
///
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.timezone.utc).date()
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.timezone.utc)
/// ```
///
/// Or, for Python 3.11 and later:
/// ```python
/// import datetime
///
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.UTC).date()
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.UTC)
/// ```
///
/// ## References

View File

@@ -11,15 +11,14 @@ use crate::checkers::ast::Checker;
/// Checks for usage of `datetime.date.today()`.
///
/// ## Why is this bad?
/// Python date objects are naive, that is, not timezone-aware. While an aware
/// Python datetime objects can be naive or timezone-aware. While an aware
/// object represents a specific moment in time, a naive object does not
/// contain enough information to unambiguously locate itself relative to other
/// datetime objects. Since this can lead to errors, it is recommended to
/// always use timezone-aware objects.
///
/// `datetime.date.today` returns a naive date object without taking timezones
/// into account. Instead, use `datetime.datetime.now(tz=...).date()` to
/// create a timezone-aware object and retrieve its date component.
/// `datetime.date.today` returns a naive datetime object. Instead, use
/// `datetime.datetime.now(tz=...).date()` to create a timezone-aware object.
///
/// ## Example
/// ```python

View File

@@ -42,9 +42,6 @@ use crate::rules::flake8_datetimez::helpers;
///
/// datetime.datetime.now(tz=datetime.UTC)
/// ```
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
#[derive(ViolationMetadata)]
pub(crate) struct CallDatetimeToday;

View File

@@ -41,9 +41,6 @@ use crate::rules::flake8_datetimez::helpers::{self, DatetimeModuleAntipattern};
///
/// datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
/// ```
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
#[derive(ViolationMetadata)]
pub(crate) struct CallDatetimeWithoutTzinfo(DatetimeModuleAntipattern);

View File

@@ -38,9 +38,6 @@ use crate::checkers::ast::Checker;
///
/// datetime.datetime.max.replace(tzinfo=datetime.UTC)
/// ```
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
#[derive(ViolationMetadata)]
pub(crate) struct DatetimeMinMax {
min_max: MinMax,

View File

@@ -1,31 +1,30 @@
---
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
---
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:2:1
|
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
| ^^^^^^
3 | "a" "b" "c
4 | "a" """b
|
help: Combine string literals
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:2:5
|
1 | # The lexer emits a string token if it's unterminated
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
| ^^
3 | "a" "b" "c
4 | "a" """b
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:2:7
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
| ^
3 | "a" "b" "c
4 | "a" """b
|
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:3:1
|
1 | # The lexer emits a string token if it's unterminated
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^^^^^^
@@ -34,22 +33,10 @@ ISC001 Implicitly concatenated string literals on one line
|
help: Combine string literals
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:3:5
|
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^^^^^
4 | "a" """b
5 | c""" "d
|
help: Combine string literals
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:3:9
|
1 | # The lexer emits a string token if it's unterminated
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^
@@ -57,6 +44,17 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:3:11
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^
4 | "a" """b
5 | c""" "d
|
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:4:1
|
@@ -66,21 +64,7 @@ ISC001 Implicitly concatenated string literals on one line
5 | | c""" "d
| |____^
6 |
7 | # This is also true for
|
help: Combine string literals
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:4:5
|
2 | "a" "b
3 | "a" "b" "c
4 | "a" """b
| _____^
5 | | c""" "d
| |_______^
6 |
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
help: Combine string literals
@@ -92,13 +76,24 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
| ^^
6 |
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:5:8
|
3 | "a" "b" "c
4 | "a" """b
5 | c""" "d
| ^
6 |
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:9:8
|
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -109,7 +104,7 @@ invalid-syntax: f-string: unterminated string
invalid-syntax: Expected FStringEnd, found newline
--> ISC_syntax_error.py:9:9
|
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -188,6 +183,14 @@ invalid-syntax: f-string: unterminated triple-quoted string
| |__^
|
invalid-syntax: unexpected EOF while parsing
--> ISC_syntax_error.py:30:1
|
28 | "i" "j"
29 | )
| ^
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:30:1
|

View File

@@ -4,17 +4,27 @@ source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:2:5
|
1 | # The lexer emits a string token if it's unterminated
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
| ^^
3 | "a" "b" "c
4 | "a" """b
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:2:7
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
| ^
3 | "a" "b" "c
4 | "a" """b
|
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:3:9
|
1 | # The lexer emits a string token if it's unterminated
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^
@@ -22,6 +32,17 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:3:11
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^
4 | "a" """b
5 | c""" "d
|
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:5:6
|
@@ -30,13 +51,24 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
| ^^
6 |
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:5:8
|
3 | "a" "b" "c
4 | "a" """b
5 | c""" "d
| ^
6 |
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:9:8
|
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -47,7 +79,7 @@ invalid-syntax: f-string: unterminated string
invalid-syntax: Expected FStringEnd, found newline
--> ISC_syntax_error.py:9:9
|
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -101,6 +133,14 @@ invalid-syntax: f-string: unterminated triple-quoted string
| |__^
|
invalid-syntax: unexpected EOF while parsing
--> ISC_syntax_error.py:30:1
|
28 | "i" "j"
29 | )
| ^
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:30:1
|

View File

@@ -23,7 +23,6 @@ mod tests {
#[test_case(Path::new("G003.py"))]
#[test_case(Path::new("G004.py"))]
#[test_case(Path::new("G004_arg_order.py"))]
#[test_case(Path::new("G004_implicit_concat.py"))]
#[test_case(Path::new("G010.py"))]
#[test_case(Path::new("G101_1.py"))]
#[test_case(Path::new("G101_2.py"))]
@@ -53,7 +52,6 @@ mod tests {
#[test_case(Rule::LoggingFString, Path::new("G004.py"))]
#[test_case(Rule::LoggingFString, Path::new("G004_arg_order.py"))]
#[test_case(Rule::LoggingFString, Path::new("G004_implicit_concat.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -42,52 +42,38 @@ fn logging_f_string(
// Default to double quotes if we can't determine it.
let quote_str = f_string
.value
.iter()
.map(|part| match part {
ast::FStringPart::Literal(literal) => literal.flags.quote_str(),
ast::FStringPart::FString(f) => f.flags.quote_str(),
})
.f_strings()
.next()
.map(|f| f.flags.quote_str())
.unwrap_or("\"");
for part in &f_string.value {
match part {
ast::FStringPart::Literal(literal) => {
let literal_text = literal.as_str();
if literal_text.contains('%') {
return;
for f in f_string.value.f_strings() {
for element in &f.elements {
match element {
InterpolatedStringElement::Literal(lit) => {
// If the literal text contains a '%' placeholder, bail out: mixing
// f-string interpolation with '%' placeholders is ambiguous for our
// automatic conversion, so don't offer a fix for this case.
if lit.value.as_ref().contains('%') {
return;
}
format_string.push_str(lit.value.as_ref());
}
format_string.push_str(literal_text);
}
ast::FStringPart::FString(f) => {
for element in &f.elements {
match element {
InterpolatedStringElement::Literal(lit) => {
// If the literal text contains a '%' placeholder, bail out: mixing
// f-string interpolation with '%' placeholders is ambiguous for our
// automatic conversion, so don't offer a fix for this case.
if lit.value.as_ref().contains('%') {
return;
}
format_string.push_str(lit.value.as_ref());
}
InterpolatedStringElement::Interpolation(interpolated) => {
if interpolated.format_spec.is_some()
|| !matches!(
interpolated.conversion,
ruff_python_ast::ConversionFlag::None
)
{
return;
}
match interpolated.expression.as_ref() {
Expr::Name(name) => {
format_string.push_str("%s");
args.push(name.id.as_str());
}
_ => return,
}
InterpolatedStringElement::Interpolation(interpolated) => {
if interpolated.format_spec.is_some()
|| !matches!(
interpolated.conversion,
ruff_python_ast::ConversionFlag::None
)
{
return;
}
match interpolated.expression.as_ref() {
Expr::Name(name) => {
format_string.push_str("%s");
args.push(name.id.as_str());
}
_ => return,
}
}
}

View File

@@ -1,35 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs
assertion_line: 50
---
G004 Logging statement uses f-string
--> G004_implicit_concat.py:6:10
|
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
| ^^^^^^^^^^^^^^^^^^^^^^^^
7 | log.info("a " f"b {variablename}")
8 | log.info("prefix " f"middle {variablename}" f" suffix")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004_implicit_concat.py:7:10
|
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
7 | log.info("a " f"b {variablename}")
| ^^^^^^^^^^^^^^^^^^^^^^^^
8 | log.info("prefix " f"middle {variablename}" f" suffix")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004_implicit_concat.py:8:10
|
6 | log.info(f"a" f"b {variablename}")
7 | log.info("a " f"b {variablename}")
8 | log.info("prefix " f"middle {variablename}" f" suffix")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting

View File

@@ -1,53 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs
assertion_line: 71
---
G004 [*] Logging statement uses f-string
--> G004_implicit_concat.py:6:10
|
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
| ^^^^^^^^^^^^^^^^^^^^^^^^
7 | log.info("a " f"b {variablename}")
8 | log.info("prefix " f"middle {variablename}" f" suffix")
|
help: Convert to lazy `%` formatting
3 | variablename = "value"
4 |
5 | log = logging.getLogger(__name__)
- log.info(f"a" f"b {variablename}")
6 + log.info("ab %s", variablename)
7 | log.info("a " f"b {variablename}")
8 | log.info("prefix " f"middle {variablename}" f" suffix")
G004 [*] Logging statement uses f-string
--> G004_implicit_concat.py:7:10
|
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
7 | log.info("a " f"b {variablename}")
| ^^^^^^^^^^^^^^^^^^^^^^^^
8 | log.info("prefix " f"middle {variablename}" f" suffix")
|
help: Convert to lazy `%` formatting
4 |
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
- log.info("a " f"b {variablename}")
7 + log.info("a b %s", variablename)
8 | log.info("prefix " f"middle {variablename}" f" suffix")
G004 [*] Logging statement uses f-string
--> G004_implicit_concat.py:8:10
|
6 | log.info(f"a" f"b {variablename}")
7 | log.info("a " f"b {variablename}")
8 | log.info("prefix " f"middle {variablename}" f" suffix")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
7 | log.info("a " f"b {variablename}")
- log.info("prefix " f"middle {variablename}" f" suffix")
8 + log.info("prefix middle %s suffix", variablename)

View File

@@ -74,8 +74,7 @@ pub(crate) fn bytestring_attribute(checker: &Checker, attribute: &Expr) {
["collections", "abc", "ByteString"] => ByteStringOrigin::CollectionsAbc,
_ => return,
};
let mut diagnostic = checker.report_diagnostic(ByteStringUsage { origin }, attribute.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
checker.report_diagnostic(ByteStringUsage { origin }, attribute.range());
}
/// PYI057
@@ -95,9 +94,7 @@ pub(crate) fn bytestring_import(checker: &Checker, import_from: &ast::StmtImport
for name in names {
if name.name.as_str() == "ByteString" {
let mut diagnostic =
checker.report_diagnostic(ByteStringUsage { origin }, name.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
checker.report_diagnostic(ByteStringUsage { origin }, name.range());
}
}
}

View File

@@ -4,8 +4,6 @@ use ruff_python_ast::{
self as ast, Expr, ExprBinOp, ExprContext, ExprNoneLiteral, Operator, PythonVersion,
helpers::{pep_604_union, typing_optional},
name::Name,
operator_precedence::OperatorPrecedence,
parenthesize::parenthesized_range,
};
use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union};
use ruff_text_size::{Ranged, TextRange};
@@ -240,19 +238,7 @@ fn create_fix(
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let union_expr = pep_604_union(&[new_literal_expr, none_expr]);
// Check if we need parentheses to preserve operator precedence
let content = if needs_parentheses_for_precedence(
semantic,
literal_expr,
checker.comment_ranges(),
checker.source(),
) {
format!("({})", checker.generator().expr(&union_expr))
} else {
checker.generator().expr(&union_expr)
};
let content = checker.generator().expr(&union_expr);
let union_edit = Edit::range_replacement(content, literal_expr.range());
Fix::applicable_edit(union_edit, applicability)
}
@@ -270,37 +256,3 @@ enum UnionKind {
TypingOptional,
BitOr,
}
/// Check if the union expression needs parentheses to preserve operator precedence.
/// This is needed when the union is part of a larger expression where the `|` operator
/// has lower precedence than the surrounding operations (like attribute access).
fn needs_parentheses_for_precedence(
semantic: &ruff_python_semantic::SemanticModel,
literal_expr: &Expr,
comment_ranges: &ruff_python_trivia::CommentRanges,
source: &str,
) -> bool {
// Get the parent expression to check if we're in a context that needs parentheses
let Some(parent_expr) = semantic.current_expression_parent() else {
return false;
};
// Check if the literal expression is already parenthesized
if parenthesized_range(
literal_expr.into(),
parent_expr.into(),
comment_ranges,
source,
)
.is_some()
{
return false; // Already parenthesized, don't add more
}
// Check if the parent expression has higher precedence than the `|` operator
let union_precedence = OperatorPrecedence::BitOr;
let parent_precedence = OperatorPrecedence::from(parent_expr);
// If the parent operation has higher precedence than `|`, we need parentheses
parent_precedence > union_precedence
}

View File

@@ -423,117 +423,5 @@ PYI061 Use `None` rather than `Literal[None]`
79 | d: None | (Literal[None] | None)
80 | e: None | ((None | Literal[None]) | None) | None
| ^^^^
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
|
help: Replace with `None`
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:83:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
| ^^^^
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
|
help: Replace with `Literal[...] | None`
80 | e: None | ((None | Literal[None]) | None) | None
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
- print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
83 + print((Literal[1] | None).__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:84:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
| ^^^^
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
|
help: Replace with `Literal[...] | None`
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
- print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
84 + print((Literal[1] | None).method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:85:18
|
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
| ^^^^
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
|
help: Replace with `Literal[...] | None`
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
- print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
85 + print((Literal[1] | None)[0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:86:18
|
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
| ^^^^
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Literal[...] | None`
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
- print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
86 + print((Literal[1] | None) + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:87:18
|
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
| ^^^^
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Literal[...] | None`
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
- print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
87 + print((Literal[1] | None) * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:88:19
|
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
| ^^^^
|
help: Replace with `Literal[...] | None`
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
- print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
88 + print((Literal[1] | None).__dict__) # Should become ((Literal[1] | None)).__dict__

View File

@@ -465,153 +465,5 @@ PYI061 Use `None` rather than `Literal[None]`
79 | d: None | (Literal[None] | None)
80 | e: None | ((None | Literal[None]) | None) | None
| ^^^^
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
|
help: Replace with `None`
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:83:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
| ^^^^
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
80 | e: None | ((None | Literal[None]) | None) | None
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
- print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
83 + print(Optional[Literal[1]].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:84:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
| ^^^^
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
- print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
84 + print(Optional[Literal[1]].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:85:18
|
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
| ^^^^
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
- print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
85 + print(Optional[Literal[1]][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:86:18
|
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
| ^^^^
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
- print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
86 + print(Optional[Literal[1]] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:87:18
|
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
| ^^^^
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
- print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
87 + print(Optional[Literal[1]] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:88:19
|
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
| ^^^^
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
- print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
88 + print((Optional[Literal[1]]).__dict__) # Should become ((Literal[1] | None)).__dict__

View File

@@ -898,9 +898,7 @@ fn check_test_function_args(checker: &Checker, parameters: &Parameters, decorato
/// PT020
fn check_fixture_decorator_name(checker: &Checker, decorator: &Decorator) {
if is_pytest_yield_fixture(decorator, checker.semantic()) {
let mut diagnostic =
checker.report_diagnostic(PytestDeprecatedYieldFixture, decorator.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
checker.report_diagnostic(PytestDeprecatedYieldFixture, decorator.range());
}
}

View File

@@ -1062,77 +1062,3 @@ help: Replace with `"bar"`
170 |
171 |
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `f"{b''}"` instead of `f"{b''}" or ...`
--> SIM222.py:202:7
|
201 | # https://github.com/astral-sh/ruff/issues/20703
202 | print(f"{b''}" or "bar") # SIM222
| ^^^^^^^^^^^^^^^^^
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
|
help: Replace with `f"{b''}"`
199 | def f(a: "'b' or 'c'"): ...
200 |
201 | # https://github.com/astral-sh/ruff/issues/20703
- print(f"{b''}" or "bar") # SIM222
202 + print(f"{b''}") # SIM222
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `f"{x=}"` instead of `f"{x=}" or ...`
--> SIM222.py:204:7
|
202 | print(f"{b''}" or "bar") # SIM222
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
| ^^^^^^^^^^^^^^^^
205 | (lambda: 1) or True # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
|
help: Replace with `f"{x=}"`
201 | # https://github.com/astral-sh/ruff/issues/20703
202 | print(f"{b''}" or "bar") # SIM222
203 | x = 1
- print(f"{x=}" or "bar") # SIM222
204 + print(f"{x=}") # SIM222
205 | (lambda: 1) or True # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `lambda: 1` instead of `lambda: 1 or ...`
--> SIM222.py:205:1
|
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
| ^^^^^^^^^^^^^^^^^^^
206 | (i for i in range(1)) or "bar" # SIM222
|
help: Replace with `lambda: 1`
202 | print(f"{b''}" or "bar") # SIM222
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
- (lambda: 1) or True # SIM222
205 + lambda: 1 # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `(i for i in range(1))` instead of `(i for i in range(1)) or ...`
--> SIM222.py:206:1
|
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with `(i for i in range(1))`
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
- (i for i in range(1)) or "bar" # SIM222
206 + (i for i in range(1)) # SIM222
note: This is an unsafe fix and may change runtime behavior

View File

@@ -223,7 +223,7 @@ enum Argumentable {
impl Argumentable {
fn check_for(self, checker: &Checker, name: String, range: TextRange) {
let mut diagnostic = match self {
match self {
Self::Function => checker.report_diagnostic(UnusedFunctionArgument { name }, range),
Self::Method => checker.report_diagnostic(UnusedMethodArgument { name }, range),
Self::ClassMethod => {
@@ -234,7 +234,6 @@ impl Argumentable {
}
Self::Lambda => checker.report_diagnostic(UnusedLambdaArgument { name }, range),
};
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary);
}
const fn rule_code(self) -> Rule {

View File

@@ -80,7 +80,6 @@ pub(crate) fn deprecated_function(checker: &Checker, expr: &Expr) {
},
expr.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from("numpy", replacement),

View File

@@ -80,7 +80,6 @@ pub(crate) fn deprecated_type_alias(checker: &Checker, expr: &Expr) {
},
expr.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
let type_name = match type_name {
"unicode" => "str",
_ => type_name,

View File

@@ -1,6 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Stmt};
use crate::Violation;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_text_size::Ranged;
use crate::{Violation, checkers::ast::Checker};
/// ## What it does
/// Checks for `break` statements outside of loops.
@@ -26,3 +29,28 @@ impl Violation for BreakOutsideLoop {
"`break` outside loop".to_string()
}
}
/// F701
pub(crate) fn break_outside_loop<'a>(
checker: &Checker,
stmt: &'a Stmt,
parents: &mut impl Iterator<Item = &'a Stmt>,
) {
let mut child = stmt;
for parent in parents {
match parent {
Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return;
}
}
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {
break;
}
_ => {}
}
child = parent;
}
checker.report_diagnostic(BreakOutsideLoop, stmt.range());
}

View File

@@ -1,6 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Stmt};
use crate::Violation;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_text_size::Ranged;
use crate::{Violation, checkers::ast::Checker};
/// ## What it does
/// Checks for `continue` statements outside of loops.
@@ -26,3 +29,28 @@ impl Violation for ContinueOutsideLoop {
"`continue` not properly in loop".to_string()
}
}
/// F702
pub(crate) fn continue_outside_loop<'a>(
checker: &Checker,
stmt: &'a Stmt,
parents: &mut impl Iterator<Item = &'a Stmt>,
) {
let mut child = stmt;
for parent in parents {
match parent {
Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return;
}
}
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {
break;
}
_ => {}
}
child = parent;
}
checker.report_diagnostic(ContinueOutsideLoop, stmt.range());
}

View File

@@ -23,17 +23,16 @@ invalid-syntax: missing closing quote in string literal
9 | # Unterminated f-string
|
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:7:6
invalid-syntax: Expected a statement
--> invalid_characters_syntax_error.py:7:7
|
5 | b = '␈'
6 | # Unterminated string
7 | b = '␈
| ^
| ^
8 | b = '␈'
9 | # Unterminated f-string
|
help: Replace with escape sequence
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:8:6
@@ -47,18 +46,6 @@ PLE2510 Invalid unescaped character backspace, use "\b" instead
|
help: Replace with escape sequence
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:10:7
|
8 | b = '␈'
9 | # Unterminated f-string
10 | b = f'␈
| ^
11 | b = f'␈'
12 | # Implicitly concatenated
|
help: Replace with escape sequence
invalid-syntax: f-string: unterminated string
--> invalid_characters_syntax_error.py:10:7
|
@@ -122,12 +109,11 @@ invalid-syntax: missing closing quote in string literal
| ^^
|
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:13:15
invalid-syntax: Expected a statement
--> invalid_characters_syntax_error.py:13:16
|
11 | b = f'␈'
12 | # Implicitly concatenated
13 | b = '␈' f'␈' '␈
| ^
| ^
|
help: Replace with escape sequence

View File

@@ -19,7 +19,7 @@ mod tests {
use crate::rules::{isort, pyupgrade};
use crate::settings::types::PreviewMode;
use crate::test::{test_path, test_snippet};
use crate::{assert_diagnostics, assert_diagnostics_diff, settings};
use crate::{assert_diagnostics, settings};
#[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))]
#[test_case(Rule::ConvertTypedDictFunctionalToClass, Path::new("UP013.py"))]
@@ -126,7 +126,6 @@ mod tests {
}
#[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))]
#[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))]
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}__preview", path.to_string_lossy());
let diagnostics = test_path(
@@ -140,28 +139,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))]
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_1.py"))]
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))]
fn type_var_default_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}__preview_diff", path.to_string_lossy());
assert_diagnostics_diff!(
snapshot,
Path::new("pyupgrade").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Disabled,
..settings::LinterSettings::for_rule(rule_code)
},
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
);
Ok(())
}
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_0.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_1.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_2.pyi"))]

View File

@@ -6,7 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_ast::{self as ast, Expr, Keyword, StringFlags};
use ruff_python_ast::{self as ast, Expr, Keyword};
use ruff_python_literal::format::{
FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate,
};
@@ -430,7 +430,7 @@ pub(crate) fn f_strings(checker: &Checker, call: &ast::ExprCall, summary: &Forma
// dot is the start of an attribute access.
break token.start();
}
TokenKind::String if !token.unwrap_string_flags().is_unclosed() => {
TokenKind::String => {
match FStringConversion::try_convert(token.range(), &mut summary, checker.locator())
{
// If the format string contains side effects that would need to be repeated,

View File

@@ -14,14 +14,13 @@ use ruff_python_ast::{
use ruff_python_semantic::SemanticModel;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::preview::is_type_var_default_enabled;
pub(crate) use non_pep695_generic_class::*;
pub(crate) use non_pep695_generic_function::*;
pub(crate) use non_pep695_type_alias::*;
pub(crate) use private_type_parameter::*;
use crate::checkers::ast::Checker;
mod non_pep695_generic_class;
mod non_pep695_generic_function;
mod non_pep695_type_alias;
@@ -123,10 +122,6 @@ impl Display for DisplayTypeVar<'_> {
}
}
}
if let Some(default) = self.type_var.default {
f.write_str(" = ")?;
f.write_str(&self.source[default.range()])?;
}
Ok(())
}
@@ -138,63 +133,66 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam {
name,
restriction,
kind,
default,
default: _, // TODO(brent) see below
}: &'a TypeVar<'a>,
) -> Self {
let default = default.map(|expr| Box::new(expr.clone()));
match kind {
TypeParamKind::TypeVar => TypeParam::TypeVar(TypeParamTypeVar {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
bound: match restriction {
Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())),
Some(TypeVarRestriction::Constraint(constraints)) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: constraints.iter().map(|expr| (*expr).clone()).collect(),
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
Some(TypeVarRestriction::AnyStr) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: vec![
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("str"),
ctx: ast::ExprContext::Load,
}),
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("bytes"),
ctx: ast::ExprContext::Load,
}),
],
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
None => None,
},
default,
}),
TypeParamKind::TypeVar => {
TypeParam::TypeVar(TypeParamTypeVar {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
bound: match restriction {
Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())),
Some(TypeVarRestriction::Constraint(constraints)) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: constraints.iter().map(|expr| (*expr).clone()).collect(),
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
Some(TypeVarRestriction::AnyStr) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: vec![
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("str"),
ctx: ast::ExprContext::Load,
}),
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("bytes"),
ctx: ast::ExprContext::Load,
}),
],
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
None => None,
},
// We don't handle defaults here yet. Should perhaps be a different rule since
// defaults are only valid in 3.13+.
default: None,
})
}
TypeParamKind::TypeVarTuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
default,
default: None,
}),
TypeParamKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
default,
default: None,
}),
}
}
@@ -320,8 +318,8 @@ pub(crate) fn expr_name_to_type_var<'a>(
.first()
.is_some_and(Expr::is_string_literal_expr)
{
// `default` was added in PEP 696 and Python 3.13. We now support converting
// TypeVars with defaults to PEP 695 type parameters.
// TODO(brent) `default` was added in PEP 696 and Python 3.13 but can't be used in
// generic type parameters before that
//
// ```python
// T = TypeVar("T", default=Any, bound=str)
@@ -369,22 +367,21 @@ fn in_nested_context(checker: &Checker) -> bool {
}
/// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found.
/// Also returns `None` if any `TypeVar` has a default value and preview mode is not enabled.
fn check_type_vars<'a>(vars: Vec<TypeVar<'a>>, checker: &Checker) -> Option<Vec<TypeVar<'a>>> {
fn check_type_vars(vars: Vec<TypeVar<'_>>) -> Option<Vec<TypeVar<'_>>> {
if vars.is_empty() {
return None;
}
// If any type variables have defaults and preview mode is not enabled, skip the rule
if vars.iter().any(|tv| tv.default.is_some())
&& !is_type_var_default_enabled(checker.settings())
{
return None;
}
// If any type variables were not unique, just bail out here. this is a runtime error and we
// can't predict what the user wanted.
(vars.iter().unique_by(|tvar| tvar.name).count() == vars.len()).then_some(vars)
// can't predict what the user wanted. also bail out if any Python 3.13+ default values are
// found on the type parameters
(vars
.iter()
.unique_by(|tvar| tvar.name)
.filter(|tvar| tvar.default.is_none())
.count()
== vars.len())
.then_some(vars)
}
/// Search `class_bases` for a `typing.Generic` base class. Returns the `Generic` expression (if

View File

@@ -186,7 +186,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD
//
// just because we can't confirm that `SomethingElse` is a `TypeVar`
if !visitor.any_skipped {
let Some(type_vars) = check_type_vars(visitor.vars, checker) else {
let Some(type_vars) = check_type_vars(visitor.vars) else {
diagnostic.defuse();
return;
};

View File

@@ -154,7 +154,7 @@ pub(crate) fn non_pep695_generic_function(checker: &Checker, function_def: &Stmt
}
}
let Some(type_vars) = check_type_vars(type_vars, checker) else {
let Some(type_vars) = check_type_vars(type_vars) else {
return;
};

View File

@@ -8,7 +8,6 @@ use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssi
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::preview::is_type_var_default_enabled;
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion;
@@ -233,10 +232,8 @@ pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) {
.unique_by(|tvar| tvar.name)
.collect::<Vec<_>>();
// Skip if any TypeVar has defaults and preview mode is not enabled
if vars.iter().any(|tv| tv.default.is_some())
&& !is_type_var_default_enabled(checker.settings())
{
// TODO(brent) handle `default` arg for Python 3.13+
if vars.iter().any(|tv| tv.default.is_some()) {
return;
}

View File

@@ -1,19 +1,15 @@
use ruff_python_ast::Expr;
use std::fmt::{Display, Formatter};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_typing_extensions_str_alias_enabled;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `typing.Text`.
///
/// In preview mode, also checks for `typing_extensions.Text`.
///
/// ## Why is this bad?
/// `typing.Text` is an alias for `str`, and only exists for Python 2
/// compatibility. As of Python 3.11, `typing.Text` is deprecated. Use `str`
@@ -34,16 +30,14 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// ## References
/// - [Python documentation: `typing.Text`](https://docs.python.org/3/library/typing.html#typing.Text)
#[derive(ViolationMetadata)]
pub(crate) struct TypingTextStrAlias {
module: TypingModule,
}
pub(crate) struct TypingTextStrAlias;
impl Violation for TypingTextStrAlias {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("`{}.Text` is deprecated, use `str`", self.module)
"`typing.Text` is deprecated, use `str`".to_string()
}
fn fix_title(&self) -> Option<String> {
@@ -53,26 +47,16 @@ impl Violation for TypingTextStrAlias {
/// UP019
pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) {
if !checker
.semantic()
.seen_module(Modules::TYPING | Modules::TYPING_EXTENSIONS)
{
if !checker.semantic().seen_module(Modules::TYPING) {
return;
}
if let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) {
let segments = qualified_name.segments();
let module = match segments {
["typing", "Text"] => TypingModule::Typing,
["typing_extensions", "Text"]
if is_typing_extensions_str_alias_enabled(checker.settings()) =>
{
TypingModule::TypingExtensions
}
_ => return,
};
let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias { module }, expr.range());
if checker
.semantic()
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["typing", "Text"]))
{
let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias, expr.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
@@ -87,18 +71,3 @@ pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) {
});
}
}
#[derive(Copy, Clone, Debug)]
enum TypingModule {
Typing,
TypingExtensions,
}
impl Display for TypingModule {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TypingModule::Typing => f.write_str("typing"),
TypingModule::TypingExtensions => f.write_str("typing_extensions"),
}
}
}

View File

@@ -66,5 +66,3 @@ help: Replace with `str`
- def print_fourth_word(word: Goodbye) -> None:
19 + def print_fourth_word(word: str) -> None:
20 | print(word)
21 |
22 |

View File

@@ -1,119 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:7:22
|
7 | def print_word(word: Text) -> None:
| ^^^^
8 | print(word)
|
help: Replace with `str`
4 | from typing import Text as Goodbye
5 |
6 |
- def print_word(word: Text) -> None:
7 + def print_word(word: str) -> None:
8 | print(word)
9 |
10 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:11:29
|
11 | def print_second_word(word: typing.Text) -> None:
| ^^^^^^^^^^^
12 | print(word)
|
help: Replace with `str`
8 | print(word)
9 |
10 |
- def print_second_word(word: typing.Text) -> None:
11 + def print_second_word(word: str) -> None:
12 | print(word)
13 |
14 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:15:28
|
15 | def print_third_word(word: Hello.Text) -> None:
| ^^^^^^^^^^
16 | print(word)
|
help: Replace with `str`
12 | print(word)
13 |
14 |
- def print_third_word(word: Hello.Text) -> None:
15 + def print_third_word(word: str) -> None:
16 | print(word)
17 |
18 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:19:29
|
19 | def print_fourth_word(word: Goodbye) -> None:
| ^^^^^^^
20 | print(word)
|
help: Replace with `str`
16 | print(word)
17 |
18 |
- def print_fourth_word(word: Goodbye) -> None:
19 + def print_fourth_word(word: str) -> None:
20 | print(word)
21 |
22 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:28:28
|
28 | def print_fifth_word(word: typing_extensions.Text) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^
29 | print(word)
|
help: Replace with `str`
25 | from typing_extensions import Text as TextAlias
26 |
27 |
- def print_fifth_word(word: typing_extensions.Text) -> None:
28 + def print_fifth_word(word: str) -> None:
29 | print(word)
30 |
31 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:32:28
|
32 | def print_sixth_word(word: TypingExt.Text) -> None:
| ^^^^^^^^^^^^^^
33 | print(word)
|
help: Replace with `str`
29 | print(word)
30 |
31 |
- def print_sixth_word(word: TypingExt.Text) -> None:
32 + def print_sixth_word(word: str) -> None:
33 | print(word)
34 |
35 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:36:30
|
36 | def print_seventh_word(word: TextAlias) -> None:
| ^^^^^^^^^
37 | print(word)
|
help: Replace with `str`
33 | print(word)
34 |
35 |
- def print_seventh_word(word: TextAlias) -> None:
36 + def print_seventh_word(word: str) -> None:
37 | print(word)

View File

@@ -217,7 +217,7 @@ UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keywo
44 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45 |
46 | # `default` was added in Python 3.13
46 | # `default` should be skipped for now, added in Python 3.13
|
help: Use the `type` keyword
41 |
@@ -226,7 +226,7 @@ help: Use the `type` keyword
- x: typing.TypeAlias = list[T]
44 + type x = list[T]
45 |
46 | # `default` was added in Python 3.13
46 | # `default` should be skipped for now, added in Python 3.13
47 | T = typing.TypeVar("T", default=Any)
note: This is an unsafe fix and may change runtime behavior
@@ -355,26 +355,6 @@ help: Use the `type` keyword
87 | # OK: Other name
88 | T = TypeVar("T", bound=SupportGt)
UP040 [*] Type alias `AnyList` uses `TypeAliasType` assignment instead of the `type` keyword
--> UP040.py:95:1
|
93 | # `default` was added in Python 3.13
94 | T = typing.TypeVar("T", default=Any)
95 | AnyList = TypeAliasType("AnyList", list[T], type_params=(T,))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
96 |
97 | # unsafe fix if comments within the fix
|
help: Use the `type` keyword
92 |
93 | # `default` was added in Python 3.13
94 | T = typing.TypeVar("T", default=Any)
- AnyList = TypeAliasType("AnyList", list[T], type_params=(T,))
95 + type AnyList[T = Any] = list[T]
96 |
97 | # unsafe fix if comments within the fix
98 | T = TypeVar("T")
UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword
--> UP040.py:99:1
|
@@ -489,8 +469,6 @@ UP040 [*] Type alias `T` uses `TypeAlias` annotation instead of the `type` keywo
129 | | # comment7
130 | | ) # comment8
| |_^
131 |
132 | # Test case for TypeVar with default - should be converted when preview mode is enabled
|
help: Use the `type` keyword
119 | | str

View File

@@ -1,49 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 2
--- Added ---
UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
--> UP040.py:48:1
|
46 | # `default` was added in Python 3.13
47 | T = typing.TypeVar("T", default=Any)
48 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 |
50 | # OK
|
help: Use the `type` keyword
45 |
46 | # `default` was added in Python 3.13
47 | T = typing.TypeVar("T", default=Any)
- x: typing.TypeAlias = list[T]
48 + type x[T = Any] = list[T]
49 |
50 | # OK
51 | x: TypeAlias
note: This is an unsafe fix and may change runtime behavior
UP040 [*] Type alias `DefaultList` uses `TypeAlias` annotation instead of the `type` keyword
--> UP040.py:134:1
|
132 | # Test case for TypeVar with default - should be converted when preview mode is enabled
133 | T_default = TypeVar("T_default", default=int)
134 | DefaultList: TypeAlias = list[T_default]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use the `type` keyword
131 |
132 | # Test case for TypeVar with default - should be converted when preview mode is enabled
133 | T_default = TypeVar("T_default", default=int)
- DefaultList: TypeAlias = list[T_default]
134 + type DefaultList[T_default = int] = list[T_default]
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,10 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 0

View File

@@ -1,48 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 2
--- Added ---
UP046 [*] Generic class `DefaultTypeVar` uses `Generic` subclass instead of type parameters
--> UP046_0.py:129:22
|
129 | class DefaultTypeVar(Generic[V]): # -> [V: str = Any]
| ^^^^^^^^^^
130 | var: V
|
help: Use type parameters
126 | V = TypeVar("V", default=Any, bound=str)
127 |
128 |
- class DefaultTypeVar(Generic[V]): # -> [V: str = Any]
129 + class DefaultTypeVar[V: str = Any]: # -> [V: str = Any]
130 | var: V
131 |
132 |
note: This is an unsafe fix and may change runtime behavior
UP046 [*] Generic class `DefaultOnlyTypeVar` uses `Generic` subclass instead of type parameters
--> UP046_0.py:137:26
|
137 | class DefaultOnlyTypeVar(Generic[W]): # -> [W = int]
| ^^^^^^^^^^
138 | var: W
|
help: Use type parameters
134 | W = TypeVar("W", default=int)
135 |
136 |
- class DefaultOnlyTypeVar(Generic[W]): # -> [W = int]
137 + class DefaultOnlyTypeVar[W = int]: # -> [W = int]
138 | var: W
139 |
140 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,10 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 0

View File

@@ -1,29 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 1
--- Added ---
UP047 [*] Generic function `default_var` should use type parameters
--> UP047.py:51:5
|
51 | def default_var(v: V) -> V:
| ^^^^^^^^^^^^^^^^^
52 | return v
|
help: Use type parameters
48 | V = TypeVar("V", default=Any, bound=str)
49 |
50 |
- def default_var(v: V) -> V:
51 + def default_var[V: str = Any](v: V) -> V:
52 | return v
53 |
54 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -48,7 +48,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
/// element. As such, any side effects that occur during iteration will be
/// delayed.
/// 2. Second, accessing members of a collection via square bracket notation
/// `[0]` or the `pop()` function will raise `IndexError` if the collection
/// `[0]` of the `pop()` function will raise `IndexError` if the collection
/// is empty, while `next(iter(...))` will raise `StopIteration`.
///
/// ## References

View File

@@ -12,7 +12,7 @@ use crate::parenthesize::parenthesized_range;
use crate::statement_visitor::StatementVisitor;
use crate::visitor::Visitor;
use crate::{
self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr, ExprNoneLiteral,
self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr,
InterpolatedStringElement, MatchCase, Operator, Pattern, Stmt, TypeParam,
};
use crate::{AnyNodeRef, ExprContext};
@@ -1219,8 +1219,6 @@ impl Truthiness {
F: Fn(&str) -> bool,
{
match expr {
Expr::Lambda(_) => Self::Truthy,
Expr::Generator(_) => Self::Truthy,
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if value.is_empty() {
Self::Falsey
@@ -1390,9 +1388,7 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
Expr::FString(f_string) => is_non_empty_f_string(f_string),
// These literals may or may not be empty.
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(),
// Confusingly, f"{b""}" renders as the string 'b""', which is non-empty.
// Therefore, any bytes interpolation is guaranteed non-empty when stringified.
Expr::BytesLiteral(_) => true,
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(),
}
}
@@ -1401,9 +1397,7 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
ast::FStringPart::FString(f_string) => {
f_string.elements.iter().all(|element| match element {
InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(),
InterpolatedStringElement::Interpolation(f_string) => {
f_string.debug_text.is_some() || inner(&f_string.expression)
}
InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression),
})
}
})
@@ -1499,7 +1493,7 @@ pub fn pep_604_optional(expr: &Expr) -> Expr {
ast::ExprBinOp {
left: Box::new(expr.clone()),
op: Operator::BitOr,
right: Box::new(Expr::NoneLiteral(ExprNoneLiteral::default())),
right: Box::new(Expr::NoneLiteral(ast::ExprNoneLiteral::default())),
range: TextRange::default(),
node_index: AtomicNodeIndex::NONE,
}

View File

@@ -735,8 +735,6 @@ pub trait StringFlags: Copy {
fn prefix(self) -> AnyStringPrefix;
fn is_unclosed(self) -> bool;
/// Is the string triple-quoted, i.e.,
/// does it begin and end with three consecutive quote characters?
fn is_triple_quoted(self) -> bool {
@@ -781,7 +779,6 @@ pub trait StringFlags: Copy {
fn as_any_string_flags(self) -> AnyStringFlags {
AnyStringFlags::new(self.prefix(), self.quote_style(), self.triple_quotes())
.with_unclosed(self.is_unclosed())
}
fn display_contents(self, contents: &str) -> DisplayFlags<'_> {
@@ -832,10 +829,6 @@ bitflags! {
/// for why we track the casing of the `r` prefix,
/// but not for any other prefix
const R_PREFIX_UPPER = 1 << 3;
/// The f-string is unclosed, meaning it is missing a closing quote.
/// For example: `f"{bar`
const UNCLOSED = 1 << 4;
}
}
@@ -894,12 +887,6 @@ impl FStringFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(InterpolatedStringFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: FStringPrefix) -> Self {
match prefix {
@@ -997,12 +984,6 @@ impl TStringFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(InterpolatedStringFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: TStringPrefix) -> Self {
match prefix {
@@ -1070,10 +1051,6 @@ impl StringFlags for FStringFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Format(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(InterpolatedStringFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for FStringFlags {
@@ -1082,7 +1059,6 @@ impl fmt::Debug for FStringFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -1114,10 +1090,6 @@ impl StringFlags for TStringFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Template(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(InterpolatedStringFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for TStringFlags {
@@ -1126,7 +1098,6 @@ impl fmt::Debug for TStringFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -1456,9 +1427,6 @@ bitflags! {
/// The string was deemed invalid by the parser.
const INVALID = 1 << 5;
/// The string literal misses the matching closing quote(s).
const UNCLOSED = 1 << 6;
}
}
@@ -1511,12 +1479,6 @@ impl StringLiteralFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(StringLiteralFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(self, prefix: StringLiteralPrefix) -> Self {
let StringLiteralFlags(flags) = self;
@@ -1598,10 +1560,6 @@ impl StringFlags for StringLiteralFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Regular(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(StringLiteralFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for StringLiteralFlags {
@@ -1610,7 +1568,6 @@ impl fmt::Debug for StringLiteralFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -1889,9 +1846,6 @@ bitflags! {
/// The bytestring was deemed invalid by the parser.
const INVALID = 1 << 4;
/// The byte string misses the matching closing quote(s).
const UNCLOSED = 1 << 5;
}
}
@@ -1943,12 +1897,6 @@ impl BytesLiteralFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(BytesLiteralFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: ByteStringPrefix) -> Self {
match prefix {
@@ -2011,10 +1959,6 @@ impl StringFlags for BytesLiteralFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Bytes(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(BytesLiteralFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for BytesLiteralFlags {
@@ -2023,7 +1967,6 @@ impl fmt::Debug for BytesLiteralFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -2085,7 +2028,7 @@ bitflags! {
/// prefix flags is by calling the `as_flags()` method on the
/// `StringPrefix` enum.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
struct AnyStringFlagsInner: u16 {
struct AnyStringFlagsInner: u8 {
/// The string uses double quotes (`"`).
/// If this flag is not set, the string uses single quotes (`'`).
const DOUBLE = 1 << 0;
@@ -2128,9 +2071,6 @@ bitflags! {
/// for why we track the casing of the `r` prefix,
/// but not for any other prefix
const R_PREFIX_UPPER = 1 << 7;
/// String without matching closing quote(s).
const UNCLOSED = 1 << 8;
}
}
@@ -2226,12 +2166,6 @@ impl AnyStringFlags {
.set(AnyStringFlagsInner::TRIPLE_QUOTED, triple_quotes.is_yes());
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(AnyStringFlagsInner::UNCLOSED, unclosed);
self
}
}
impl StringFlags for AnyStringFlags {
@@ -2300,10 +2234,6 @@ impl StringFlags for AnyStringFlags {
}
AnyStringPrefix::Regular(StringLiteralPrefix::Empty)
}
fn is_unclosed(self) -> bool {
self.0.intersects(AnyStringFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for AnyStringFlags {
@@ -2312,7 +2242,6 @@ impl fmt::Debug for AnyStringFlags {
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("quote_style", &self.quote_style())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -2329,7 +2258,6 @@ impl From<AnyStringFlags> for StringLiteralFlags {
.with_quote_style(value.quote_style())
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}
@@ -2351,7 +2279,6 @@ impl From<AnyStringFlags> for BytesLiteralFlags {
.with_quote_style(value.quote_style())
.with_prefix(bytestring_prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}
@@ -2373,7 +2300,6 @@ impl From<AnyStringFlags> for FStringFlags {
.with_quote_style(value.quote_style())
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}
@@ -2395,7 +2321,6 @@ impl From<AnyStringFlags> for TStringFlags {
.with_quote_style(value.quote_style())
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}

View File

@@ -1,7 +0,0 @@
# regression test for #1765
class Foo:
def foo(self):
if True:
content_ids: Mapping[
str, Optional[ContentId]
] = self.publisher_content_store.store_config_contents(files)

View File

@@ -1,7 +0,0 @@
# regression test for #1765
class Foo:
def foo(self):
if True:
content_ids: Mapping[str, Optional[ContentId]] = (
self.publisher_content_store.store_config_contents(files)
)

View File

@@ -1 +1 @@
{"target_version": "3.10"}
{"target_version": "3.10"}

View File

@@ -1,35 +0,0 @@
# long variable name
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 0
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 1 # with a comment
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = [
1, 2, 3
]
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function()
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
arg1, arg2, arg3
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
# long function name
normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying()
normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
arg1, arg2, arg3
)
normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
string_variable_name = (
"a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa
)
for key in """
hostname
port
username
""".split():
if key in self.connect_kwargs:
raise ValueError(err.format(key))
concatenated_strings = "some strings that are " "concatenated implicitly, so if you put them on separate " "lines it will fit"
del concatenated_strings, string_variable_name, normal_function_name, normal_name, need_more_to_make_the_line_long_enough
del ([], name_1, name_2), [(), [], name_4, name_3], name_1[[name_2 for name_1 in name_0]]
del (),

View File

@@ -1,61 +0,0 @@
# long variable name
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = (
0
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = (
1 # with a comment
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = [
1,
2,
3,
]
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = (
function()
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
arg1, arg2, arg3
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
# long function name
normal_name = (
but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying()
)
normal_name = (
but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
arg1, arg2, arg3
)
)
normal_name = (
but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
)
string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa
for key in """
hostname
port
username
""".split():
if key in self.connect_kwargs:
raise ValueError(err.format(key))
concatenated_strings = (
"some strings that are "
"concatenated implicitly, so if you put them on separate "
"lines it will fit"
)
del (
concatenated_strings,
string_variable_name,
normal_function_name,
normal_name,
need_more_to_make_the_line_long_enough,
)
del (
([], name_1, name_2),
[(), [], name_4, name_3],
name_1[[name_2 for name_1 in name_0]],
)
del ((),)

View File

@@ -1 +1 @@
{"target_version": "3.8"}
{"target_version": "3.8"}

View File

@@ -82,9 +82,3 @@ async def func():
argument1, argument2, argument3="some_value"
):
pass
# don't remove the brackets here, it changes the meaning of the code.
with (x, y) as z:
pass

View File

@@ -83,8 +83,3 @@ async def func():
some_other_function(argument1, argument2, argument3="some_value"),
):
pass
# don't remove the brackets here, it changes the meaning of the code.
with (x, y) as z:
pass

View File

@@ -1,3 +0,0 @@
"""
87 characters ............................................................................
"""

View File

@@ -1,3 +0,0 @@
"""
87 characters ............................................................................
"""

View File

@@ -1,8 +0,0 @@
def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip

View File

@@ -1,8 +0,0 @@
def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip

View File

@@ -1,6 +0,0 @@
def foo():
pass
# comment 1 # fmt: skip
# comment 2

View File

@@ -1,6 +0,0 @@
def foo():
pass
# comment 1 # fmt: skip
# comment 2

View File

@@ -1,15 +0,0 @@
x = "\x1F"
x = "\\x1B"
x = "\\\x1B"
x = "\U0001F60E"
x = "\u0001F60E"
x = r"\u0001F60E"
x = "don't format me"
x = "\xA3"
x = "\u2717"
x = "\uFaCe"
x = "\N{ox}\N{OX}"
x = "\N{lAtIn smaLL letteR x}"
x = "\N{CYRILLIC small LETTER BYELORUSSIAN-UKRAINIAN I}"
x = b"\x1Fdon't byte"
x = rb"\x1Fdon't format"

View File

@@ -1,15 +0,0 @@
x = "\x1f"
x = "\\x1B"
x = "\\\x1b"
x = "\U0001f60e"
x = "\u0001F60E"
x = r"\u0001F60E"
x = "don't format me"
x = "\xa3"
x = "\u2717"
x = "\uface"
x = "\N{OX}\N{OX}"
x = "\N{LATIN SMALL LETTER X}"
x = "\N{CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I}"
x = b"\x1fdon't byte"
x = rb"\x1Fdon't format"

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