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
602 changed files with 8526 additions and 24120 deletions

View File

@@ -1,12 +1,3 @@
# Define serial test group for running tests sequentially.
[test-groups]
serial = { max-threads = 1 }
# Run ty file watching tests sequentially to avoid race conditions.
[[profile.default.overrides]]
filter = 'binary(file_watching)'
test-group = 'serial'
[profile.ci]
# Print out output for failing tests as soon as they fail, and also at the end
# of the run (for easy scrollability).

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

@@ -12,10 +12,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
defaults:
run:
shell: bash
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
@@ -23,7 +19,6 @@ env:
RUSTUP_MAX_RETRIES: 10
PACKAGE_NAME: ruff
PYTHON_VERSION: "3.13"
NEXTEST_PROFILE: ci
jobs:
determine_changes:
@@ -147,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
@@ -242,7 +237,7 @@ jobs:
cargo-test-linux:
name: "cargo test (linux)"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
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
@@ -276,6 +271,9 @@ jobs:
# This step is just to get nice GitHub annotations on the PR diff in the files-changed tab.
run: cargo test -p ty_python_semantic --test mdtest || true
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
# Check for broken links in the documentation.
@@ -301,13 +299,9 @@ jobs:
cargo-test-linux-release:
name: "cargo test (linux, release)"
# release builds timeout on GitHub runners, so this job is just skipped on forks in the `if` check
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
!contains(github.event.pull_request.labels.*.name, 'no-test') &&
(needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main')
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
@@ -331,11 +325,14 @@ jobs:
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
cargo-test-windows:
name: "cargo test (windows)"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}
runs-on: depot-windows-2022-16
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
@@ -355,41 +352,15 @@ jobs:
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
run: |
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"
run: |
cargo nextest run --all-features --profile ci
cargo test --all-features --doc
cargo-test-wasm:
name: "cargo test (wasm)"
runs-on: ubuntu-latest
@@ -420,9 +391,26 @@ 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: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-latest-8
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
@@ -443,6 +431,7 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Build tests"
shell: bash
env:
MSRV: ${{ steps.msrv.outputs.value }}
run: cargo "+${MSRV}" test --no-run --all-features
@@ -463,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
@@ -535,7 +524,7 @@ jobs:
ecosystem:
name: "ecosystem"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-latest-8
needs:
- cargo-test-linux
- determine_changes
@@ -660,7 +649,7 @@ jobs:
fuzz-ty:
name: "Fuzz for new ty panics"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
needs:
- cargo-test-linux
- determine_changes
@@ -714,13 +703,13 @@ 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
ty-completion-evaluation:
name: "ty completion evaluation"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: ${{ needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main' }}
steps:
@@ -732,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
@@ -766,7 +755,7 @@ jobs:
pre-commit:
name: "pre-commit"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -940,12 +929,8 @@ jobs:
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
(
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.formatter == 'true' ||
needs.determine_changes.outputs.linter == 'true'
)
github.ref == 'refs/heads/main' ||
(needs.determine_changes.outputs.formatter == 'true' || needs.determine_changes.outputs.linter == 'true')
timeout-minutes: 20
steps:
- name: "Checkout Branch"
@@ -968,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
@@ -979,11 +964,8 @@ jobs:
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
(
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.ty == 'true'
)
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.ty == 'true'
timeout-minutes: 20
steps:
- name: "Checkout Branch"
@@ -1006,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
@@ -1044,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
@@ -1052,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

@@ -19,10 +19,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
defaults:
run:
shell: bash
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
@@ -33,7 +29,7 @@ env:
jobs:
mypy_primer:
name: Run mypy_primer
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -53,6 +49,7 @@ jobs:
run: rustup show
- name: Run mypy_primer
shell: bash
env:
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
DIFF_FILE: mypy_primer.diff
@@ -75,7 +72,7 @@ jobs:
memory_usage:
name: Run memory statistics
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -95,6 +92,7 @@ jobs:
run: rustup show
- name: Run mypy_primer
shell: bash
env:
TY_MAX_PARALLELISM: 1 # for deterministic memory numbers
TY_MEMORY_REPORT: mypy_primer

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:
@@ -28,18 +26,8 @@ on:
# Run on the 1st and the 15th of every month:
- cron: "0 0 1,15 * *"
defaults:
run:
shell: bash
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
NEXTEST_PROFILE: "ci"
FORCE_COLOR: 1
GH_TOKEN: ${{ github.token }}
# The name of the upstream branch that the first worker creates,
@@ -98,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
@@ -138,8 +124,7 @@ jobs:
git config --global user.email '<>'
- name: Sync Windows docstrings
id: docstrings
env:
FORCE_COLOR: 1
shell: bash
run: ./scripts/codemod_docstrings.sh
- name: Commit the changes
if: ${{ steps.docstrings.outcome == 'success' }}
@@ -176,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

@@ -22,7 +22,7 @@ env:
jobs:
ty-ecosystem-analyzer:
name: Compute diagnostic diff
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
if: contains(github.event.label.name, 'ecosystem-analyzer')
steps:
@@ -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

@@ -19,7 +19,7 @@ env:
jobs:
ty-ecosystem-report:
name: Create ecosystem report
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -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

View File

@@ -29,7 +29,7 @@ env:
jobs:
typing_conformance:
name: Compute diagnostic diff
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

View File

@@ -1,63 +1,5 @@
# Changelog
## 0.14.1
Released on 2025-10-16.
### Preview features
- [formatter] Remove parentheses around multiple exception types on Python 3.14+ ([#20768](https://github.com/astral-sh/ruff/pull/20768))
- \[`flake8-bugbear`\] Omit annotation in preview fix for `B006` ([#20877](https://github.com/astral-sh/ruff/pull/20877))
- \[`flake8-logging-format`\] Avoid dropping implicitly concatenated pieces in the `G004` fix ([#20793](https://github.com/astral-sh/ruff/pull/20793))
- \[`pydoclint`\] Implement `docstring-extraneous-parameter` (`DOC102`) ([#20376](https://github.com/astral-sh/ruff/pull/20376))
- \[`pyupgrade`\] Extend `UP019` to detect `typing_extensions.Text` (`UP019`) ([#20825](https://github.com/astral-sh/ruff/pull/20825))
- \[`pyupgrade`\] Fix false negative for `TypeVar` with default argument in `non-pep695-generic-class` (`UP046`) ([#20660](https://github.com/astral-sh/ruff/pull/20660))
### Bug fixes
- Fix false negatives in `Truthiness::from_expr` for lambdas, generators, and f-strings ([#20704](https://github.com/astral-sh/ruff/pull/20704))
- Fix syntax error false positives for escapes and quotes in f-strings ([#20867](https://github.com/astral-sh/ruff/pull/20867))
- Fix syntax error false positives on parenthesized context managers ([#20846](https://github.com/astral-sh/ruff/pull/20846))
- \[`fastapi`\] Fix false positives for path parameters that FastAPI doesn't recognize (`FAST003`) ([#20687](https://github.com/astral-sh/ruff/pull/20687))
- \[`flake8-pyi`\] Fix operator precedence by adding parentheses when needed (`PYI061`) ([#20508](https://github.com/astral-sh/ruff/pull/20508))
- \[`ruff`\] Suppress diagnostic for f-string interpolations with debug text (`RUF010`) ([#20525](https://github.com/astral-sh/ruff/pull/20525))
### Rule changes
- \[`airflow`\] Add warning to `airflow.datasets.DatasetEvent` usage (`AIR301`) ([#20551](https://github.com/astral-sh/ruff/pull/20551))
- \[`flake8-bugbear`\] Mark `B905` and `B912` fixes as unsafe ([#20695](https://github.com/astral-sh/ruff/pull/20695))
- Use `DiagnosticTag` for more rules - changes display in editors ([#20758](https://github.com/astral-sh/ruff/pull/20758),[#20734](https://github.com/astral-sh/ruff/pull/20734))
### Documentation
- Update Python compatibility from 3.13 to 3.14 in README.md ([#20852](https://github.com/astral-sh/ruff/pull/20852))
- Update `lint.flake8-type-checking.quoted-annotations` docs ([#20765](https://github.com/astral-sh/ruff/pull/20765))
- Update setup instructions for Zed 0.208.0+ ([#20902](https://github.com/astral-sh/ruff/pull/20902))
- \[`flake8-datetimez`\] Clarify docs for several rules ([#20778](https://github.com/astral-sh/ruff/pull/20778))
- Fix typo in `RUF015` description ([#20873](https://github.com/astral-sh/ruff/pull/20873))
### Other changes
- Reduce binary size ([#20863](https://github.com/astral-sh/ruff/pull/20863))
- Improved error recovery for unclosed strings (including f- and t-strings) ([#20848](https://github.com/astral-sh/ruff/pull/20848))
### Contributors
- [@ntBre](https://github.com/ntBre)
- [@Paillat-dev](https://github.com/Paillat-dev)
- [@terror](https://github.com/terror)
- [@pieterh-oai](https://github.com/pieterh-oai)
- [@MichaReiser](https://github.com/MichaReiser)
- [@TaKO8Ki](https://github.com/TaKO8Ki)
- [@ageorgou](https://github.com/ageorgou)
- [@danparizher](https://github.com/danparizher)
- [@mgaitan](https://github.com/mgaitan)
- [@augustelalande](https://github.com/augustelalande)
- [@dylwil3](https://github.com/dylwil3)
- [@Lee-W](https://github.com/Lee-W)
- [@injust](https://github.com/injust)
- [@CarrotManMatt](https://github.com/CarrotManMatt)
## 0.14.0
Released on 2025-10-07.

166
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",
@@ -2815,12 +2759,12 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.1"
version = "0.14.0"
dependencies = [
"anyhow",
"argfile",
"assert_fs",
"bincode",
"bincode 2.0.1",
"bitflags 2.9.4",
"cachedir",
"clap",
@@ -3072,7 +3016,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.1"
version = "0.14.0"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3426,7 +3370,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.1"
version = "0.14.0"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -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)
@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.1/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.0/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.1
rev: v0.14.0
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

@@ -13,6 +13,7 @@ use log::{debug, warn};
use ruff_db::diagnostic::Diagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only};
use ruff_linter::message::create_syntax_error_diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::settings::types::UnsafeFixes;
@@ -102,7 +103,11 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Diagnostic::invalid_syntax(dummy, err, TextRange::default())],
vec![create_syntax_error_diagnostic(
dummy,
err,
TextRange::default(),
)],
FxHashMap::default(),
)
}

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};
@@ -84,14 +83,17 @@ impl Diagnostic {
/// at time of writing, `ruff_db` depends on `ruff_python_parser` instead of
/// the other way around. And since we want to do this conversion in a couple
/// places, it makes sense to centralize it _somewhere_. So it's here for now.
///
/// Note that `message` is stored in the primary annotation, _not_ in the primary diagnostic
/// message.
pub fn invalid_syntax(
span: impl Into<Span>,
message: impl IntoDiagnosticMessage,
range: impl Ranged,
) -> Diagnostic {
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, "");
let span = span.into().with_range(range.range());
diag.annotate(Annotation::primary(span));
diag.annotate(Annotation::primary(span).message(message));
diag
}

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

@@ -193,17 +193,6 @@ impl Files {
roots.at(&absolute)
}
/// The same as [`Self::root`] but panics if no root is found.
#[track_caller]
pub fn expect_root(&self, db: &dyn Db, path: &SystemPath) -> FileRoot {
if let Some(root) = self.root(db, path) {
return root;
}
let roots = self.inner.roots.read().unwrap();
panic!("No root found for path '{path}'. Known roots: {roots:#?}");
}
/// Adds a new root for `path` and returns the root.
///
/// The root isn't added nor is the file root's kind updated if a root for `path` already exists.

View File

@@ -81,8 +81,6 @@ impl FileRoots {
}
}
tracing::debug!("Adding new file root '{path}' of kind {kind:?}");
// normalize the path to use `/` separators and escape the '{' and '}' characters,
// which matchit uses for routing parameters
let mut route = normalized_path.replace('{', "{{").replace('}', "}}");

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

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

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

@@ -1,264 +0,0 @@
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Args:
a (int): The first number to add.
b (int): The second number to add.
Returns:
int: The sum of the two numbers.
"""
return a + b
# DOC102
def multiply_list_elements(lst):
"""
Multiplies each element in a list by a given multiplier.
Args:
lst (list of int): A list of integers.
multiplier (int): The multiplier for each element in the list.
Returns:
list of int: A new list with each element multiplied.
"""
return [x * multiplier for x in lst]
# DOC102
def find_max_value():
"""
Finds the maximum value in a list of numbers.
Args:
numbers (list of int): A list of integers to search through.
Returns:
int: The maximum value found in the list.
"""
return max(numbers)
# DOC102
def create_user_profile(location="here"):
"""
Creates a user profile with basic information.
Args:
name (str): The name of the user.
age (int): The age of the user.
email (str): The user's email address.
location (str): The location of the user.
Returns:
dict: A dictionary containing the user's profile.
"""
return {
'name': name,
'age': age,
'email': email,
'location': location
}
# DOC102
def calculate_total_price(item_prices, discount):
"""
Calculates the total price after applying tax and a discount.
Args:
item_prices (list of float): A list of prices for each item.
tax_rate (float): The tax rate to apply.
discount (float): The discount to subtract from the total.
Returns:
float: The final total price after tax and discount.
"""
total = sum(item_prices)
total_with_tax = total + (total * tax_rate)
final_total = total_with_tax - discount
return final_total
# DOC102
def send_email(subject, body, bcc_address=None):
"""
Sends an email to the specified recipients.
Args:
subject (str): The subject of the email.
body (str): The content of the email.
to_address (str): The recipient's email address.
cc_address (str, optional): The email address for CC. Defaults to None.
bcc_address (str, optional): The email address for BCC. Defaults to None.
Returns:
bool: True if the email was sent successfully, False otherwise.
"""
return True
# DOC102
def concatenate_strings(*args):
"""
Concatenates multiple strings with a specified separator.
Args:
separator (str): The separator to use between strings.
*args (str): Variable length argument list of strings to concatenate.
Returns:
str: A single concatenated string.
"""
return separator.join(args)
# DOC102
def process_order(order_id):
"""
Processes an order with a list of items and optional order details.
Args:
order_id (int): The unique identifier for the order.
*items (str): Variable length argument list of items in the order.
**details (dict): Additional details such as shipping method and address.
Returns:
dict: A dictionary containing the order summary.
"""
return {
'order_id': order_id,
'items': items,
'details': details
}
class Calculator:
"""
A simple calculator class that can perform basic arithmetic operations.
"""
# DOC102
def __init__(self):
"""
Initializes the calculator with an initial value.
Args:
value (int, optional): The initial value of the calculator. Defaults to 0.
"""
self.value = value
# DOC102
def add(self, number2):
"""
Adds a number to the current value.
Args:
number (int or float): The number to add to the current value.
Returns:
int or float: The updated value after addition.
"""
self.value += number + number2
return self.value
# DOC102
@classmethod
def from_string(cls):
"""
Creates a Calculator instance from a string representation of a number.
Args:
value_str (str): The string representing the initial value.
Returns:
Calculator: A new instance of Calculator initialized with the value from the string.
"""
value = float(value_str)
return cls(value)
# DOC102
@staticmethod
def is_valid_number():
"""
Checks if a given number is valid (int or float).
Args:
number (any): The value to check.
Returns:
bool: True if the number is valid, False otherwise.
"""
return isinstance(number, (int, float))
# OK
def foo(param1, param2, *args, **kwargs):
"""Foo.
Args:
param1 (int): The first parameter.
param2 (:obj:`str`, optional): The second parameter. Defaults to None.
Second line of description: should be indented.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
"""
return
# OK
def on_server_unloaded(self, server_context: ServerContext) -> None:
''' Execute ``on_server_unloaded`` from ``server_lifecycle.py`` (if
it is defined) when the server cleanly exits. (Before stopping the
server's ``IOLoop``.)
Args:
server_context (ServerContext) :
.. warning::
In practice this code may not run, since servers are often killed
by a signal.
'''
return self._lifecycle_handler.on_server_unloaded(server_context)
# OK
def function_with_kwargs(param1, param2, **kwargs):
"""Function with **kwargs parameter.
Args:
param1 (int): The first parameter.
param2 (str): The second parameter.
extra_param (str): An extra parameter that may be passed via **kwargs.
another_extra (int): Another extra parameter.
"""
return
# OK
def add_numbers(b):
"""
Adds two numbers and returns the result.
Args:
b: The second number to add.
Returns:
int: The sum of the two numbers.
"""
return
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Args:
a: The first number to add.
b: The second number to add.
Returns:
int: The sum of the two numbers.
"""
return a + b

View File

@@ -1,372 +0,0 @@
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Parameters
----------
a : int
The first number to add.
b : int
The second number to add.
Returns
-------
int
The sum of the two numbers.
"""
return a + b
# DOC102
def multiply_list_elements(lst):
"""
Multiplies each element in a list by a given multiplier.
Parameters
----------
lst : list of int
A list of integers.
multiplier : int
The multiplier for each element in the list.
Returns
-------
list of int
A new list with each element multiplied.
"""
return [x * multiplier for x in lst]
# DOC102
def find_max_value():
"""
Finds the maximum value in a list of numbers.
Parameters
----------
numbers : list of int
A list of integers to search through.
Returns
-------
int
The maximum value found in the list.
"""
return max(numbers)
# DOC102
def create_user_profile(location="here"):
"""
Creates a user profile with basic information.
Parameters
----------
name : str
The name of the user.
age : int
The age of the user.
email : str
The user's email address.
location : str, optional
The location of the user, by default "here".
Returns
-------
dict
A dictionary containing the user's profile.
"""
return {
'name': name,
'age': age,
'email': email,
'location': location
}
# DOC102
def calculate_total_price(item_prices, discount):
"""
Calculates the total price after applying tax and a discount.
Parameters
----------
item_prices : list of float
A list of prices for each item.
tax_rate : float
The tax rate to apply.
discount : float
The discount to subtract from the total.
Returns
-------
float
The final total price after tax and discount.
"""
total = sum(item_prices)
total_with_tax = total + (total * tax_rate)
final_total = total_with_tax - discount
return final_total
# DOC102
def send_email(subject, body, bcc_address=None):
"""
Sends an email to the specified recipients.
Parameters
----------
subject : str
The subject of the email.
body : str
The content of the email.
to_address : str
The recipient's email address.
cc_address : str, optional
The email address for CC, by default None.
bcc_address : str, optional
The email address for BCC, by default None.
Returns
-------
bool
True if the email was sent successfully, False otherwise.
"""
return True
# DOC102
def concatenate_strings(*args):
"""
Concatenates multiple strings with a specified separator.
Parameters
----------
separator : str
The separator to use between strings.
*args : str
Variable length argument list of strings to concatenate.
Returns
-------
str
A single concatenated string.
"""
return True
# DOC102
def process_order(order_id):
"""
Processes an order with a list of items and optional order details.
Parameters
----------
order_id : int
The unique identifier for the order.
*items : str
Variable length argument list of items in the order.
**details : dict
Additional details such as shipping method and address.
Returns
-------
dict
A dictionary containing the order summary.
"""
return {
'order_id': order_id,
'items': items,
'details': details
}
class Calculator:
"""
A simple calculator class that can perform basic arithmetic operations.
"""
# DOC102
def __init__(self):
"""
Initializes the calculator with an initial value.
Parameters
----------
value : int, optional
The initial value of the calculator, by default 0.
"""
self.value = value
# DOC102
def add(self, number2):
"""
Adds two numbers to the current value.
Parameters
----------
number : int or float
The first number to add.
number2 : int or float
The second number to add.
Returns
-------
int or float
The updated value after addition.
"""
self.value += number + number2
return self.value
# DOC102
@classmethod
def from_string(cls):
"""
Creates a Calculator instance from a string representation of a number.
Parameters
----------
value_str : str
The string representing the initial value.
Returns
-------
Calculator
A new instance of Calculator initialized with the value from the string.
"""
value = float(value_str)
return cls(value)
# DOC102
@staticmethod
def is_valid_number():
"""
Checks if a given number is valid (int or float).
Parameters
----------
number : any
The value to check.
Returns
-------
bool
True if the number is valid, False otherwise.
"""
return isinstance(number, (int, float))
# OK
def function_with_kwargs(param1, param2, **kwargs):
"""Function with **kwargs parameter.
Parameters
----------
param1 : int
The first parameter.
param2 : str
The second parameter.
extra_param : str
An extra parameter that may be passed via **kwargs.
another_extra : int
Another extra parameter.
"""
return True
# OK
def add_numbers(b):
"""
Adds two numbers and returns the result.
Parameters
----------
b
The second number to add.
Returns
-------
int
The sum of the two numbers.
"""
return a + b
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Parameters
----------
a
The first number to add.
b
The second number to add.
Returns
-------
int
The sum of the two numbers.
"""
return a + b
class Foo:
# OK
def send_help(self, *args: Any) -> Any:
"""|coro|
Shows the help command for the specified entity if given.
The entity can be a command or a cog.
If no entity is given, then it'll show help for the
entire bot.
If the entity is a string, then it looks up whether it's a
:class:`Cog` or a :class:`Command`.
.. note::
Due to the way this function works, instead of returning
something similar to :meth:`~.commands.HelpCommand.command_not_found`
this returns :class:`None` on bad input or no help command.
Parameters
----------
entity: Optional[Union[:class:`Command`, :class:`Cog`, :class:`str`]]
The entity to show help for.
Returns
-------
Any
The result of the help command, if any.
"""
return
# OK
@classmethod
async def convert(cls, ctx: Context, argument: str) -> Self:
"""|coro|
The method that actually converters an argument to the flag mapping.
Parameters
----------
cls: Type[:class:`FlagConverter`]
The flag converter class.
ctx: :class:`Context`
The invocation context.
argument: :class:`str`
The argument to convert from.
Raises
------
FlagError
A flag related parsing error.
CommandError
A command related error.
Returns
-------
:class:`FlagConverter`
The flag converter instance with all flags parsed.
"""
return

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

@@ -81,7 +81,6 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::UndocumentedPublicPackage,
]);
let enforce_pydoclint = checker.any_rule_enabled(&[
Rule::DocstringExtraneousParameter,
Rule::DocstringMissingReturns,
Rule::DocstringExtraneousReturns,
Rule::DocstringMissingYields,

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

@@ -988,7 +988,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(FastApi, "003") => (RuleGroup::Stable, rules::fastapi::rules::FastApiUnusedPathParameter),
// pydoclint
(Pydoclint, "102") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousParameter),
(Pydoclint, "201") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingReturns),
(Pydoclint, "202") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousReturns),
(Pydoclint, "402") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingYields),

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

@@ -24,6 +24,7 @@ use crate::checkers::tokens::check_tokens;
use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::fix::{FixResult, fix_file};
use crate::message::create_syntax_error_diagnostic;
use crate::noqa::add_noqa;
use crate::package::PackageRoot;
use crate::registry::Rule;
@@ -495,15 +496,15 @@ fn diagnostics_to_messages(
parse_errors
.iter()
.map(|parse_error| {
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
})
.chain(unsupported_syntax_errors.iter().map(|syntax_error| {
Diagnostic::invalid_syntax(source_file.clone(), syntax_error, syntax_error)
create_syntax_error_diagnostic(source_file.clone(), syntax_error, syntax_error)
}))
.chain(
semantic_syntax_errors
.iter()
.map(|error| Diagnostic::invalid_syntax(source_file.clone(), error, error)),
.map(|error| create_syntax_error_diagnostic(source_file.clone(), error, error)),
)
.chain(diagnostics.into_iter().map(|mut diagnostic| {
if let Some(range) = diagnostic.range() {

View File

@@ -16,7 +16,7 @@ use ruff_db::files::File;
pub use grouped::GroupedEmitter;
use ruff_notebook::NotebookIndex;
use ruff_source_file::{SourceFile, SourceFileBuilder};
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use sarif::SarifEmitter;
use crate::Fix;
@@ -26,6 +26,24 @@ use crate::settings::types::{OutputFormat, RuffOutputFormat};
mod grouped;
mod sarif;
/// Creates a `Diagnostic` from a syntax error, with the format expected by Ruff.
///
/// This is almost identical to `ruff_db::diagnostic::create_syntax_error_diagnostic`, except the
/// `message` is stored as the primary diagnostic message instead of on the primary annotation.
///
/// TODO(brent) These should be unified at some point, but we keep them separate for now to avoid a
/// ton of snapshot changes while combining ruff's diagnostic type with `Diagnostic`.
pub fn create_syntax_error_diagnostic(
span: impl Into<Span>,
message: impl std::fmt::Display,
range: impl Ranged,
) -> Diagnostic {
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
let span = span.into().with_range(range.range());
diag.annotate(Annotation::primary(span));
diag
}
/// Create a `Diagnostic` from a panic.
pub fn create_panic_diagnostic(error: &PanicError, path: Option<&Path>) -> Diagnostic {
let mut diagnostic = Diagnostic::new(
@@ -242,6 +260,8 @@ mod tests {
use crate::message::{Emitter, EmitterContext, create_lint_diagnostic};
use crate::{Edit, Fix};
use super::create_syntax_error_diagnostic;
pub(super) fn create_syntax_error_diagnostics() -> Vec<Diagnostic> {
let source = r"from os import
@@ -254,7 +274,7 @@ if call(foo
.errors()
.iter()
.map(|parse_error| {
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
})
.collect()
}

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

@@ -28,7 +28,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::DocstringExtraneousParameter, Path::new("DOC102_google.py"))]
#[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_google.py"))]
#[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_google.py"))]
#[test_case(Rule::DocstringMissingYields, Path::new("DOC402_google.py"))]
@@ -51,7 +50,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::DocstringExtraneousParameter, Path::new("DOC102_numpy.py"))]
#[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_numpy.py"))]
#[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_numpy.py"))]
#[test_case(Rule::DocstringMissingYields, Path::new("DOC402_numpy.py"))]

View File

@@ -1,14 +1,14 @@
use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, Stmt, visitor};
use ruff_python_semantic::analyze::{function_type, visibility};
use ruff_python_semantic::{Definition, SemanticModel};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_source_file::NewlineWithTrailingNewline;
use ruff_text_size::{Ranged, TextRange};
use crate::Violation;
use crate::checkers::ast::Checker;
@@ -18,62 +18,6 @@ use crate::docstrings::styles::SectionStyle;
use crate::registry::Rule;
use crate::rules::pydocstyle::settings::Convention;
/// ## What it does
/// Checks for function docstrings that include parameters which are not
/// in the function signature.
///
/// ## Why is this bad?
/// If a docstring documents a parameter which is not in the function signature,
/// it can be misleading to users and/or a sign of incomplete documentation or
/// refactors.
///
/// ## Example
/// ```python
/// def calculate_speed(distance: float, time: float) -> float:
/// """Calculate speed as distance divided by time.
///
/// Args:
/// distance: Distance traveled.
/// time: Time spent traveling.
/// acceleration: Rate of change of speed.
///
/// Returns:
/// Speed as distance divided by time.
/// """
/// return distance / time
/// ```
///
/// Use instead:
/// ```python
/// def calculate_speed(distance: float, time: float) -> float:
/// """Calculate speed as distance divided by time.
///
/// Args:
/// distance: Distance traveled.
/// time: Time spent traveling.
///
/// Returns:
/// Speed as distance divided by time.
/// """
/// return distance / time
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct DocstringExtraneousParameter {
id: String,
}
impl Violation for DocstringExtraneousParameter {
#[derive_message_formats]
fn message(&self) -> String {
let DocstringExtraneousParameter { id } = self;
format!("Documented parameter `{id}` is not in the function's signature")
}
fn fix_title(&self) -> Option<String> {
Some("Remove the extraneous parameter from the docstring".to_string())
}
}
/// ## What it does
/// Checks for functions with `return` statements that do not have "Returns"
/// sections in their docstrings.
@@ -452,19 +396,6 @@ impl GenericSection {
}
}
/// A parameter in a docstring with its text range.
#[derive(Debug, Clone)]
struct ParameterEntry<'a> {
name: &'a str,
range: TextRange,
}
impl Ranged for ParameterEntry<'_> {
fn range(&self) -> TextRange {
self.range
}
}
/// A "Raises" section in a docstring.
#[derive(Debug)]
struct RaisesSection<'a> {
@@ -483,46 +414,17 @@ impl<'a> RaisesSection<'a> {
/// a "Raises" section.
fn from_section(section: &SectionContext<'a>, style: Option<SectionStyle>) -> Self {
Self {
raised_exceptions: parse_raises(section.following_lines_str(), style),
raised_exceptions: parse_entries(section.following_lines_str(), style),
range: section.range(),
}
}
}
/// An "Args" or "Parameters" section in a docstring.
#[derive(Debug)]
struct ParametersSection<'a> {
parameters: Vec<ParameterEntry<'a>>,
range: TextRange,
}
impl Ranged for ParametersSection<'_> {
fn range(&self) -> TextRange {
self.range
}
}
impl<'a> ParametersSection<'a> {
/// Return the parameters for the docstring, or `None` if the docstring does not contain
/// an "Args" or "Parameters" section.
fn from_section(section: &SectionContext<'a>, style: Option<SectionStyle>) -> Self {
Self {
parameters: parse_parameters(
section.following_lines_str(),
section.following_range().start(),
style,
),
range: section.section_name_range(),
}
}
}
#[derive(Debug, Default)]
struct DocstringSections<'a> {
returns: Option<GenericSection>,
yields: Option<GenericSection>,
raises: Option<RaisesSection<'a>>,
parameters: Option<ParametersSection<'a>>,
}
impl<'a> DocstringSections<'a> {
@@ -530,10 +432,6 @@ impl<'a> DocstringSections<'a> {
let mut docstring_sections = Self::default();
for section in sections {
match section.kind() {
SectionKind::Args | SectionKind::Arguments | SectionKind::Parameters => {
docstring_sections.parameters =
Some(ParametersSection::from_section(&section, style));
}
SectionKind::Raises => {
docstring_sections.raises = Some(RaisesSection::from_section(&section, style));
}
@@ -550,142 +448,18 @@ impl<'a> DocstringSections<'a> {
}
}
/// Parse the entries in a "Parameters" section of a docstring.
///
/// Attempts to parse using the specified [`SectionStyle`], falling back to the other style if no
/// entries are found.
fn parse_parameters(
content: &str,
content_start: TextSize,
style: Option<SectionStyle>,
) -> Vec<ParameterEntry<'_>> {
match style {
Some(SectionStyle::Google) => parse_parameters_google(content, content_start),
Some(SectionStyle::Numpy) => parse_parameters_numpy(content, content_start),
None => {
let entries = parse_parameters_google(content, content_start);
if entries.is_empty() {
parse_parameters_numpy(content, content_start)
} else {
entries
}
}
}
}
/// Parses Google-style "Args" sections of the form:
///
/// ```python
/// Args:
/// a (int): The first number to add.
/// b (int): The second number to add.
/// ```
fn parse_parameters_google(content: &str, content_start: TextSize) -> Vec<ParameterEntry<'_>> {
let mut entries: Vec<ParameterEntry> = Vec::new();
// Find first entry to determine indentation
let Some(first_arg) = content.lines().next() else {
return entries;
};
let indentation = &first_arg[..first_arg.len() - first_arg.trim_start().len()];
let mut current_pos = TextSize::ZERO;
for line in content.lines() {
let line_start = current_pos;
current_pos = content.full_line_end(line_start);
if let Some(entry) = line.strip_prefix(indentation) {
if entry
.chars()
.next()
.is_some_and(|first_char| !first_char.is_whitespace())
{
let Some((before_colon, _)) = entry.split_once(':') else {
continue;
};
if let Some(param) = before_colon.split_whitespace().next() {
let param_name = param.trim_start_matches('*');
if is_identifier(param_name) {
let param_start = line_start + indentation.text_len();
let param_end = param_start + param.text_len();
entries.push(ParameterEntry {
name: param_name,
range: TextRange::new(
content_start + param_start,
content_start + param_end,
),
});
}
}
}
}
}
entries
}
/// Parses NumPy-style "Parameters" sections of the form:
///
/// ```python
/// Parameters
/// ----------
/// a : int
/// The first number to add.
/// b : int
/// The second number to add.
/// ```
fn parse_parameters_numpy(content: &str, content_start: TextSize) -> Vec<ParameterEntry<'_>> {
let mut entries: Vec<ParameterEntry> = Vec::new();
let mut lines = content.lines();
let Some(dashes) = lines.next() else {
return entries;
};
let indentation = &dashes[..dashes.len() - dashes.trim_start().len()];
let mut current_pos = content.full_line_end(dashes.text_len());
for potential in lines {
let line_start = current_pos;
current_pos = content.full_line_end(line_start);
if let Some(entry) = potential.strip_prefix(indentation) {
if entry
.chars()
.next()
.is_some_and(|first_char| !first_char.is_whitespace())
{
if let Some(before_colon) = entry.split(':').next() {
let param = before_colon.trim_end();
let param_name = param.trim_start_matches('*');
if is_identifier(param_name) {
let param_start = line_start + indentation.text_len();
let param_end = param_start + param.text_len();
entries.push(ParameterEntry {
name: param_name,
range: TextRange::new(
content_start + param_start,
content_start + param_end,
),
});
}
}
}
}
}
entries
}
/// Parse the entries in a "Raises" section of a docstring.
///
/// Attempts to parse using the specified [`SectionStyle`], falling back to the other style if no
/// entries are found.
fn parse_raises(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName<'_>> {
fn parse_entries(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName<'_>> {
match style {
Some(SectionStyle::Google) => parse_raises_google(content),
Some(SectionStyle::Numpy) => parse_raises_numpy(content),
Some(SectionStyle::Google) => parse_entries_google(content),
Some(SectionStyle::Numpy) => parse_entries_numpy(content),
None => {
let entries = parse_raises_google(content);
let entries = parse_entries_google(content);
if entries.is_empty() {
parse_raises_numpy(content)
parse_entries_numpy(content)
} else {
entries
}
@@ -693,14 +467,14 @@ fn parse_raises(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName
}
}
/// Parses Google-style "Raises" section of the form:
/// Parses Google-style docstring sections of the form:
///
/// ```python
/// Raises:
/// FasterThanLightError: If speed is greater than the speed of light.
/// DivisionByZero: If attempting to divide by zero.
/// ```
fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
fn parse_entries_google(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = Vec::new();
for potential in content.lines() {
let Some(colon_idx) = potential.find(':') else {
@@ -712,7 +486,7 @@ fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
entries
}
/// Parses NumPy-style "Raises" section of the form:
/// Parses NumPy-style docstring sections of the form:
///
/// ```python
/// Raises
@@ -722,7 +496,7 @@ fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
/// DivisionByZero
/// If attempting to divide by zero.
/// ```
fn parse_raises_numpy(content: &str) -> Vec<QualifiedName<'_>> {
fn parse_entries_numpy(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = Vec::new();
let mut lines = content.lines();
let Some(dashes) = lines.next() else {
@@ -1093,17 +867,6 @@ fn is_generator_function_annotated_as_returning_none(
.is_some_and(GeneratorOrIteratorArguments::indicates_none_returned)
}
fn parameters_from_signature<'a>(docstring: &'a Docstring) -> Vec<&'a str> {
let mut parameters = Vec::new();
let Some(function) = docstring.definition.as_function_def() else {
return parameters;
};
for param in &function.parameters {
parameters.push(param.name());
}
parameters
}
fn is_one_line(docstring: &Docstring) -> bool {
let mut non_empty_line_count = 0;
for line in NewlineWithTrailingNewline::from(docstring.body().as_str()) {
@@ -1117,7 +880,7 @@ fn is_one_line(docstring: &Docstring) -> bool {
true
}
/// DOC102, DOC201, DOC202, DOC402, DOC403, DOC501, DOC502
/// DOC201, DOC202, DOC402, DOC403, DOC501, DOC502
pub(crate) fn check_docstring(
checker: &Checker,
definition: &Definition,
@@ -1157,8 +920,6 @@ pub(crate) fn check_docstring(
visitor.finish()
};
let signature_parameters = parameters_from_signature(docstring);
// DOC201
if checker.is_rule_enabled(Rule::DocstringMissingReturns) {
if should_document_returns(function_def)
@@ -1247,25 +1008,6 @@ pub(crate) fn check_docstring(
}
}
// DOC102
if checker.is_rule_enabled(Rule::DocstringExtraneousParameter) {
// Don't report extraneous parameters if the signature defines *args or **kwargs
if function_def.parameters.vararg.is_none() && function_def.parameters.kwarg.is_none() {
if let Some(docstring_params) = docstring_sections.parameters {
for docstring_param in &docstring_params.parameters {
if !signature_parameters.contains(&docstring_param.name) {
checker.report_diagnostic(
DocstringExtraneousParameter {
id: docstring_param.name.to_string(),
},
docstring_param.range(),
);
}
}
}
}
}
// Avoid applying "extraneous" rules to abstract methods. An abstract method's docstring _could_
// document that it raises an exception without including the exception in the implementation.
if !visibility::is_abstract(&function_def.decorator_list, semantic) {

View File

@@ -1,180 +0,0 @@
---
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
---
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_google.py:7:9
|
6 | Args:
7 | a (int): The first number to add.
| ^
8 | b (int): The second number to add.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `multiplier` is not in the function's signature
--> DOC102_google.py:23:9
|
21 | Args:
22 | lst (list of int): A list of integers.
23 | multiplier (int): The multiplier for each element in the list.
| ^^^^^^^^^^
24 |
25 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `numbers` is not in the function's signature
--> DOC102_google.py:37:9
|
36 | Args:
37 | numbers (list of int): A list of integers to search through.
| ^^^^^^^
38 |
39 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `name` is not in the function's signature
--> DOC102_google.py:51:9
|
50 | Args:
51 | name (str): The name of the user.
| ^^^^
52 | age (int): The age of the user.
53 | email (str): The user's email address.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `age` is not in the function's signature
--> DOC102_google.py:52:9
|
50 | Args:
51 | name (str): The name of the user.
52 | age (int): The age of the user.
| ^^^
53 | email (str): The user's email address.
54 | location (str): The location of the user.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `email` is not in the function's signature
--> DOC102_google.py:53:9
|
51 | name (str): The name of the user.
52 | age (int): The age of the user.
53 | email (str): The user's email address.
| ^^^^^
54 | location (str): The location of the user.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `tax_rate` is not in the function's signature
--> DOC102_google.py:74:9
|
72 | Args:
73 | item_prices (list of float): A list of prices for each item.
74 | tax_rate (float): The tax rate to apply.
| ^^^^^^^^
75 | discount (float): The discount to subtract from the total.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `to_address` is not in the function's signature
--> DOC102_google.py:94:9
|
92 | subject (str): The subject of the email.
93 | body (str): The content of the email.
94 | to_address (str): The recipient's email address.
| ^^^^^^^^^^
95 | cc_address (str, optional): The email address for CC. Defaults to None.
96 | bcc_address (str, optional): The email address for BCC. Defaults to None.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `cc_address` is not in the function's signature
--> DOC102_google.py:95:9
|
93 | body (str): The content of the email.
94 | to_address (str): The recipient's email address.
95 | cc_address (str, optional): The email address for CC. Defaults to None.
| ^^^^^^^^^^
96 | bcc_address (str, optional): The email address for BCC. Defaults to None.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `items` is not in the function's signature
--> DOC102_google.py:126:9
|
124 | Args:
125 | order_id (int): The unique identifier for the order.
126 | *items (str): Variable length argument list of items in the order.
| ^^^^^^
127 | **details (dict): Additional details such as shipping method and address.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `details` is not in the function's signature
--> DOC102_google.py:127:9
|
125 | order_id (int): The unique identifier for the order.
126 | *items (str): Variable length argument list of items in the order.
127 | **details (dict): Additional details such as shipping method and address.
| ^^^^^^^^^
128 |
129 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value` is not in the function's signature
--> DOC102_google.py:150:13
|
149 | Args:
150 | value (int, optional): The initial value of the calculator. Defaults to 0.
| ^^^^^
151 | """
152 | self.value = value
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_google.py:160:13
|
159 | Args:
160 | number (int or float): The number to add to the current value.
| ^^^^^^
161 |
162 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value_str` is not in the function's signature
--> DOC102_google.py:175:13
|
174 | Args:
175 | value_str (str): The string representing the initial value.
| ^^^^^^^^^
176 |
177 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_google.py:190:13
|
189 | Args:
190 | number (any): The value to check.
| ^^^^^^
191 |
192 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_google.py:258:9
|
257 | Args:
258 | a: The first number to add.
| ^
259 | b: The second number to add.
|
help: Remove the extraneous parameter from the docstring

View File

@@ -1,189 +0,0 @@
---
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
---
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_numpy.py:8:5
|
6 | Parameters
7 | ----------
8 | a : int
| ^
9 | The first number to add.
10 | b : int
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `multiplier` is not in the function's signature
--> DOC102_numpy.py:30:5
|
28 | lst : list of int
29 | A list of integers.
30 | multiplier : int
| ^^^^^^^^^^
31 | The multiplier for each element in the list.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `numbers` is not in the function's signature
--> DOC102_numpy.py:48:5
|
46 | Parameters
47 | ----------
48 | numbers : list of int
| ^^^^^^^
49 | A list of integers to search through.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `name` is not in the function's signature
--> DOC102_numpy.py:66:5
|
64 | Parameters
65 | ----------
66 | name : str
| ^^^^
67 | The name of the user.
68 | age : int
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `age` is not in the function's signature
--> DOC102_numpy.py:68:5
|
66 | name : str
67 | The name of the user.
68 | age : int
| ^^^
69 | The age of the user.
70 | email : str
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `email` is not in the function's signature
--> DOC102_numpy.py:70:5
|
68 | age : int
69 | The age of the user.
70 | email : str
| ^^^^^
71 | The user's email address.
72 | location : str, optional
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `tax_rate` is not in the function's signature
--> DOC102_numpy.py:97:5
|
95 | item_prices : list of float
96 | A list of prices for each item.
97 | tax_rate : float
| ^^^^^^^^
98 | The tax rate to apply.
99 | discount : float
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `to_address` is not in the function's signature
--> DOC102_numpy.py:124:5
|
122 | body : str
123 | The content of the email.
124 | to_address : str
| ^^^^^^^^^^
125 | The recipient's email address.
126 | cc_address : str, optional
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `cc_address` is not in the function's signature
--> DOC102_numpy.py:126:5
|
124 | to_address : str
125 | The recipient's email address.
126 | cc_address : str, optional
| ^^^^^^^^^^
127 | The email address for CC, by default None.
128 | bcc_address : str, optional
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `items` is not in the function's signature
--> DOC102_numpy.py:168:5
|
166 | order_id : int
167 | The unique identifier for the order.
168 | *items : str
| ^^^^^^
169 | Variable length argument list of items in the order.
170 | **details : dict
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `details` is not in the function's signature
--> DOC102_numpy.py:170:5
|
168 | *items : str
169 | Variable length argument list of items in the order.
170 | **details : dict
| ^^^^^^^^^
171 | Additional details such as shipping method and address.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value` is not in the function's signature
--> DOC102_numpy.py:197:9
|
195 | Parameters
196 | ----------
197 | value : int, optional
| ^^^^^
198 | The initial value of the calculator, by default 0.
199 | """
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_numpy.py:209:9
|
207 | Parameters
208 | ----------
209 | number : int or float
| ^^^^^^
210 | The first number to add.
211 | number2 : int or float
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value_str` is not in the function's signature
--> DOC102_numpy.py:230:9
|
228 | Parameters
229 | ----------
230 | value_str : str
| ^^^^^^^^^
231 | The string representing the initial value.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_numpy.py:249:9
|
247 | Parameters
248 | ----------
249 | number : any
| ^^^^^^
250 | The value to check.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_numpy.py:300:5
|
298 | Parameters
299 | ----------
300 | a
| ^
301 | The first number to add.
302 | b
|
help: Remove the extraneous parameter from the docstring

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

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