Compare commits

...

33 Commits

Author SHA1 Message Date
Ibraheem Ahmed
992e77e4d0 suggestions from code review 2025-08-22 19:36:40 -04:00
Ibraheem Ahmed
8d05367d60 reload Project after deserialization 2025-08-22 19:09:22 -04:00
Ibraheem Ahmed
2402831223 experimental persistent caching 2025-08-22 19:09:21 -04:00
Ibraheem Ahmed
7abc41727b [ty] Shrink size of AstNodeRef (#20028)
## Summary

Removes the `module_ptr` field from `AstNodeRef` in release mode, and
change `NodeIndex` to a `NonZeroU32` to reduce the size of
`Option<AstNodeRef<_>>` fields.

I believe CI runs in debug mode, so this won't show up in the memory
report, but this reduces memory by ~2% in release mode.
2025-08-22 17:03:22 -04:00
chiri
886c4e4773 [flake8-use-pathlib] Fix PTH211 autofix (#20049)
## Summary
Part of #20009
2025-08-22 13:35:08 -05:00
Alex Waygood
bc6ea68733 [ty] Add precise iteration and unpacking inference for string literals and bytes literals (#20023)
## Summary

Previously we held off from doing this because we weren't sure that it
was worth the added complexity cost. But our code has changed in the
months since we made that initial decision, and I think the structure of
the code is such that it no longer really leads to much added complexity
to add precise inference when unpacking a string literal or a bytes
literal.

The improved inference we gain from this has real benefits to users (see
the mypy_primer report), and this PR doesn't appear to have a
performance impact.

## Test plan

mdtests
2025-08-22 19:33:08 +01:00
Micha Reiser
796819e7a0 [ty] Disallow std::env and io methods in most ty crates (#20046)
## Summary

We use the `System` abstraction in ty to abstract away the host/system
on which ty runs.
This has a few benefits:

* Tests can run in full isolation using a memory system (that uses an
in-memory file system)
* The LSP has a custom implementation where `read_to_string` returns the
content as seen by the editor (e.g. unsaved changes) instead of always
returning the content as it is stored on disk
* We don't require any file system polyfills for wasm in the browser


However, it does require extra care that we don't accidentally use
`std::fs` or `std::env` (etc.) methods in ty's code base (which is very
easy).

This PR sets up Clippy and disallows the most common methods, instead
pointing users towards the corresponding `System` methods.

The setup is a bit awkward because clippy doesn't support inheriting
configurations. That means, a crate can only override the entire
workspace configuration or not at all.
The approach taken in this PR is:

* Configure the disallowed methods at the workspace level
* Allow `disallowed_methods` at the workspace level
* Enable the lint at the crate level using the warn attribute (in code)


The obvious downside is that it won't work if we ever want to disallow
other methods, but we can figure that out once we reach that point.

What about false positives: Just add an `allow` and move on with your
life :) This isn't something that we have to enforce strictly; the goal
is to catch accidental misuse.

## Test Plan

Clippy found a place where we incorrectly used `std::fs::read_to_string`
2025-08-22 11:13:47 -07:00
Vivek Dasari
5508e8e528 Add testing helper to compare stable vs preview snapshots (#19715)
## Summary
This PR implements a diff test helper `assert_diagnostics_diff` as
described in #19351. The diff file includes both the settings ( e.g.
`+linter.preview = enabled`) and the snapshot data itself.

The current implementation looks for each old diagnostic in the new
snapshot. This works when the preview behavior adds/removes a couple
diagnostics. This implementation does not work well when every
diagnostic is modified (e.g. a "fix" is added).
https://github.com/astral-sh/ruff/pull/19715#discussion_r2259410763 has
ideas for future improvements to this implementation.

The example usage in this PR writes the diff to `preview_diff` file
instead of `preview` file, which might be a useful convention to keep.


## Test Plan
- Included a unit test at:
https://github.com/astral-sh/ruff/pull/19715/files#diff-d49487fe3e8a8585529f62c2df2a2b0a4c44267a1f93d1e859dff1d9f8771d36R523
- Example usage of this new test helper:
https://github.com/astral-sh/ruff/pull/19715/files#diff-2a33ac11146d1794c01a29549a6041d3af6fb6f9b423a31ade12a88d1951b0c2R1
2025-08-22 12:49:34 -05:00
chiri
0be3e1fbbf [flake8-use-pathlib] Add autofix for PTH211 (#20009)
## Summary
Part of https://github.com/astral-sh/ruff/issues/2331
2025-08-22 12:38:37 -05:00
Micha Reiser
5d217b7f46 [ty] Add type as detail to completion items (#20047)
## Summary

@BurntSushi was so kind as to find me an easy task to do some coding
before I'm off to PTO.

This PR adds the type to completion items (see the gray little text at
the end of a completion item).



https://github.com/user-attachments/assets/c0a86061-fa12-47b4-b43c-3c646771a69d
2025-08-22 12:32:53 -04:00
Dylan
0b6ce1c788 [ruff] Handle empty t-strings in unnecessary-empty-iterable-within-deque-call (RUF037) (#20045)
Adds a method to `TStringValue` to detect whether the t-string is empty
_as an iterable_. Note the subtlety here that, unlike f-strings, an
empty t-string is still truthy (i.e. `bool(t"")==True`).

Closes #19951
2025-08-22 10:23:49 -05:00
Matthew Mckee
0e9d77e43a Fix incorrect lsp inlay hint type (#20044) 2025-08-22 17:12:49 +02:00
Carl Meyer
8b827c3c6c [ty] rename BareTypeAliasType to ManualPEP695TypeAliasType (#20037)
## Summary

Rename `TypeAliasType::Bare` to `TypeAliasType::ManualPEP695`, and
`BareTypeAliasType` to `ManualPEP695TypeAliasType`.

Why?

Both existing variants of `TypeAliasType` are specific to features added
in PEP 695 (which introduced both the `type` statement and
`types.TypeAliasType`), so it doesn't make sense to name one with the
name `PEP695` and not the other.

A "bare" type alias, in my mind, is a legacy type alias like `IntOrStr =
int | str`, which is "bare" in that there is nothing at all
distinguishing it as a type alias. I will want to use the "bare" name
for this variant, in a future PR.

The renamed variant here describes a type alias created with `IntOrStr =
types.TypeAliasType("IntOrStr", int | str)`, which is not "bare", it's
just "manually" instantiated instead of using the `type` statement
syntax sugar. (This is useful when using the `typing_extensions`
backport of `TypeAliasType` on older Python versions.)

## Test Plan

Pure rename, existing tests pass.
2025-08-22 07:40:29 -07:00
Max Mynter
c22395dbc6 [ruff] Fix false positive for t-strings in default-factory-kwarg (RUF026) (#20032)
Closes #19993

## Summary
Recognize t strings as never being callable to avoid false positives on
RUF026.
2025-08-22 09:29:42 -05:00
Micha Reiser
11f521c768 [ty] Close signature help after ) (#20017) 2025-08-22 16:09:22 +02:00
Micha Reiser
c5e05df966 [ty] Cancel background tasks when shutdown is requested (#20039) 2025-08-22 10:20:13 +02:00
github-actions[bot]
7a44ea680e [ty] Sync vendored typeshed stubs (#20031)
Co-authored-by: typeshedbot <>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-08-21 21:32:48 +00:00
Alex Waygood
f82025d919 [ty] Improve diagnostics for bad calls to functions (#20022) 2025-08-21 22:00:44 +01:00
Micha Reiser
365f521c37 [ty] Fix incorrect docstring in call signature completion (#20021)
## Summary

This PR fixes https://github.com/astral-sh/ty/issues/1071

The core issue is that `CallableType` is a salsa interned but
`Signature` (which `CallableType` stores) ignores the `Definition` in
its `Eq` and `Hash` implementation.

This PR tries to simplest fix by removing the custom `Eq` and `Hash`
implementation. The main downside of this fix is that it can increase
memory usage because `CallableType`s that are equal except for their
`Definition` are now interned separately.

The alternative is to remove `Definition` from `CallableType` and
instead, call `bindings` directly on the callee (call_expression.func).
However, this would require
addressing the TODO 

here
39ee71c2a5/crates/ty_python_semantic/src/types.rs (L4582-L4586)

This might probably be worth addressing anyway, but is the more involved
fix. That's why I opted for removing the custom `Eq` implementation.

We already "ignore" the definition during normalization, thank's to
Alex's work in https://github.com/astral-sh/ruff/pull/19615

## Test Plan



https://github.com/user-attachments/assets/248d1cb1-12fd-4441-adab-b7e0866d23eb
2025-08-21 16:36:40 -04:00
Aria Desires
fc5321e000 [ty] fix GotoTargets for keyword args in nested function calls (#20013)
While implementing similar logic for initializers I noticed that this
code appeared to be walking the ancestors in the wrong direction, and so
if you have nested function calls it would always grab the outermost one
instead of the closest-ancestor.

The four copies of the test are because there's something really evil in
our caching that can't seem to be demonstrated in our cursor testing
framework, which I'm filing a followup for.
2025-08-21 20:19:52 +00:00
Dylan
c68ff8d90b Bump 0.12.10 (#20025) 2025-08-21 13:09:31 -05:00
Andrew Gallant
5931a5207d [ty] Stop running every mdtest twice
This was an accidental oversight introduced in commit
468eb37d75.
2025-08-21 13:37:08 -04:00
Brent Westbrook
692be72f5a Move diff rendering to ruff_db (#20006)
Summary
--

This is a preparatory PR in support of #19919. It moves our `Diff`
rendering code from `ruff_linter` to `ruff_db`, where we have direct
access to the `DiagnosticStylesheet` used by our other diagnostic
rendering code. As shown by the tests, this shouldn't cause any visible
changes. The colors aren't exactly the same, as I note in a TODO
comment, but I don't think there's any existing way to see those, even
in tests.

The `Diff` implementation is mostly unchanged. I just switched from a
Ruff-specific `SourceFile` to a `DiagnosticSource` (removing an
`expect_ruff_source_file` call) and updated the `LineStyle` struct and
other styling calls to use `fmt_styled` and our existing stylesheet.

In support of these changes, I added three styles to our stylesheet:
`insertion` and `deletion` for the corresponding diff operations, and
`underline`, which apparently we _can_ use, as I hoped on Discord. This
isn't supported in all terminals, though. It worked in ghostty but not
in st for me.

I moved the `calculate_print_width` function from the now-deleted
`diff.rs` to a method on `OneIndexed`, where it was available everywhere
we needed it. I'm not sure if that's desirable, or if my other changes
to the function are either (using `ilog10` instead of a loop). This does
make it `const` and slightly simplifies things in my opinion, but I'm
happy to revert it if preferred.

I also inlined a version of `show_nonprinting` from the
`ShowNonprinting` trait in `ruff_linter`:


f4be05a83b/crates/ruff_linter/src/text_helpers.rs (L3-L5)

This trait is now only used in `source_kind.rs`, so I'm not sure it's
worth having the trait or the macro-generated implementation (which is
only called once). This is obviously closely related to our unprintable
character handling in diagnostic rendering, but the usage seems
different enough not to try to combine them.


f4be05a83b/crates/ruff_db/src/diagnostic/render.rs (L990-L998)

We could also move the trait to another crate where we can use it in
`ruff_db` instead of inlining here, of course.

Finally, this PR makes `TextEmitter` a very thin wrapper around a
`DisplayDiagnosticsConfig`. It's still used in a few places, though,
unlike the other emitters we've replaced, so I figured it was worth
keeping around. It's a pretty nice API for setting all of the options on
the config and then passing that along to a `DisplayDiagnostics`.

Test Plan
--

Existing snapshot tests with diffs
2025-08-21 09:47:00 -04:00
Douglas Creager
14fe1228e7 [ty] Perform assignability etc checks using new Constraints trait (#19838)
"Why would you do this? This looks like you just replaced `bool` with an
overly complex trait"

Yes that's correct!

This should be a no-op refactoring. It replaces all of the logic in our
assignability, subtyping, equivalence, and disjointness methods to work
over an arbitrary `Constraints` trait instead of only working on `bool`.

The methods that `Constraints` provides looks very much like what we get
from `bool`. But soon we will add a new impl of this trait, and some new
methods, that let us express "fuzzy" constraints that aren't always true
or false. (In particular, a constraint will express the upper and lower
bounds of the allowed specializations of a typevar.)

Even once we have that, most of the operations that we perform on
constraint sets will be the usual boolean operations, just on sets.
(`false` becomes empty/never; `true` becomes universe/always; `or`
becomes union; `and` becomes intersection; `not` becomes negation.) So
it's helpful to have this separate PR to refactor how we invoke those
operations without introducing the new functionality yet.

Note that we also have translations of `Option::is_some_and` and
`is_none_or`, and of `Iterator::any` and `all`, and that the `and`,
`or`, `when_any`, and `when_all` methods are meant to short-circuit,
just like the corresponding boolean operations. For constraint sets,
that depends on being able to implement the `is_always` and `is_never`
trait methods.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-08-21 09:30:09 -04:00
Micha Reiser
045cba382a [ty] Use dedent in cursor tests (#20019) 2025-08-21 10:31:54 +02:00
Brent Westbrook
a5cbca156c Fix rust feature activation (#20012) 2025-08-21 09:26:06 +02:00
Dhruv Manilawala
d43a3d34dd [ty] Avoid unnecessary argument type expansion (#19999)
## Summary

Part of: https://github.com/astral-sh/ty/issues/868

This PR adds a heuristic to avoid argument type expansion if it's going
to eventually lead to no matching overload.

This is done by checking whether the non-expandable argument types are
assignable to the corresponding annotated parameter type. If one of them
is not assignable to all of the remaining overloads, then argument type
expansion isn't going to help.

## Test Plan

Add mdtest that would otherwise take a long time because of the number
of arguments that it would need to expand (30).
2025-08-21 06:13:11 +00:00
Aria Desires
99111961c0 [ty] Add link for namespaces being partial (#20015)
As requested
2025-08-20 21:28:57 -07:00
Aria Desires
859475f017 [ty] add docstrings to completions based on type (#20008)
This is a fairly simple but effective way to add docstrings to like 95%
of completions from initial experimentation.

Fixes https://github.com/astral-sh/ty/issues/1036

Although ironically this approach *does not* work specifically for
`print` and I haven't looked into why.
2025-08-20 17:00:09 -04:00
Igor Drokin
7b75aee21d [pyupgrade] Avoid reporting __future__ features as unnecessary when they are used (UP010) (#19769)
## Summary
Resolves #19561

Fixes the [unnecessary-future-import
(UP010)](https://docs.astral.sh/ruff/rules/unnecessary-future-import/)
rule to correctly identify when imported __future__ modules are actually
used in the code, preventing false positives.

I assume there is no way to check usage in `analyze::statements`,
because we don't have any usage bindings for imports. To determine
unused imports, we have to fully scan the file to create bindings and
then check usage, similar to [unused-import
(F401)](https://docs.astral.sh/ruff/rules/unused-import/#unused-import-f401).
So, `Rule::UnnecessaryFutureImport` was moved from the
`analyze::statements` to the `analyze::deferred_scopes` stage. This
caused the need to change the logic of future import handling to a
bindings-based approach.

Also, the diagnostic report was changed.
Before
```
  |
1 | from __future__ import nested_scopes, generators
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP010
```
after
```
  |
1 | from __future__ import nested_scopes, generators
  |                        ^^^^^^^^^^^^^ UP010
```

I believe this is the correct way, because `generators` may be used, but
`nested_scopes` is not.

### Special case
I've found out about some specific case.
```python
from __future__ import nested_scopes

nested_scopes = 1
```
Here we can treat `nested_scopes` as an unused import because the
variable `nested_scopes` shadows it and we can safely remove the future
import (my fix does it).

But
[F401](https://docs.astral.sh/ruff/rules/unused-import/#unused-import-f401)
not triggered for such case
([sandbox](https://play.ruff.rs/296d9c7e-0f02-4659-b0c0-78cc21f3de76))
```
from foo import print_function

print_function = 1
```
In my mind, `print_function` here is an unused import and should be
deleted (my IDE highlight it). What do you think?

## Test Plan

Added test cases and snapshots:
- Split test file into separate _0 and _1 files for appropriate checks.
- Added test cases to verify fixes when future module are used.

---------

Co-authored-by: Igor Drokin <drokinii1017@gmail.com>
2025-08-20 15:22:03 -04:00
chiri
d04dcd991b [flake8-use-pathlib] Add fixes for PTH102 and PTH103 (#19514)
## Summary

Part of https://github.com/astral-sh/ruff/issues/2331

## Test Plan

<!-- How was it tested? -->
`cargo nextest run flake8_use_pathlib`
2025-08-20 14:36:07 -04:00
Leandro Braga
39ee71c2a5 [ty] correctly ignore field specifiers when not specified (#20002)
This commit corrects the type checker's behavior when handling
`dataclass_transform` decorators that don't explicitly specify
`field_specifiers`. According to [PEP 681 (Data Class
Transforms)](https://peps.python.org/pep-0681/#dataclass-transform-parameters),
when `field_specifiers` is not provided, it defaults to an empty tuple,
meaning no field specifiers are supported and
`dataclasses.field`/`dataclasses.Field` calls should be ignored.

Fixes https://github.com/astral-sh/ty/issues/980
2025-08-20 11:33:23 -07:00
Brent Westbrook
1a38831d53 Option::unwrap is now const (#20007)
Summary
--

I noticed while working on #20006 that we had a custom `unwrap` function
for `Option`. This has been const on stable since 1.83
([docs](https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap),
[release notes](https://blog.rust-lang.org/2024/11/28/Rust-1.83.0/)), so
I think it's safe to use now. I grepped a bit for related todos and
found this one for `AsciiCharSet` but no others.

Test Plan
--

Existing tests
2025-08-20 13:40:49 -04:00
967 changed files with 29089 additions and 25759 deletions

View File

@@ -1,5 +1,29 @@
# Changelog
## 0.12.10
### Preview features
- \[`flake8-simplify`\] Implement fix for `maxsplit` without separator (`SIM905`) ([#19851](https://github.com/astral-sh/ruff/pull/19851))
- \[`flake8-use-pathlib`\] Add fixes for `PTH102` and `PTH103` ([#19514](https://github.com/astral-sh/ruff/pull/19514))
### Bug fixes
- \[`isort`\] Handle multiple continuation lines after module docstring (`I002`) ([#19818](https://github.com/astral-sh/ruff/pull/19818))
- \[`pyupgrade`\] Avoid reporting `__future__` features as unnecessary when they are used (`UP010`) ([#19769](https://github.com/astral-sh/ruff/pull/19769))
- \[`pyupgrade`\] Handle nested `Optional`s (`UP045`) ([#19770](https://github.com/astral-sh/ruff/pull/19770))
### Rule changes
- \[`pycodestyle`\] Make `E731` fix unsafe instead of display-only for class assignments ([#19700](https://github.com/astral-sh/ruff/pull/19700))
- \[`pyflakes`\] Add secondary annotation showing previous definition (`F811`) ([#19900](https://github.com/astral-sh/ruff/pull/19900))
### Documentation
- Fix description of global config file discovery strategy ([#19188](https://github.com/astral-sh/ruff/pull/19188))
- Update outdated links to <https://typing.python.org/en/latest/source/stubs.html> ([#19992](https://github.com/astral-sh/ruff/pull/19992))
- \[`flake8-annotations`\] Remove unused import in example (`ANN401`) ([#20000](https://github.com/astral-sh/ruff/pull/20000))
## 0.12.9
### Preview features

42
Cargo.lock generated
View File

@@ -260,6 +260,9 @@ name = "bitflags"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
dependencies = [
"serde",
]
[[package]]
name = "bitvec"
@@ -1028,6 +1031,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased-serde"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
dependencies = [
"serde",
"typeid",
]
[[package]]
name = "errno"
version = "0.3.13"
@@ -2743,7 +2756,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.9"
version = "0.12.10"
dependencies = [
"anyhow",
"argfile",
@@ -2886,6 +2899,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"similar",
"tempfile",
"thiserror 2.0.12",
"tracing",
@@ -2996,7 +3010,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.9"
version = "0.12.10"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3335,7 +3349,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.9"
version = "0.12.10"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3450,12 +3464,13 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a0e7a06#a0e7a0660c93136f23bf08b4f1604eee3d1f6b11"
dependencies = [
"boxcar",
"compact_str",
"crossbeam-queue",
"crossbeam-utils",
"erased-serde",
"hashbrown 0.15.5",
"hashlink",
"indexmap",
@@ -3466,6 +3481,7 @@ dependencies = [
"rustc-hash",
"salsa-macro-rules",
"salsa-macros",
"serde",
"smallvec",
"thin-vec",
"tracing",
@@ -3474,12 +3490,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a0e7a06#a0e7a0660c93136f23bf08b4f1604eee3d1f6b11"
[[package]]
name = "salsa-macros"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a0e7a06#a0e7a0660c93136f23bf08b4f1604eee3d1f6b11"
dependencies = [
"proc-macro2",
"quote",
@@ -3699,6 +3715,9 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
name = "snapbox"
@@ -3903,6 +3922,9 @@ name = "thin-vec"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
dependencies = [
"serde",
]
[[package]]
name = "thiserror"
@@ -4260,6 +4282,7 @@ name = "ty_project"
version = "0.0.0"
dependencies = [
"anyhow",
"bincode 2.0.1",
"camino",
"colored 3.0.0",
"crossbeam",
@@ -4289,6 +4312,7 @@ dependencies = [
"tracing",
"ty_combine",
"ty_python_semantic",
"ty_static",
"ty_vendored",
]
@@ -4460,6 +4484,12 @@ version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.18.0"

View File

@@ -57,8 +57,8 @@ anyhow = { version = "1.0.80" }
arc-swap = { version = "1.7.1" }
assert_fs = { version = "1.1.0" }
argfile = { version = "0.2.0" }
bincode = { version = "2.0.0" }
bitflags = { version = "2.5.0" }
bincode = { version = "2.0.0", features = ["serde"] }
bitflags = { version = "2.5.0", features = ["serde"] }
bitvec = { version = "1.0.1", default-features = false, features = [
"alloc",
] }
@@ -126,7 +126,7 @@ memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "8.0.0" }
ordermap = { version = "0.5.0" }
ordermap = { version = "0.5.0", features = ["serde"] }
path-absolutize = { version = "3.1.1" }
path-slash = { version = "0.2.1" }
pathdiff = { version = "0.2.1" }
@@ -143,24 +143,25 @@ 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 = "a3ffa22cb26756473d56f867aedec3fd907c4dd9", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a0e7a06", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
"inventory",
"persistence",
] }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
serde = { version = "1.0.197", features = ["derive", "rc"] }
serde-wasm-bindgen = { version = "0.6.4" }
serde_json = { version = "1.0.113" }
serde_json = { version = "1.0.142" }
serde_test = { version = "1.0.152" }
serde_with = { version = "3.6.0", default-features = false, features = [
"macros",
] }
shellexpand = { version = "3.0.0" }
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.2", features = ["union", "const_generics", "const_new"] }
smallvec = { version = "1.13.2", features = ["union", "const_generics", "const_new", "serde"] }
snapbox = { version = "0.6.0", features = [
"diff",
"term-svg",
@@ -215,6 +216,8 @@ unexpected_cfgs = { level = "warn", check-cfg = [
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -2 }
# Enabled at the crate level
disallowed_methods = "allow"
# Allowed pedantic lints
char_lit_as_u8 = "allow"
collapsible_else_if = "allow"
@@ -253,6 +256,7 @@ unused_peekable = "warn"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"
[profile.release]
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.12.9/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.9/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.10/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.10/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.9
rev: v0.12.10
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -24,3 +24,20 @@ ignore-interior-mutability = [
# The expression is read-only.
"ruff_python_ast::hashable::HashableExpr",
]
disallowed-methods = [
{ path = "std::env::var", reason = "Use System::env_var instead in ty crates" },
{ path = "std::env::current_dir", reason = "Use System::current_directory instead in ty crates" },
{ path = "std::fs::read_to_string", reason = "Use System::read_to_string instead in ty crates" },
{ path = "std::fs::metadata", reason = "Use System::path_metadata instead in ty crates" },
{ path = "std::fs::canonicalize", reason = "Use System::canonicalize_path instead in ty crates" },
{ path = "dunce::canonicalize", reason = "Use System::canonicalize_path instead in ty crates" },
{ path = "std::fs::read_dir", reason = "Use System::read_directory instead in ty crates" },
{ path = "std::fs::write", reason = "Use WritableSystem::write_file instead in ty crates" },
{ path = "std::fs::create_dir_all", reason = "Use WritableSystem::create_directory_all instead in ty crates" },
{ path = "std::fs::File::create_new", reason = "Use WritableSystem::create_new_file instead in ty crates" },
# Path methods that have System trait equivalents
{ path = "std::path::Path::exists", reason = "Use System::path_exists instead in ty crates" },
{ path = "std::path::Path::is_dir", reason = "Use System::is_directory instead in ty crates" },
{ path = "std::path::Path::is_file", reason = "Use System::is_file instead in ty crates" },
]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.12.9"
version = "0.12.10"
publish = true
authors = { workspace = true }
edition = { workspace = true }
@@ -31,7 +31,7 @@ ruff_workspace = { workspace = true }
anyhow = { workspace = true }
argfile = { workspace = true }
bincode = { workspace = true, features = ["serde"] }
bincode = { workspace = true }
bitflags = { workspace = true }
cachedir = { workspace = true }
clap = { workspace = true, features = ["derive", "env", "wrap_help"] }

View File

@@ -40,6 +40,7 @@ salsa = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
similar = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, optional = true }

View File

@@ -22,6 +22,7 @@ mod stylesheet;
/// a characteristic is a deficiency. An example of a characteristic that is
/// _not_ a deficiency is the `reveal_type` diagnostic for our type checker.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Diagnostic {
/// The actual diagnostic.
///
@@ -500,6 +501,7 @@ impl Diagnostic {
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct DiagnosticInner {
id: DiagnosticId,
severity: Severity,
@@ -576,6 +578,7 @@ impl Eq for RenderingSortKey<'_> {}
/// another (for a single parent diagnostic) is the order in which they were
/// attached to the diagnostic.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct SubDiagnostic {
/// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
/// pointer-sized.
@@ -685,6 +688,7 @@ impl SubDiagnostic {
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct SubDiagnosticInner {
severity: SubDiagnosticSeverity,
message: DiagnosticMessage,
@@ -713,6 +717,7 @@ struct SubDiagnosticInner {
/// Messages attached to annotations should also be as brief and specific as
/// possible. Long messages could negative impact the quality of rendering.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Annotation {
/// The span of this annotation, corresponding to some subsequence of the
/// user's input that we want to highlight.
@@ -855,6 +860,7 @@ impl Annotation {
/// These tags are used to provide additional information about the annotation.
/// and are passed through to the language server protocol.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum DiagnosticTag {
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
Unnecessary,
@@ -869,6 +875,7 @@ pub enum DiagnosticTag {
///
/// Rules use kebab case, e.g. `no-foo`.
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
pub struct LintName(&'static str);
impl LintName {
@@ -881,6 +888,66 @@ impl LintName {
}
}
#[cfg(feature = "serde")]
pub use lint_name_serde::LintRegistryGuard;
#[cfg(feature = "serde")]
mod lint_name_serde {
use super::LintName;
use std::cell::RefCell;
thread_local! {
/// Serde doesn't provide any easy means to pass a value to a [`Deserialize`] implementation,
/// but we need a way to retrieve static [`LintName`]s from the lint registry when deserializing.
///
/// Use the [`LintRegistryGuard`] to initialize the thread local before calling into any
/// deserialization code. It ensures that the thread local variable gets cleaned up
/// once deserialization is done (once the guard gets dropped).
static LINT_REGISTRY: RefCell<Option<LintRegistry>> = const { RefCell::new(None) };
}
type LintRegistry = fn(&str) -> Option<LintName>;
/// Guard to safely change the lint registry for the current thread.
#[must_use]
pub struct LintRegistryGuard {
prev_value: Option<LintRegistry>,
}
impl LintRegistryGuard {
pub fn new(registry: LintRegistry) -> Self {
let prev = LINT_REGISTRY.replace(Some(registry));
Self { prev_value: prev }
}
}
impl Drop for LintRegistryGuard {
fn drop(&mut self) {
LINT_REGISTRY.set(self.prev_value.take());
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for LintName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let name: &str = serde::Deserialize::deserialize(deserializer)?;
LINT_REGISTRY.with_borrow(|registry| {
let registry = registry
.expect("must set the `LintRegistryGuard` when deserializing a `LintName`");
registry(name).ok_or(serde::de::Error::custom(format!(
"invalid `LintName` {name}"
)))
})
}
}
}
impl std::ops::Deref for LintName {
type Target = str;
@@ -909,6 +976,7 @@ impl PartialEq<&str> for LintName {
/// Uniquely identifies the kind of a diagnostic.
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum DiagnosticId {
Panic,
@@ -1097,6 +1165,30 @@ impl UnifiedFile {
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for UnifiedFile {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
UnifiedFile::Ty(file) => serde::Serialize::serialize(file, serializer),
// Persistent caching is only used in ty.
UnifiedFile::Ruff(..) => panic!("Ruff files are not persistable"),
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for UnifiedFile {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
serde::Deserialize::deserialize(deserializer).map(UnifiedFile::Ty)
}
}
/// A unified wrapper for types that can be converted to a [`SourceCode`].
///
/// As with [`UnifiedFile`], ruff and ty use slightly different representations for source code.
@@ -1128,6 +1220,7 @@ impl DiagnosticSource {
/// range isn't present, it semantically implies that the diagnostic refers to
/// the entire file. For example, when the file should be executable but isn't.
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Span {
file: UnifiedFile,
range: Option<TextRange>,
@@ -1206,6 +1299,7 @@ impl From<crate::files::FileRange> for Span {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Severity {
Info,
Warning,
@@ -1241,6 +1335,7 @@ impl Severity {
/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
/// deleted and the two combined again.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum SubDiagnosticSeverity {
Help,
Info,
@@ -1294,6 +1389,10 @@ pub struct DisplayDiagnosticConfig {
hide_severity: bool,
/// Whether to show the availability of a fix in a diagnostic.
show_fix_status: bool,
/// Whether to show the diff for an available fix after the main diagnostic.
///
/// This currently only applies to `DiagnosticFormat::Full`.
show_fix_diff: bool,
/// The lowest applicability that should be shown when reporting diagnostics.
fix_applicability: Applicability,
}
@@ -1341,6 +1440,14 @@ impl DisplayDiagnosticConfig {
}
}
/// Whether to show a diff for an available fix after the main diagnostic.
pub fn show_fix_diff(self, yes: bool) -> DisplayDiagnosticConfig {
DisplayDiagnosticConfig {
show_fix_diff: yes,
..self
}
}
/// Set the lowest fix applicability that should be shown.
///
/// In other words, an applicability of `Safe` (the default) would suppress showing fixes or fix
@@ -1364,6 +1471,7 @@ impl Default for DisplayDiagnosticConfig {
preview: false,
hide_severity: false,
show_fix_status: false,
show_fix_diff: false,
fix_applicability: Applicability::Safe,
}
}
@@ -1476,6 +1584,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
/// a blanket trait implementation for `IntoDiagnosticMessage` for
/// anything that implements `std::fmt::Display`.
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DiagnosticMessage(Box<str>);
impl DiagnosticMessage {
@@ -1539,7 +1648,11 @@ impl<T: std::fmt::Display> IntoDiagnosticMessage for T {
///
/// For Ruff rules this means the noqa code.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(transparent)
)]
pub struct SecondaryCode(String);
impl SecondaryCode {

View File

@@ -1,7 +1,17 @@
use std::borrow::Cow;
use std::num::NonZeroUsize;
use anstyle::Style;
use similar::{ChangeTag, TextDiff};
use ruff_annotate_snippets::Renderer as AnnotateRenderer;
use ruff_diagnostics::{Applicability, Fix};
use ruff_source_file::OneIndexed;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::diagnostic::render::{FileResolver, Resolved};
use crate::diagnostic::{Diagnostic, DisplayDiagnosticConfig, stylesheet::DiagnosticStylesheet};
use crate::diagnostic::stylesheet::{DiagnosticStylesheet, fmt_styled};
use crate::diagnostic::{Diagnostic, DiagnosticSource, DisplayDiagnosticConfig};
pub(super) struct FullRenderer<'a> {
resolver: &'a dyn FileResolver,
@@ -48,12 +58,199 @@ impl<'a> FullRenderer<'a> {
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
}
writeln!(f)?;
if self.config.show_fix_diff {
if let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver) {
writeln!(f, "{diff}")?;
}
}
}
Ok(())
}
}
/// Renders a diff that shows the code fixes.
///
/// The implementation isn't fully fledged out and only used by tests. Before using in production, try
/// * Improve layout
/// * Replace tabs with spaces for a consistent experience across terminals
/// * Replace zero-width whitespaces
/// * Print a simpler diff if only a single line has changed
/// * Compute the diff from the `Edit` because diff calculation is expensive.
struct Diff<'a> {
fix: &'a Fix,
diagnostic_source: DiagnosticSource,
stylesheet: &'a DiagnosticStylesheet,
}
impl<'a> Diff<'a> {
fn from_diagnostic(
diagnostic: &'a Diagnostic,
stylesheet: &'a DiagnosticStylesheet,
resolver: &'a dyn FileResolver,
) -> Option<Diff<'a>> {
Some(Diff {
fix: diagnostic.fix()?,
diagnostic_source: diagnostic
.primary_span_ref()?
.file
.diagnostic_source(resolver),
stylesheet,
})
}
}
impl std::fmt::Display for Diff<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let source_code = self.diagnostic_source.as_source_code();
let source_text = source_code.text();
// TODO(dhruvmanila): Add support for Notebook cells once it's user-facing
let mut output = String::with_capacity(source_text.len());
let mut last_end = TextSize::default();
for edit in self.fix.edits() {
output.push_str(source_code.slice(TextRange::new(last_end, edit.start())));
output.push_str(edit.content().unwrap_or_default());
last_end = edit.end();
}
output.push_str(&source_text[usize::from(last_end)..]);
let diff = TextDiff::from_lines(source_text, &output);
let message = match self.fix.applicability() {
// TODO(zanieb): Adjust this messaging once it's user-facing
Applicability::Safe => "Safe fix",
Applicability::Unsafe => "Unsafe fix",
Applicability::DisplayOnly => "Display-only fix",
};
// TODO(brent) `stylesheet.separator` is cyan rather than blue, as we had before. I think
// we're getting rid of this soon anyway, so I didn't think it was worth adding another
// style to the stylesheet temporarily. The color doesn't appear at all in the snapshot
// tests, which is the only place these are currently used.
writeln!(f, " {}", fmt_styled(message, self.stylesheet.separator))?;
let (largest_old, largest_new) = diff
.ops()
.last()
.map(|op| (op.old_range().start, op.new_range().start))
.unwrap_or_default();
let digit_with = OneIndexed::from_zero_indexed(largest_new.max(largest_old)).digits();
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
if idx > 0 {
writeln!(f, "{:-^1$}", "-", 80)?;
}
for op in group {
for change in diff.iter_inline_changes(op) {
let sign = match change.tag() {
ChangeTag::Delete => "-",
ChangeTag::Insert => "+",
ChangeTag::Equal => " ",
};
let line_style = LineStyle::from(change.tag(), self.stylesheet);
let old_index = change.old_index().map(OneIndexed::from_zero_indexed);
let new_index = change.new_index().map(OneIndexed::from_zero_indexed);
write!(
f,
"{} {} |{}",
Line {
index: old_index,
width: digit_with
},
Line {
index: new_index,
width: digit_with
},
fmt_styled(line_style.apply_to(sign), self.stylesheet.emphasis),
)?;
for (emphasized, value) in change.iter_strings_lossy() {
let value = show_nonprinting(&value);
if emphasized {
write!(
f,
"{}",
fmt_styled(line_style.apply_to(&value), self.stylesheet.underline)
)?;
} else {
write!(f, "{}", line_style.apply_to(&value))?;
}
}
if change.missing_newline() {
writeln!(f)?;
}
}
}
}
Ok(())
}
}
struct LineStyle {
style: Style,
}
impl LineStyle {
fn apply_to(&self, input: &str) -> impl std::fmt::Display {
fmt_styled(input, self.style)
}
fn from(value: ChangeTag, stylesheet: &DiagnosticStylesheet) -> LineStyle {
match value {
ChangeTag::Equal => LineStyle {
style: stylesheet.none,
},
ChangeTag::Delete => LineStyle {
style: stylesheet.deletion,
},
ChangeTag::Insert => LineStyle {
style: stylesheet.insertion,
},
}
}
}
struct Line {
index: Option<OneIndexed>,
width: NonZeroUsize,
}
impl std::fmt::Display for Line {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.index {
None => {
for _ in 0..self.width.get() {
f.write_str(" ")?;
}
Ok(())
}
Some(idx) => write!(f, "{:<width$}", idx, width = self.width.get()),
}
}
}
fn show_nonprinting(s: &str) -> Cow<'_, str> {
if s.find(['\x07', '\x08', '\x1b', '\x7f']).is_some() {
Cow::Owned(
s.replace('\x07', "")
.replace('\x08', "")
.replace('\x1b', "")
.replace('\x7f', ""),
)
} else {
Cow::Borrowed(s)
}
}
#[cfg(test)]
mod tests {
use ruff_diagnostics::Applicability;

View File

@@ -40,9 +40,12 @@ pub struct DiagnosticStylesheet {
pub(crate) help: Style,
pub(crate) line_no: Style,
pub(crate) emphasis: Style,
pub(crate) underline: Style,
pub(crate) none: Style,
pub(crate) separator: Style,
pub(crate) secondary_code: Style,
pub(crate) insertion: Style,
pub(crate) deletion: Style,
}
impl Default for DiagnosticStylesheet {
@@ -63,9 +66,12 @@ impl DiagnosticStylesheet {
help: AnsiColor::BrightCyan.on_default().effects(Effects::BOLD),
line_no: bright_blue.effects(Effects::BOLD),
emphasis: Style::new().effects(Effects::BOLD),
underline: Style::new().effects(Effects::UNDERLINE),
none: Style::new(),
separator: AnsiColor::Cyan.on_default(),
secondary_code: AnsiColor::Red.on_default().effects(Effects::BOLD),
insertion: AnsiColor::Green.on_default(),
deletion: AnsiColor::Red.on_default(),
}
}
@@ -78,9 +84,12 @@ impl DiagnosticStylesheet {
help: Style::new(),
line_no: Style::new(),
emphasis: Style::new(),
underline: Style::new(),
none: Style::new(),
separator: Style::new(),
secondary_code: Style::new(),
insertion: Style::new(),
deletion: Style::new(),
}
}
}

View File

@@ -9,7 +9,17 @@ use crate::system::file_time_now;
/// * The last modification time of the file.
/// * The hash of the file's content.
/// * The revision as it comes from an external system, for example the LSP.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)]
#[derive(
Copy,
Clone,
Debug,
Eq,
PartialEq,
Default,
get_size2::GetSize,
serde::Serialize,
serde::Deserialize,
)]
pub struct FileRevision(u128);
impl FileRevision {

View File

@@ -14,7 +14,7 @@ use crate::diagnostic::{Span, UnifiedFile};
use crate::file_revision::FileRevision;
use crate::files::file_root::FileRoots;
use crate::files::private::FileStatus;
use crate::system::{SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf};
use crate::system::{FileType, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf};
use crate::vendored::{VendoredPath, VendoredPathBuf};
use crate::{Db, FxDashMap, vendored};
@@ -139,6 +139,7 @@ impl Files {
};
tracing::trace!("Adding vendored file `{}`", path);
let file = File::builder(FilePath::Vendored(path.to_path_buf()))
.permissions(Some(0o444))
.revision(metadata.revision())
@@ -200,7 +201,15 @@ impl Files {
let mut roots = self.inner.roots.write().unwrap();
let absolute = SystemPath::absolute(path, db.system().current_directory());
roots.try_add(db, absolute, kind)
let (Ok(root) | Err(root)) = roots.try_add(db, absolute, |absolute| {
FileRoot::builder(absolute, kind, FileRevision::now())
.durability(Durability::HIGH)
.revision_durability(kind.durability())
.new(db)
});
root
}
/// Updates the revision of the root for `path`.
@@ -259,6 +268,51 @@ impl Files {
root.set_revision(db).to(FileRevision::now());
}
}
/// Seed the files with an existing [`File`] instance.
pub fn seed(&self, file: File, db: &dyn Db) {
let seeded = match file.path(db) {
FilePath::System(path) => self
.inner
.system_by_path
.insert(path.clone(), file)
.is_none(),
FilePath::SystemVirtual(path) => self
.inner
.system_virtual_by_path
.insert(path.clone(), VirtualFile(file))
.is_none(),
FilePath::Vendored(path) => self
.inner
.vendored_by_path
.insert(path.clone(), file)
.is_none(),
};
// Recreating a `File` input means the persisted queries depending on that file
// will be invalidated.
assert!(
seeded,
"unexpected `File` input recreated for path `{}`",
file.path(db)
);
}
/// Seed the files with an existing [`FileRoot`] instance.
pub fn seed_root(&self, root: FileRoot, db: &dyn Db) {
let mut roots = self.inner.roots.write().unwrap();
let seeded = roots
.try_add(db, root.path(db).to_path_buf(), |_| root)
.is_ok();
// Recreating a `FileRoot` input means the persisted queries depending on that file
// root will be invalidated.
assert!(
seeded,
"unexpected `FileRoot` input recreated for path `{}`",
root.path(db)
);
}
}
impl fmt::Debug for Files {
@@ -290,7 +344,7 @@ impl std::panic::RefUnwindSafe for Files {}
/// # Ordering
/// Ordering is based on the file's salsa-assigned id and not on its values.
/// The id may change between runs.
#[salsa::input(heap_size=ruff_memory_usage::heap_size)]
#[salsa::input(persist, heap_size=ruff_memory_usage::heap_size)]
#[derive(PartialOrd, Ord)]
pub struct File {
/// The path of the file (immutable).
@@ -414,6 +468,15 @@ impl File {
}
}
/// Loads all existing [`File`]s in the database.
pub fn load_all(db: &dyn Db) -> Vec<File> {
// TODO: Prune deleted paths.
File::ingredient(db)
.entries(db.zalsa())
.map(|entry| entry.as_struct())
.collect()
}
/// Private method providing the implementation for [`Self::sync_path`] and [`Self::sync`] for
/// system paths.
fn sync_system_path(db: &mut dyn Db, path: &SystemPath, file: Option<File>) {
@@ -522,7 +585,17 @@ impl VirtualFile {
// The types in here need to be public because they're salsa ingredients but we
// don't want them to be publicly accessible. That's why we put them into a private module.
mod private {
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)]
#[derive(
Copy,
Clone,
Debug,
Eq,
PartialEq,
Default,
get_size2::GetSize,
serde::Serialize,
serde::Deserialize,
)]
pub enum FileStatus {
/// The file exists.
#[default]
@@ -536,6 +609,16 @@ mod private {
}
}
impl From<FileType> for FileStatus {
fn from(value: FileType) -> Self {
match value {
FileType::File => FileStatus::Exists,
FileType::Symlink => FileStatus::Exists,
FileType::Directory => FileStatus::IsADirectory,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum FileError {
IsADirectory,

View File

@@ -16,7 +16,7 @@ use crate::system::{SystemPath, SystemPathBuf};
/// The main usage of file roots is to determine a file's durability. But it can also be used
/// to make a salsa query dependent on whether a file in a root has changed without writing any
/// manual invalidation logic.
#[salsa::input(debug, heap_size=ruff_memory_usage::heap_size)]
#[salsa::input(persist, debug, heap_size=ruff_memory_usage::heap_size)]
pub struct FileRoot {
/// The path of a root is guaranteed to never change.
#[returns(deref)]
@@ -35,9 +35,20 @@ impl FileRoot {
pub fn durability(self, db: &dyn Db) -> salsa::Durability {
self.kind_at_time_of_creation(db).durability()
}
/// Loads all existing [`FileRoot`]s in the database.
pub fn load_all(db: &dyn Db) -> Vec<FileRoot> {
// TODO: Prune deleted paths.
FileRoot::ingredient(db)
.entries(db.zalsa())
.map(|entry| entry.as_struct())
.collect()
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
#[derive(
Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize, serde::Serialize, serde::Deserialize,
)]
pub enum FileRootKind {
/// The root of a project.
Project,
@@ -47,7 +58,7 @@ pub enum FileRootKind {
}
impl FileRootKind {
const fn durability(self) -> Durability {
pub const fn durability(self) -> Durability {
match self {
FileRootKind::Project => Durability::LOW,
FileRootKind::LibrarySearchPath => Durability::HIGH,
@@ -62,34 +73,34 @@ pub(super) struct FileRoots {
}
impl FileRoots {
/// Tries to add a new root for `path` and returns the root.
/// Tries to add a new root for `path`.
///
/// The root isn't added nor is the file root's kind updated if a root for `path` already exists.
///
/// Returns `Ok(root)` if the `FileRoot` was successfully added, and returns `Err(root)` with
/// the previous root if one already existed at that path.
pub(super) fn try_add(
&mut self,
db: &dyn Db,
path: SystemPathBuf,
kind: FileRootKind,
) -> FileRoot {
create_root: impl FnOnce(SystemPathBuf) -> FileRoot,
) -> Result<FileRoot, FileRoot> {
// SAFETY: Guaranteed to succeed because `path` is a UTF-8 that only contains Unicode characters.
let normalized_path = path.as_std_path().to_slash().unwrap();
if let Ok(existing) = self.by_path.at(&normalized_path) {
// Only if it is an exact match
if existing.value.path(db) == &*path {
return *existing.value;
return Err(*existing.value);
}
}
// normalize the path to use `/` separators and escape the '{' and '}' characters,
// which matchit uses for routing parameters
// Normalize the path to use `/` separators and escape the '{' and '}' characters,
// which `matchit` uses for routing parameters.
let mut route = normalized_path.replace('{', "{{").replace('}', "}}");
// Insert a new source root
let root = FileRoot::builder(path, kind, FileRevision::now())
.durability(Durability::HIGH)
.revision_durability(kind.durability())
.new(db);
let root = create_root(path);
// Insert a path that matches the root itself
self.by_path.insert(route.clone(), root).unwrap();
@@ -100,7 +111,7 @@ impl FileRoots {
self.by_path.insert(route, root).unwrap();
self.roots.push(root);
root
Ok(root)
}
/// Returns the closest root for `path` or `None` if no root contains `path`.

View File

@@ -11,7 +11,9 @@ use std::fmt::{Display, Formatter};
/// * a file stored on the [host system](crate::system::System).
/// * a virtual file stored on the [host system](crate::system::System).
/// * a vendored file stored in the [vendored file system](crate::vendored::VendoredFileSystem).
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(
Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize, serde::Serialize, serde::Deserialize,
)]
pub enum FilePath {
/// Path to a file on the [host system](crate::system::System).
System(SystemPathBuf),

View File

@@ -1,3 +1,8 @@
#![warn(
clippy::disallowed_methods,
reason = "Prefer System trait methods over std methods"
)]
use crate::files::Files;
use crate::system::System;
use crate::vendored::VendoredFileSystem;
@@ -65,6 +70,10 @@ pub trait Db: salsa::Database {
/// to process work in parallel. For example, to index a directory or checking the files of a project.
/// ty can still spawn more threads for other tasks, e.g. to wait for a Ctrl+C signal or
/// watching the files for changes.
#[expect(
clippy::disallowed_methods,
reason = "We don't have access to System here, but this is also only used by the CLI and the server which always run on a real system."
)]
pub fn max_parallelism() -> NonZeroUsize {
std::env::var(EnvVars::TY_MAX_PARALLELISM)
.or_else(|_| std::env::var(EnvVars::RAYON_NUM_THREADS))

View File

@@ -92,14 +92,14 @@ impl ParsedModule {
self.inner.store(None);
}
/// Returns a pointer for this [`ParsedModule`].
/// Returns the pointer address of this [`ParsedModule`].
///
/// The pointer uniquely identifies the module within the current Salsa revision,
/// regardless of whether particular [`ParsedModuleRef`] instances are garbage collected.
pub fn as_ptr(&self) -> *const () {
pub fn addr(&self) -> usize {
// Note that the outer `Arc` in `inner` is stable across garbage collection, while the inner
// `Arc` within the `ArcSwap` may change.
Arc::as_ptr(&self.inner).cast()
Arc::as_ptr(&self.inner).addr()
}
}
@@ -202,9 +202,13 @@ mod indexed {
/// Returns the node at the given index.
pub fn get_by_index<'ast>(&'ast self, index: NodeIndex) -> AnyRootNodeRef<'ast> {
let index = index
.as_u32()
.expect("attempted to access uninitialized `NodeIndex`");
// Note that this method restores the correct lifetime: the nodes are valid for as
// long as the reference to `IndexedModule` is alive.
self.index[index.as_usize()]
self.index[index as usize]
}
}
@@ -220,7 +224,7 @@ mod indexed {
T: HasNodeIndex + std::fmt::Debug,
AnyRootNodeRef<'a>: From<&'a T>,
{
node.node_index().set(self.index);
node.node_index().set(NodeIndex::from(self.index));
self.nodes.push(AnyRootNodeRef::from(node));
self.index += 1;
}

View File

@@ -148,7 +148,16 @@ impl From<Notebook> for SourceTextKind {
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone, get_size2::GetSize)]
#[derive(
Debug,
thiserror::Error,
PartialEq,
Eq,
Clone,
get_size2::GetSize,
serde::Serialize,
serde::Deserialize,
)]
pub enum SourceTextError {
#[error("Failed to read notebook: {0}`")]
FailedToReadNotebook(String),

View File

@@ -46,7 +46,7 @@ pub type Result<T> = std::io::Result<T>;
/// * File watching isn't supported.
///
/// Abstracting the system also enables tests to use a more efficient in-memory file system.
pub trait System: Debug {
pub trait System: Debug + Sync + Send {
/// Reads the metadata of the file or directory at `path`.
///
/// This function will traverse symbolic links to query information about the destination file.
@@ -66,6 +66,9 @@ pub trait System: Debug {
/// See [dunce::canonicalize] for more information.
fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf>;
/// Reads the content of the file at `path` into a bytes buffer.
fn read_to_end(&self, path: &SystemPath) -> Result<Vec<u8>>;
/// Reads the content of the file at `path` into a [`String`].
fn read_to_string(&self, path: &SystemPath) -> Result<String>;
@@ -197,6 +200,8 @@ pub trait System: Debug {
fn as_any(&self) -> &dyn std::any::Any;
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
fn dyn_clone(&self) -> Box<dyn System>;
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
@@ -240,7 +245,7 @@ pub trait WritableSystem: System {
fn create_new_file(&self, path: &SystemPath) -> Result<()>;
/// Writes the given content to the file at the given path.
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()>;
fn write_file(&self, path: &SystemPath, content: &[u8]) -> Result<()>;
/// Creates a directory at `path` as well as any intermediate directories.
fn create_directory_all(&self, path: &SystemPath) -> Result<()>;
@@ -276,7 +281,7 @@ pub trait WritableSystem: System {
// ensures that only one thread/process ever attempts to write to it to avoid corrupting
// the cache.
self.create_new_file(&cache_path)?;
self.write_file(&cache_path, &contents)?;
self.write_file(&cache_path, contents.as_bytes())?;
Ok(Some(cache_path))
}

View File

@@ -114,8 +114,8 @@ impl MemoryFileSystem {
matches!(by_path.get(&normalized), Some(Entry::Directory(_)))
}
pub fn read_to_string(&self, path: impl AsRef<SystemPath>) -> Result<String> {
fn read_to_string(fs: &MemoryFileSystem, path: &SystemPath) -> Result<String> {
pub fn read_to_end(&self, path: impl AsRef<SystemPath>) -> Result<Vec<u8>> {
fn read_to_end(fs: &MemoryFileSystem, path: &SystemPath) -> Result<Vec<u8>> {
let by_path = fs.inner.by_path.read().unwrap();
let normalized = fs.normalize_path(path);
@@ -127,13 +127,18 @@ impl MemoryFileSystem {
}
}
read_to_string(self, path.as_ref())
read_to_end(self, path.as_ref())
}
pub(crate) fn read_virtual_path_to_string(
pub fn read_to_string(&self, path: impl AsRef<SystemPath>) -> Result<String> {
self.read_to_end(path)
.and_then(|bytes| String::from_utf8(bytes).map_err(io::Error::other))
}
pub(crate) fn read_virtual_path_to_end(
&self,
path: impl AsRef<SystemVirtualPath>,
) -> Result<String> {
) -> Result<Vec<u8>> {
let virtual_files = self.inner.virtual_files.read().unwrap();
let file = virtual_files
.get(&path.as_ref().to_path_buf())
@@ -142,6 +147,14 @@ impl MemoryFileSystem {
Ok(file.content.clone())
}
pub(crate) fn read_virtual_path_to_string(
&self,
path: impl AsRef<SystemVirtualPath>,
) -> Result<String> {
self.read_virtual_path_to_end(path)
.and_then(|bytes| String::from_utf8(bytes).map_err(io::Error::other))
}
pub fn exists(&self, path: &SystemPath) -> bool {
let by_path = self.inner.by_path.read().unwrap();
let normalized = self.normalize_path(path);
@@ -161,7 +174,7 @@ impl MemoryFileSystem {
match by_path.entry(normalized) {
btree_map::Entry::Vacant(entry) => {
entry.insert(Entry::File(File {
content: String::new(),
content: Vec::new(),
last_modified: file_time_now(),
}));
@@ -177,13 +190,17 @@ impl MemoryFileSystem {
/// Stores a new file in the file system.
///
/// The operation overrides the content for an existing file with the same normalized `path`.
pub fn write_file(&self, path: impl AsRef<SystemPath>, content: impl ToString) -> Result<()> {
pub fn write_file(
&self,
path: impl AsRef<SystemPath>,
content: impl Into<Vec<u8>>,
) -> Result<()> {
let mut by_path = self.inner.by_path.write().unwrap();
let normalized = self.normalize_path(path.as_ref());
let file = get_or_create_file(&mut by_path, &normalized)?;
file.content = content.to_string();
file.content = content.into();
file.last_modified = file_time_now();
Ok(())
@@ -214,7 +231,7 @@ impl MemoryFileSystem {
pub fn write_file_all(
&self,
path: impl AsRef<SystemPath>,
content: impl ToString,
content: impl Into<Vec<u8>>,
) -> Result<()> {
let path = path.as_ref();
@@ -228,19 +245,23 @@ impl MemoryFileSystem {
/// Stores a new virtual file in the file system.
///
/// The operation overrides the content for an existing virtual file with the same `path`.
pub fn write_virtual_file(&self, path: impl AsRef<SystemVirtualPath>, content: impl ToString) {
pub fn write_virtual_file(
&self,
path: impl AsRef<SystemVirtualPath>,
content: impl Into<Vec<u8>>,
) {
let path = path.as_ref();
let mut virtual_files = self.inner.virtual_files.write().unwrap();
match virtual_files.entry(path.to_path_buf()) {
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(File {
content: content.to_string(),
content: content.into(),
last_modified: file_time_now(),
});
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
entry.get_mut().content = content.to_string();
entry.get_mut().content = content.into();
}
}
}
@@ -468,7 +489,7 @@ impl Entry {
#[derive(Debug)]
struct File {
content: String,
content: Vec<u8>,
last_modified: FileTime,
}
@@ -533,7 +554,7 @@ fn get_or_create_file<'a>(
let entry = paths.entry(normalized.to_path_buf()).or_insert_with(|| {
Entry::File(File {
content: String::new(),
content: Vec::new(),
last_modified: file_time_now(),
})
});

View File

@@ -1,3 +1,5 @@
#![allow(clippy::disallowed_methods)]
use super::walk_directory::{
self, DirectoryWalker, WalkDirectoryBuilder, WalkDirectoryConfiguration,
WalkDirectoryVisitorBuilder, WalkState,
@@ -91,6 +93,10 @@ impl System for OsSystem {
})
}
fn read_to_end(&self, path: &SystemPath) -> Result<Vec<u8>> {
std::fs::read(path.as_std_path())
}
fn read_to_string(&self, path: &SystemPath) -> Result<String> {
std::fs::read_to_string(path.as_std_path())
}
@@ -255,6 +261,10 @@ impl System for OsSystem {
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
std::env::var(name)
}
fn dyn_clone(&self) -> Box<dyn System> {
Box::new(self.clone())
}
}
impl OsSystem {
@@ -351,7 +361,7 @@ impl WritableSystem for OsSystem {
std::fs::File::create_new(path).map(drop)
}
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
fn write_file(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
std::fs::write(path.as_std_path(), content)
}

View File

@@ -762,7 +762,17 @@ impl SystemVirtualPath {
}
/// An owned, virtual path on [`System`](`super::System`) (akin to [`String`]).
#[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord, get_size2::GetSize)]
#[derive(
Eq,
PartialEq,
Clone,
Hash,
PartialOrd,
Ord,
get_size2::GetSize,
serde::Serialize,
serde::Deserialize,
)]
pub struct SystemVirtualPathBuf(String);
impl SystemVirtualPathBuf {

View File

@@ -75,6 +75,10 @@ impl System for TestSystem {
self.system().canonicalize_path(path)
}
fn read_to_end(&self, path: &SystemPath) -> Result<Vec<u8>> {
self.system().read_to_end(path)
}
fn read_to_string(&self, path: &SystemPath) -> Result<String> {
self.system().read_to_string(path)
}
@@ -146,6 +150,10 @@ impl System for TestSystem {
fn case_sensitivity(&self) -> CaseSensitivity {
self.system().case_sensitivity()
}
fn dyn_clone(&self) -> Box<dyn System> {
Box::new(self.clone())
}
}
impl Default for TestSystem {
@@ -161,7 +169,7 @@ impl WritableSystem for TestSystem {
self.system().create_new_file(path)
}
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
fn write_file(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
self.system().write_file(path, content)
}
@@ -181,7 +189,9 @@ pub trait DbWithWritableSystem: Db + Sized {
/// Writes the content of the given file and notifies the Db about the change.
fn write_file(&mut self, path: impl AsRef<SystemPath>, content: impl AsRef<str>) -> Result<()> {
let path = path.as_ref();
match self.writable_system().write_file(path, content.as_ref()) {
let content = content.as_ref();
match self.writable_system().write_file(path, content.as_bytes()) {
Ok(()) => {
File::sync_path(self, path);
Ok(())
@@ -194,7 +204,8 @@ pub trait DbWithWritableSystem: Db + Sized {
File::sync_path(self, ancestor);
}
self.writable_system().write_file(path, content.as_ref())?;
self.writable_system()
.write_file(path, content.as_bytes())?;
File::sync_path(self, path);
Ok(())
@@ -239,8 +250,14 @@ pub trait DbWithTestSystem: Db + Sized {
///
/// ## Panics
/// If the db isn't using the [`InMemorySystem`].
fn write_virtual_file(&mut self, path: impl AsRef<SystemVirtualPath>, content: impl ToString) {
fn write_virtual_file(
&mut self,
path: impl AsRef<SystemVirtualPath>,
content: impl Into<Vec<u8>>,
) {
let path = path.as_ref();
let content = content.into();
self.test_system()
.memory_file_system()
.write_virtual_file(path, content);
@@ -318,6 +335,10 @@ impl System for InMemorySystem {
self.memory_fs.canonicalize(path)
}
fn read_to_end(&self, path: &SystemPath) -> Result<Vec<u8>> {
self.memory_fs.read_to_end(path)
}
fn read_to_string(&self, path: &SystemPath) -> Result<String> {
self.memory_fs.read_to_string(path)
}
@@ -394,6 +415,13 @@ impl System for InMemorySystem {
fn case_sensitivity(&self) -> CaseSensitivity {
CaseSensitivity::CaseSensitive
}
fn dyn_clone(&self) -> Box<dyn System> {
Box::new(Self {
user_config_directory: Mutex::new(self.user_config_directory.lock().unwrap().clone()),
memory_fs: self.memory_fs.clone(),
})
}
}
impl WritableSystem for InMemorySystem {
@@ -401,7 +429,7 @@ impl WritableSystem for InMemorySystem {
self.memory_fs.create_new_file(path)
}
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
fn write_file(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
self.memory_fs.write_file(path, content)
}

View File

@@ -88,7 +88,7 @@ impl ToOwned for VendoredPath {
}
#[repr(transparent)]
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
#[derive(Debug, Eq, PartialEq, Clone, Hash, serde::Serialize, serde::Deserialize)]
pub struct VendoredPathBuf(Utf8PathBuf);
impl get_size2::GetSize for VendoredPathBuf {

View File

@@ -14,8 +14,11 @@ license = { workspace = true }
doctest = false
[dependencies]
ruff_text_size = { workspace = true }
ruff_text_size = { workspace = true, features = ["get-size"] }
get-size2 = { workspace = true }
is-macro = { workspace = true }
serde = { workspace = true, optional = true, features = [] }
[features]
serde = ["dep:serde", "ruff_text_size/serde"]

View File

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

View File

@@ -13,3 +13,11 @@ Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
fd = os.open(".", os.O_RDONLY)
os.symlink("source.txt", "link.txt", dir_fd=fd) # Ok: dir_fd is not supported by pathlib
os.close(fd)
os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")

View File

@@ -106,4 +106,22 @@ os.replace("src", "dst", src_dir_fd=1)
os.replace("src", "dst", dst_dir_fd=2)
os.getcwd()
os.getcwdb()
os.getcwdb()
os.mkdir(path="directory")
os.mkdir(
# comment 1
"directory",
mode=0o777
)
os.mkdir("directory", mode=0o777, dir_fd=1)
os.makedirs("name", 0o777, exist_ok=False)
os.makedirs("name", 0o777, False)
os.makedirs(name="name", mode=0o777, exist_ok=False)
os.makedirs("name", unknown_kwarg=True)

View File

@@ -0,0 +1,18 @@
from __future__ import nested_scopes, generators
from __future__ import with_statement, unicode_literals
from __future__ import absolute_import, division
from __future__ import generator_stop
from __future__ import print_function, nested_scopes, generator_stop
print(with_statement)
generators = 1
class Foo():
def boo(self):
print(division)
__all__ = ["print_function", "generator_stop"]

View File

@@ -118,3 +118,10 @@ def func():
return lambda: value
defaultdict(constant_factory("<missing>"))
def func():
defaultdict(default_factory=t"") # OK
def func():
defaultdict(default_factory=t"hello") # OK

View File

@@ -102,3 +102,8 @@ deque("abc") # OK
deque(b"abc") # OK
deque(f"" "a") # OK
deque(f"{x}" "") # OK
# https://github.com/astral-sh/ruff/issues/19951
deque(t"")
deque(t"" t"")
deque(t"{""}") # OK

View File

@@ -1,10 +1,11 @@
use ruff_python_ast::PythonVersion;
use ruff_python_semantic::{Binding, ScopeKind};
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{
flake8_builtins, flake8_pyi, flake8_type_checking, flake8_unused_arguments, pep8_naming,
pyflakes, pylint, ruff,
pyflakes, pylint, pyupgrade, ruff,
};
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
@@ -45,6 +46,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
Rule::UnusedStaticMethodArgument,
Rule::UnusedUnpackedVariable,
Rule::UnusedVariable,
Rule::UnnecessaryFutureImport,
]) {
return;
}
@@ -224,6 +226,11 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
if checker.is_rule_enabled(Rule::UnusedImport) {
pyflakes::rules::unused_import(checker, scope);
}
if checker.is_rule_enabled(Rule::UnnecessaryFutureImport) {
if checker.target_version() >= PythonVersion::PY37 {
pyupgrade::rules::unnecessary_future_import(checker, scope);
}
}
if checker.is_rule_enabled(Rule::ImportPrivateName) {
pylint::rules::import_private_name(checker, scope);

View File

@@ -1039,8 +1039,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
}
if checker.any_rule_enabled(&[
Rule::OsMkdir,
Rule::OsMakedirs,
Rule::OsStat,
Rule::OsPathJoin,
Rule::OsPathSplitext,
@@ -1120,6 +1118,15 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::OsPathSamefile) {
flake8_use_pathlib::rules::os_path_samefile(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsMkdir) {
flake8_use_pathlib::rules::os_mkdir(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsMakedirs) {
flake8_use_pathlib::rules::os_makedirs(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsSymlink) {
flake8_use_pathlib::rules::os_symlink(checker, call, segments);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(
checker, call, segments,

View File

@@ -728,13 +728,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::non_ascii_module_import(checker, alias);
}
}
if checker.is_rule_enabled(Rule::UnnecessaryFutureImport) {
if checker.target_version() >= PythonVersion::PY37 {
if let Some("__future__") = module {
pyupgrade::rules::unnecessary_future_import(checker, stmt, names);
}
}
}
if checker.is_rule_enabled(Rule::DeprecatedMockImport) {
pyupgrade::rules::deprecated_mock_import(checker, stmt);
}

View File

@@ -921,8 +921,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-use-pathlib
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath),
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsChmod),
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir),
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs),
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsMkdir),
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsMakedirs),
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRename),
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReplace),
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir),
@@ -954,7 +954,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "207") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::Glob),
(Flake8UsePathlib, "208") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsListdir),
(Flake8UsePathlib, "210") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::InvalidPathlibWithSuffix),
(Flake8UsePathlib, "211") => (RuleGroup::Preview, rules::flake8_use_pathlib::violations::OsSymlink),
(Flake8UsePathlib, "211") => (RuleGroup::Preview, rules::flake8_use_pathlib::rules::OsSymlink),
// flake8-logging-format
(Flake8LoggingFormat, "001") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingStringFormat),

View File

@@ -1,202 +0,0 @@
use std::fmt::{Display, Formatter};
use std::num::NonZeroUsize;
use colored::{Color, ColoredString, Colorize, Styles};
use similar::{ChangeTag, TextDiff};
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::{OneIndexed, SourceFile};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::text_helpers::ShowNonprinting;
use crate::{Applicability, Fix};
/// Renders a diff that shows the code fixes.
///
/// The implementation isn't fully fledged out and only used by tests. Before using in production, try
/// * Improve layout
/// * Replace tabs with spaces for a consistent experience across terminals
/// * Replace zero-width whitespaces
/// * Print a simpler diff if only a single line has changed
/// * Compute the diff from the [`Edit`] because diff calculation is expensive.
pub(super) struct Diff<'a> {
fix: &'a Fix,
source_code: &'a SourceFile,
}
impl<'a> Diff<'a> {
pub(crate) fn from_message(message: &'a Diagnostic) -> Option<Diff<'a>> {
message.fix().map(|fix| Diff {
source_code: message.expect_ruff_source_file(),
fix,
})
}
}
impl Display for Diff<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// TODO(dhruvmanila): Add support for Notebook cells once it's user-facing
let mut output = String::with_capacity(self.source_code.source_text().len());
let mut last_end = TextSize::default();
for edit in self.fix.edits() {
output.push_str(
self.source_code
.slice(TextRange::new(last_end, edit.start())),
);
output.push_str(edit.content().unwrap_or_default());
last_end = edit.end();
}
output.push_str(&self.source_code.source_text()[usize::from(last_end)..]);
let diff = TextDiff::from_lines(self.source_code.source_text(), &output);
let message = match self.fix.applicability() {
// TODO(zanieb): Adjust this messaging once it's user-facing
Applicability::Safe => "Safe fix",
Applicability::Unsafe => "Unsafe fix",
Applicability::DisplayOnly => "Display-only fix",
};
writeln!(f, " {}", message.blue())?;
let (largest_old, largest_new) = diff
.ops()
.last()
.map(|op| (op.old_range().start, op.new_range().start))
.unwrap_or_default();
let digit_with =
calculate_print_width(OneIndexed::from_zero_indexed(largest_new.max(largest_old)));
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
if idx > 0 {
writeln!(f, "{:-^1$}", "-", 80)?;
}
for op in group {
for change in diff.iter_inline_changes(op) {
let sign = match change.tag() {
ChangeTag::Delete => "-",
ChangeTag::Insert => "+",
ChangeTag::Equal => " ",
};
let line_style = LineStyle::from(change.tag());
let old_index = change.old_index().map(OneIndexed::from_zero_indexed);
let new_index = change.new_index().map(OneIndexed::from_zero_indexed);
write!(
f,
"{} {} |{}",
Line {
index: old_index,
width: digit_with
},
Line {
index: new_index,
width: digit_with
},
line_style.apply_to(sign).bold()
)?;
for (emphasized, value) in change.iter_strings_lossy() {
let value = value.show_nonprinting();
if emphasized {
write!(f, "{}", line_style.apply_to(&value).underline().on_black())?;
} else {
write!(f, "{}", line_style.apply_to(&value))?;
}
}
if change.missing_newline() {
writeln!(f)?;
}
}
}
}
Ok(())
}
}
struct LineStyle {
fgcolor: Option<Color>,
style: Option<Styles>,
}
impl LineStyle {
fn apply_to(&self, input: &str) -> ColoredString {
let mut colored = ColoredString::from(input);
if let Some(color) = self.fgcolor {
colored = colored.color(color);
}
if let Some(style) = self.style {
match style {
Styles::Clear => colored.clear(),
Styles::Bold => colored.bold(),
Styles::Dimmed => colored.dimmed(),
Styles::Underline => colored.underline(),
Styles::Reversed => colored.reversed(),
Styles::Italic => colored.italic(),
Styles::Blink => colored.blink(),
Styles::Hidden => colored.hidden(),
Styles::Strikethrough => colored.strikethrough(),
}
} else {
colored
}
}
}
impl From<ChangeTag> for LineStyle {
fn from(value: ChangeTag) -> Self {
match value {
ChangeTag::Equal => LineStyle {
fgcolor: None,
style: Some(Styles::Dimmed),
},
ChangeTag::Delete => LineStyle {
fgcolor: Some(Color::Red),
style: None,
},
ChangeTag::Insert => LineStyle {
fgcolor: Some(Color::Green),
style: None,
},
}
}
}
struct Line {
index: Option<OneIndexed>,
width: NonZeroUsize,
}
impl Display for Line {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self.index {
None => {
for _ in 0..self.width.get() {
f.write_str(" ")?;
}
Ok(())
}
Some(idx) => write!(f, "{:<width$}", idx, width = self.width.get()),
}
}
}
/// Calculate the length of the string representation of `value`
pub(super) fn calculate_print_width(mut value: OneIndexed) -> NonZeroUsize {
const TEN: OneIndexed = OneIndexed::from_zero_indexed(9);
let mut width = OneIndexed::ONE;
while value >= TEN {
value = OneIndexed::new(value.get() / 10).unwrap_or(OneIndexed::MIN);
width = width.checked_add(1).unwrap();
}
width
}

View File

@@ -10,7 +10,6 @@ use ruff_notebook::NotebookIndex;
use ruff_source_file::{LineColumn, OneIndexed};
use crate::fs::relativize_path;
use crate::message::diff::calculate_print_width;
use crate::message::{Emitter, EmitterContext};
use crate::settings::types::UnsafeFixes;
@@ -53,8 +52,8 @@ impl Emitter for GroupedEmitter {
max_column_length = max_column_length.max(message.start_location.column);
}
let row_length = calculate_print_width(max_row_length);
let column_length = calculate_print_width(max_column_length);
let row_length = max_row_length.digits();
let column_length = max_column_length.digits();
// Print the filename.
writeln!(writer, "{}:", relativize_path(&*filename).underline())?;
@@ -131,8 +130,7 @@ impl Display for DisplayGroupedMessage<'_> {
write!(
f,
" {row_padding}",
row_padding = " "
.repeat(self.row_length.get() - calculate_print_width(start_location.line).get())
row_padding = " ".repeat(self.row_length.get() - start_location.line.digits().get())
)?;
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
@@ -159,9 +157,8 @@ impl Display for DisplayGroupedMessage<'_> {
f,
"{row}{sep}{col}{col_padding} {code_and_body}",
sep = ":".cyan(),
col_padding = " ".repeat(
self.column_length.get() - calculate_print_width(start_location.column).get()
),
col_padding =
" ".repeat(self.column_length.get() - start_location.column.digits().get()),
code_and_body = RuleCodeAndBody {
message,
show_fix_status: self.show_fix_status,

View File

@@ -21,7 +21,6 @@ pub use text::TextEmitter;
use crate::Fix;
use crate::registry::Rule;
mod diff;
mod github;
mod gitlab;
mod grouped;

View File

@@ -1,23 +1,19 @@
use std::io::Write;
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
use ruff_db::diagnostic::{
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics,
};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext};
use crate::settings::types::UnsafeFixes;
pub struct TextEmitter {
/// Whether to show the diff of a fix, for diagnostics that have a fix.
///
/// Note that this is not currently exposed in the CLI (#7352) and is only used in tests.
show_fix_diff: bool,
config: DisplayDiagnosticConfig,
}
impl Default for TextEmitter {
fn default() -> Self {
Self {
show_fix_diff: false,
config: DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Concise)
.hide_severity(true)
@@ -35,7 +31,7 @@ impl TextEmitter {
#[must_use]
pub fn with_show_fix_diff(mut self, show_fix_diff: bool) -> Self {
self.show_fix_diff = show_fix_diff;
self.config = self.config.show_fix_diff(show_fix_diff);
self
}
@@ -77,15 +73,11 @@ impl Emitter for TextEmitter {
diagnostics: &[Diagnostic],
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in diagnostics {
write!(writer, "{}", message.display(context, &self.config))?;
if self.show_fix_diff {
if let Some(diff) = Diff::from_message(message) {
writeln!(writer, "{diff}")?;
}
}
}
write!(
writer,
"{}",
DisplayDiagnostics::new(context, &self.config, diagnostics)
)?;
Ok(())
}

View File

@@ -159,6 +159,21 @@ pub(crate) const fn is_fix_os_getcwd_enabled(settings: &LinterSettings) -> bool
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19514
pub(crate) const fn is_fix_os_mkdir_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19514
pub(crate) const fn is_fix_os_makedirs_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20009
pub(crate) const fn is_fix_os_symlink_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/11436
// https://github.com/astral-sh/ruff/pull/11168
pub(crate) const fn is_dunder_init_fix_unused_import_enabled(settings: &LinterSettings) -> bool {

View File

@@ -137,7 +137,7 @@ impl AutoPythonType {
let expr = Expr::Name(ast::ExprName {
id: Name::from(binding),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
});
Some((expr, vec![no_return_edit]))
@@ -204,7 +204,7 @@ fn type_expr(python_type: PythonType) -> Option<Expr> {
Expr::Name(ast::ExprName {
id: name.into(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
})
}

View File

@@ -52,13 +52,13 @@ impl AlwaysFixableViolation for AssertFalse {
fn assertion_error(msg: Option<&Expr>) -> Stmt {
Stmt::Raise(ast::StmtRaise {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
exc: Some(Box::new(Expr::Call(ast::ExprCall {
func: Box::new(Expr::Name(ast::ExprName {
id: "AssertionError".into(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})),
arguments: Arguments {
args: if let Some(msg) = msg {
@@ -68,10 +68,10 @@ fn assertion_error(msg: Option<&Expr>) -> Stmt {
},
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}))),
cause: None,
})

View File

@@ -113,7 +113,7 @@ fn type_pattern(elts: Vec<&Expr>) -> Expr {
elts: elts.into_iter().cloned().collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
parenthesized: true,
}
.into()

View File

@@ -53,11 +53,11 @@ fn assignment(obj: &Expr, name: &str, value: &Expr, generator: Generator) -> Str
attr: Identifier::new(name.to_string(), TextRange::default()),
ctx: ExprContext::Store,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})],
value: Box::new(value.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
generator.stmt(&stmt)
}

View File

@@ -10,7 +10,7 @@ mod tests {
use crate::registry::Rule;
use crate::test::test_path;
use crate::{assert_diagnostics, settings};
use crate::{assert_diagnostics, assert_diagnostics_diff, settings};
#[test_case(Path::new("COM81.py"))]
#[test_case(Path::new("COM81_syntax_error.py"))]
@@ -31,19 +31,24 @@ mod tests {
#[test_case(Path::new("COM81.py"))]
#[test_case(Path::new("COM81_syntax_error.py"))]
fn preview_rules(path: &Path) -> Result<()> {
let snapshot = format!("preview__{}", path.to_string_lossy());
let diagnostics = test_path(
let snapshot = format!("preview_diff__{}", path.to_string_lossy());
let rules = vec![
Rule::MissingTrailingComma,
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
];
let settings_before = settings::LinterSettings::for_rules(rules.clone());
let settings_after = settings::LinterSettings {
preview: crate::settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rules(rules)
};
assert_diagnostics_diff!(
snapshot,
Path::new("flake8_commas").join(path).as_path(),
&settings::LinterSettings {
preview: crate::settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rules(vec![
Rule::MissingTrailingComma,
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
])
},
)?;
assert_diagnostics!(snapshot, diagnostics);
&settings_before,
&settings_after
);
Ok(())
}
}

View File

@@ -1,33 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
---
invalid-syntax: Starred expression cannot be used here
--> COM81_syntax_error.py:3:5
|
1 | # Check for `flake8-commas` violation for a file containing syntax errors.
2 | (
3 | *args
| ^^^^^
4 | )
|
invalid-syntax: Type parameter list cannot be empty
--> COM81_syntax_error.py:6:9
|
4 | )
5 |
6 | def foo[(param1='test', param2='test',):
| ^
7 | pass
|
COM819 Trailing comma prohibited
--> COM81_syntax_error.py:6:38
|
4 | )
5 |
6 | def foo[(param1='test', param2='test',):
| ^
7 | pass
|
help: Remove trailing comma

View File

@@ -0,0 +1,136 @@
---
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 6
--- Added ---
COM812 [*] Trailing comma missing
--> COM81.py:655:6
|
654 | type X[
655 | T
| ^
656 | ] = T
657 | def f[
|
help: Add trailing comma
Safe fix
652 652 | }"""
653 653 |
654 654 | type X[
655 |- T
655 |+ T,
656 656 | ] = T
657 657 | def f[
658 658 | T
COM812 [*] Trailing comma missing
--> COM81.py:658:6
|
656 | ] = T
657 | def f[
658 | T
| ^
659 | ](): pass
660 | class C[
|
help: Add trailing comma
Safe fix
655 655 | T
656 656 | ] = T
657 657 | def f[
658 |- T
658 |+ T,
659 659 | ](): pass
660 660 | class C[
661 661 | T
COM812 [*] Trailing comma missing
--> COM81.py:661:6
|
659 | ](): pass
660 | class C[
661 | T
| ^
662 | ]: pass
|
help: Add trailing comma
Safe fix
658 658 | T
659 659 | ](): pass
660 660 | class C[
661 |- T
661 |+ T,
662 662 | ]: pass
663 663 |
664 664 | type X[T,] = T
COM819 [*] Trailing comma prohibited
--> COM81.py:664:9
|
662 | ]: pass
663 |
664 | type X[T,] = T
| ^
665 | def f[T,](): pass
666 | class C[T,]: pass
|
help: Remove trailing comma
Safe fix
661 661 | T
662 662 | ]: pass
663 663 |
664 |-type X[T,] = T
664 |+type X[T] = T
665 665 | def f[T,](): pass
666 666 | class C[T,]: pass
COM819 [*] Trailing comma prohibited
--> COM81.py:665:8
|
664 | type X[T,] = T
665 | def f[T,](): pass
| ^
666 | class C[T,]: pass
|
help: Remove trailing comma
Safe fix
662 662 | ]: pass
663 663 |
664 664 | type X[T,] = T
665 |-def f[T,](): pass
665 |+def f[T](): pass
666 666 | class C[T,]: pass
COM819 [*] Trailing comma prohibited
--> COM81.py:666:10
|
664 | type X[T,] = T
665 | def f[T,](): pass
666 | class C[T,]: pass
| ^
|
help: Remove trailing comma
Safe fix
663 663 |
664 664 | type X[T,] = T
665 665 | def f[T,](): pass
666 |-class C[T,]: pass
666 |+class C[T]: pass

View File

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

View File

@@ -209,18 +209,18 @@ fn fix_unnecessary_dict_comprehension(value: &Expr, generator: &Comprehension) -
},
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
Expr::Call(ExprCall {
func: Box::new(Expr::Name(ExprName {
id: "dict.fromkeys".into(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})),
arguments: args,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})
}

View File

@@ -178,21 +178,21 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) {
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
parenthesized: true,
});
let node1 = Expr::Name(ast::ExprName {
id: arg_name.into(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let node2 = Expr::Attribute(ast::ExprAttribute {
value: Box::new(node1),
attr: Identifier::new(attr_name.to_string(), TextRange::default()),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let node3 = Expr::Call(ast::ExprCall {
func: Box::new(node2),
@@ -200,10 +200,10 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) {
args: Box::from([node]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let call = node3;
@@ -223,7 +223,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) {
})
.collect(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let bool_op = node;
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(

View File

@@ -92,14 +92,14 @@ pub(crate) fn duplicate_literal_member<'a>(checker: &Checker, expr: &'a Expr) {
Expr::Tuple(ast::ExprTuple {
elts: unique_nodes.into_iter().cloned().collect(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
parenthesized: false,
})
}),
value: subscript.value.clone(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
});
let fix = Fix::applicable_edit(

View File

@@ -187,7 +187,7 @@ fn generate_pep604_fix(
op: Operator::BitOr,
right: Box::new(right.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}))
} else {
Some(right.clone())
@@ -202,7 +202,7 @@ fn generate_pep604_fix(
}
static VIRTUAL_NONE_LITERAL: Expr = Expr::NoneLiteral(ExprNoneLiteral {
node_index: AtomicNodeIndex::dummy(),
node_index: AtomicNodeIndex::NONE,
range: TextRange::new(TextSize::new(0), TextSize::new(0)),
});

View File

@@ -133,17 +133,17 @@ fn generate_union_fix(
// Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]`
let new_expr = Expr::Subscript(ExprSubscript {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
value: Box::new(Expr::Name(ExprName {
id: Name::new(binding),
ctx: ExprContext::Store,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})),
slice: Box::new(Expr::Tuple(ExprTuple {
elts: nodes.into_iter().cloned().collect(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
parenthesized: false,
})),

View File

@@ -205,13 +205,13 @@ fn create_fix(
let new_literal_expr = Expr::Subscript(ast::ExprSubscript {
value: Box::new(literal_subscript.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
slice: Box::new(if literal_elements.len() > 1 {
Expr::Tuple(ast::ExprTuple {
elts: literal_elements.into_iter().cloned().collect(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
parenthesized: true,
})
@@ -235,7 +235,7 @@ fn create_fix(
UnionKind::BitOr => {
let none_expr = Expr::NoneLiteral(ExprNoneLiteral {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let union_expr = pep_604_union(&[new_literal_expr, none_expr]);
let content = checker.generator().expr(&union_expr);

View File

@@ -261,7 +261,7 @@ fn generate_pep604_fix(
op: Operator::BitOr,
right: Box::new(right.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}))
} else {
Some(right.clone())

View File

@@ -140,12 +140,12 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &Checker, expr: &'a Expr) {
slice: Box::new(Expr::Tuple(ast::ExprTuple {
elts: literal_exprs.into_iter().cloned().collect(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
parenthesized: true,
})),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
});
@@ -164,12 +164,12 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &Checker, expr: &'a Expr) {
slice: Box::new(Expr::Tuple(ast::ExprTuple {
elts,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
parenthesized: true,
})),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
}))
} else {

View File

@@ -134,12 +134,12 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &Checker, union: &'a Expr) {
id: Name::new_static("type"),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})),
slice: Box::new(pep_604_union(&elts)),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
if other_exprs.is_empty() {
@@ -159,7 +159,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &Checker, union: &'a Expr) {
id: Name::new_static("type"),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})),
slice: Box::new(Expr::Subscript(ast::ExprSubscript {
value: subscript.value.clone(),
@@ -171,22 +171,22 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &Checker, union: &'a Expr) {
id: type_member,
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})
})
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
parenthesized: true,
})),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
if other_exprs.is_empty() {
@@ -202,12 +202,12 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &Checker, union: &'a Expr) {
elts: exprs.into_iter().cloned().collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
parenthesized: true,
})),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
checker.generator().expr(&union)

View File

@@ -301,7 +301,7 @@ fn elts_to_csv(elts: &[Expr], generator: Generator, flags: StringLiteralFlags) -
})
.into_boxed_str(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
flags,
});
Some(generator.expr(&node))
@@ -367,14 +367,14 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr
Expr::from(ast::StringLiteral {
value: Box::from(*name),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
flags: checker.default_string_flags(),
})
})
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
parenthesized: true,
});
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
@@ -404,14 +404,14 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr
Expr::from(ast::StringLiteral {
value: Box::from(*name),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
flags: checker.default_string_flags(),
})
})
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
checker.generator().expr(&node),
@@ -440,7 +440,7 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr
elts: elts.clone(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
checker.generator().expr(&node),
@@ -485,7 +485,7 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr
elts: elts.clone(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
parenthesized: true,
});
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(

View File

@@ -166,7 +166,7 @@ fn assert(expr: &Expr, msg: Option<&Expr>) -> Stmt {
test: Box::new(expr.clone()),
msg: msg.map(|msg| Box::new(msg.clone())),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})
}
@@ -176,7 +176,7 @@ fn compare(left: &Expr, cmp_op: CmpOp, right: &Expr) -> Expr {
ops: Box::from([cmp_op]),
comparators: Box::from([right.clone()]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})
}
@@ -296,7 +296,7 @@ impl UnittestAssert {
op: UnaryOp::Not,
operand: Box::new(expr.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}),
msg,
)
@@ -370,7 +370,7 @@ impl UnittestAssert {
};
let node = Expr::NoneLiteral(ast::ExprNoneLiteral {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let expr = compare(expr, cmp_op, &node);
Ok(assert(&expr, msg))
@@ -387,7 +387,7 @@ impl UnittestAssert {
id: Name::new_static("isinstance"),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node1 = ast::ExprCall {
func: Box::new(node.into()),
@@ -395,10 +395,10 @@ impl UnittestAssert {
args: Box::from([(**obj).clone(), (**cls).clone()]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let isinstance = node1.into();
if matches!(self, UnittestAssert::IsInstance) {
@@ -408,7 +408,7 @@ impl UnittestAssert {
op: UnaryOp::Not,
operand: Box::new(isinstance),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let expr = node.into();
Ok(assert(&expr, msg))
@@ -429,14 +429,14 @@ impl UnittestAssert {
id: Name::new_static("re"),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node1 = ast::ExprAttribute {
value: Box::new(node.into()),
attr: Identifier::new("search".to_string(), TextRange::default()),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node2 = ast::ExprCall {
func: Box::new(node1.into()),
@@ -444,10 +444,10 @@ impl UnittestAssert {
args: Box::from([(**regex).clone(), (**text).clone()]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let re_search = node2.into();
if matches!(self, UnittestAssert::Regex | UnittestAssert::RegexpMatches) {
@@ -457,7 +457,7 @@ impl UnittestAssert {
op: UnaryOp::Not,
operand: Box::new(re_search),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
Ok(assert(&node.into(), msg))
}

View File

@@ -421,7 +421,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) {
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
parenthesized: true,
};
let isinstance_call = ast::ExprCall {
@@ -430,7 +430,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) {
id: Name::new_static("isinstance"),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into(),
),
@@ -438,10 +438,10 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) {
args: Box::from([target.clone(), tuple.into()]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into();
@@ -458,7 +458,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) {
.chain(after)
.collect(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into();
let fixed_source = checker.generator().expr(&bool_op);
@@ -552,21 +552,21 @@ pub(crate) fn compare_with_tuple(checker: &Checker, expr: &Expr) {
elts: comparators.into_iter().cloned().collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
parenthesized: true,
};
let node1 = ast::ExprName {
id: id.clone(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node2 = ast::ExprCompare {
left: Box::new(node1.into()),
ops: Box::from([CmpOp::In]),
comparators: Box::from([node.into()]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let in_expr = node2.into();
let mut diagnostic = checker.report_diagnostic(
@@ -589,7 +589,7 @@ pub(crate) fn compare_with_tuple(checker: &Checker, expr: &Expr) {
op: BoolOp::Or,
values: iter::once(in_expr).chain(unmatched).collect(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
node.into()
};

View File

@@ -232,7 +232,7 @@ fn check_os_environ_subscript(checker: &Checker, expr: &Expr) {
}
}),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let new_env_var = node.into();
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(

View File

@@ -188,7 +188,7 @@ pub(crate) fn if_expr_with_true_false(
id: Name::new_static("bool"),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into(),
),
@@ -196,10 +196,10 @@ pub(crate) fn if_expr_with_true_false(
args: Box::from([test.clone()]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into(),
),
@@ -227,7 +227,7 @@ pub(crate) fn if_expr_with_false_true(
op: UnaryOp::Not,
operand: Box::new(test.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into(),
),
@@ -282,7 +282,7 @@ pub(crate) fn twisted_arms_in_ifexpr(
body: Box::new(node1),
orelse: Box::new(node),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
checker.generator().expr(&node3.into()),

View File

@@ -186,7 +186,7 @@ pub(crate) fn negation_with_equal_op(checker: &Checker, expr: &Expr, op: UnaryOp
ops: Box::from([CmpOp::NotEq]),
comparators: comparators.clone(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
checker.generator().expr(&node.into()),
@@ -242,7 +242,7 @@ pub(crate) fn negation_with_not_equal_op(
ops: Box::from([CmpOp::Eq]),
comparators: comparators.clone(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
checker.generator().expr(&node.into()),
@@ -284,7 +284,7 @@ pub(crate) fn double_negation(checker: &Checker, expr: &Expr, op: UnaryOp, opera
id: Name::new_static("bool"),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node1 = ast::ExprCall {
func: Box::new(node.into()),
@@ -292,10 +292,10 @@ pub(crate) fn double_negation(checker: &Checker, expr: &Expr, op: UnaryOp, opera
args: Box::from([*operand.clone()]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
checker.generator().expr(&node1.into()),

View File

@@ -190,7 +190,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &Checker, stmt_if: &ast
attr: Identifier::new("get".to_string(), TextRange::default()),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node3 = ast::ExprCall {
func: Box::new(node2.into()),
@@ -198,17 +198,17 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &Checker, stmt_if: &ast
args: Box::from([node1, node]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node4 = expected_var.clone();
let node5 = ast::StmtAssign {
targets: vec![node4],
value: Box::new(node3.into()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let contents = checker.generator().stmt(&node5.into());
@@ -299,7 +299,7 @@ pub(crate) fn if_exp_instead_of_dict_get(
attr: Identifier::new("get".to_string(), TextRange::default()),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let fixed_node = ast::ExprCall {
func: Box::new(dict_get_node.into()),
@@ -307,10 +307,10 @@ pub(crate) fn if_exp_instead_of_dict_get(
args: Box::from([dict_key_node, default_value_node]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let contents = checker.generator().expr(&fixed_node.into());

View File

@@ -266,13 +266,13 @@ fn assignment_ternary(
body: Box::new(body_value.clone()),
orelse: Box::new(orelse_value.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node1 = ast::StmtAssign {
targets: vec![target_var.clone()],
value: Box::new(node.into()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
node1.into()
}
@@ -282,13 +282,13 @@ fn assignment_binary_and(target_var: &Expr, left_value: &Expr, right_value: &Exp
op: BoolOp::And,
values: vec![left_value.clone(), right_value.clone()],
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node1 = ast::StmtAssign {
targets: vec![target_var.clone()],
value: Box::new(node.into()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
node1.into()
}
@@ -296,12 +296,12 @@ fn assignment_binary_and(target_var: &Expr, left_value: &Expr, right_value: &Exp
fn assignment_binary_or(target_var: &Expr, left_value: &Expr, right_value: &Expr) -> Stmt {
(ast::StmtAssign {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
targets: vec![target_var.clone()],
value: Box::new(
(ast::ExprBoolOp {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
op: BoolOp::Or,
values: vec![left_value.clone(), right_value.clone()],
})

View File

@@ -256,7 +256,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) {
left: left.clone(),
comparators: Box::new([right.clone()]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}))
}
@@ -264,7 +264,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) {
op: ast::UnaryOp::Not,
operand: Box::new(if_test.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})),
}
} else if if_test.is_compare_expr() {
@@ -277,7 +277,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) {
id: Name::new_static("bool"),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let call_node = ast::ExprCall {
func: Box::new(func_node.into()),
@@ -285,10 +285,10 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) {
args: Box::from([if_test.clone()]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
Some(Expr::Call(call_node))
} else {
@@ -301,7 +301,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) {
Stmt::Return(ast::StmtReturn {
value: Some(Box::new(expr.clone())),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})
});

View File

@@ -165,7 +165,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) {
ops: Box::from([op]),
comparators: Box::from([comparator.clone()]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
node.into()
} else {
@@ -173,7 +173,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) {
op: UnaryOp::Not,
operand: Box::new(loop_.test.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
node.into()
}
@@ -182,7 +182,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) {
op: UnaryOp::Not,
operand: Box::new(loop_.test.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
node.into()
}
@@ -406,17 +406,17 @@ fn return_stmt(id: Name, test: &Expr, target: &Expr, iter: &Expr, generator: Gen
ifs: vec![],
is_async: false,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}],
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
parenthesized: false,
};
let node1 = ast::ExprName {
id,
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node2 = ast::ExprCall {
func: Box::new(node1.into()),
@@ -424,15 +424,15 @@ fn return_stmt(id: Name, test: &Expr, target: &Expr, iter: &Expr, generator: Gen
args: Box::from([node.into()]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let node3 = ast::StmtReturn {
value: Some(Box::new(node2.into())),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
generator.stmt(&node3.into())
}

View File

@@ -163,14 +163,14 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
Expr::from(StringLiteral {
value: Box::from(*elt),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
flags: element_flags,
})
})
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})
}

View File

@@ -101,7 +101,7 @@ fn fix_banned_relative_import(
names: names.clone(),
level: 0,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let content = generator.stmt(&node.into());
Some(Fix::unsafe_edit(Edit::range_replacement(

View File

@@ -436,7 +436,7 @@ impl<'a> QuoteAnnotator<'a> {
let annotation = subgenerator.expr(&expr_without_forward_references);
generator.expr(&Expr::from(ast::StringLiteral {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
value: annotation.into_boxed_str(),
flags: self.flags,
}))

View File

@@ -1,10 +1,11 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_python_semantic::{SemanticModel, analyze::typing};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation};
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
arguments
.find_keyword(name)
@@ -183,3 +184,17 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
});
}
}
pub(crate) fn has_unknown_keywords_or_starred_expr(
arguments: &ast::Arguments,
allowed: &[&str],
) -> bool {
if arguments.args.iter().any(Expr::is_starred_expr) {
return true;
}
arguments.keywords.iter().any(|kw| match &kw.arg {
Some(arg) => !allowed.contains(&arg.as_str()),
None => true,
})
}

View File

@@ -129,6 +129,7 @@ mod tests {
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]
#[test_case(Rule::OsPathGetmtime, Path::new("PTH204.py"))]
#[test_case(Rule::OsPathGetctime, Path::new("PTH205.py"))]
#[test_case(Rule::OsSymlink, Path::new("PTH211.py"))]
fn preview_flake8_use_pathlib(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -2,6 +2,8 @@ pub(crate) use glob_rule::*;
pub(crate) use invalid_pathlib_with_suffix::*;
pub(crate) use os_chmod::*;
pub(crate) use os_getcwd::*;
pub(crate) use os_makedirs::*;
pub(crate) use os_mkdir::*;
pub(crate) use os_path_abspath::*;
pub(crate) use os_path_basename::*;
pub(crate) use os_path_dirname::*;
@@ -22,6 +24,7 @@ pub(crate) use os_rename::*;
pub(crate) use os_replace::*;
pub(crate) use os_rmdir::*;
pub(crate) use os_sep_split::*;
pub(crate) use os_symlink::*;
pub(crate) use os_unlink::*;
pub(crate) use path_constructor_current_directory::*;
pub(crate) use replaceable_by_pathlib::*;
@@ -30,6 +33,8 @@ mod glob_rule;
mod invalid_pathlib_with_suffix;
mod os_chmod;
mod os_getcwd;
mod os_makedirs;
mod os_mkdir;
mod os_path_abspath;
mod os_path_basename;
mod os_path_dirname;
@@ -50,6 +55,7 @@ mod os_rename;
mod os_replace;
mod os_rmdir;
mod os_sep_split;
mod os_symlink;
mod os_unlink;
mod path_constructor_current_directory;
mod replaceable_by_pathlib;

View File

@@ -0,0 +1,152 @@
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ArgOrKeyword, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_makedirs_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
has_unknown_keywords_or_starred_expr, is_pathlib_path_call,
};
use crate::{FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `os.makedirs`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.mkdir(parents=True)` can improve readability over the
/// `os` module's counterparts (e.g., `os.makedirs()`.
///
/// ## Examples
/// ```python
/// import os
///
/// os.makedirs("./nested/directory/")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("./nested/directory/").mkdir(parents=True)
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir)
/// - [Python documentation: `os.makedirs`](https://docs.python.org/3/library/os.html#os.makedirs)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsMakedirs;
impl Violation for OsMakedirs {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.makedirs()` should be replaced by `Path.mkdir(parents=True)`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).mkdir(parents=True)`".to_string())
}
}
/// PTH103
pub(crate) fn os_makedirs(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "makedirs"] {
return;
}
let range = call.range();
let mut diagnostic = checker.report_diagnostic(OsMakedirs, call.func.range());
let Some(name) = call.arguments.find_argument_value("name", 0) else {
return;
};
if !is_fix_os_makedirs_enabled(checker.settings()) {
return;
}
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.makedirs)
// ```text
// 0 1 2
// os.makedirs(name, mode=0o777, exist_ok=False)
// ```
// We should not offer autofixes if there are more arguments
// than in the original signature
if call.arguments.len() > 3 {
return;
}
// We should not offer autofixes if there are keyword arguments
// that don't match the original function signature
if has_unknown_keywords_or_starred_expr(&call.arguments, &["name", "mode", "exist_ok"]) {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let locator = checker.locator();
let name_code = locator.slice(name.range());
let mode = call.arguments.find_argument("mode", 1);
let exist_ok = call.arguments.find_argument("exist_ok", 2);
let mkdir_args = match (mode, exist_ok) {
// Default to a keyword argument when alone.
(None, None) => "parents=True".to_string(),
// If either argument is missing, it's safe to add `parents` at the end.
(None, Some(arg)) | (Some(arg), None) => {
format!("{}, parents=True", locator.slice(arg))
}
// If they're all positional, `parents` has to be positional too.
(Some(ArgOrKeyword::Arg(mode)), Some(ArgOrKeyword::Arg(exist_ok))) => {
format!("{}, True, {}", locator.slice(mode), locator.slice(exist_ok))
}
// If either argument is a keyword, we can put `parents` at the end again.
(Some(mode), Some(exist_ok)) => format!(
"{}, {}, parents=True",
locator.slice(mode),
locator.slice(exist_ok)
),
};
let replacement = if is_pathlib_path_call(checker, name) {
format!("{name_code}.mkdir({mkdir_args})")
} else {
format!("{binding}({name_code}).mkdir({mkdir_args})")
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}

View File

@@ -0,0 +1,136 @@
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_mkdir_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
has_unknown_keywords_or_starred_expr, is_keyword_only_argument_non_default,
is_pathlib_path_call,
};
use crate::{FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `os.mkdir`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.mkdir()` can improve readability over the `os`
/// module's counterparts (e.g., `os.mkdir()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.mkdir("./directory/")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("./directory/").mkdir()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir)
/// - [Python documentation: `os.mkdir`](https://docs.python.org/3/library/os.html#os.mkdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsMkdir;
impl Violation for OsMkdir {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.mkdir()` should be replaced by `Path.mkdir()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).mkdir()`".to_string())
}
}
/// PTH102
pub(crate) fn os_mkdir(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "mkdir"] {
return;
}
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.mkdir)
// ```text
// 0 1 2
// os.mkdir(path, mode=0o777, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
let range = call.range();
let mut diagnostic = checker.report_diagnostic(OsMkdir, call.func.range());
let Some(path) = call.arguments.find_argument_value("path", 0) else {
return;
};
if !is_fix_os_mkdir_enabled(checker.settings()) {
return;
}
if call.arguments.len() > 2 {
return;
}
if has_unknown_keywords_or_starred_expr(&call.arguments, &["path", "mode"]) {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let path_code = checker.locator().slice(path.range());
let mkdir_args = call
.arguments
.find_argument_value("mode", 1)
.map(|expr| format!("mode={}", checker.locator().slice(expr.range())))
.unwrap_or_default();
let replacement = if is_pathlib_path_call(checker, path) {
format!("{path_code}.mkdir({mkdir_args})")
} else {
format!("{binding}({path_code}).mkdir({mkdir_args})")
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}

View File

@@ -0,0 +1,153 @@
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_symlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
has_unknown_keywords_or_starred_expr, is_keyword_only_argument_non_default,
is_pathlib_path_call,
};
use crate::{FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `os.symlink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.symlink`.
///
/// ## Example
/// ```python
/// import os
///
/// os.symlink("usr/bin/python", "tmp/python", target_is_directory=False)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("tmp/python").symlink_to("usr/bin/python")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.symlink_to`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.symlink_to)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsSymlink;
impl Violation for OsSymlink {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.symlink` should be replaced by `Path.symlink_to`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).symlink_to(...)`".to_string())
}
}
/// PTH211
pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "symlink"] {
return;
}
// `dir_fd` is not supported by pathlib, so check if there are non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.symlink)
// ```text
// 0 1 2 3
// os.symlink(src, dst, target_is_directory=False, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
let range = call.range();
let mut diagnostic = checker.report_diagnostic(OsSymlink, call.func.range());
if !is_fix_os_symlink_enabled(checker.settings()) {
return;
}
if call.arguments.len() > 3 {
return;
}
if has_unknown_keywords_or_starred_expr(
&call.arguments,
&["src", "dst", "target_is_directory", "dir_fd"],
) {
return;
}
let (Some(src), Some(dst)) = (
call.arguments.find_argument_value("src", 0),
call.arguments.find_argument_value("dst", 1),
) else {
return;
};
let target_is_directory_arg = call.arguments.find_argument_value("target_is_directory", 2);
if let Some(expr) = &target_is_directory_arg {
if expr.as_boolean_literal_expr().is_none() {
return;
}
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let locator = checker.locator();
let src_code = locator.slice(src.range());
let dst_code = locator.slice(dst.range());
let target_is_directory = target_is_directory_arg
.and_then(|expr| {
let code = locator.slice(expr.range());
expr.as_boolean_literal_expr()
.is_none_or(|bl| bl.value)
.then_some(format!(", target_is_directory={code}"))
})
.unwrap_or_default();
let replacement = if is_pathlib_path_call(checker, dst) {
format!("{dst_code}.symlink_to({src_code}{target_is_directory})")
} else {
format!("{binding}({dst_code}).symlink_to({src_code}{target_is_directory})")
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}

View File

@@ -7,10 +7,7 @@ use crate::rules::flake8_use_pathlib::helpers::{
};
use crate::rules::flake8_use_pathlib::{
rules::Glob,
violations::{
BuiltinOpen, Joiner, OsListdir, OsMakedirs, OsMkdir, OsPathJoin, OsPathSplitext, OsStat,
OsSymlink, PyPath,
},
violations::{BuiltinOpen, Joiner, OsListdir, OsPathJoin, OsPathSplitext, OsStat, PyPath},
};
pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
@@ -20,21 +17,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
let range = call.func.range();
match qualified_name.segments() {
// PTH102
["os", "makedirs"] => checker.report_diagnostic_if_enabled(OsMakedirs, range),
// PTH103
["os", "mkdir"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.mkdir)
// ```text
// 0 1 2
// os.mkdir(path, mode=0o777, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
checker.report_diagnostic_if_enabled(OsMkdir, range)
}
// PTH116
["os", "stat"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
@@ -78,20 +60,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
),
// PTH122
["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range),
// PTH211
["os", "symlink"] => {
// `dir_fd` is not supported by pathlib, so check if there are non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.symlink)
// ```text
// 0 1 2 3
// os.symlink(src, dst, target_is_directory=False, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
checker.report_diagnostic_if_enabled(OsSymlink, range)
}
// PTH123
["" | "builtins", "open"] => {
// `closefd` and `opener` are not supported by pathlib, so check if they

View File

@@ -9,6 +9,7 @@ PTH211 `os.symlink` should be replaced by `Path.symlink_to`
6 | os.symlink(b"usr/bin/python", b"tmp/python")
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
help: Replace with `Path(...).symlink_to(...)`
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:6:1
@@ -18,6 +19,7 @@ PTH211 `os.symlink` should be replaced by `Path.symlink_to`
| ^^^^^^^^^^
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
help: Replace with `Path(...).symlink_to(...)`
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:9:1
@@ -29,6 +31,7 @@ PTH211 `os.symlink` should be replaced by `Path.symlink_to`
10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
help: Replace with `Path(...).symlink_to(...)`
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:10:1
@@ -38,3 +41,58 @@ PTH211 `os.symlink` should be replaced by `Path.symlink_to`
| ^^^^^^^^^^
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
help: Replace with `Path(...).symlink_to(...)`
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:17:1
|
15 | os.close(fd)
16 |
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
| ^^^^^^^^^^
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
|
help: Replace with `Path(...).symlink_to(...)`
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:18:1
|
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
| ^^^^^^^^^^
19 |
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
|
help: Replace with `Path(...).symlink_to(...)`
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:20:1
|
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
19 |
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
| ^^^^^^^^^^
21 |
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
|
help: Replace with `Path(...).symlink_to(...)`
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:22:1
|
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
21 |
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
| ^^^^^^^^^^
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
|
help: Replace with `Path(...).symlink_to(...)`
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:23:1
|
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
| ^^^^^^^^^^
|
help: Replace with `Path(...).symlink_to(...)`

View File

@@ -34,6 +34,7 @@ PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
10 | os.makedirs(p)
11 | os.rename(p)
|
help: Replace with `Path(...).mkdir()`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:10:1
@@ -45,6 +46,7 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
11 | os.rename(p)
12 | os.replace(p)
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> full_name.py:11:1
@@ -419,5 +421,77 @@ PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
108 | os.getcwd()
109 | os.getcwdb()
| ^^^^^^^^^^
110 |
111 | os.mkdir(path="directory")
|
help: Replace with `Path.cwd()`
PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
--> full_name.py:111:1
|
109 | os.getcwdb()
110 |
111 | os.mkdir(path="directory")
| ^^^^^^^^
112 |
113 | os.mkdir(
|
help: Replace with `Path(...).mkdir()`
PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
--> full_name.py:113:1
|
111 | os.mkdir(path="directory")
112 |
113 | os.mkdir(
| ^^^^^^^^
114 | # comment 1
115 | "directory",
|
help: Replace with `Path(...).mkdir()`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:121:1
|
119 | os.mkdir("directory", mode=0o777, dir_fd=1)
120 |
121 | os.makedirs("name", 0o777, exist_ok=False)
| ^^^^^^^^^^^
122 |
123 | os.makedirs("name", 0o777, False)
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:123:1
|
121 | os.makedirs("name", 0o777, exist_ok=False)
122 |
123 | os.makedirs("name", 0o777, False)
| ^^^^^^^^^^^
124 |
125 | os.makedirs(name="name", mode=0o777, exist_ok=False)
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:125:1
|
123 | os.makedirs("name", 0o777, False)
124 |
125 | os.makedirs(name="name", mode=0o777, exist_ok=False)
| ^^^^^^^^^^^
126 |
127 | os.makedirs("name", unknown_kwarg=True)
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:127:1
|
125 | os.makedirs(name="name", mode=0o777, exist_ok=False)
126 |
127 | os.makedirs("name", unknown_kwarg=True)
| ^^^^^^^^^^^
|
help: Replace with `Path(...).mkdir(parents=True)`

View File

@@ -34,6 +34,7 @@ PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
10 | foo.makedirs(p)
11 | foo.rename(p)
|
help: Replace with `Path(...).mkdir()`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> import_as.py:10:1
@@ -45,6 +46,7 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
11 | foo.rename(p)
12 | foo.replace(p)
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> import_as.py:11:1

View File

@@ -34,6 +34,7 @@ PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
12 | makedirs(p)
13 | rename(p)
|
help: Replace with `Path(...).mkdir()`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> import_from.py:12:1
@@ -45,6 +46,7 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
13 | rename(p)
14 | replace(p)
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> import_from.py:13:1

View File

@@ -34,6 +34,7 @@ PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
17 | xmakedirs(p)
18 | xrename(p)
|
help: Replace with `Path(...).mkdir()`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> import_from_as.py:17:1
@@ -45,6 +46,7 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
18 | xrename(p)
19 | xreplace(p)
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> import_from_as.py:18:1

View File

@@ -0,0 +1,166 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:5:1
|
5 | os.symlink("usr/bin/python", "tmp/python")
| ^^^^^^^^^^
6 | os.symlink(b"usr/bin/python", b"tmp/python")
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
help: Replace with `Path(...).symlink_to(...)`
Safe fix
2 2 | from pathlib import Path
3 3 |
4 4 |
5 |-os.symlink("usr/bin/python", "tmp/python")
5 |+Path("tmp/python").symlink_to("usr/bin/python")
6 6 | os.symlink(b"usr/bin/python", b"tmp/python")
7 7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
8 8 |
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:6:1
|
5 | os.symlink("usr/bin/python", "tmp/python")
6 | os.symlink(b"usr/bin/python", b"tmp/python")
| ^^^^^^^^^^
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
help: Replace with `Path(...).symlink_to(...)`
Safe fix
3 3 |
4 4 |
5 5 | os.symlink("usr/bin/python", "tmp/python")
6 |-os.symlink(b"usr/bin/python", b"tmp/python")
6 |+Path(b"tmp/python").symlink_to(b"usr/bin/python")
7 7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
8 8 |
9 9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:9:1
|
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
8 |
9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
| ^^^^^^^^^^
10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
help: Replace with `Path(...).symlink_to(...)`
Safe fix
6 6 | os.symlink(b"usr/bin/python", b"tmp/python")
7 7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
8 8 |
9 |-os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
9 |+Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True)
10 10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
11 11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
12 12 |
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:10:1
|
9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
| ^^^^^^^^^^
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
help: Replace with `Path(...).symlink_to(...)`
Safe fix
7 7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
8 8 |
9 9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
10 |-os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
10 |+Path(b"tmp/python").symlink_to(b"usr/bin/python", target_is_directory=True)
11 11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
12 12 |
13 13 | fd = os.open(".", os.O_RDONLY)
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:17:1
|
15 | os.close(fd)
16 |
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
| ^^^^^^^^^^
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
|
help: Replace with `Path(...).symlink_to(...)`
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:18:1
|
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
| ^^^^^^^^^^
19 |
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
|
help: Replace with `Path(...).symlink_to(...)`
Safe fix
15 15 | os.close(fd)
16 16 |
17 17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
18 |-os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
18 |+Path("tmp/python").symlink_to("usr/bin/python")
19 19 |
20 20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
21 21 |
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:20:1
|
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
19 |
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
| ^^^^^^^^^^
21 |
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
|
help: Replace with `Path(...).symlink_to(...)`
Safe fix
17 17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
18 18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
19 19 |
20 |-os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
20 |+Path("tmp/python").symlink_to("usr/bin/python")
21 21 |
22 22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
23 23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:22:1
|
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
21 |
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
| ^^^^^^^^^^
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
|
help: Replace with `Path(...).symlink_to(...)`
Safe fix
19 19 |
20 20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
21 21 |
22 |-os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
22 |+Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True)
23 23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:23:1
|
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
| ^^^^^^^^^^
|
help: Replace with `Path(...).symlink_to(...)`

View File

@@ -38,7 +38,7 @@ PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
help: Replace with `Path(...).chmod(...)`
PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
PTH102 [*] `os.mkdir()` should be replaced by `Path.mkdir()`
--> full_name.py:9:7
|
7 | a = os.path.abspath(p)
@@ -48,8 +48,25 @@ PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
10 | os.makedirs(p)
11 | os.rename(p)
|
help: Replace with `Path(...).mkdir()`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
6 7 |
7 8 | a = os.path.abspath(p)
8 9 | aa = os.chmod(p)
9 |-aaa = os.mkdir(p)
10 |+aaa = pathlib.Path(p).mkdir()
10 11 | os.makedirs(p)
11 12 | os.rename(p)
12 13 | os.replace(p)
PTH103 [*] `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:10:1
|
8 | aa = os.chmod(p)
@@ -59,6 +76,24 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
11 | os.rename(p)
12 | os.replace(p)
|
help: Replace with `Path(...).mkdir(parents=True)`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
7 8 | a = os.path.abspath(p)
8 9 | aa = os.chmod(p)
9 10 | aaa = os.mkdir(p)
10 |-os.makedirs(p)
11 |+pathlib.Path(p).mkdir(parents=True)
11 12 | os.rename(p)
12 13 | os.replace(p)
13 14 | os.rmdir(p)
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> full_name.py:11:1
@@ -645,6 +680,8 @@ help: Replace with `Path.cwd()`
108 |-os.getcwd()
109 |+pathlib.Path.cwd()
109 110 | os.getcwdb()
110 111 |
111 112 | os.mkdir(path="directory")
PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:109:1
@@ -652,6 +689,8 @@ PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
108 | os.getcwd()
109 | os.getcwdb()
| ^^^^^^^^^^
110 |
111 | os.mkdir(path="directory")
|
help: Replace with `Path.cwd()`
@@ -668,3 +707,164 @@ help: Replace with `Path.cwd()`
108 109 | os.getcwd()
109 |-os.getcwdb()
110 |+pathlib.Path.cwd()
110 111 |
111 112 | os.mkdir(path="directory")
112 113 |
PTH102 [*] `os.mkdir()` should be replaced by `Path.mkdir()`
--> full_name.py:111:1
|
109 | os.getcwdb()
110 |
111 | os.mkdir(path="directory")
| ^^^^^^^^
112 |
113 | os.mkdir(
|
help: Replace with `Path(...).mkdir()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
108 109 | os.getcwd()
109 110 | os.getcwdb()
110 111 |
111 |-os.mkdir(path="directory")
112 |+pathlib.Path("directory").mkdir()
112 113 |
113 114 | os.mkdir(
114 115 | # comment 1
PTH102 [*] `os.mkdir()` should be replaced by `Path.mkdir()`
--> full_name.py:113:1
|
111 | os.mkdir(path="directory")
112 |
113 | os.mkdir(
| ^^^^^^^^
114 | # comment 1
115 | "directory",
|
help: Replace with `Path(...).mkdir()`
Unsafe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
110 111 |
111 112 | os.mkdir(path="directory")
112 113 |
113 |-os.mkdir(
114 |- # comment 1
115 |- "directory",
116 |- mode=0o777
117 |-)
114 |+pathlib.Path("directory").mkdir(mode=0o777)
118 115 |
119 116 | os.mkdir("directory", mode=0o777, dir_fd=1)
120 117 |
PTH103 [*] `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:121:1
|
119 | os.mkdir("directory", mode=0o777, dir_fd=1)
120 |
121 | os.makedirs("name", 0o777, exist_ok=False)
| ^^^^^^^^^^^
122 |
123 | os.makedirs("name", 0o777, False)
|
help: Replace with `Path(...).mkdir(parents=True)`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
118 119 |
119 120 | os.mkdir("directory", mode=0o777, dir_fd=1)
120 121 |
121 |-os.makedirs("name", 0o777, exist_ok=False)
122 |+pathlib.Path("name").mkdir(0o777, exist_ok=False, parents=True)
122 123 |
123 124 | os.makedirs("name", 0o777, False)
124 125 |
PTH103 [*] `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:123:1
|
121 | os.makedirs("name", 0o777, exist_ok=False)
122 |
123 | os.makedirs("name", 0o777, False)
| ^^^^^^^^^^^
124 |
125 | os.makedirs(name="name", mode=0o777, exist_ok=False)
|
help: Replace with `Path(...).mkdir(parents=True)`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
120 121 |
121 122 | os.makedirs("name", 0o777, exist_ok=False)
122 123 |
123 |-os.makedirs("name", 0o777, False)
124 |+pathlib.Path("name").mkdir(0o777, True, False)
124 125 |
125 126 | os.makedirs(name="name", mode=0o777, exist_ok=False)
126 127 |
PTH103 [*] `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:125:1
|
123 | os.makedirs("name", 0o777, False)
124 |
125 | os.makedirs(name="name", mode=0o777, exist_ok=False)
| ^^^^^^^^^^^
126 |
127 | os.makedirs("name", unknown_kwarg=True)
|
help: Replace with `Path(...).mkdir(parents=True)`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
122 123 |
123 124 | os.makedirs("name", 0o777, False)
124 125 |
125 |-os.makedirs(name="name", mode=0o777, exist_ok=False)
126 |+pathlib.Path("name").mkdir(mode=0o777, exist_ok=False, parents=True)
126 127 |
127 128 | os.makedirs("name", unknown_kwarg=True)
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:127:1
|
125 | os.makedirs(name="name", mode=0o777, exist_ok=False)
126 |
127 | os.makedirs("name", unknown_kwarg=True)
| ^^^^^^^^^^^
|
help: Replace with `Path(...).mkdir(parents=True)`

View File

@@ -38,7 +38,7 @@ PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
help: Replace with `Path(...).chmod(...)`
PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
PTH102 [*] `os.mkdir()` should be replaced by `Path.mkdir()`
--> import_as.py:9:7
|
7 | a = foo_p.abspath(p)
@@ -48,8 +48,25 @@ PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
10 | foo.makedirs(p)
11 | foo.rename(p)
|
help: Replace with `Path(...).mkdir()`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
6 7 |
7 8 | a = foo_p.abspath(p)
8 9 | aa = foo.chmod(p)
9 |-aaa = foo.mkdir(p)
10 |+aaa = pathlib.Path(p).mkdir()
10 11 | foo.makedirs(p)
11 12 | foo.rename(p)
12 13 | foo.replace(p)
PTH103 [*] `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> import_as.py:10:1
|
8 | aa = foo.chmod(p)
@@ -59,6 +76,24 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
11 | foo.rename(p)
12 | foo.replace(p)
|
help: Replace with `Path(...).mkdir(parents=True)`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
7 8 | a = foo_p.abspath(p)
8 9 | aa = foo.chmod(p)
9 10 | aaa = foo.mkdir(p)
10 |-foo.makedirs(p)
11 |+pathlib.Path(p).mkdir(parents=True)
11 12 | foo.rename(p)
12 13 | foo.replace(p)
13 14 | foo.rmdir(p)
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> import_as.py:11:1

View File

@@ -39,7 +39,7 @@ PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
help: Replace with `Path(...).chmod(...)`
PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
PTH102 [*] `os.mkdir()` should be replaced by `Path.mkdir()`
--> import_from.py:11:7
|
9 | a = abspath(p)
@@ -49,8 +49,26 @@ PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
12 | makedirs(p)
13 | rename(p)
|
help: Replace with `Path(...).mkdir()`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
8 9 |
9 10 | a = abspath(p)
10 11 | aa = chmod(p)
11 |-aaa = mkdir(p)
12 |+aaa = pathlib.Path(p).mkdir()
12 13 | makedirs(p)
13 14 | rename(p)
14 15 | replace(p)
PTH103 [*] `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> import_from.py:12:1
|
10 | aa = chmod(p)
@@ -60,6 +78,25 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
13 | rename(p)
14 | replace(p)
|
help: Replace with `Path(...).mkdir(parents=True)`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
9 10 | a = abspath(p)
10 11 | aa = chmod(p)
11 12 | aaa = mkdir(p)
12 |-makedirs(p)
13 |+pathlib.Path(p).mkdir(parents=True)
13 14 | rename(p)
14 15 | replace(p)
15 16 | rmdir(p)
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> import_from.py:13:1

View File

@@ -39,7 +39,7 @@ PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
help: Replace with `Path(...).chmod(...)`
PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
PTH102 [*] `os.mkdir()` should be replaced by `Path.mkdir()`
--> import_from_as.py:16:7
|
14 | a = xabspath(p)
@@ -49,8 +49,26 @@ PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
17 | xmakedirs(p)
18 | xrename(p)
|
help: Replace with `Path(...).mkdir()`
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
13 14 |
14 15 | a = xabspath(p)
15 16 | aa = xchmod(p)
16 |-aaa = xmkdir(p)
17 |+aaa = pathlib.Path(p).mkdir()
17 18 | xmakedirs(p)
18 19 | xrename(p)
19 20 | xreplace(p)
PTH103 [*] `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> import_from_as.py:17:1
|
15 | aa = xchmod(p)
@@ -60,6 +78,25 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
18 | xrename(p)
19 | xreplace(p)
|
help: Replace with `Path(...).mkdir(parents=True)`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
14 15 | a = xabspath(p)
15 16 | aa = xchmod(p)
16 17 | aaa = xmkdir(p)
17 |-xmakedirs(p)
18 |+pathlib.Path(p).mkdir(parents=True)
18 19 | xrename(p)
19 20 | xreplace(p)
20 21 | xrmdir(p)
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> import_from_as.py:18:1

View File

@@ -2,96 +2,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
/// ## What it does
/// Checks for uses of `os.makedirs`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.mkdir(parents=True)` can improve readability over the
/// `os` module's counterparts (e.g., `os.makedirs()`.
///
/// ## Examples
/// ```python
/// import os
///
/// os.makedirs("./nested/directory/")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("./nested/directory/").mkdir(parents=True)
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir)
/// - [Python documentation: `os.makedirs`](https://docs.python.org/3/library/os.html#os.makedirs)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsMakedirs;
impl Violation for OsMakedirs {
#[derive_message_formats]
fn message(&self) -> String {
"`os.makedirs()` should be replaced by `Path.mkdir(parents=True)`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.mkdir`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.mkdir()` can improve readability over the `os`
/// module's counterparts (e.g., `os.mkdir()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.mkdir("./directory/")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("./directory/").mkdir()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir)
/// - [Python documentation: `os.mkdir`](https://docs.python.org/3/library/os.html#os.mkdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsMkdir;
impl Violation for OsMkdir {
#[derive_message_formats]
fn message(&self) -> String {
"`os.mkdir()` should be replaced by `Path.mkdir()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.stat`.
///
@@ -400,45 +310,3 @@ impl Violation for OsListdir {
"Use `pathlib.Path.iterdir()` instead.".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.symlink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.symlink`.
///
/// ## Example
/// ```python
/// import os
///
/// os.symlink("usr/bin/python", "tmp/python", target_is_directory=False)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("tmp/python").symlink_to("usr/bin/python")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.symlink_to`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.symlink_to)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsSymlink;
impl Violation for OsSymlink {
#[derive_message_formats]
fn message(&self) -> String {
"`os.symlink` should be replaced by `Path.symlink_to`".to_string()
}
}

View File

@@ -9,7 +9,7 @@ fn to_interpolated_string_interpolation_element(inner: &Expr) -> ast::Interpolat
conversion: ConversionFlag::None,
format_spec: None,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})
}
@@ -18,7 +18,7 @@ pub(super) fn to_interpolated_string_literal_element(s: &str) -> ast::Interpolat
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
value: Box::from(s),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})
}

View File

@@ -91,7 +91,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option<
.into_boxed_str(),
flags: flags?,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
return Some(node.into());
}
@@ -114,7 +114,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option<
let node = ast::FString {
elements: f_string_elements.into(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
flags,
};
Some(node.into())

View File

@@ -182,7 +182,7 @@ fn function(
ExprEllipsisLiteral::default(),
))),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let parameters = lambda.parameters.as_deref().cloned().unwrap_or_default();
if let Some(annotation) = annotation {
@@ -230,7 +230,7 @@ fn function(
returns: Some(Box::new(return_type)),
type_params: None,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let generated = checker.generator().stmt(&func);
@@ -246,7 +246,7 @@ fn function(
returns: None,
type_params: None,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let generated = checker.generator().stmt(&function);

View File

@@ -72,11 +72,11 @@ pub(crate) fn manual_from_import(checker: &Checker, stmt: &Stmt, alias: &Alias,
name: asname.clone(),
asname: None,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}],
level: 0,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
checker.generator().stmt(&node.into()),

View File

@@ -147,7 +147,7 @@ fn collect_nested_args(min_max: MinMax, args: &[Expr], semantic: &SemanticModel)
value: Box::new(arg.clone()),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
new_args.push(new_arg);
continue;
@@ -204,10 +204,10 @@ pub(crate) fn nested_min_max(
args: collect_nested_args(min_max, args, checker.semantic()).into_boxed_slice(),
keywords: Box::from(keywords),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
checker.generator().expr(&flattened_expr),

View File

@@ -170,13 +170,13 @@ pub(crate) fn repeated_equality_comparison(checker: &Checker, bool_op: &ast::Exp
Expr::Set(ast::ExprSet {
elts: comparators.iter().copied().cloned().collect(),
range: TextRange::default(),
node_index: AtomicNodeIndex::dummy(),
node_index: AtomicNodeIndex::NONE,
})
} else {
Expr::Tuple(ast::ExprTuple {
elts: comparators.iter().copied().cloned().collect(),
range: TextRange::default(),
node_index: AtomicNodeIndex::dummy(),
node_index: AtomicNodeIndex::NONE,
ctx: ExprContext::Load,
parenthesized: true,
})
@@ -194,12 +194,12 @@ pub(crate) fn repeated_equality_comparison(checker: &Checker, bool_op: &ast::Exp
},
comparators: Box::from([comparator]),
range: bool_op.range(),
node_index: AtomicNodeIndex::dummy(),
node_index: AtomicNodeIndex::NONE,
})))
.chain(after)
.collect(),
range: bool_op.range(),
node_index: AtomicNodeIndex::dummy(),
node_index: AtomicNodeIndex::NONE,
})),
bool_op.range(),
)));

View File

@@ -188,7 +188,7 @@ fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix {
value: Box::from("utf-8"),
flags: checker.default_string_flags(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}))
),
&call.arguments,

View File

@@ -101,7 +101,8 @@ mod tests {
#[test_case(Rule::UnnecessaryClassParentheses, Path::new("UP039.py"))]
#[test_case(Rule::UnnecessaryDefaultTypeArgs, Path::new("UP043.py"))]
#[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))]
#[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010.py"))]
#[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010_0.py"))]
#[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010_1.py"))]
#[test_case(Rule::UselessMetaclassType, Path::new("UP001.py"))]
#[test_case(Rule::UselessObjectInheritance, Path::new("UP004.py"))]
#[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))]

View File

@@ -87,7 +87,7 @@ pub(crate) fn convert_named_tuple_functional_to_class(
// Ex) `NamedTuple("MyType")`
([_typename], []) => vec![Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})],
// Ex) `NamedTuple("MyType", [("a", int), ("b", str)])`
([_typename, fields], []) => {
@@ -165,7 +165,7 @@ fn create_field_assignment_stmt(field: Name, annotation: &Expr) -> Stmt {
id: field,
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into(),
),
@@ -173,7 +173,7 @@ fn create_field_assignment_stmt(field: Name, annotation: &Expr) -> Stmt {
value: None,
simple: true,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into()
}
@@ -184,7 +184,7 @@ fn create_fields_from_fields_arg(fields: &Expr) -> Option<Vec<Stmt>> {
if fields.is_empty() {
let node = Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
Some(vec![node])
} else {
@@ -236,13 +236,13 @@ fn create_class_def_stmt(typename: &str, body: Vec<Stmt>, base_class: &Expr) ->
args: Box::from([base_class.clone()]),
keywords: Box::from([]),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})),
body,
type_params: None,
decorator_list: vec![],
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into()
}

View File

@@ -150,7 +150,7 @@ fn create_field_assignment_stmt(field: &str, annotation: &Expr) -> Stmt {
id: field.into(),
ctx: ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into(),
),
@@ -158,7 +158,7 @@ fn create_field_assignment_stmt(field: &str, annotation: &Expr) -> Stmt {
value: None,
simple: true,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into()
}
@@ -179,13 +179,13 @@ fn create_class_def_stmt(
None => Box::from([]),
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})),
body,
type_params: None,
decorator_list: vec![],
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into()
}
@@ -194,7 +194,7 @@ fn fields_from_dict_literal(items: &[ast::DictItem]) -> Option<Vec<Stmt>> {
if items.is_empty() {
let node = Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
Some(vec![node])
} else {
@@ -228,7 +228,7 @@ fn fields_from_dict_call(func: &Expr, keywords: &[Keyword]) -> Option<Vec<Stmt>>
if keywords.is_empty() {
let node = Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
Some(vec![node])
} else {
@@ -241,7 +241,7 @@ fn fields_from_keywords(keywords: &[Keyword]) -> Option<Vec<Stmt>> {
if keywords.is_empty() {
let node = Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
return Some(vec![node]);
}
@@ -282,7 +282,7 @@ fn match_fields_and_total(arguments: &Arguments) -> Option<(Vec<Stmt>, Option<&K
([_typename], []) => {
let node = Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
Some((vec![node], None))
}

View File

@@ -39,27 +39,27 @@ impl LiteralType {
LiteralType::Str => ast::StringLiteral {
value: Box::default(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
flags: checker.default_string_flags(),
}
.into(),
LiteralType::Bytes => ast::BytesLiteral {
value: Box::default(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
flags: checker.default_bytes_flags(),
}
.into(),
LiteralType::Int => ast::ExprNumberLiteral {
value: ast::Number::Int(Int::from(0u8)),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into(),
LiteralType::Float => ast::ExprNumberLiteral {
value: ast::Number::Float(0.0),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into(),
LiteralType::Bool => ast::ExprBooleanLiteral::default().into(),

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