Compare commits

...

29 Commits

Author SHA1 Message Date
Dylan
f51a228f04 Bump 0.12.8 (#19813) 2025-08-07 13:52:16 -05:00
Andrew Gallant
d5e1b7983e [ty] Fix static assertion size check (#19814)
A `Segment` has a `Box` in it, which has a platform dependent size.
Restrict the check to only 64-bit targets.
2025-08-07 13:38:16 -05:00
Micha Reiser
7dfde3b929 Update Rust toolchain to 1.89 (#19807) 2025-08-07 18:21:50 +02:00
Dhruv Manilawala
b22586fa0e [ty] Add ty.inlayHints.variableTypes server option (#19780)
## Summary

This PR adds a new `ty.inlayHints.variableTypes` server setting to
configure ty to include / exclude inlay hints at variable position.

Currently, we only support inlay hints at this position so this option
basically translates to enabling / disabling inlay hints for now :)

The VS Code extension PR is
https://github.com/astral-sh/ty-vscode/pull/112.

closes: astral-sh/ty#472

## Test Plan

Add E2E tests.
2025-08-07 19:16:51 +05:30
Alex Waygood
c401a6d86e [ty] Add failing tests for tuple subclasses (#19803) 2025-08-07 13:11:15 +00:00
Dhruv Manilawala
7b6abfb030 [ty] Add ty.experimental.rename server setting (#19800)
## Summary

This PR is a follow-up from https://github.com/astral-sh/ruff/pull/19551
and adds a new `ty.experimental.rename` setting to conditionally
register for the rename capability. The complementary PR in ty VS Code
extension is https://github.com/astral-sh/ty-vscode/pull/111.

This is done using dynamic registration after the settings have been
resolved. The experimental group is part of the global settings because
they're applied for all workspaces that are managed by the client.

## Test Plan

Add E2E tests.

In VS Code, with the following setting:
```json
{
	"ty.experimental.rename": "true",
	"python.languageServer": "None"
}
```

I get the relevant log entry:
```
2025-08-07 16:05:40.598709000 DEBUG client_response{id=3 method="client/registerCapability"}: Registered rename capability
```

And, I'm able to rename a symbol. Once I set it to `false`, then I can
see this log entry:

```
2025-08-07 16:08:39.027876000 DEBUG Rename capability is disabled in the client settings
```

And, I don't see the "Rename Symbol" open in the VS Code dropdown.


https://github.com/user-attachments/assets/501659df-ba96-4252-bf51-6f22acb4920b
2025-08-07 12:54:58 +00:00
UnboundVariable
b005cdb7ff [ty] Implemented support for "rename" language server feature (#19551)
This PR adds support for the "rename" language server feature. It builds
upon existing functionality used for "go to references".

The "rename" feature involves two language server requests. The first is
a "prepare rename" request that determines whether renaming should be
possible for the identifier at the current offset. The second is a
"rename" request that returns a list of file ranges where the rename
should be applied.

Care must be taken when attempting to rename symbols that span files,
especially if the symbols are defined in files that are not part of the
project. We don't want to modify code in the user's Python environment
or in the vendored stub files.

I found a few bugs in the "go to references" feature when implementing
"rename", and those bug fixes are included in this PR.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-08-07 15:58:18 +05:30
Micha Reiser
b96aa4605b [ty] Reduce size of member table (#19572) 2025-08-07 11:16:04 +02:00
Dhruv Manilawala
cc97579c3b [ty] Move server capabilities creation (#19798) 2025-08-07 04:28:08 +00:00
Matthew Mckee
ef1802b94f [ty] Repurpose FunctionType.into_bound_method_type to return BoundMethodType (#19793)
## Summary

As per our naming scheme (at least for callable types) this should
return a `BoundMethodType`, or be renamed, but it makes more sense to
change the return type.

I also ensure `ClassType.into_callable` returns a `Type::Callable` in
the changed branch.

Ideally we could return a `CallableType` from these `into_callable`
functions (and rename to `into_callable_type` but because of unions we
cannot do this.
2025-08-06 15:24:59 -07:00
David Peter
98df62db79 [ty] Validate writes to TypedDict keys (#19782)
## Summary

Validates writes to `TypedDict` keys, for example:

```py
class Person(TypedDict):
    name: str
    age: int | None


def f(person: Person):
    person["naem"] = "Alice"  # error: [invalid-key]

    person["age"] = "42"  # error: [invalid-assignment]
```

The new specialized `invalid-assignment` diagnostic looks like this:

<img width="1160" height="279" alt="image"
src="https://github.com/user-attachments/assets/51259455-3501-4829-a84e-df26ff90bd89"
/>

## Ecosystem analysis

As far as I can tell, all true positives!

There are some extremely long diagnostic messages. We should truncate
our display of overload sets somehow.

## Test Plan

New Markdown tests
2025-08-06 15:19:13 -07:00
Matthew Mckee
65b39f2ca9 [ty] Add support for using the test command emitted when a mdtest fails (#19794)
## Summary

When seeing a failed test like 

```bash
is_subtype_of.md - Subtype relation - Callable - Class literals - Classes with `__new_… (1e9782853227c019)

  crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md:1810 unexpected error: [unresolved-reference] "Name `Aa` used when not defined"

To rerun this specific test, set the environment variable: MDTEST_TEST_FILTER='is_subtype_of.md - Subtype relation - Callable - Class literals - Classes with `__new_… (1e9782853227c019)'
MDTEST_TEST_FILTER='is_subtype_of.md - Subtype relation - Callable - Class literals - Classes with `__new_… (1e9782853227c019)' cargo test -p ty_python_semantic --test mdtest -- mdtest__type_properties_is_subtype_of
```

running the following now works

```bash
MDTEST_TEST_FILTER='is_subtype_of.md - Subtype relation - Callable - Class literals - Classes with `__new_… (1e9782853227c019)' cargo test -p ty_python_semantic --test mdtest -- mdtest__type_properties_is_subtype_of
```


## Test Plan

Do we have tests for the test runner? :)
2025-08-06 15:02:10 -07:00
Douglas Creager
585ce12ace [ty] typing.Self is bound by the method, not the class (#19784)
This fixes our logic for binding a legacy typevar with its binding
context. (To recap, a legacy typevar starts out "unbound" when it is
first created, and each time it's used in a generic class or function,
we "bind" it with the corresponding `Definition`.)

We treat `typing.Self` the same as a legacy typevar, and so we apply
this binding logic to it too. Before, we were using the enclosing class
as its binding context. But that's not correct — it's the method where
`typing.Self` is used that binds the typevar. (Each invocation of the
method will find a new specialization of `Self` based on the specific
instance type containing the invoked method.)

This required plumbing through some additional state to the
`in_type_expression` method.

This also revealed that we weren't handling `Self`-typed instance
attributes correctly (but were coincidentally not getting the expected
false positive diagnostics).
2025-08-06 17:26:17 -04:00
Ibraheem Ahmed
21ac16db85 [ty] Avoid overcounting shared memory usage (#19773)
## Summary

Use a global tracker to avoid double counting `Arc` instances.
2025-08-06 15:32:02 -04:00
Dan Parizher
745742e414 [pylint] Mark PLC0207 fixes as unsafe when *args unpacking is present (#19679)
## Summary

Fixes #19660
2025-08-06 14:19:49 -04:00
Dhruv Manilawala
ec5660d786 [ty] Avoid warning for old settings schema too aggresively (#19787)
## Summary

This PR avoids warning the users too aggressively by checking the
structure of the initialization and workspace options and avoids the
warning if they conform to the old schema.

## Test Plan



https://github.com/user-attachments/assets/9ade9dc4-90cb-4fd4-abd0-4bc4177df3db
2025-08-06 16:16:59 +00:00
David Peter
b96929ee19 [ty] Disallow typing.TypedDict in type expressions (#19777)
## Summary

Disallow `typing.TypedDict` in type expressions.

Related reference: https://github.com/python/mypy/issues/11030

## Test Plan

New Markdown tests, checked ecosystem and conformance test impact.
2025-08-06 15:58:35 +02:00
Dhruv Manilawala
fa711fa40f [ty] Warn users if server received unknown options (#19779)
## Summary

This PR updates the client settings handling to recognize unknown
options provided by the user and show a warning popup along with a
warning log message.

## Test Plan

Add E2E tests.
2025-08-06 13:11:13 +00:00
Dhruv Manilawala
1f29a04e9a [ty] Support LSP client settings (#19614)
## Summary

This PR implements support for providing LSP client settings.

The complementary PR in the ty VS Code extension:
astral-sh/ty-vscode#106.

Notes for the previous iteration of this PR is in
https://github.com/astral-sh/ruff/pull/19614#issuecomment-3136477864
(click on "Details").

Specifically, this PR splits the client settings into 3 distinct groups.
Keep in mind that these groups are not visible to the user, they're
merely an implementation detail. The groups are:
1. `GlobalOptions` - these are the options that are global to the
language server and will be the same for all the workspaces that are
handled by the server
2. `WorkspaceOptions` - these are the options that are specific to a
workspace and will be applied only when running any logic for that
workspace
3. `InitializationOptions` - these are the options that can be specified
during initialization

The initialization options are a superset that contains both the global
and workspace options flattened into a 1-dimensional structure. This
means that the user can specify any and all fields present in
`GlobalOptions` and `WorkspaceOptions` in the initialization options in
addition to the fields that are _specific_ to initialization options.

From the current set of available settings, following are only available
during initialization because they are required at that time, are static
during the runtime of the server and changing their values require a
restart to take effect:
- `logLevel`
- `logFile`

And, following are available under `GlobalOptions`:
- `diagnosticMode`

And, following under `WorkspaceOptions`:
- `disableLanguageServices`
- `pythonExtension` (Python environment information that is populated by
the ty VS Code extension)

### `workspace/configuration`

This request allows server to ask the client for configuration to a
specific workspace. But, this is only supported by the client that has
the `workspace.configuration` client capability set to `true`. What to
do for clients that don't support pulling configurations?

In that case, the settings needs to be provided in the initialization
options and updating the values of those settings can only be done by
restarting the server. With the way this is implemented, this means that
if the client does not support pulling workspace configuration then
there's no way to specify settings specific to a workspace. Earlier,
this would've been possible by providing an array of client options with
an additional field which specifies which workspace the options belong
to but that adds complexity and clients that actually do not support
`workspace/configuration` would usually not support multiple workspaces
either.

Now, for the clients that do support this, the server will initiate the
request to get the configuration for all the workspaces at the start of
the server. Once the server receives these options, it will resolve them
for each workspace as follows:
1. Combine the client options sent during initialization with the
options specific to the workspace creating the final client options
that's specific to this workspace
2. Create a global options by combining the global options from (1) for
all workspaces which in turn will also combine the global options sent
during initialization

The global options are resolved into the global settings and are
available on the `Session` which is initialized with the default global
settings. The workspace options are resolved into the workspace settings
and are available on the respective `Workspace`.

The `SessionSnapshot` contains the global settings while the document
snapshot contains the workspace settings. We could add the global
settings to the document snapshot but that's currently not needed.

### Document diagnostic dynamic registration

Currently, the document diagnostic server capability is created based on
the `diagnosticMode` sent during initialization. But, that wouldn't
provide us with the complete picture. This means the server needs to
defer registering the document diagnostic capability at a later point
once the settings have been resolved.

This is done using dynamic registration for clients that support it. For
clients that do not support dynamic registration for document diagnostic
capability, the server advertises itself as always supporting workspace
diagnostics and work done progress token.

This dynamic registration now allows us to change the server capability
for workspace diagnostics based on the resolved `diagnosticMode` value.
In the future, once `workspace/didChangeConfiguration` is supported, we
can avoid the server restart when users have changed any client
settings.

## Test Plan

Add integration tests and recorded videos on the user experience in
various editors:

### VS Code

For VS Code users, the settings experience is unchanged because the
extension defines it's own interface on how the user can specify the
server setting. This means everything is under the `ty.*` namespace as
usual.


https://github.com/user-attachments/assets/c2e5ba5c-7617-406e-a09d-e397ce9c3b93

### Zed

For Zed, the settings experience has changed. Users can specify settings
during initialization:

```json
{
  "lsp": {
    "ty": {
      "initialization_options": {
        "logLevel": "debug",
        "logFile": "~/.cache/ty.log",
        "diagnosticMode": "workspace",
        "disableLanguageServices": true
      }
    },
  }
}
```

Or, can specify the options under the `settings` key:

```json
{
  "lsp": {
    "ty": {
      "settings": {
        "ty": {
          "diagnosticMode": "openFilesOnly",
          "disableLanguageServices": true
        }
      },
      "initialization_options": {
        "logLevel": "debug",
        "logFile": "~/.cache/ty.log"
      }
    },
  }
}
```

The `logLevel` and `logFile` setting still needs to go under the
initialization options because they're required by the server during
initialization.

We can remove the nesting of the settings under the "ty" namespace by
updating the return type of
db9ea0cdfd/src/tychecker.rs (L45-L49)
to be wrapped inside `ty` directly so that users can avoid doing the
double nesting.

There's one issue here which is that if the `diagnosticMode` is
specified in both the initialization option and settings key, then the
resolution is a bit different - if either of them is set to be
`workspace`, then it wins which means that in the following
configuration, the diagnostic mode is `workspace`:

```json
{
  "lsp": {
    "ty": {
      "settings": {
        "ty": {
          "diagnosticMode": "openFilesOnly"
        }
      },
      "initialization_options": {
        "diagnosticMode": "workspace"
      }
    },
  }
}
```

This behavior is mainly a result of combining global options from
various workspace configuration results. Users should not be able to
provide global options in multiple workspaces but that restriction
cannot be done on the server side. The ty VS Code extension restricts
these global settings to only be set in the user settings and not in
workspace settings but we do not control extensions in other editors.


https://github.com/user-attachments/assets/8e2d6c09-18e6-49e5-ab78-6cf942fe1255

### Neovim

Same as in Zed.

### Other

Other editors that do not support `workspace/configuration`, the users
would need to provide the server settings during initialization.
2025-08-06 18:37:21 +05:30
Alex Waygood
529d81daca [ty] Improve subscript narrowing for "safe mutable classes" (#19781)
## Summary

This PR improves the `is_safe_mutable_class` function in `infer.rs` in
several ways:
- It uses `KnownClass::to_instance()` for all "safe mutable classes".
Previously, we were using `SpecialFormType::instance_fallback()` for
some variants -- I'm not totally sure why. Switching to
`KnownClass::to_instance()` for all "safe mutable classes" fixes a
number of TODOs in the `assignment.md` mdtest suite
- Rather than eagerly calling `.to_instance(db)` on all "safe mutable
classes" every time `is_safe_mutable_class` is called, we now only call
it lazily on each element, allowing us to short-circuit more
effectively.
- I removed the entry entirely for `TypedDict` from the list of "safe
mutable classes", as it's not correct.
`SpecialFormType::TypedDict.instance_fallback(db)` just returns an
instance type representing "any instance of `typing._SpecialForm`",
which I don't think was the intent of this code. No tests fail as a
result of removing this entry, as we already check separately whether an
object is an inhabitant of a `TypedDict` type (and consider that object
safe-mutable if so!).

## Test Plan

mdtests updated
2025-08-06 12:26:25 +01:00
David Peter
4887bdf205 [ty] Infer types for key-based access on TypedDicts (#19763)
## Summary

This PR adds type inference for key-based access on `TypedDict`s and a
new diagnostic for invalid subscript accesses:

```py
class Person(TypedDict):
    name: str
    age: int | None

alice = Person(name="Alice", age=25)

reveal_type(alice["name"])  # revealed: str
reveal_type(alice["age"])  # revealed: int | None

alice["naem"]  # Unknown key "naem" - did you mean "name"?
```

## Test Plan

Updated Markdown tests
2025-08-06 09:36:33 +02:00
Dan Parizher
e917d309f1 [flake8_import_conventions] Avoid false positives for NFKC-normalized __debug__ import aliases in ICN001 (#19411)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-08-06 06:42:51 +00:00
Matthew Mckee
18ad2848e3 Display generic function signature properly (#19544)
## Summary

Resolves https://github.com/astral-sh/ty/issues/817

## Test Plan

Update mdtest

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-08-05 16:35:08 -07:00
Brent Westbrook
5bfffe1aa7 [ty] Remap Jupyter notebook cell indices in ruff_db (#19698)
## Summary

This PR remaps ranges in Jupyter notebooks from simple `row:column`
indices in the concatenated source code to `cell:row:col` to match
Ruff's output. This is probably not a likely change to land upstream in
`annotate-snippets`, but I didn't see a good way around it.

The remapping logic is taken nearly verbatim from here:


cd6bf1457d/crates/ruff_linter/src/message/text.rs (L212-L222)


## Test Plan

New `full` rendering test for a notebook

I was mainly focused on Ruff, but in local tests this also works for ty:

```
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to `str`
 --> Untitled.ipynb:cell 1:3:1
  |
1 | import math
2 |
3 | x: str = 1
  | ^
  |
info: rule `invalid-assignment` is enabled by default

error[invalid-assignment]: Object of type `Literal[1]` is not assignable to `str`
 --> Untitled.ipynb:cell 2:3:1
  |
1 | import math
2 |
3 | x: str = 1
  | ^
  |
info: rule `invalid-assignment` is enabled by default
```

This isn't a duplicate diagnostic, just an unimaginative example:

```py
# cell 1
import math

x: str = 1
# cell 2
import math

x: str = 1
```
2025-08-05 14:10:35 -04:00
Brent Westbrook
b324ae1be3 Hide empty snippets for full-file diagnostics (#19653)
Summary
--

This is the other commit I wanted to spin off from #19415, currently
stacked on #19644.

This PR suppresses blank snippets for empty ranges at the very beginning
of a file, and for empty ranges in non-existent files. Ruff includes
empty ranges for IO errors, for example.


f4e93b6335/crates/ruff_linter/src/message/text.rs (L100-L110)

The diagnostics now look like this (new snapshot test):

```
error[test-diagnostic]: main diagnostic message
--> example.py:1:1                             
```

Instead of [^*]

```
error[test-diagnostic]: main diagnostic message
--> example.py:1:1
 |
 |
```

Test Plan
--

A new `ruff_db` test showing the expected output format

[^*]: This doesn't correspond precisely to the example in the PR because
of some details of the diagnostic builder helper methods in `ruff_db`,
but you can see another example in the current version of the summary in
#19415.
2025-08-05 11:20:31 -04:00
Brent Westbrook
2db4e5dbea Use fixed hash width for ty_server diagnostics (#19766)
Summary
--

Fixes a snapshot test failure I saw in #19653 locally and in Windows CI
by
padding the hex ID to 16 digits to match the regex in
`filter_result_id`.


78e5fe0a51/crates/ty_server/tests/e2e/pull_diagnostics.rs (L380-L384)

Test Plan
--

I applied this to the branch from #19653 locally and saw that the tests
now
pass. I couldn't reproduce this failure directly on `main` or this
branch,
though.
2025-08-05 10:55:17 -04:00
Alex Waygood
4090297a11 [ty] Fix more false positives related to Generic or Protocol being subscripted with a ParamSpec or TypeVarTuple (#19764) 2025-08-05 15:45:56 +01:00
Simon Lamon
934fd37d2b [ty] Diagnostics for async context managers (#19704)
## Summary

Implements diagnostics for async context managers. Fixes
https://github.com/astral-sh/ty/issues/918.

## Test Plan

Mdtests have been added.
2025-08-05 07:41:37 -07:00
Brent Westbrook
78e5fe0a51 Allow hiding the diagnostic severity in ruff_db (#19644)
## Summary

This PR is a spin-off from https://github.com/astral-sh/ruff/pull/19415.
It enables replacing the severity and lint name in a ty-style
diagnostic:

```
error[unused-import]: `os` imported but unused
```

with the noqa code and optional fix availability icon for a Ruff
diagnostic:

```
F401 [*] `os` imported but unused
F821 Undefined name `a`
```

or nothing at all for a Ruff syntax error:

```
SyntaxError: Expected one or more symbol names after import
```

Ruff adds the `SyntaxError` prefix to these messages manually.

Initially (d912458), I just passed a `hide_severity` flag through a
bunch of calls to get it into `annotate-snippets`, but after looking at
it again today, I think reusing the `None` severity/level gave a nicer
result. As I note in a lengthy code comment, I think all of this code
should be temporary and reverted when Ruff gets real severities, so
hopefully it's okay if it feels a little hacky.

I think the main visible downside of this approach is that we can't
style the asterisk in the fix availabilty icon in cyan, as in Ruff's
current output. It's part of the message in this PR and any styling gets
overwritten in `annotate-snippets`.

<img width="400" height="342" alt="image"
src="https://github.com/user-attachments/assets/57542ec9-a81c-4a01-91c7-bd6d7ec99f99"
/>

Hmm, I guess reusing `Level::None` also means the `F401` isn't red
anymore. Maybe my initial approach was better after all. In any case,
the rest of the PR should be basically the same, it just depends how we
want to toggle the severity.

## Test Plan

New `ruff_db` tests. These snapshots should be compared to the two tests
just above them (`hide_severity_output` vs `output` and
`hide_severity_syntax_errors` against `syntax_errors`).
2025-08-05 09:56:18 -04:00
285 changed files with 7093 additions and 1923 deletions

View File

@@ -1,5 +1,39 @@
# Changelog
## 0.12.8
### Preview features
- \[`flake8-use-pathlib`\] Expand `PTH201` to check all `PurePath` subclasses ([#19440](https://github.com/astral-sh/ruff/pull/19440))
### Bug fixes
- \[`flake8-blind-except`\] Change `BLE001` to correctly parse exception tuples ([#19747](https://github.com/astral-sh/ruff/pull/19747))
- \[`flake8-errmsg`\] Exclude `typing.cast` from `EM101` ([#19656](https://github.com/astral-sh/ruff/pull/19656))
- \[`flake8-simplify`\] Fix raw string handling in `SIM905` for embedded quotes ([#19591](https://github.com/astral-sh/ruff/pull/19591))
- \[`flake8-import-conventions`\] Avoid false positives for NFKC-normalized `__debug__` import aliases in `ICN001` ([#19411](https://github.com/astral-sh/ruff/pull/19411))
- \[`isort`\] Fix syntax error after docstring ending with backslash (`I002`) ([#19505](https://github.com/astral-sh/ruff/pull/19505))
- \[`pylint`\] Mark `PLC0207` fixes as unsafe when `*args` unpacking is present ([#19679](https://github.com/astral-sh/ruff/pull/19679))
- \[`pyupgrade`\] Prevent infinite loop with `I002` (`UP010`, `UP035`) ([#19413](https://github.com/astral-sh/ruff/pull/19413))
- \[`ruff`\] Parenthesize generator expressions in f-strings (`RUF010`) ([#19434](https://github.com/astral-sh/ruff/pull/19434))
### Rule changes
- \[`eradicate`\] Don't flag `pyrefly` pragmas as unused code (`ERA001`) ([#19731](https://github.com/astral-sh/ruff/pull/19731))
### Documentation
- Replace "associative" with "commutative" in docs for `RUF036` ([#19706](https://github.com/astral-sh/ruff/pull/19706))
- Fix copy and line separator colors in dark mode ([#19630](https://github.com/astral-sh/ruff/pull/19630))
- Fix link to `typing` documentation ([#19648](https://github.com/astral-sh/ruff/pull/19648))
- \[`refurb`\] Make more examples error out-of-the-box ([#19695](https://github.com/astral-sh/ruff/pull/19695),[#19673](https://github.com/astral-sh/ruff/pull/19673),[#19672](https://github.com/astral-sh/ruff/pull/19672))
### Other changes
- Include column numbers in GitLab output format ([#19708](https://github.com/astral-sh/ruff/pull/19708))
- Always expand tabs to four spaces in diagnostics ([#19618](https://github.com/astral-sh/ruff/pull/19618))
- Update pre-commit's `ruff` id ([#19654](https://github.com/astral-sh/ruff/pull/19654))
## 0.12.7
This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.

148
Cargo.lock generated
View File

@@ -56,9 +56,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.19"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -95,18 +95,18 @@ dependencies = [
[[package]]
name = "anstyle-query"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
name = "anstyle-svg"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a43964079ef399480603125d5afae2b219aceffb77478956e25f17b9bc3435c"
checksum = "dc03a770ef506fe1396c0e476120ac0e6523cf14b74218dd5f18cd6833326fa9"
dependencies = [
"anstyle",
"anstyle-lossy",
@@ -117,13 +117,13 @@ dependencies = [
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -346,9 +346,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.30"
version = "1.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
dependencies = [
"jobserver",
"libc",
@@ -408,9 +408,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.42"
version = "4.5.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882"
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
dependencies = [
"clap_builder",
"clap_derive",
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.42"
version = "4.5.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966"
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
dependencies = [
"anstream",
"anstyle",
@@ -492,9 +492,9 @@ dependencies = [
[[package]]
name = "codspeed"
version = "3.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29180405ab3b37bb020246ea66bf8ae233708766fd59581ae929feaef10ce91"
checksum = "35584c5fcba8059780748866387fb97c5a203bcfc563fc3d0790af406727a117"
dependencies = [
"anyhow",
"bincode 1.3.3",
@@ -510,9 +510,9 @@ dependencies = [
[[package]]
name = "codspeed-criterion-compat"
version = "3.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2454d874ca820ffd71273565530ad318f413195bbc99dce6c958ca07db362c63"
checksum = "78f6c1c6bed5fd84d319e8b0889da051daa361c79b7709c9394dfe1a882bba67"
dependencies = [
"codspeed",
"codspeed-criterion-compat-walltime",
@@ -521,9 +521,9 @@ dependencies = [
[[package]]
name = "codspeed-criterion-compat-walltime"
version = "3.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093a9383cdd1a5a0bd1a47cdafb49ae0c6dcd0793c8fb8f79768bab423128c9c"
checksum = "c989289ce6b1cbde72ed560496cb8fbf5aa14d5ef5666f168e7f87751038352e"
dependencies = [
"anes",
"cast",
@@ -546,9 +546,9 @@ dependencies = [
[[package]]
name = "codspeed-divan-compat"
version = "3.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c73bce1e3f47738bf74a6b58b72a49b4f40c837ce420d8d65a270298592aac"
checksum = "adf64eda57508448d59efd940bad62ede7c50b0d451a150b8d6a0eca642792a6"
dependencies = [
"codspeed",
"codspeed-divan-compat-macros",
@@ -557,9 +557,9 @@ dependencies = [
[[package]]
name = "codspeed-divan-compat-macros"
version = "3.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea51dd8add7eba774cc24b4a98324252ac3ec092ccb5f07e52bbe1cb72a6d373"
checksum = "058167258e819b16a4ba601fdfe270349ef191154758dbce122c62a698f70ba8"
dependencies = [
"divan-macros",
"itertools 0.14.0",
@@ -571,9 +571,9 @@ dependencies = [
[[package]]
name = "codspeed-divan-compat-walltime"
version = "3.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417e9edfc4b0289d4b9b48e62f98c6168d5e30c0e612b2935e394b0dd930fe83"
checksum = "48f9866ee3a4ef9d2868823ea5811886763af244f2df584ca247f49281c43f1f"
dependencies = [
"cfg-if",
"clap",
@@ -885,9 +885,9 @@ dependencies = [
[[package]]
name = "derive-where"
version = "1.5.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "510c292c8cf384b1a340b816a9a6cf2599eb8f566a44949024af88418000c50b"
checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f"
dependencies = [
"proc-macro2",
"quote",
@@ -1161,9 +1161,9 @@ dependencies = [
[[package]]
name = "get-size-derive2"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca171f9f8ed2f416ac044de2dc4acde3e356662a14ac990345639653bdc7fc28"
checksum = "75a17a226478b2e8294ded60782c03efe54476aa8cd1371d0e5ad9d1071e74e0"
dependencies = [
"attribute-derive",
"quote",
@@ -1172,9 +1172,9 @@ dependencies = [
[[package]]
name = "get-size2"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "965bc5c1c5fe05c5bbd398bb9b3f0f14d750261ebdd1af959f2c8a603fedb5ad"
checksum = "5697765925a05c9d401dd04a93dfd662d336cc25fdcc3301220385a1ffcfdde5"
dependencies = [
"compact_str",
"get-size-derive2",
@@ -1805,9 +1805,9 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "360e552c93fa0e8152ab463bc4c4837fce76a225df11dfaeea66c313de5e61f7"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
dependencies = [
"bitflags 2.9.1",
"libc",
@@ -1856,9 +1856,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lsp-server"
version = "0.7.8"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9462c4dc73e17f971ec1f171d44bfffb72e65a130117233388a0ebc7ec5656f9"
checksum = "7d6ada348dbc2703cbe7637b2dda05cff84d3da2819c24abcb305dd613e0ba2e"
dependencies = [
"crossbeam-channel",
"log",
@@ -2671,9 +2671,9 @@ dependencies = [
[[package]]
name = "redox_users"
version = "0.5.0"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.16",
"libredox",
@@ -2743,7 +2743,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.7"
version = "0.12.8"
dependencies = [
"anyhow",
"argfile",
@@ -2795,7 +2795,7 @@ dependencies = [
"test-case",
"thiserror 2.0.12",
"tikv-jemallocator",
"toml 0.9.4",
"toml 0.9.5",
"tracing",
"walkdir",
"wild",
@@ -2811,7 +2811,7 @@ dependencies = [
"ruff_annotate_snippets",
"serde",
"snapbox",
"toml 0.9.4",
"toml 0.9.5",
"tryfn",
"unicode-width 0.2.1",
]
@@ -2874,6 +2874,7 @@ dependencies = [
"ruff_annotate_snippets",
"ruff_cache",
"ruff_diagnostics",
"ruff_memory_usage",
"ruff_notebook",
"ruff_python_ast",
"ruff_python_parser",
@@ -2927,7 +2928,7 @@ dependencies = [
"similar",
"strum",
"tempfile",
"toml 0.9.4",
"toml 0.9.5",
"tracing",
"tracing-indicatif",
"tracing-subscriber",
@@ -2995,7 +2996,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.7"
version = "0.12.8"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3048,7 +3049,7 @@ dependencies = [
"tempfile",
"test-case",
"thiserror 2.0.12",
"toml 0.9.4",
"toml 0.9.5",
"typed-arena",
"unicode-normalization",
"unicode-width 0.2.1",
@@ -3068,6 +3069,13 @@ dependencies = [
"syn",
]
[[package]]
name = "ruff_memory_usage"
version = "0.0.0"
dependencies = [
"get-size2",
]
[[package]]
name = "ruff_notebook"
version = "0.0.0"
@@ -3298,7 +3306,7 @@ dependencies = [
"serde_json",
"shellexpand",
"thiserror 2.0.12",
"toml 0.9.4",
"toml 0.9.5",
"tracing",
"tracing-log",
"tracing-subscriber",
@@ -3327,7 +3335,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.7"
version = "0.12.8"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3388,7 +3396,8 @@ dependencies = [
"shellexpand",
"strum",
"tempfile",
"toml 0.9.4",
"toml 0.9.5",
"unicode-normalization",
]
[[package]]
@@ -3441,7 +3450,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
source = "git+https://github.com/salsa-rs/salsa.git?rev=b121ee46c4483ba74c19e933a3522bd548eb7343#b121ee46c4483ba74c19e933a3522bd548eb7343"
dependencies = [
"boxcar",
"compact_str",
@@ -3465,12 +3474,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
source = "git+https://github.com/salsa-rs/salsa.git?rev=b121ee46c4483ba74c19e933a3522bd548eb7343#b121ee46c4483ba74c19e933a3522bd548eb7343"
[[package]]
name = "salsa-macros"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
source = "git+https://github.com/salsa-rs/salsa.git?rev=b121ee46c4483ba74c19e933a3522bd548eb7343#b121ee46c4483ba74c19e933a3522bd548eb7343"
dependencies = [
"proc-macro2",
"quote",
@@ -4022,9 +4031,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.4"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [
"indexmap",
"serde",
@@ -4068,9 +4077,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
dependencies = [
"winnow",
]
@@ -4203,10 +4212,11 @@ dependencies = [
"ruff_python_trivia",
"salsa",
"tempfile",
"toml 0.9.4",
"toml 0.9.5",
"tracing",
"tracing-flame",
"tracing-subscriber",
"ty_combine",
"ty_project",
"ty_python_semantic",
"ty_server",
@@ -4214,6 +4224,16 @@ dependencies = [
"wild",
]
[[package]]
name = "ty_combine"
version = "0.0.0"
dependencies = [
"ordermap",
"ruff_db",
"ruff_python_ast",
"ty_python_semantic",
]
[[package]]
name = "ty_ide"
version = "0.0.0"
@@ -4255,6 +4275,7 @@ dependencies = [
"ruff_cache",
"ruff_db",
"ruff_macros",
"ruff_memory_usage",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_formatter",
@@ -4264,8 +4285,9 @@ dependencies = [
"schemars",
"serde",
"thiserror 2.0.12",
"toml 0.9.4",
"toml 0.9.5",
"tracing",
"ty_combine",
"ty_python_semantic",
"ty_vendored",
]
@@ -4296,6 +4318,7 @@ dependencies = [
"ruff_db",
"ruff_index",
"ruff_macros",
"ruff_memory_usage",
"ruff_python_ast",
"ruff_python_literal",
"ruff_python_parser",
@@ -4309,6 +4332,7 @@ dependencies = [
"serde",
"smallvec",
"static_assertions",
"strsim",
"strum",
"strum_macros",
"tempfile",
@@ -4336,6 +4360,7 @@ dependencies = [
"lsp-types",
"regex",
"ruff_db",
"ruff_macros",
"ruff_notebook",
"ruff_python_ast",
"ruff_source_file",
@@ -4349,6 +4374,7 @@ dependencies = [
"thiserror 2.0.12",
"tracing",
"tracing-subscriber",
"ty_combine",
"ty_ide",
"ty_project",
"ty_python_semantic",
@@ -4387,7 +4413,7 @@ dependencies = [
"smallvec",
"tempfile",
"thiserror 2.0.12",
"toml 0.9.4",
"toml 0.9.5",
"tracing",
"ty_python_semantic",
"ty_static",
@@ -5219,9 +5245,9 @@ dependencies = [
[[package]]
name = "zerovec"
version = "0.11.2"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
dependencies = [
"yoke",
"zerofrom",

View File

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

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.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.7/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.8/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.8/install.ps1 | iex"
```
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.7
rev: v0.12.8
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,6 @@ exit_code: 1
----- stdout -----
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=1;columnnumber=8;code=F401;]`os` imported but unused
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=2;columnnumber=5;code=F821;]Undefined name `y`
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=3;columnnumber=1;]SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=3;columnnumber=1;code=invalid-syntax;]Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -18,7 +18,7 @@ exit_code: 1
----- stdout -----
input.py:1:8: F401 [*] `os` imported but unused
input.py:2:5: F821 Undefined name `y`
input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
input.py:3:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 3 errors.
[*] 1 fixable with the `--fix` option.

View File

@@ -34,7 +34,7 @@ input.py:2:5: F821 Undefined name `y`
4 | case _: ...
|
input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
input.py:3:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
1 | import os # F401
2 | x = y # F821

View File

@@ -18,6 +18,6 @@ exit_code: 1
----- stdout -----
::error title=Ruff (F401),file=[TMP]/input.py,line=1,col=8,endLine=1,endColumn=10::input.py:1:8: F401 `os` imported but unused
::error title=Ruff (F821),file=[TMP]/input.py,line=2,col=5,endLine=2,endColumn=6::input.py:2:5: F821 Undefined name `y`
::error title=Ruff,file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
::error title=Ruff (invalid-syntax),file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -19,7 +19,7 @@ exit_code: 1
input.py:
1:8 F401 [*] `os` imported but unused
2:5 F821 Undefined name `y`
3:1 SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
3:1 invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 3 errors.
[*] 1 fixable with the `--fix` option.

View File

@@ -18,6 +18,6 @@ exit_code: 1
----- stdout -----
{"cell":null,"code":"F401","end_location":{"column":10,"row":1},"filename":"[TMP]/input.py","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
{"cell":null,"code":"F821","end_location":{"column":6,"row":2},"filename":"[TMP]/input.py","fix":null,"location":{"column":5,"row":2},"message":"Undefined name `y`","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/undefined-name"}
{"cell":null,"code":null,"end_location":{"column":6,"row":3},"filename":"[TMP]/input.py","fix":null,"location":{"column":1,"row":3},"message":"SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)","noqa_row":null,"url":null}
{"cell":null,"code":"invalid-syntax","end_location":{"column":6,"row":3},"filename":"[TMP]/input.py","fix":null,"location":{"column":1,"row":3},"message":"Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)","noqa_row":null,"url":null}
----- stderr -----

View File

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

View File

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

View File

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

View File

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

View File

@@ -83,9 +83,9 @@ exit_code: 1
}
],
"message": {
"text": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
"text": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
},
"ruleId": null
"ruleId": "invalid-syntax"
}
],
"tool": {

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ license = { workspace = true }
ruff_annotate_snippets = { workspace = true }
ruff_cache = { workspace = true, optional = true }
ruff_diagnostics = { workspace = true }
ruff_memory_usage = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true, features = ["get-size"] }
ruff_python_parser = { workspace = true }

View File

@@ -212,7 +212,7 @@ impl Diagnostic {
/// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what
/// you want.
pub fn concise_message(&self) -> ConciseMessage {
pub fn concise_message(&self) -> ConciseMessage<'_> {
let main = self.inner.message.as_str();
let annotation = self
.primary_annotation()
@@ -366,6 +366,16 @@ impl Diagnostic {
self.inner.secondary_code.as_ref()
}
/// Returns the secondary code for the diagnostic if it exists, or the lint name otherwise.
///
/// This is a common pattern for Ruff diagnostics, which want to use the noqa code in general,
/// but fall back on the `invalid-syntax` identifier for syntax errors, which don't have
/// secondary codes.
pub fn secondary_code_or_id(&self) -> &str {
self.secondary_code()
.map_or_else(|| self.inner.id.as_str(), SecondaryCode::as_str)
}
/// Set the secondary code for this diagnostic.
pub fn set_secondary_code(&mut self, code: SecondaryCode) {
Arc::make_mut(&mut self.inner).secondary_code = Some(code);
@@ -644,7 +654,7 @@ impl SubDiagnostic {
/// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what
/// you want.
pub fn concise_message(&self) -> ConciseMessage {
pub fn concise_message(&self) -> ConciseMessage<'_> {
let main = self.inner.message.as_str();
let annotation = self
.primary_annotation()
@@ -702,6 +712,11 @@ pub struct Annotation {
is_primary: bool,
/// The diagnostic tags associated with this annotation.
tags: Vec<DiagnosticTag>,
/// Whether this annotation is a file-level or full-file annotation.
///
/// When set, rendering will only include the file's name and (optional) range. Everything else
/// is omitted, including any file snippet or message.
is_file_level: bool,
}
impl Annotation {
@@ -720,6 +735,7 @@ impl Annotation {
message: None,
is_primary: true,
tags: Vec::new(),
is_file_level: false,
}
}
@@ -736,6 +752,7 @@ impl Annotation {
message: None,
is_primary: false,
tags: Vec::new(),
is_file_level: false,
}
}
@@ -801,6 +818,21 @@ impl Annotation {
pub fn push_tag(&mut self, tag: DiagnosticTag) {
self.tags.push(tag);
}
/// Set whether or not this annotation is file-level.
///
/// File-level annotations are only rendered with their file name and range, if available. This
/// is intended for backwards compatibility with Ruff diagnostics, which historically used
/// `TextRange::default` to indicate a file-level diagnostic. In the new diagnostic model, a
/// [`Span`] with a range of `None` should be used instead, as mentioned in the `Span`
/// documentation.
///
/// TODO(brent) update this usage in Ruff and remove `is_file_level` entirely. See
/// <https://github.com/astral-sh/ruff/issues/19688>, especially my first comment, for more
/// details.
pub fn set_file_level(&mut self, yes: bool) {
self.is_file_level = yes;
}
}
/// Tags that can be associated with an annotation.
@@ -1067,7 +1099,7 @@ enum DiagnosticSource {
impl DiagnosticSource {
/// Returns this input as a `SourceCode` for convenient querying.
fn as_source_code(&self) -> SourceCode {
fn as_source_code(&self) -> SourceCode<'_, '_> {
match self {
DiagnosticSource::Ty(input) => SourceCode::new(input.text.as_str(), &input.line_index),
DiagnosticSource::Ruff(source) => SourceCode::new(source.source_text(), source.index()),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ use crate::Db;
use crate::files::{File, FilePath};
/// Reads the source text of a python text file (must be valid UTF8) or notebook.
#[salsa::tracked(heap_size=get_size2::heap_size)]
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
pub fn source_text(db: &dyn Db, file: File) -> SourceText {
let path = file.path(db);
let _span = tracing::trace_span!("source_text", file = %path).entered();
@@ -157,7 +157,7 @@ pub enum SourceTextError {
}
/// Computes the [`LineIndex`] for `file`.
#[salsa::tracked(heap_size=get_size2::heap_size)]
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
pub fn line_index(db: &dyn Db, file: File) -> LineIndex {
let _span = tracing::trace_span!("line_index", ?file).entered();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -315,7 +315,7 @@ impl<'a> Checker<'a> {
}
/// Create a [`Generator`] to generate source code based on the current AST state.
pub(crate) fn generator(&self) -> Generator {
pub(crate) fn generator(&self) -> Generator<'_> {
Generator::new(self.stylist.indentation(), self.stylist.line_ending())
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -329,7 +329,7 @@ mod tests {
#[test]
fn start_of_file() -> Result<()> {
fn insert(contents: &str) -> Result<Insertion> {
fn insert(contents: &str) -> Result<Insertion<'_>> {
let parsed = parse_module(contents)?;
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents());
@@ -450,7 +450,7 @@ x = 1
#[test]
fn start_of_block() {
fn insert(contents: &str, offset: TextSize) -> Insertion {
fn insert(contents: &str, offset: TextSize) -> Insertion<'_> {
let parsed = parse_module(contents).unwrap();
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -154,7 +154,12 @@ impl Display for RuleCodeAndBody<'_> {
body = self.message.body(),
)
} else {
f.write_str(self.message.body())
write!(
f,
"{code}: {body}",
code = self.message.id().as_str().red().bold(),
body = self.message.body(),
)
}
}
}
@@ -281,7 +286,7 @@ impl Display for MessageCodeFrame<'_> {
/// modify the annotation ranges by inserting 3-byte Unicode replacements
/// because `annotate-snippets` will account for their actual width when
/// rendering and displaying the column to the user.
fn replace_unprintable(source: &str, annotation_range: TextRange) -> SourceCode {
fn replace_unprintable(source: &str, annotation_range: TextRange) -> SourceCode<'_> {
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -178,7 +178,7 @@ impl<'a> From<&NestedIf<'a>> for AnyNodeRef<'a> {
}
/// Returns the body, the range of the `if` or `elif` and whether the range is for an `if` or `elif`
fn nested_if_body(stmt_if: &ast::StmtIf) -> Option<NestedIf> {
fn nested_if_body(stmt_if: &ast::StmtIf) -> Option<NestedIf<'_>> {
let ast::StmtIf {
test,
body,

View File

@@ -267,7 +267,7 @@ struct Terminal<'a> {
stmt: &'a Stmt,
}
fn match_loop(stmt: &Stmt) -> Option<Loop> {
fn match_loop(stmt: &Stmt) -> Option<Loop<'_>> {
let Stmt::For(ast::StmtFor {
body, target, iter, ..
}) = stmt
@@ -324,7 +324,7 @@ fn match_loop(stmt: &Stmt) -> Option<Loop> {
/// return True
/// return False
/// ```
fn match_else_return(stmt: &Stmt) -> Option<Terminal> {
fn match_else_return(stmt: &Stmt) -> Option<Terminal<'_>> {
let Stmt::For(ast::StmtFor { orelse, .. }) = stmt else {
return None;
};

View File

@@ -388,7 +388,7 @@ impl KnownModules {
/// Return the [`ImportSection`] for a given module, if it's been categorized as a known module
/// by the user.
fn categorize(&self, module_name: &str) -> Option<(&ImportSection, Reason)> {
fn categorize(&self, module_name: &str) -> Option<(&ImportSection, Reason<'_>)> {
if self.has_submodules {
// Check all module prefixes from the longest to the shortest (e.g., given
// `foo.bar.baz`, check `foo.bar.baz`, then `foo.bar`, then `foo`, taking the first,
@@ -412,7 +412,7 @@ impl KnownModules {
}
}
fn categorize_submodule(&self, submodule: &str) -> Option<(&ImportSection, Reason)> {
fn categorize_submodule(&self, submodule: &str) -> Option<(&ImportSection, Reason<'_>)> {
let section = self.known.iter().find_map(|(pattern, section)| {
if pattern.matches(submodule) {
Some(section)

View File

@@ -21,7 +21,7 @@ E11.py:6:1: E111 Indentation is not a multiple of 4
8 | if False:
|
E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
E11.py:9:1: invalid-syntax: Expected an indented block after `if` statement
|
7 | #: E112
8 | if False:
@@ -31,7 +31,7 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
11 | print()
|
E11.py:12:1: SyntaxError: Unexpected indentation
E11.py:12:1: invalid-syntax: Unexpected indentation
|
10 | #: E113
11 | print()
@@ -41,7 +41,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation
14 | mimetype = 'application/x-directory'
|
E11.py:14:1: SyntaxError: Expected a statement
E11.py:14:1: invalid-syntax: Expected a statement
|
12 | print()
13 | #: E114 E116
@@ -51,7 +51,7 @@ E11.py:14:1: SyntaxError: Expected a statement
16 | create_date = False
|
E11.py:45:1: SyntaxError: Expected an indented block after `if` statement
E11.py:45:1: invalid-syntax: Expected an indented block after `if` statement
|
43 | #: E112
44 | if False: #

View File

@@ -11,7 +11,7 @@ E11.py:9:1: E112 Expected an indented block
11 | print()
|
E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
E11.py:9:1: invalid-syntax: Expected an indented block after `if` statement
|
7 | #: E112
8 | if False:
@@ -21,7 +21,7 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
11 | print()
|
E11.py:12:1: SyntaxError: Unexpected indentation
E11.py:12:1: invalid-syntax: Unexpected indentation
|
10 | #: E113
11 | print()
@@ -31,7 +31,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation
14 | mimetype = 'application/x-directory'
|
E11.py:14:1: SyntaxError: Expected a statement
E11.py:14:1: invalid-syntax: Expected a statement
|
12 | print()
13 | #: E114 E116
@@ -51,7 +51,7 @@ E11.py:45:1: E112 Expected an indented block
47 | if False:
|
E11.py:45:1: SyntaxError: Expected an indented block after `if` statement
E11.py:45:1: invalid-syntax: Expected an indented block after `if` statement
|
43 | #: E112
44 | if False: #

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
E11.py:9:1: invalid-syntax: Expected an indented block after `if` statement
|
7 | #: E112
8 | if False:
@@ -21,7 +21,7 @@ E11.py:12:1: E113 Unexpected indentation
14 | mimetype = 'application/x-directory'
|
E11.py:12:1: SyntaxError: Unexpected indentation
E11.py:12:1: invalid-syntax: Unexpected indentation
|
10 | #: E113
11 | print()
@@ -31,7 +31,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation
14 | mimetype = 'application/x-directory'
|
E11.py:14:1: SyntaxError: Expected a statement
E11.py:14:1: invalid-syntax: Expected a statement
|
12 | print()
13 | #: E114 E116
@@ -41,7 +41,7 @@ E11.py:14:1: SyntaxError: Expected a statement
16 | create_date = False
|
E11.py:45:1: SyntaxError: Expected an indented block after `if` statement
E11.py:45:1: invalid-syntax: Expected an indented block after `if` statement
|
43 | #: E112
44 | if False: #

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
E11.py:9:1: invalid-syntax: Expected an indented block after `if` statement
|
7 | #: E112
8 | if False:
@@ -11,7 +11,7 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
11 | print()
|
E11.py:12:1: SyntaxError: Unexpected indentation
E11.py:12:1: invalid-syntax: Unexpected indentation
|
10 | #: E113
11 | print()
@@ -21,7 +21,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation
14 | mimetype = 'application/x-directory'
|
E11.py:14:1: SyntaxError: Expected a statement
E11.py:14:1: invalid-syntax: Expected a statement
|
12 | print()
13 | #: E114 E116
@@ -41,7 +41,7 @@ E11.py:15:1: E114 Indentation is not a multiple of 4 (comment)
17 | #: E116 E116 E116
|
E11.py:45:1: SyntaxError: Expected an indented block after `if` statement
E11.py:45:1: invalid-syntax: Expected an indented block after `if` statement
|
43 | #: E112
44 | if False: #

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
E11.py:9:1: invalid-syntax: Expected an indented block after `if` statement
|
7 | #: E112
8 | if False:
@@ -11,7 +11,7 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
11 | print()
|
E11.py:12:1: SyntaxError: Unexpected indentation
E11.py:12:1: invalid-syntax: Unexpected indentation
|
10 | #: E113
11 | print()
@@ -21,7 +21,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation
14 | mimetype = 'application/x-directory'
|
E11.py:14:1: SyntaxError: Expected a statement
E11.py:14:1: invalid-syntax: Expected a statement
|
12 | print()
13 | #: E114 E116
@@ -91,7 +91,7 @@ E11.py:35:1: E115 Expected an indented block (comment)
37 | #: E117
|
E11.py:45:1: SyntaxError: Expected an indented block after `if` statement
E11.py:45:1: invalid-syntax: Expected an indented block after `if` statement
|
43 | #: E112
44 | if False: #

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
E11.py:9:1: invalid-syntax: Expected an indented block after `if` statement
|
7 | #: E112
8 | if False:
@@ -11,7 +11,7 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
11 | print()
|
E11.py:12:1: SyntaxError: Unexpected indentation
E11.py:12:1: invalid-syntax: Unexpected indentation
|
10 | #: E113
11 | print()
@@ -21,7 +21,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation
14 | mimetype = 'application/x-directory'
|
E11.py:14:1: SyntaxError: Expected a statement
E11.py:14:1: invalid-syntax: Expected a statement
|
12 | print()
13 | #: E114 E116
@@ -71,7 +71,7 @@ E11.py:26:1: E116 Unexpected indentation (comment)
28 | def start(self):
|
E11.py:45:1: SyntaxError: Expected an indented block after `if` statement
E11.py:45:1: invalid-syntax: Expected an indented block after `if` statement
|
43 | #: E112
44 | if False: #

View File

@@ -11,7 +11,7 @@ E11.py:6:1: E117 Over-indented
8 | if False:
|
E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
E11.py:9:1: invalid-syntax: Expected an indented block after `if` statement
|
7 | #: E112
8 | if False:
@@ -21,7 +21,7 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
11 | print()
|
E11.py:12:1: SyntaxError: Unexpected indentation
E11.py:12:1: invalid-syntax: Unexpected indentation
|
10 | #: E113
11 | print()
@@ -31,7 +31,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation
14 | mimetype = 'application/x-directory'
|
E11.py:14:1: SyntaxError: Expected a statement
E11.py:14:1: invalid-syntax: Expected a statement
|
12 | print()
13 | #: E114 E116
@@ -61,7 +61,7 @@ E11.py:42:1: E117 Over-indented
44 | if False: #
|
E11.py:45:1: SyntaxError: Expected an indented block after `if` statement
E11.py:45:1: invalid-syntax: Expected an indented block after `if` statement
|
43 | #: E112
44 | if False: #

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30_syntax_error.py:4:15: SyntaxError: Expected ']', found '('
E30_syntax_error.py:4:15: invalid-syntax: Expected ']', found '('
|
2 | # parenthesis.
3 |
@@ -10,7 +10,7 @@ E30_syntax_error.py:4:15: SyntaxError: Expected ']', found '('
5 | pass
|
E30_syntax_error.py:13:18: SyntaxError: Expected ')', found newline
E30_syntax_error.py:13:18: invalid-syntax: Expected ')', found newline
|
12 | class Foo:
13 | def __init__(
@@ -30,7 +30,7 @@ E30_syntax_error.py:15:5: E301 Expected 1 blank line, found 0
|
= help: Add missing blank line
E30_syntax_error.py:18:11: SyntaxError: Expected ')', found newline
E30_syntax_error.py:18:11: invalid-syntax: Expected ')', found newline
|
16 | pass
17 |
@@ -41,7 +41,7 @@ E30_syntax_error.py:18:11: SyntaxError: Expected ')', found newline
21 | def top(
|
E30_syntax_error.py:21:9: SyntaxError: Expected ')', found newline
E30_syntax_error.py:21:9: invalid-syntax: Expected ')', found newline
|
21 | def top(
| ^

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30_syntax_error.py:4:15: SyntaxError: Expected ']', found '('
E30_syntax_error.py:4:15: invalid-syntax: Expected ']', found '('
|
2 | # parenthesis.
3 |
@@ -20,7 +20,7 @@ E30_syntax_error.py:7:1: E302 Expected 2 blank lines, found 1
|
= help: Add missing blank line(s)
E30_syntax_error.py:13:18: SyntaxError: Expected ')', found newline
E30_syntax_error.py:13:18: invalid-syntax: Expected ')', found newline
|
12 | class Foo:
13 | def __init__(
@@ -30,7 +30,7 @@ E30_syntax_error.py:13:18: SyntaxError: Expected ')', found newline
16 | pass
|
E30_syntax_error.py:18:11: SyntaxError: Expected ')', found newline
E30_syntax_error.py:18:11: invalid-syntax: Expected ')', found newline
|
16 | pass
17 |
@@ -41,7 +41,7 @@ E30_syntax_error.py:18:11: SyntaxError: Expected ')', found newline
21 | def top(
|
E30_syntax_error.py:21:9: SyntaxError: Expected ')', found newline
E30_syntax_error.py:21:9: invalid-syntax: Expected ')', found newline
|
21 | def top(
| ^

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30_syntax_error.py:4:15: SyntaxError: Expected ']', found '('
E30_syntax_error.py:4:15: invalid-syntax: Expected ']', found '('
|
2 | # parenthesis.
3 |
@@ -19,7 +19,7 @@ E30_syntax_error.py:12:1: E303 Too many blank lines (3)
|
= help: Remove extraneous blank line(s)
E30_syntax_error.py:13:18: SyntaxError: Expected ')', found newline
E30_syntax_error.py:13:18: invalid-syntax: Expected ')', found newline
|
12 | class Foo:
13 | def __init__(
@@ -29,7 +29,7 @@ E30_syntax_error.py:13:18: SyntaxError: Expected ')', found newline
16 | pass
|
E30_syntax_error.py:18:11: SyntaxError: Expected ')', found newline
E30_syntax_error.py:18:11: invalid-syntax: Expected ')', found newline
|
16 | pass
17 |
@@ -40,7 +40,7 @@ E30_syntax_error.py:18:11: SyntaxError: Expected ')', found newline
21 | def top(
|
E30_syntax_error.py:21:9: SyntaxError: Expected ')', found newline
E30_syntax_error.py:21:9: invalid-syntax: Expected ')', found newline
|
21 | def top(
| ^

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30_syntax_error.py:4:15: SyntaxError: Expected ']', found '('
E30_syntax_error.py:4:15: invalid-syntax: Expected ']', found '('
|
2 | # parenthesis.
3 |
@@ -10,7 +10,7 @@ E30_syntax_error.py:4:15: SyntaxError: Expected ']', found '('
5 | pass
|
E30_syntax_error.py:13:18: SyntaxError: Expected ')', found newline
E30_syntax_error.py:13:18: invalid-syntax: Expected ')', found newline
|
12 | class Foo:
13 | def __init__(
@@ -29,7 +29,7 @@ E30_syntax_error.py:18:1: E305 Expected 2 blank lines after class or function de
|
= help: Add missing blank line(s)
E30_syntax_error.py:18:11: SyntaxError: Expected ')', found newline
E30_syntax_error.py:18:11: invalid-syntax: Expected ')', found newline
|
16 | pass
17 |
@@ -40,7 +40,7 @@ E30_syntax_error.py:18:11: SyntaxError: Expected ')', found newline
21 | def top(
|
E30_syntax_error.py:21:9: SyntaxError: Expected ')', found newline
E30_syntax_error.py:21:9: invalid-syntax: Expected ')', found newline
|
21 | def top(
| ^

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30_syntax_error.py:4:15: SyntaxError: Expected ']', found '('
E30_syntax_error.py:4:15: invalid-syntax: Expected ']', found '('
|
2 | # parenthesis.
3 |
@@ -10,7 +10,7 @@ E30_syntax_error.py:4:15: SyntaxError: Expected ']', found '('
5 | pass
|
E30_syntax_error.py:13:18: SyntaxError: Expected ')', found newline
E30_syntax_error.py:13:18: invalid-syntax: Expected ')', found newline
|
12 | class Foo:
13 | def __init__(
@@ -20,7 +20,7 @@ E30_syntax_error.py:13:18: SyntaxError: Expected ')', found newline
16 | pass
|
E30_syntax_error.py:18:11: SyntaxError: Expected ')', found newline
E30_syntax_error.py:18:11: invalid-syntax: Expected ')', found newline
|
16 | pass
17 |
@@ -31,7 +31,7 @@ E30_syntax_error.py:18:11: SyntaxError: Expected ')', found newline
21 | def top(
|
E30_syntax_error.py:21:9: SyntaxError: Expected ')', found newline
E30_syntax_error.py:21:9: invalid-syntax: Expected ')', found newline
|
21 | def top(
| ^

View File

@@ -8,14 +8,14 @@ W19.py:1:1: W191 Indentation contains tabs
2 | multiline string with tab in it'''
|
W19.py:1:1: SyntaxError: Unexpected indentation
W19.py:1:1: invalid-syntax: Unexpected indentation
|
1 | '''File starts with a tab
| ^^^^
2 | multiline string with tab in it'''
|
W19.py:5:1: SyntaxError: Expected a statement
W19.py:5:1: invalid-syntax: Expected a statement
|
4 | #: W191
5 | if False:

View File

@@ -1,8 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
snapshot_kind: text
---
E2_syntax_error.py:1:10: SyntaxError: Expected an expression
E2_syntax_error.py:1:10: invalid-syntax: Expected an expression
|
1 | a = (1 or)
| ^

View File

@@ -452,7 +452,7 @@ impl<'a> DocstringSections<'a> {
///
/// Attempts to parse using the specified [`SectionStyle`], falling back to the other style if no
/// entries are found.
fn parse_entries(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName> {
fn parse_entries(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName<'_>> {
match style {
Some(SectionStyle::Google) => parse_entries_google(content),
Some(SectionStyle::Numpy) => parse_entries_numpy(content),
@@ -474,7 +474,7 @@ fn parse_entries(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedNam
/// FasterThanLightError: If speed is greater than the speed of light.
/// DivisionByZero: If attempting to divide by zero.
/// ```
fn parse_entries_google(content: &str) -> Vec<QualifiedName> {
fn parse_entries_google(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = Vec::new();
for potential in content.lines() {
let Some(colon_idx) = potential.find(':') else {
@@ -496,7 +496,7 @@ fn parse_entries_google(content: &str) -> Vec<QualifiedName> {
/// DivisionByZero
/// If attempting to divide by zero.
/// ```
fn parse_entries_numpy(content: &str) -> Vec<QualifiedName> {
fn parse_entries_numpy(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = Vec::new();
let mut lines = content.lines();
let Some(dashes) = lines.next() else {

View File

@@ -177,7 +177,6 @@ pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring)
// "\
// "
// ```
return;
} else {
if checker.is_rule_enabled(Rule::MultiLineSummarySecondLine) {
let mut diagnostic =

View File

@@ -98,11 +98,11 @@ impl Settings {
self.convention
}
pub fn ignore_decorators(&self) -> DecoratorIterator {
pub fn ignore_decorators(&self) -> DecoratorIterator<'_> {
DecoratorIterator::new(&self.ignore_decorators)
}
pub fn property_decorators(&self) -> DecoratorIterator {
pub fn property_decorators(&self) -> DecoratorIterator<'_> {
DecoratorIterator::new(&self.property_decorators)
}

View File

@@ -38,8 +38,8 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
/// ```
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe for `split()`/`rsplit()` calls that contain `**kwargs`, as
/// adding a `maxsplit` keyword to such a call may lead to a duplicate keyword argument error.
/// This rule's fix is marked as unsafe for `split()`/`rsplit()` calls that contain `*args` or `**kwargs` arguments, as
/// adding a `maxsplit` argument to such a call may lead to duplicated arguments.
#[derive(ViolationMetadata)]
pub(crate) struct MissingMaxsplitArg {
actual_split_type: String,
@@ -201,11 +201,12 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
diagnostic.set_fix(Fix::applicable_edits(
maxsplit_argument_edit,
split_type_edit,
// If keyword.arg is `None` (i.e. if the function call contains `**kwargs`), mark the fix as unsafe
if arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none())
// Mark the fix as unsafe, if there are `*args` or `**kwargs`
if arguments.args.iter().any(Expr::is_starred_expr)
|| arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none())
{
Applicability::Unsafe
} else {

View File

@@ -100,7 +100,7 @@ impl Ranged for AttributeAssignment<'_> {
/// Return a list of attributes that are assigned to but not included in `__slots__`.
///
/// If the `__slots__` attribute cannot be statically determined, returns an empty vector.
fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment<'_>> {
// First, collect all the attributes that are assigned to `__slots__`.
let mut slots = FxHashSet::default();
for statement in body {

View File

@@ -115,7 +115,7 @@ fn check_super_slots(checker: &Checker, class_def: &ast::StmtClassDef, slot: &Sl
}
}
fn slots_members(body: &[Stmt]) -> FxHashSet<Slot> {
fn slots_members(body: &[Stmt]) -> FxHashSet<Slot<'_>> {
let mut members = FxHashSet::default();
for stmt in body {
match stmt {
@@ -161,7 +161,7 @@ fn slots_members(body: &[Stmt]) -> FxHashSet<Slot> {
members
}
fn slots_attributes(expr: &Expr) -> impl Iterator<Item = Slot> {
fn slots_attributes(expr: &Expr) -> impl Iterator<Item = Slot<'_>> {
// Ex) `__slots__ = ("name",)`
let elts_iter = match expr {
Expr::Tuple(ast::ExprTuple { elts, .. })

View File

@@ -676,6 +676,7 @@ missing_maxsplit_arg.py:182:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`
182 |+"1,2,3".split(",", maxsplit=1, **kwargs_with_maxsplit)[0] # TODO: false positive
183 183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
184 184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
185 185 |
missing_maxsplit_arg.py:184:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
@@ -692,3 +693,43 @@ missing_maxsplit_arg.py:184:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`
183 183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
184 |-"1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
184 |+"1,2,3".split(maxsplit=1, **kwargs_with_maxsplit)[0] # TODO: false positive
185 185 |
186 186 |
187 187 | ## Test unpacked list literal args (starred expressions)
missing_maxsplit_arg.py:189:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
187 | ## Test unpacked list literal args (starred expressions)
188 | # Errors
189 | "1,2,3".split(",", *[-1])[0]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
190 |
191 | ## Test unpacked list variable args
|
= help: Pass `maxsplit=1` into `str.split()`
Unsafe fix
186 186 |
187 187 | ## Test unpacked list literal args (starred expressions)
188 188 | # Errors
189 |-"1,2,3".split(",", *[-1])[0]
189 |+"1,2,3".split(",", *[-1], maxsplit=1)[0]
190 190 |
191 191 | ## Test unpacked list variable args
192 192 | # Errors
missing_maxsplit_arg.py:194:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
192 | # Errors
193 | args_list = [-1]
194 | "1,2,3".split(",", *args_list)[0]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
|
= help: Pass `maxsplit=1` into `str.split()`
Unsafe fix
191 191 | ## Test unpacked list variable args
192 192 | # Errors
193 193 | args_list = [-1]
194 |-"1,2,3".split(",", *args_list)[0]
194 |+"1,2,3".split(",", *args_list, maxsplit=1)[0]

View File

@@ -11,7 +11,7 @@ invalid_characters_syntax_error.py:5:6: PLE2510 Invalid unescaped character back
|
= help: Replace with escape sequence
invalid_characters_syntax_error.py:7:5: SyntaxError: missing closing quote in string literal
invalid_characters_syntax_error.py:7:5: invalid-syntax: missing closing quote in string literal
|
5 | b = '␈'
6 | # Unterminated string
@@ -21,7 +21,7 @@ invalid_characters_syntax_error.py:7:5: SyntaxError: missing closing quote in st
9 | # Unterminated f-string
|
invalid_characters_syntax_error.py:7:7: SyntaxError: Expected a statement
invalid_characters_syntax_error.py:7:7: invalid-syntax: Expected a statement
|
5 | b = '␈'
6 | # Unterminated string
@@ -43,7 +43,7 @@ invalid_characters_syntax_error.py:8:6: PLE2510 Invalid unescaped character back
|
= help: Replace with escape sequence
invalid_characters_syntax_error.py:10:7: SyntaxError: f-string: unterminated string
invalid_characters_syntax_error.py:10:7: invalid-syntax: f-string: unterminated string
|
8 | b = '␈'
9 | # Unterminated f-string
@@ -53,7 +53,7 @@ invalid_characters_syntax_error.py:10:7: SyntaxError: f-string: unterminated str
12 | # Implicitly concatenated
|
invalid_characters_syntax_error.py:10:8: SyntaxError: Expected FStringEnd, found newline
invalid_characters_syntax_error.py:10:8: invalid-syntax: Expected FStringEnd, found newline
|
8 | b = '␈'
9 | # Unterminated f-string
@@ -93,7 +93,7 @@ invalid_characters_syntax_error.py:13:11: PLE2510 Invalid unescaped character ba
|
= help: Replace with escape sequence
invalid_characters_syntax_error.py:13:14: SyntaxError: missing closing quote in string literal
invalid_characters_syntax_error.py:13:14: invalid-syntax: missing closing quote in string literal
|
11 | b = f'␈'
12 | # Implicitly concatenated
@@ -101,7 +101,7 @@ invalid_characters_syntax_error.py:13:14: SyntaxError: missing closing quote in
| ^^
|
invalid_characters_syntax_error.py:13:16: SyntaxError: Expected a statement
invalid_characters_syntax_error.py:13:16: invalid-syntax: Expected a statement
|
11 | b = f'␈'
12 | # Implicitly concatenated

View File

@@ -96,7 +96,7 @@ enum EncodingArg<'a> {
/// Return the encoding argument to an `encode` call, if it can be determined to be a
/// UTF-8-equivalent encoding.
fn match_encoding_arg(arguments: &Arguments) -> Option<EncodingArg> {
fn match_encoding_arg(arguments: &Arguments) -> Option<EncodingArg<'_>> {
match (&*arguments.args, &*arguments.keywords) {
// Ex `"".encode()`
([], []) => return Some(EncodingArg::Empty),

View File

@@ -343,7 +343,9 @@ enum ComprehensionTarget<'a> {
}
/// Extract the target from the comprehension (e.g., `(x, y, z)` in `(x, y, z, ...) in iter`).
fn match_comprehension_target(comprehension: &ast::Comprehension) -> Option<ComprehensionTarget> {
fn match_comprehension_target(
comprehension: &ast::Comprehension,
) -> Option<ComprehensionTarget<'_>> {
if comprehension.is_async || !comprehension.ifs.is_empty() {
return None;
}

View File

@@ -89,7 +89,7 @@ pub(crate) fn single_item_membership_test(
generate_comparison(
left,
&[membership_test.replacement_op()],
&[item.clone()],
std::slice::from_ref(item),
expr.into(),
checker.comment_ranges(),
checker.source(),

View File

@@ -139,7 +139,7 @@ pub(crate) fn slice_to_remove_affix_stmt(checker: &Checker, if_stmt: &ast::StmtI
/// where `func` is either `startswith` or `endswith`,
/// this function collects `text`,`func`, `affix`, and the non-null
/// bound of the slice. Otherwise, returns `None`.
fn affix_removal_data_expr(if_expr: &ast::ExprIf) -> Option<RemoveAffixData> {
fn affix_removal_data_expr(if_expr: &ast::ExprIf) -> Option<RemoveAffixData<'_>> {
let ast::ExprIf {
test,
body,
@@ -166,7 +166,7 @@ fn affix_removal_data_expr(if_expr: &ast::ExprIf) -> Option<RemoveAffixData> {
/// where `func` is either `startswith` or `endswith`,
/// this function collects `text`,`func`, `affix`, and the non-null
/// bound of the slice. Otherwise, returns `None`.
fn affix_removal_data_stmt(if_stmt: &ast::StmtIf) -> Option<RemoveAffixData> {
fn affix_removal_data_stmt(if_stmt: &ast::StmtIf) -> Option<RemoveAffixData<'_>> {
let ast::StmtIf {
test,
body,

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/linter.rs
---
resources/test/fixtures/syntax_errors/async_comprehension.ipynb:3:5: SyntaxError: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
resources/test/fixtures/syntax_errors/async_comprehension.ipynb:3:5: invalid-syntax: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
|
1 | async def elements(n): yield n
2 | [x async for x in elements(5)] # okay, async at top level

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:1:27: SyntaxError: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
<filename>:1:27: invalid-syntax: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
|
1 | async def f(): return [[x async for x in foo(n)] for n in range(3)]
| ^^^^^^^^^^^^^^^^^^^^^

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:3:21: SyntaxError: attribute name `x` repeated in class pattern
<filename>:3:21: invalid-syntax: attribute name `x` repeated in class pattern
|
2 | match x:
3 | case Point(x=1, x=2):

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>:3:21: SyntaxError: mapping pattern checks duplicate key `'key'`
<filename>:3:21: invalid-syntax: mapping pattern checks duplicate key `'key'`
|
2 | match x:
3 | case {'key': 1, 'key': 2}:

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