Compare commits
1 Commits
0.9.5
...
zb/fix-win
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70cf8a94d5 |
5
.github/workflows/ci.yaml
vendored
5
.github/workflows/ci.yaml
vendored
@@ -217,6 +217,11 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
# There are spurious CRL server offline errors when downloading
|
||||
# `cargo-bloat` with curl below, so we just disable them for now
|
||||
- name: "Disable SChannel CRL checks"
|
||||
run: |
|
||||
reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL" /v EnableCRLCheck /t REG_DWORD /d 0 /f
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -1,64 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.5
|
||||
|
||||
### Preview features
|
||||
|
||||
- Recognize all symbols named `TYPE_CHECKING` for `in_type_checking_block` ([#15719](https://github.com/astral-sh/ruff/pull/15719))
|
||||
- \[`flake8-comprehensions`\] Handle builtins at top of file correctly for `unnecessary-dict-comprehension-for-iterable` (`C420`) ([#15837](https://github.com/astral-sh/ruff/pull/15837))
|
||||
- \[`flake8-logging`\] `.exception()` and `exc_info=` outside exception handlers (`LOG004`, `LOG014`) ([#15799](https://github.com/astral-sh/ruff/pull/15799))
|
||||
- \[`flake8-pyi`\] Fix incorrect behaviour of `custom-typevar-return-type` preview-mode autofix if `typing` was already imported (`PYI019`) ([#15853](https://github.com/astral-sh/ruff/pull/15853))
|
||||
- \[`flake8-pyi`\] Fix more complex cases (`PYI019`) ([#15821](https://github.com/astral-sh/ruff/pull/15821))
|
||||
- \[`flake8-pyi`\] Make `PYI019` autofixable for `.py` files in preview mode as well as stubs ([#15889](https://github.com/astral-sh/ruff/pull/15889))
|
||||
- \[`flake8-pyi`\] Remove type parameter correctly when it is the last (`PYI019`) ([#15854](https://github.com/astral-sh/ruff/pull/15854))
|
||||
- \[`pylint`\] Fix missing parens in unsafe fix for `unnecessary-dunder-call` (`PLC2801`) ([#15762](https://github.com/astral-sh/ruff/pull/15762))
|
||||
- \[`pyupgrade`\] Better messages and diagnostic range (`UP015`) ([#15872](https://github.com/astral-sh/ruff/pull/15872))
|
||||
- \[`pyupgrade`\] Rename private type parameters in PEP 695 generics (`UP049`) ([#15862](https://github.com/astral-sh/ruff/pull/15862))
|
||||
- \[`refurb`\] Also report non-name expressions (`FURB169`) ([#15905](https://github.com/astral-sh/ruff/pull/15905))
|
||||
- \[`refurb`\] Mark fix as unsafe if there are comments (`FURB171`) ([#15832](https://github.com/astral-sh/ruff/pull/15832))
|
||||
- \[`ruff`\] Classes with mixed type variable style (`RUF053`) ([#15841](https://github.com/astral-sh/ruff/pull/15841))
|
||||
- \[`airflow`\] `BashOperator` has been moved to `airflow.providers.standard.operators.bash.BashOperator` (`AIR302`) ([#15922](https://github.com/astral-sh/ruff/pull/15922))
|
||||
- \[`flake8-pyi`\] Add autofix for unused-private-type-var (`PYI018`) ([#15999](https://github.com/astral-sh/ruff/pull/15999))
|
||||
- \[`flake8-pyi`\] Significantly improve accuracy of `PYI019` if preview mode is enabled ([#15888](https://github.com/astral-sh/ruff/pull/15888))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Preserve triple quotes and prefixes for strings ([#15818](https://github.com/astral-sh/ruff/pull/15818))
|
||||
- \[`flake8-comprehensions`\] Skip when `TypeError` present from too many (kw)args for `C410`,`C411`, and `C418` ([#15838](https://github.com/astral-sh/ruff/pull/15838))
|
||||
- \[`flake8-pyi`\] Rename `PYI019` and improve its diagnostic message ([#15885](https://github.com/astral-sh/ruff/pull/15885))
|
||||
- \[`pep8-naming`\] Ignore `@override` methods (`N803`) ([#15954](https://github.com/astral-sh/ruff/pull/15954))
|
||||
- \[`pyupgrade`\] Reuse replacement logic from `UP046` and `UP047` to preserve more comments (`UP040`) ([#15840](https://github.com/astral-sh/ruff/pull/15840))
|
||||
- \[`ruff`\] Analyze deferred annotations before enforcing `mutable-(data)class-default` and `function-call-in-dataclass-default-argument` (`RUF008`,`RUF009`,`RUF012`) ([#15921](https://github.com/astral-sh/ruff/pull/15921))
|
||||
- \[`pycodestyle`\] Exempt `sys.path += ...` calls (`E402`) ([#15980](https://github.com/astral-sh/ruff/pull/15980))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Config error only when `flake8-import-conventions` alias conflicts with `isort.required-imports` bound name ([#15918](https://github.com/astral-sh/ruff/pull/15918))
|
||||
- Workaround Even Better TOML crash related to `allOf` ([#15992](https://github.com/astral-sh/ruff/pull/15992))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-comprehensions`\] Unnecessary `list` comprehension (rewrite as a `set` comprehension) (`C403`) - Handle extraneous parentheses around list comprehension ([#15877](https://github.com/astral-sh/ruff/pull/15877))
|
||||
- \[`flake8-comprehensions`\] Handle trailing comma in fixes for `unnecessary-generator-list/set` (`C400`,`C401`) ([#15929](https://github.com/astral-sh/ruff/pull/15929))
|
||||
- \[`flake8-pyi`\] Fix several correctness issues with `custom-type-var-return-type` (`PYI019`) ([#15851](https://github.com/astral-sh/ruff/pull/15851))
|
||||
- \[`pep8-naming`\] Consider any number of leading underscore for `N801` ([#15988](https://github.com/astral-sh/ruff/pull/15988))
|
||||
- \[`pyflakes`\] Visit forward annotations in `TypeAliasType` as types (`F401`) ([#15829](https://github.com/astral-sh/ruff/pull/15829))
|
||||
- \[`pylint`\] Correct min/max auto-fix and suggestion for (`PL1730`) ([#15930](https://github.com/astral-sh/ruff/pull/15930))
|
||||
- \[`refurb`\] Handle unparenthesized tuples correctly (`FURB122`, `FURB142`) ([#15953](https://github.com/astral-sh/ruff/pull/15953))
|
||||
- \[`refurb`\] Avoid `None | None` as well as better detection and fix (`FURB168`) ([#15779](https://github.com/astral-sh/ruff/pull/15779))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add deprecation warning for `ruff-lsp` related settings ([#15850](https://github.com/astral-sh/ruff/pull/15850))
|
||||
- Docs (`linter.md`): clarify that Python files are always searched for in subdirectories ([#15882](https://github.com/astral-sh/ruff/pull/15882))
|
||||
- Fix a typo in `non_pep695_generic_class.rs` ([#15946](https://github.com/astral-sh/ruff/pull/15946))
|
||||
- Improve Docs: Pylint subcategories' codes ([#15909](https://github.com/astral-sh/ruff/pull/15909))
|
||||
- Remove non-existing `lint.extendIgnore` editor setting ([#15844](https://github.com/astral-sh/ruff/pull/15844))
|
||||
- Update black deviations ([#15928](https://github.com/astral-sh/ruff/pull/15928))
|
||||
- Mention `UP049` in `UP046` and `UP047`, add `See also` section to `UP040` ([#15956](https://github.com/astral-sh/ruff/pull/15956))
|
||||
- Add instance variable examples to `RUF012` ([#15982](https://github.com/astral-sh/ruff/pull/15982))
|
||||
- Explain precedence for `ignore` and `select` config ([#15883](https://github.com/astral-sh/ruff/pull/15883))
|
||||
|
||||
## 0.9.4
|
||||
|
||||
### Preview features
|
||||
|
||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -2525,7 +2525,6 @@ dependencies = [
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
@@ -2640,7 +2639,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2872,7 +2871,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3190,7 +3189,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
|
||||
@@ -149,8 +149,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.9.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.5/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.9.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.4/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -183,7 +183,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.9.5
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -270,8 +270,6 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
||||
/// Whether or not the .py/.pyi version of this file is expected to fail
|
||||
#[rustfmt::skip]
|
||||
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
|
||||
// related to circular references in nested functions
|
||||
("crates/ruff_linter/resources/test/fixtures/flake8_return/RET503.py", false, true),
|
||||
// related to circular references in class definitions
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true),
|
||||
|
||||
@@ -393,6 +393,8 @@ reveal_type(D().x) # revealed: Unknown | Literal[1]
|
||||
|
||||
If `staticmethod` is something else, that should not influence the behavior:
|
||||
|
||||
`other.py`:
|
||||
|
||||
```py
|
||||
def staticmethod(f):
|
||||
return f
|
||||
@@ -407,6 +409,8 @@ reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
|
||||
And if `staticmethod` is fully qualified, that should also be recognized:
|
||||
|
||||
`fully_qualified.py`:
|
||||
|
||||
```py
|
||||
import builtins
|
||||
|
||||
@@ -845,6 +849,8 @@ outer.nested.inner.Outer.Nested.Inner.attr = "a"
|
||||
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
|
||||
functions are instances of that class:
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
@@ -854,7 +860,11 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
|
||||
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
|
||||
```
|
||||
@@ -864,6 +874,8 @@ reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
|
||||
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
|
||||
integers are instances of that class:
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
reveal_type((2).bit_length) # revealed: @Todo(bound method)
|
||||
reveal_type((2).denominator) # revealed: @Todo(@property)
|
||||
@@ -871,6 +883,8 @@ reveal_type((2).denominator) # revealed: @Todo(@property)
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
reveal_type((2).numerator) # revealed: Literal[2]
|
||||
reveal_type((2).real) # revealed: Literal[2]
|
||||
@@ -881,6 +895,8 @@ reveal_type((2).real) # revealed: Literal[2]
|
||||
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
|
||||
bols are instances of that class:
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
reveal_type(True.__and__) # revealed: @Todo(bound method)
|
||||
reveal_type(False.__or__) # revealed: @Todo(bound method)
|
||||
@@ -888,6 +904,8 @@ reveal_type(False.__or__) # revealed: @Todo(bound method)
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
reveal_type(True.numerator) # revealed: Literal[1]
|
||||
reveal_type(False.real) # revealed: Literal[0]
|
||||
|
||||
@@ -33,6 +33,8 @@ reveal_type(a >= b) # revealed: Literal[False]
|
||||
|
||||
Even when tuples have different lengths, comparisons should be handled appropriately.
|
||||
|
||||
`different_length.py`:
|
||||
|
||||
```py
|
||||
a = (1, 2, 3)
|
||||
b = (1, 2, 3, 4)
|
||||
@@ -102,6 +104,8 @@ reveal_type(a >= b) # revealed: bool
|
||||
However, if the lexicographic comparison completes without reaching a point where str and int are
|
||||
compared, Python will still produce a result based on the prior elements.
|
||||
|
||||
`short_circuit.py`:
|
||||
|
||||
```py
|
||||
a = (1, 2)
|
||||
b = (999999, "hello")
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Unpacking
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Right hand side not iterable
|
||||
|
||||
```py
|
||||
a, b = 1 # error: [not-iterable]
|
||||
```
|
||||
|
||||
## Too many values to unpack
|
||||
|
||||
```py
|
||||
a, b = (1, 2, 3) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Too few values to unpack
|
||||
|
||||
```py
|
||||
a, b = (1,) # error: [invalid-assignment]
|
||||
```
|
||||
@@ -124,49 +124,42 @@ def _(e: Exception | type[Exception] | None):
|
||||
## Exception cause is not an exception
|
||||
|
||||
```py
|
||||
def _():
|
||||
try:
|
||||
raise EOFError() from GeneratorExit # fine
|
||||
except:
|
||||
...
|
||||
try:
|
||||
raise EOFError() from GeneratorExit # fine
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise StopIteration from MemoryError() # fine
|
||||
except:
|
||||
...
|
||||
try:
|
||||
raise StopIteration from MemoryError() # fine
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise BufferError() from None # fine
|
||||
except:
|
||||
...
|
||||
try:
|
||||
raise BufferError() from None # fine
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise ZeroDivisionError from False # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
try:
|
||||
raise ZeroDivisionError from False # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise SystemExit from bool() # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
try:
|
||||
raise SystemExit from bool() # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise
|
||||
except KeyboardInterrupt as e: # fine
|
||||
reveal_type(e) # revealed: KeyboardInterrupt
|
||||
raise LookupError from e # fine
|
||||
try:
|
||||
raise
|
||||
except KeyboardInterrupt as e: # fine
|
||||
reveal_type(e) # revealed: KeyboardInterrupt
|
||||
raise LookupError from e # fine
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise
|
||||
except int as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
raise KeyError from e
|
||||
try:
|
||||
raise
|
||||
except int as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
raise KeyError from e
|
||||
|
||||
def _(e: Exception | type[Exception]):
|
||||
raise ModuleNotFoundError from e # fine
|
||||
|
||||
@@ -29,6 +29,8 @@ completing. The type of `x` at the beginning of the `except` suite in this examp
|
||||
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
|
||||
*after* that redefinition.
|
||||
|
||||
`union_type_inferred.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
@@ -50,7 +52,12 @@ reveal_type(x) # revealed: str | Literal[2]
|
||||
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
|
||||
inferred as having a union type following the `try`/`except` block:
|
||||
|
||||
`branches_unify_to_non_union_type.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -130,6 +137,8 @@ the `except` suite:
|
||||
- At the end of `else`, `x == 3`
|
||||
- At the end of `except`, `x == 2`
|
||||
|
||||
`single_except.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
@@ -158,6 +167,9 @@ been executed in its entirety, or the `try` suite and the `else` suite must both
|
||||
in their entireties:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -186,6 +198,8 @@ A `finally` suite is *always* executed. As such, if we reach the `reveal_type` c
|
||||
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
|
||||
type of `x` at the end of the example is therefore `Literal[2]`:
|
||||
|
||||
`redef_in_finally.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
@@ -211,7 +225,12 @@ at this point than there were when we were inside the `finally` block.
|
||||
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
|
||||
still a TODO item for us.)
|
||||
|
||||
`no_redef_in_finally.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -240,6 +259,8 @@ suites:
|
||||
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
|
||||
`except` suite ran to completion
|
||||
|
||||
`redef_in_finally.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
@@ -277,7 +298,18 @@ itself. (In some control-flow possibilities, some exceptions were merely *suspen
|
||||
`finally` suite; these lead to the scope's termination following the conclusion of the `finally`
|
||||
suite.)
|
||||
|
||||
`no_redef_in_finally.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -299,7 +331,18 @@ reveal_type(x) # revealed: str | bool
|
||||
|
||||
An example with multiple `except` branches and a `finally` branch:
|
||||
|
||||
`multiple_except_branches.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
@@ -337,6 +380,8 @@ If the exception handler has an `else` branch, we must also take into account th
|
||||
control flow could have jumped to the `finally` suite from partway through the `else` suite due to
|
||||
an exception raised *there*.
|
||||
|
||||
`single_except_branch.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
@@ -380,7 +425,24 @@ reveal_type(x) # revealed: bool | float
|
||||
|
||||
The same again, this time with multiple `except` branches:
|
||||
|
||||
`multiple_except_branches.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
def could_raise_returns_float() -> float:
|
||||
return 3.14
|
||||
|
||||
def could_raise_returns_range() -> range:
|
||||
return range(42)
|
||||
|
||||
|
||||
@@ -218,33 +218,3 @@ import package
|
||||
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
|
||||
reveal_type(package.foo.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
## In the src-root
|
||||
|
||||
`parser.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
`__main__.py`:
|
||||
|
||||
```py
|
||||
from .parser import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
```
|
||||
|
||||
## Beyond the src-root
|
||||
|
||||
`parser.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
`__main__.py`:
|
||||
|
||||
```py
|
||||
from ..parser import X # error: [unresolved-import]
|
||||
```
|
||||
|
||||
@@ -29,6 +29,8 @@ def foo():
|
||||
However, three attributes on `types.ModuleType` are not present as implicit module globals; these
|
||||
are excluded:
|
||||
|
||||
`unbound_dunders.py`:
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unknown
|
||||
@@ -70,7 +72,11 @@ Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType`
|
||||
dynamic imports; but we ignore that for module-literal types where we know exactly which module
|
||||
we're dealing with:
|
||||
|
||||
`__getattr__.py`:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(typing.__getattr__) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function.
|
||||
No diagnostics should be generated.
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
def f(x: str):
|
||||
x: int = int(x)
|
||||
@@ -12,6 +14,8 @@ def f(x: str):
|
||||
|
||||
## Implicit error
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
@@ -20,6 +24,8 @@ f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explici
|
||||
|
||||
## Explicit shadowing
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
|
||||
@@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:8
|
||||
--> /src/mdtest_snippet__1.py:1:8
|
||||
|
|
||||
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq`
|
||||
|
||||
@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | # Topmost component resolvable, submodule not resolvable:
|
||||
@@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:2:8
|
||||
--> /src/mdtest_snippet__1.py:2:8
|
||||
|
|
||||
1 | # Topmost component resolvable, submodule not resolvable:
|
||||
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
|
||||
@@ -41,7 +41,7 @@ error: lint:unresolved-import
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:5:8
|
||||
--> /src/mdtest_snippet__1.py:5:8
|
||||
|
|
||||
4 | # Topmost component unresolvable:
|
||||
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unpacking.md - Unpacking - Right hand side not iterable
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | a, b = 1 # error: [not-iterable]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:1:8
|
||||
|
|
||||
1 | a, b = 1 # error: [not-iterable]
|
||||
| ^ Object of type `Literal[1]` is not iterable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unpacking.md - Unpacking - Too few values to unpack
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | a, b = (1,) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-assignment
|
||||
--> /src/mdtest_snippet.py:1:1
|
||||
|
|
||||
1 | a, b = (1,) # error: [invalid-assignment]
|
||||
| ^^^^ Not enough values to unpack (expected 2, got 1)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unpacking.md - Unpacking - Too many values to unpack
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-assignment
|
||||
--> /src/mdtest_snippet.py:1:1
|
||||
|
|
||||
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
|
||||
| ^^^^ Too many values to unpack (expected 2, got 3)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | import does_not_exist # error: [unresolved-import]
|
||||
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:8
|
||||
--> /src/mdtest_snippet__1.py:1:8
|
||||
|
|
||||
1 | import does_not_exist # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
||||
|
||||
@@ -16,7 +16,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
|
||||
2 | does_exist2 = 2
|
||||
```
|
||||
|
||||
## mdtest_snippet.py
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
|
||||
@@ -26,7 +26,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:28
|
||||
--> /src/mdtest_snippet__1.py:1:28
|
||||
|
|
||||
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Module `a` has no member `does_not_exist`
|
||||
|
||||
@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | from .does_not_exist import add # error: [unresolved-import]
|
||||
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:7
|
||||
--> /src/mdtest_snippet__1.py:1:7
|
||||
|
|
||||
1 | from .does_not_exist import add # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist`
|
||||
|
||||
@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
|
||||
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:7
|
||||
--> /src/mdtest_snippet__1.py:1:7
|
||||
|
|
||||
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist.foo.bar`
|
||||
|
||||
@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | from does_not_exist import add # error: [unresolved-import]
|
||||
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:6
|
||||
--> /src/mdtest_snippet__1.py:1:6
|
||||
|
|
||||
1 | from does_not_exist import add # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
||||
|
||||
@@ -7,36 +7,43 @@ branches whose conditions we can statically determine to be always true or alway
|
||||
useful for `sys.version_info` branches, which can make new features available based on the Python
|
||||
version:
|
||||
|
||||
If we can statically determine that the condition is always true, then we can also understand that
|
||||
`SomeFeature` is always bound, without raising any errors:
|
||||
`module1.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
class C:
|
||||
if sys.version_info >= (3, 9):
|
||||
SomeFeature: str = "available"
|
||||
if sys.version_info >= (3, 9):
|
||||
SomeFeature: str = "available"
|
||||
```
|
||||
|
||||
# C.SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
|
||||
reveal_type(C.SomeFeature) # revealed: str
|
||||
If we can statically determine that the condition is always true, then we can also understand that
|
||||
`SomeFeature` is always bound, without raising any errors:
|
||||
|
||||
`test1.py`:
|
||||
|
||||
```py
|
||||
from module1 import SomeFeature
|
||||
|
||||
# SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
|
||||
reveal_type(SomeFeature) # revealed: str
|
||||
```
|
||||
|
||||
Another scenario where this is useful is for `typing.TYPE_CHECKING` branches, which are often used
|
||||
for conditional imports:
|
||||
|
||||
`module.py`:
|
||||
`module2.py`:
|
||||
|
||||
```py
|
||||
class SomeType: ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
`test2.py`:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from module import SomeType
|
||||
from module2 import SomeType
|
||||
|
||||
# `SomeType` is unconditionally available here for type checkers:
|
||||
def f(s: SomeType) -> None: ...
|
||||
|
||||
@@ -37,6 +37,8 @@ child expression now suppresses errors in the outer expression.
|
||||
For example, the `type: ignore` comment in this example suppresses the error of adding `2` to
|
||||
`"test"` and adding `"other"` to the result of the cast.
|
||||
|
||||
`nested.py`:
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
from typing import cast
|
||||
|
||||
@@ -109,6 +109,8 @@ reveal_type(version_info >= (3, 9)) # revealed: bool
|
||||
|
||||
The fields of `sys.version_info` can be accessed by name:
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
@@ -120,7 +122,11 @@ reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
|
||||
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
|
||||
properties on instance types:
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo(@property)
|
||||
|
||||
@@ -452,9 +452,6 @@ def raise_in_both_branches(cond: bool):
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||
else:
|
||||
# This branch is unreachable, since all control flows in the `try` clause raise exceptions.
|
||||
# As a result, this binding should never be reachable, since new bindings are visible only
|
||||
# when they are reachable.
|
||||
x = "unreachable"
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
@@ -626,9 +623,9 @@ def return_from_nested_if(cond1: bool, cond2: bool):
|
||||
|
||||
## Statically known terminal statements
|
||||
|
||||
We model reachability using the same visibility constraints that we use to model statically known
|
||||
bounds. In this example, we see that the `return` statement is always executed, and therefore that
|
||||
the `"b"` assignment is not visible to the `reveal_type`.
|
||||
Terminal statements do not yet interact correctly with statically known bounds. In this example, we
|
||||
should see that the `return` statement is always executed, and therefore that the `"b"` assignment
|
||||
is not visible to the `reveal_type`.
|
||||
|
||||
```py
|
||||
def _(cond: bool):
|
||||
@@ -638,26 +635,6 @@ def _(cond: bool):
|
||||
if True:
|
||||
return
|
||||
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
```
|
||||
|
||||
## Bindings after a terminal statement are unreachable
|
||||
|
||||
Any bindings introduced after a terminal statement are unreachable, and are currently considered not
|
||||
visible. We [anticipate](https://github.com/astral-sh/ruff/issues/15797) that we want to provide a
|
||||
more useful analysis for code after terminal statements.
|
||||
|
||||
```py
|
||||
def f(cond: bool) -> str:
|
||||
x = "before"
|
||||
if cond:
|
||||
reveal_type(x) # revealed: Literal["before"]
|
||||
return
|
||||
x = "after-return"
|
||||
# TODO: no unresolved-reference error
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(x) # revealed: Unknown
|
||||
else:
|
||||
x = "else"
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
# TODO: Literal["a"]
|
||||
reveal_type(x) # revealed: Literal["a", "b"]
|
||||
```
|
||||
|
||||
@@ -84,11 +84,8 @@ def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: tuple[str, Unknown]
|
||||
reveal_type(z) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
`Unknown` can be subclassed, just like `Any`:
|
||||
|
||||
```py
|
||||
# Unknown can be subclassed, just like Any
|
||||
class C(Unknown): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
@@ -241,12 +238,9 @@ error_message = "A custom message "
|
||||
error_message += "constructed from multiple string literals"
|
||||
# error: "Static assertion error: A custom message constructed from multiple string literals"
|
||||
static_assert(False, error_message)
|
||||
```
|
||||
|
||||
There are limitations to what we can still infer as a string literal. In those cases, we simply fall
|
||||
back to the default message:
|
||||
|
||||
```py
|
||||
# There are limitations to what we can still infer as a string literal. In those cases,
|
||||
# we simply fall back to the default message.
|
||||
shouted_message = "A custom message".upper()
|
||||
# error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(False, shouted_message)
|
||||
@@ -377,11 +371,8 @@ static_assert(is_subtype_of(TypeOf[str], type[str]))
|
||||
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
```
|
||||
|
||||
`TypeOf` can also be used in annotations:
|
||||
|
||||
```py
|
||||
# `TypeOf` can be used in annotations:
|
||||
def type_of_annotation() -> None:
|
||||
t1: TypeOf[Base] = Base
|
||||
t2: TypeOf[Base] = Derived # error: [invalid-assignment]
|
||||
|
||||
@@ -19,17 +19,11 @@ static_assert(is_equivalent_to(Never, tuple[int, Never]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, Never, str]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, tuple[str, Never]]))
|
||||
static_assert(is_equivalent_to(Never, tuple[tuple[str, Never], int]))
|
||||
```
|
||||
|
||||
The empty `tuple` is *not* equivalent to `Never`!
|
||||
|
||||
```py
|
||||
# The empty tuple is *not* equivalent to Never!
|
||||
static_assert(not is_equivalent_to(Never, tuple[()]))
|
||||
```
|
||||
|
||||
`NoReturn` is just a different spelling of `Never`, so the same is true for `NoReturn`:
|
||||
|
||||
```py
|
||||
# NoReturn is just a different spelling of Never, so the same is true for NoReturn
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn, int]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn]))
|
||||
|
||||
@@ -793,30 +793,9 @@ where
|
||||
&mut builder.current_first_parameter_name,
|
||||
&mut first_parameter_name,
|
||||
);
|
||||
|
||||
// TODO: Fix how we determine the public types of symbols in a
|
||||
// function-like scope: https://github.com/astral-sh/ruff/issues/15777
|
||||
//
|
||||
// In the meantime, visit the function body, but treat the last statement
|
||||
// specially if it is a return. If it is, this would cause all definitions
|
||||
// in the function to be marked as non-visible with our current treatment
|
||||
// of terminal statements. Since we currently model the externally visible
|
||||
// definitions in a function scope as the set of bindings that are visible
|
||||
// at the end of the body, we then consider this function to have no
|
||||
// externally visible definitions. To get around this, we take a flow
|
||||
// snapshot just before processing the return statement, and use _that_ as
|
||||
// the "end-of-body" state that we resolve external references against.
|
||||
if let Some((last_stmt, first_stmts)) = body.split_last() {
|
||||
builder.visit_body(first_stmts);
|
||||
let pre_return_state = matches!(last_stmt, ast::Stmt::Return(_))
|
||||
.then(|| builder.flow_snapshot());
|
||||
builder.visit_stmt(last_stmt);
|
||||
if let Some(pre_return_state) = pre_return_state {
|
||||
builder.flow_restore(pre_return_state);
|
||||
}
|
||||
}
|
||||
|
||||
builder.visit_body(body);
|
||||
builder.current_first_parameter_name = first_parameter_name;
|
||||
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
|
||||
@@ -478,6 +478,7 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
|
||||
pub(super) struct FlowSnapshot {
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
scope_start_visibility: ScopedVisibilityConstraintId,
|
||||
reachable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -505,6 +506,8 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
|
||||
/// Currently live bindings and declarations for each symbol.
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
|
||||
reachable: bool,
|
||||
}
|
||||
|
||||
impl Default for UseDefMapBuilder<'_> {
|
||||
@@ -517,13 +520,14 @@ impl Default for UseDefMapBuilder<'_> {
|
||||
bindings_by_use: IndexVec::new(),
|
||||
definitions_by_definition: FxHashMap::default(),
|
||||
symbol_states: IndexVec::new(),
|
||||
reachable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn mark_unreachable(&mut self) {
|
||||
self.record_visibility_constraint(ScopedVisibilityConstraintId::ALWAYS_FALSE);
|
||||
self.reachable = false;
|
||||
}
|
||||
|
||||
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
||||
@@ -540,7 +544,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
binding,
|
||||
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
|
||||
);
|
||||
symbol_state.record_binding(def_id, self.scope_start_visibility);
|
||||
symbol_state.record_binding(def_id);
|
||||
}
|
||||
|
||||
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
@@ -592,11 +596,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
|
||||
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
|
||||
|
||||
// If there are any control flow paths that have become unreachable between `snapshot` and
|
||||
// now, then it's not valid to simplify any visibility constraints to `snapshot`.
|
||||
if self.scope_start_visibility != snapshot.scope_start_visibility {
|
||||
return;
|
||||
}
|
||||
self.scope_start_visibility = snapshot.scope_start_visibility;
|
||||
|
||||
// Note that this loop terminates when we reach a symbol not present in the snapshot.
|
||||
// This means we keep visibility constraints for all new symbols, which is intended,
|
||||
@@ -632,7 +632,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
let def_id = self.all_definitions.push(Some(definition));
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
symbol_state.record_declaration(def_id);
|
||||
symbol_state.record_binding(def_id, self.scope_start_visibility);
|
||||
symbol_state.record_binding(def_id);
|
||||
}
|
||||
|
||||
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
|
||||
@@ -649,6 +649,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
FlowSnapshot {
|
||||
symbol_states: self.symbol_states.clone(),
|
||||
scope_start_visibility: self.scope_start_visibility,
|
||||
reachable: self.reachable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,23 +672,21 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
num_symbols,
|
||||
SymbolState::undefined(self.scope_start_visibility),
|
||||
);
|
||||
|
||||
self.reachable = snapshot.reachable;
|
||||
}
|
||||
|
||||
/// Merge the given snapshot into the current state, reflecting that we might have taken either
|
||||
/// path to get here. The new state for each symbol should include definitions from both the
|
||||
/// prior state and the snapshot.
|
||||
pub(super) fn merge(&mut self, snapshot: FlowSnapshot) {
|
||||
// As an optimization, if we know statically that either of the snapshots is always
|
||||
// unreachable, we can leave it out of the merged result entirely. Note that we cannot
|
||||
// perform any type inference at this point, so this is largely limited to unreachability
|
||||
// via terminal statements. If a flow's reachability depends on an expression in the code,
|
||||
// we will include the flow in the merged result; the visibility constraints of its
|
||||
// bindings will include this reachability condition, so that later during type inference,
|
||||
// we can determine whether any particular binding is non-visible due to unreachability.
|
||||
if snapshot.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
|
||||
// Unreachable snapshots should not be merged: If the current snapshot is unreachable, it
|
||||
// should be completely overwritten by the snapshot we're merging in. If the other snapshot
|
||||
// is unreachable, we should return without merging.
|
||||
if !snapshot.reachable {
|
||||
return;
|
||||
}
|
||||
if self.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
|
||||
if !self.reachable {
|
||||
self.restore(snapshot);
|
||||
return;
|
||||
}
|
||||
@@ -713,6 +712,9 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
self.scope_start_visibility = self
|
||||
.visibility_constraints
|
||||
.add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);
|
||||
|
||||
// Both of the snapshots are reachable, so the merged result is too.
|
||||
self.reachable = true;
|
||||
}
|
||||
|
||||
pub(super) fn finish(mut self) -> UseDefMap<'db> {
|
||||
|
||||
@@ -237,11 +237,7 @@ impl SymbolBindings {
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(
|
||||
&mut self,
|
||||
binding_id: ScopedDefinitionId,
|
||||
visibility_constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
|
||||
// The new binding replaces all previous live bindings in this path, and has no
|
||||
// constraints.
|
||||
self.live_bindings = Bindings::with(binding_id.into());
|
||||
@@ -249,7 +245,8 @@ impl SymbolBindings {
|
||||
self.constraints.push(Constraints::default());
|
||||
|
||||
self.visibility_constraints = VisibilityConstraintPerBinding::with_capacity(1);
|
||||
self.visibility_constraints.push(visibility_constraint);
|
||||
self.visibility_constraints
|
||||
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
@@ -352,14 +349,9 @@ impl SymbolState {
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(
|
||||
&mut self,
|
||||
binding_id: ScopedDefinitionId,
|
||||
visibility_constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
|
||||
debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND);
|
||||
self.bindings
|
||||
.record_binding(binding_id, visibility_constraint);
|
||||
self.bindings.record_binding(binding_id);
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
@@ -565,10 +557,7 @@ mod tests {
|
||||
#[test]
|
||||
fn with() {
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
assert_bindings(&sym, &["1<>"]);
|
||||
}
|
||||
@@ -576,10 +565,7 @@ mod tests {
|
||||
#[test]
|
||||
fn record_constraint() {
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
assert_bindings(&sym, &["1<0>"]);
|
||||
@@ -591,17 +577,11 @@ mod tests {
|
||||
|
||||
// merging the same definition with the same constraint keeps the constraint
|
||||
let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1a.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1a.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1b.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1b.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
sym1a.merge(sym1b, &mut visibility_constraints);
|
||||
@@ -610,17 +590,11 @@ mod tests {
|
||||
|
||||
// merging the same definition with differing constraints drops all constraints
|
||||
let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym2a.record_binding(
|
||||
ScopedDefinitionId::from_u32(2),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
|
||||
sym2a.record_constraint(ScopedConstraintId::from_u32(1));
|
||||
|
||||
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1b.record_binding(
|
||||
ScopedDefinitionId::from_u32(2),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym1b.record_binding(ScopedDefinitionId::from_u32(2));
|
||||
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
|
||||
|
||||
sym2a.merge(sym1b, &mut visibility_constraints);
|
||||
@@ -629,10 +603,7 @@ mod tests {
|
||||
|
||||
// merging a constrained definition with unbound keeps both
|
||||
let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym3a.record_binding(
|
||||
ScopedDefinitionId::from_u32(3),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym3a.record_binding(ScopedDefinitionId::from_u32(3));
|
||||
sym3a.record_constraint(ScopedConstraintId::from_u32(3));
|
||||
|
||||
let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
|
||||
@@ -2513,32 +2513,18 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.ok_or(ModuleNameResolutionError::UnknownCurrentModule)?;
|
||||
let mut level = level.get();
|
||||
if module.kind().is_package() {
|
||||
level = level.saturating_sub(1);
|
||||
level -= 1;
|
||||
}
|
||||
|
||||
let mut module_name = module.name().clone();
|
||||
let tail = tail
|
||||
.map(|tail| ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax))
|
||||
.transpose()?;
|
||||
|
||||
for remaining_dots in (0..level).rev() {
|
||||
if let Some(parent) = module_name.parent() {
|
||||
module_name = parent;
|
||||
} else if remaining_dots == 0 {
|
||||
// If we reached a search path root and this was the last dot return the tail if any.
|
||||
// If there's no tail, then we have a relative import that's too deep.
|
||||
return tail.ok_or(ModuleNameResolutionError::TooManyDots);
|
||||
} else {
|
||||
// We're at a search path root. This is a too deep relative import if there's more than
|
||||
// one dot remaining.
|
||||
return Err(ModuleNameResolutionError::TooManyDots);
|
||||
}
|
||||
for _ in 0..level {
|
||||
module_name = module_name
|
||||
.parent()
|
||||
.ok_or(ModuleNameResolutionError::TooManyDots)?;
|
||||
}
|
||||
|
||||
if let Some(tail) = tail {
|
||||
let tail = ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax)?;
|
||||
module_name.extend(&tail);
|
||||
}
|
||||
|
||||
Ok(module_name)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,15 +62,10 @@ impl<'db> Unpacker<'db> {
|
||||
.unwrap_with_diagnostic(&self.context, value.as_any_node_ref(self.db()));
|
||||
}
|
||||
|
||||
self.unpack_inner(target, value.as_any_node_ref(self.db()), value_ty);
|
||||
self.unpack_inner(target, value_ty);
|
||||
}
|
||||
|
||||
fn unpack_inner(
|
||||
&mut self,
|
||||
target: &ast::Expr,
|
||||
value_expr: AnyNodeRef<'db>,
|
||||
value_ty: Type<'db>,
|
||||
) {
|
||||
fn unpack_inner(&mut self, target: &ast::Expr, value_ty: Type<'db>) {
|
||||
match target {
|
||||
ast::Expr::Name(target_name) => {
|
||||
self.targets.insert(
|
||||
@@ -79,7 +74,7 @@ impl<'db> Unpacker<'db> {
|
||||
);
|
||||
}
|
||||
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
|
||||
self.unpack_inner(value, value_expr, value_ty);
|
||||
self.unpack_inner(value, value_ty);
|
||||
}
|
||||
ast::Expr::List(ast::ExprList { elts, .. })
|
||||
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
@@ -158,7 +153,7 @@ impl<'db> Unpacker<'db> {
|
||||
Type::LiteralString
|
||||
} else {
|
||||
ty.iterate(self.db())
|
||||
.unwrap_with_diagnostic(&self.context, value_expr)
|
||||
.unwrap_with_diagnostic(&self.context, AnyNodeRef::from(target))
|
||||
};
|
||||
for target_type in &mut target_types {
|
||||
target_type.push(ty);
|
||||
@@ -172,7 +167,7 @@ impl<'db> Unpacker<'db> {
|
||||
[] => Type::unknown(),
|
||||
types => UnionType::from_elements(self.db(), types),
|
||||
};
|
||||
self.unpack_inner(element, value_expr, element_ty);
|
||||
self.unpack_inner(element, element_ty);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -18,7 +18,6 @@ ruff_index = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
|
||||
@@ -20,7 +20,7 @@ reveal_type(1) # revealed: Literal[1]
|
||||
````
|
||||
|
||||
When running this test, the mdtest framework will write a file with these contents to the default
|
||||
file path (`/src/mdtest_snippet.py`) in its in-memory file system, run a type check on that file,
|
||||
file path (`/src/mdtest_snippet__1.py`) in its in-memory file system, run a type check on that file,
|
||||
and then match the resulting diagnostics with the assertions in the test. Assertions are in the form
|
||||
of Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise,
|
||||
it fails.
|
||||
@@ -34,8 +34,7 @@ syntax, it's just how this README embeds an example mdtest Markdown document.)
|
||||
See actual example mdtest suites in
|
||||
[`crates/red_knot_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/resources/mdtest).
|
||||
|
||||
> [!NOTE]
|
||||
> If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory,
|
||||
> ℹ️ Note: If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory,
|
||||
> as with the example in `crates/red_knot_python_semantic/tests/mdtest.rs`,
|
||||
> you will likely want to also make sure that the crate the tests are in is rebuilt every time a
|
||||
> Markdown file is added or removed from the directory. See
|
||||
@@ -127,31 +126,6 @@ Intervening empty lines or non-assertion comments are not allowed; an assertion
|
||||
assertion per line, immediately following each other, with the line immediately following the last
|
||||
assertion as the line of source code on which the matched diagnostics are emitted.
|
||||
|
||||
## Literate style
|
||||
|
||||
If multiple code blocks (without an explicit path, see below) are present in a single test, they will
|
||||
be merged into a single file in the order they appear in the Markdown file. This allows for tests that
|
||||
interleave code and explanations:
|
||||
|
||||
````markdown
|
||||
# My literate test
|
||||
|
||||
This first snippet here:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f(x: Literal[1]):
|
||||
pass
|
||||
```
|
||||
|
||||
will be merged with this second snippet here, i.e. `f` is defined here:
|
||||
|
||||
```py
|
||||
f(2) # error: [invalid-argument-type]
|
||||
```
|
||||
````
|
||||
|
||||
## Diagnostic Snapshotting
|
||||
|
||||
In addition to inline assertions, one can also snapshot the full diagnostic
|
||||
@@ -182,8 +156,13 @@ snapshotting of specific diagnostics.
|
||||
|
||||
## Multi-file tests
|
||||
|
||||
Some tests require multiple files, with imports from one file into another. For this purpose,
|
||||
tests can specify explicit file paths in a separate line before the code block (`b.py` below):
|
||||
Some tests require multiple files, with imports from one file into another. Multiple fenced code
|
||||
blocks represent multiple embedded files. If there are multiple unnamed files, mdtest will name them
|
||||
according to the numbered scheme `/src/mdtest_snippet__1.py`, `/src/mdtest_snippet__2.py`, etc. (If
|
||||
they are `pyi` files, they will be named with a `pyi` extension instead.)
|
||||
|
||||
Tests should not rely on these default names. If a test must import from a file, then it should
|
||||
explicitly specify the file name:
|
||||
|
||||
````markdown
|
||||
```py
|
||||
@@ -204,8 +183,8 @@ is, the equivalent of a runtime entry on `sys.path`).
|
||||
The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but
|
||||
this is a feature we will want to add in the future.
|
||||
|
||||
So the above test creates two files, `/src/mdtest_snippet.py` and `/src/b.py`, and sets the workspace
|
||||
root to `/src/`, allowing imports from `b.py` using the module name `b`.
|
||||
So the above test creates two files, `/src/mdtest_snippet__1.py` and `/src/b.py`, and sets the
|
||||
workspace root to `/src/`, allowing imports from `b.py` using the module name `b`.
|
||||
|
||||
## Multi-test suites
|
||||
|
||||
@@ -419,7 +398,7 @@ This is just an example, not a proposal that red-knot would ever actually output
|
||||
precisely this format:
|
||||
|
||||
```output
|
||||
mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[1]'
|
||||
mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[1]'
|
||||
```
|
||||
````
|
||||
|
||||
@@ -427,7 +406,7 @@ We will want to build tooling to automatically capture and update these “full
|
||||
blocks, when tests are run in an update-output mode (probably specified by an environment variable.)
|
||||
|
||||
By default, an `output` block will specify diagnostic output for the file
|
||||
`<workspace-root>/mdtest_snippet.py`. An `output` block can be prefixed by a
|
||||
`<workspace-root>/mdtest_snippet__1.py`. An `output` block can be prefixed by a
|
||||
<code>`<path>`:</code> label as usual, to explicitly specify the Python file for which it asserts
|
||||
diagnostic output.
|
||||
|
||||
@@ -463,7 +442,7 @@ x = 1
|
||||
Initial expected output for the unnamed file:
|
||||
|
||||
```output
|
||||
/src/mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[1]'
|
||||
/src/mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[1]'
|
||||
```
|
||||
|
||||
Now in our first incremental stage, modify the contents of `b.py`:
|
||||
@@ -478,12 +457,12 @@ x = 2
|
||||
And this is our updated expected output for the unnamed file at stage 1:
|
||||
|
||||
```output stage=1
|
||||
/src/mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[2]'
|
||||
/src/mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[2]'
|
||||
```
|
||||
|
||||
(One reason to use full-diagnostic-output blocks in this test is that updating inline-comment
|
||||
diagnostic assertions for `mdtest_snippet.py` would require specifying new contents for
|
||||
`mdtest_snippet.py` in stage 1, which we don't want to do in this test.)
|
||||
diagnostic assertions for `mdtest_snippet__1.py` would require specifying new contents for
|
||||
`mdtest_snippet__1.py` in stage 1, which we don't want to do in this test.)
|
||||
````
|
||||
|
||||
It will be possible to provide any number of stages in an incremental test. If a stage re-specifies
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::config::Log;
|
||||
use crate::parser::{BacktickOffsets, EmbeddedFileSourceMap};
|
||||
use camino::Utf8Path;
|
||||
use colored::Colorize;
|
||||
use parser as test_parser;
|
||||
@@ -12,6 +11,7 @@ use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::TextSize;
|
||||
use std::fmt::Write;
|
||||
|
||||
mod assertion;
|
||||
@@ -67,14 +67,12 @@ pub fn run(
|
||||
let md_index = LineIndex::from_source_text(&source);
|
||||
|
||||
for test_failures in failures {
|
||||
let source_map =
|
||||
EmbeddedFileSourceMap::new(&md_index, test_failures.backtick_offsets);
|
||||
let backtick_line = md_index.line_index(test_failures.backtick_offset);
|
||||
|
||||
for (relative_line_number, failures) in test_failures.by_line.iter() {
|
||||
let absolute_line_number =
|
||||
source_map.to_absolute_line_number(relative_line_number);
|
||||
|
||||
for failure in failures {
|
||||
let absolute_line_number =
|
||||
backtick_line.checked_add(relative_line_number).unwrap();
|
||||
let line_info =
|
||||
format!("{relative_fixture_path}:{absolute_line_number}").cyan();
|
||||
println!(" {line_info} {failure}");
|
||||
@@ -122,7 +120,11 @@ fn run_test(
|
||||
"Supported file types are: py, pyi, text"
|
||||
);
|
||||
|
||||
let full_path = embedded.full_path(&project_root);
|
||||
let full_path = if embedded.path.starts_with('/') {
|
||||
SystemPathBuf::from(embedded.path.clone())
|
||||
} else {
|
||||
project_root.join(&embedded.path)
|
||||
};
|
||||
|
||||
if let Some(ref typeshed_path) = custom_typeshed_path {
|
||||
if let Ok(relative_path) = full_path.strip_prefix(typeshed_path.join("stdlib")) {
|
||||
@@ -134,7 +136,7 @@ fn run_test(
|
||||
}
|
||||
}
|
||||
|
||||
db.write_file(&full_path, &embedded.code).unwrap();
|
||||
db.write_file(&full_path, embedded.code).unwrap();
|
||||
|
||||
if !full_path.starts_with(&src_path) || embedded.lang == "text" {
|
||||
// These files need to be written to the file system (above), but we don't run any checks on them.
|
||||
@@ -145,7 +147,7 @@ fn run_test(
|
||||
|
||||
Some(TestFile {
|
||||
file,
|
||||
backtick_offsets: embedded.backtick_offsets.clone(),
|
||||
backtick_offset: embedded.backtick_offset,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -228,7 +230,7 @@ fn run_test(
|
||||
}
|
||||
by_line.push(OneIndexed::from_zero_indexed(0), messages);
|
||||
return Some(FileFailures {
|
||||
backtick_offsets: test_file.backtick_offsets,
|
||||
backtick_offset: test_file.backtick_offset,
|
||||
by_line,
|
||||
});
|
||||
}
|
||||
@@ -242,7 +244,7 @@ fn run_test(
|
||||
match matcher::match_file(db, test_file.file, diagnostics.iter().map(|d| &**d)) {
|
||||
Ok(()) => None,
|
||||
Err(line_failures) => Some(FileFailures {
|
||||
backtick_offsets: test_file.backtick_offsets,
|
||||
backtick_offset: test_file.backtick_offset,
|
||||
by_line: line_failures,
|
||||
}),
|
||||
};
|
||||
@@ -278,11 +280,11 @@ fn run_test(
|
||||
type Failures = Vec<FileFailures>;
|
||||
|
||||
/// The failures for a single file in a test by line number.
|
||||
#[derive(Debug)]
|
||||
struct FileFailures {
|
||||
/// Positional information about the code block(s) to reconstruct absolute line numbers.
|
||||
backtick_offsets: Vec<BacktickOffsets>,
|
||||
|
||||
/// The failures by lines in the file.
|
||||
/// The offset of the backticks that starts the code block in the Markdown file
|
||||
backtick_offset: TextSize,
|
||||
/// The failures by lines in the code block.
|
||||
by_line: matcher::FailuresByLine,
|
||||
}
|
||||
|
||||
@@ -290,8 +292,8 @@ struct FileFailures {
|
||||
struct TestFile {
|
||||
file: File,
|
||||
|
||||
/// Positional information about the code block(s) to reconstruct absolute line numbers.
|
||||
backtick_offsets: Vec<BacktickOffsets>,
|
||||
// Offset of the backticks that starts the code block in the Markdown file
|
||||
backtick_offset: TextSize,
|
||||
}
|
||||
|
||||
fn create_diagnostic_snapshot<D: Diagnostic>(
|
||||
@@ -315,7 +317,7 @@ fn create_diagnostic_snapshot<D: Diagnostic>(
|
||||
writeln!(snapshot, "# Python source files").unwrap();
|
||||
writeln!(snapshot).unwrap();
|
||||
for file in test.files() {
|
||||
writeln!(snapshot, "## {}", file.relative_path()).unwrap();
|
||||
writeln!(snapshot, "## {}", file.path).unwrap();
|
||||
writeln!(snapshot).unwrap();
|
||||
// Note that we don't use ```py here because the line numbering
|
||||
// we add makes it invalid Python. This sacrifices syntax
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
use std::{borrow::Cow, collections::hash_map::Entry};
|
||||
|
||||
use anyhow::bail;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_trivia::Cursor;
|
||||
use ruff_source_file::{LineIndex, LineRanges, OneIndexed};
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::config::MarkdownTestConfig;
|
||||
@@ -136,112 +132,6 @@ struct Section<'s> {
|
||||
#[newtype_index]
|
||||
struct EmbeddedFileId;
|
||||
|
||||
/// Holds information about the start and the end of a code block in a Markdown file.
|
||||
///
|
||||
/// The start is the offset of the first triple-backtick in the code block, and the end is the
|
||||
/// offset of the (start of the) closing triple-backtick.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct BacktickOffsets(TextSize, TextSize);
|
||||
|
||||
/// Holds information about the position and length of all code blocks that are part of
|
||||
/// a single embedded file in a Markdown file. This is used to reconstruct absolute line
|
||||
/// numbers (in the Markdown file) from relative line numbers (in the embedded file).
|
||||
///
|
||||
/// If we have a Markdown section with multiple code blocks like this:
|
||||
///
|
||||
/// 01 # Test
|
||||
/// 02
|
||||
/// 03 Part 1:
|
||||
/// 04
|
||||
/// 05 ```py
|
||||
/// 06 a = 1 # Relative line number: 1
|
||||
/// 07 b = 2 # Relative line number: 2
|
||||
/// 08 ```
|
||||
/// 09
|
||||
/// 10 Part 2:
|
||||
/// 11
|
||||
/// 12 ```py
|
||||
/// 13 c = 3 # Relative line number: 3
|
||||
/// 14 ```
|
||||
///
|
||||
/// We want to reconstruct the absolute line number (left) from relative
|
||||
/// line numbers. The information we have is the start line and the line
|
||||
/// count of each code block:
|
||||
///
|
||||
/// - Block 1: (start = 5, count = 2)
|
||||
/// - Block 2: (start = 12, count = 1)
|
||||
///
|
||||
/// For example, if we see a relative line number of 3, we see that it is
|
||||
/// larger than the line count of the first block, so we subtract the line
|
||||
/// count of the first block, and then add the new relative line number (1)
|
||||
/// to the absolute start line of the second block (12), resulting in an
|
||||
/// absolute line number of 13.
|
||||
pub(crate) struct EmbeddedFileSourceMap {
|
||||
start_line_and_line_count: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl EmbeddedFileSourceMap {
|
||||
pub(crate) fn new(
|
||||
md_index: &LineIndex,
|
||||
dimensions: impl IntoIterator<Item = BacktickOffsets>,
|
||||
) -> EmbeddedFileSourceMap {
|
||||
EmbeddedFileSourceMap {
|
||||
start_line_and_line_count: dimensions
|
||||
.into_iter()
|
||||
.map(|d| {
|
||||
let start_line = md_index.line_index(d.0).get();
|
||||
let end_line = md_index.line_index(d.1).get();
|
||||
let code_line_count = (end_line - start_line) - 1;
|
||||
(start_line, code_line_count)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_absolute_line_number(&self, relative_line_number: OneIndexed) -> OneIndexed {
|
||||
let mut absolute_line_number = 0;
|
||||
let mut relative_line_number = relative_line_number.get();
|
||||
|
||||
for (start_line, line_count) in &self.start_line_and_line_count {
|
||||
if relative_line_number > *line_count {
|
||||
relative_line_number -= *line_count;
|
||||
} else {
|
||||
absolute_line_number = start_line + relative_line_number;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
OneIndexed::new(absolute_line_number).expect("Relative line number out of bounds")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum EmbeddedFilePath<'s> {
|
||||
Autogenerated(PySourceType),
|
||||
Explicit(&'s str),
|
||||
}
|
||||
|
||||
impl EmbeddedFilePath<'_> {
|
||||
pub(crate) fn as_str(&self) -> &str {
|
||||
match self {
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python) => "mdtest_snippet.py",
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Stub) => "mdtest_snippet.pyi",
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Ipynb) => "mdtest_snippet.ipynb",
|
||||
EmbeddedFilePath::Explicit(path) => path,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_explicit(&self) -> bool {
|
||||
matches!(self, EmbeddedFilePath::Explicit(_))
|
||||
}
|
||||
|
||||
fn is_allowed_explicit_path(path: &str) -> bool {
|
||||
[PySourceType::Python, PySourceType::Stub]
|
||||
.iter()
|
||||
.all(|source_type| path != EmbeddedFilePath::Autogenerated(*source_type).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// A single file embedded in a [`Section`] as a fenced code block.
|
||||
///
|
||||
/// Currently must be a Python file (`py` language), a type stub (`pyi`) or a [typeshed `VERSIONS`]
|
||||
@@ -258,39 +148,12 @@ impl EmbeddedFilePath<'_> {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EmbeddedFile<'s> {
|
||||
section: SectionId,
|
||||
path: EmbeddedFilePath<'s>,
|
||||
pub(crate) path: String,
|
||||
pub(crate) lang: &'s str,
|
||||
pub(crate) code: Cow<'s, str>,
|
||||
pub(crate) backtick_offsets: Vec<BacktickOffsets>,
|
||||
}
|
||||
pub(crate) code: &'s str,
|
||||
|
||||
impl EmbeddedFile<'_> {
|
||||
fn append_code(&mut self, backtick_offsets: BacktickOffsets, new_code: &str) {
|
||||
// Treat empty code blocks as non-existent, instead of creating
|
||||
// an additional empty line:
|
||||
if new_code.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.backtick_offsets.push(backtick_offsets);
|
||||
|
||||
let existing_code = self.code.to_mut();
|
||||
existing_code.push('\n');
|
||||
existing_code.push_str(new_code);
|
||||
}
|
||||
|
||||
pub(crate) fn relative_path(&self) -> &str {
|
||||
self.path.as_str()
|
||||
}
|
||||
|
||||
pub(crate) fn full_path(&self, project_root: &SystemPath) -> SystemPathBuf {
|
||||
let relative_path = self.relative_path();
|
||||
if relative_path.starts_with('/') {
|
||||
SystemPathBuf::from(relative_path)
|
||||
} else {
|
||||
project_root.join(relative_path)
|
||||
}
|
||||
}
|
||||
/// The offset of the backticks beginning the code block within the markdown file
|
||||
pub(crate) backtick_offset: TextSize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -332,6 +195,12 @@ struct Parser<'s> {
|
||||
/// [`EmbeddedFile`]s of the final [`MarkdownTestSuite`].
|
||||
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
|
||||
|
||||
/// The counts are done by section. This gives each code block a
|
||||
/// somewhat locally derived name. That is, adding new sections
|
||||
/// won't change the names of files in other sections. This is
|
||||
/// important for avoiding snapshot churn.
|
||||
unnamed_file_count: FxHashMap<SectionId, usize>,
|
||||
|
||||
/// The unparsed remainder of the Markdown source.
|
||||
cursor: Cursor<'s>,
|
||||
|
||||
@@ -348,7 +217,7 @@ struct Parser<'s> {
|
||||
stack: SectionStack,
|
||||
|
||||
/// Names of embedded files in current active section.
|
||||
current_section_files: FxHashMap<EmbeddedFilePath<'s>, EmbeddedFileId>,
|
||||
current_section_files: Option<FxHashSet<String>>,
|
||||
|
||||
/// Whether or not the current section has a config block.
|
||||
current_section_has_config: bool,
|
||||
@@ -368,12 +237,13 @@ impl<'s> Parser<'s> {
|
||||
sections,
|
||||
source,
|
||||
files: IndexVec::default(),
|
||||
unnamed_file_count: FxHashMap::default(),
|
||||
cursor: Cursor::new(source),
|
||||
preceding_blank_lines: 0,
|
||||
explicit_path: None,
|
||||
source_len: source.text_len(),
|
||||
stack: SectionStack::new(root_section_id),
|
||||
current_section_files: FxHashMap::default(),
|
||||
current_section_files: None,
|
||||
current_section_has_config: false,
|
||||
}
|
||||
}
|
||||
@@ -464,7 +334,7 @@ impl<'s> Parser<'s> {
|
||||
if self.cursor.eat_char2('`', '`') {
|
||||
// We saw the triple-backtick beginning of a code block.
|
||||
|
||||
let backtick_offset_start = self.offset() - "```".text_len();
|
||||
let backtick_offset = self.offset() - "```".text_len();
|
||||
|
||||
if self.preceding_blank_lines < 1 && self.explicit_path.is_none() {
|
||||
bail!("Code blocks must start on a new line and be preceded by at least one blank line.");
|
||||
@@ -493,13 +363,7 @@ impl<'s> Parser<'s> {
|
||||
code = &code[..code.len() - '\n'.len_utf8()];
|
||||
}
|
||||
|
||||
let backtick_offset_end = self.offset() - "```".text_len();
|
||||
|
||||
self.process_code_block(
|
||||
lang,
|
||||
code,
|
||||
BacktickOffsets(backtick_offset_start, backtick_offset_end),
|
||||
)?;
|
||||
self.process_code_block(lang, code, backtick_offset)?;
|
||||
} else {
|
||||
let code_block_start = self.cursor.token_len();
|
||||
let line = self.source.count_lines(TextRange::up_to(code_block_start));
|
||||
@@ -564,7 +428,7 @@ impl<'s> Parser<'s> {
|
||||
snapshot_diagnostics: self.sections[parent].snapshot_diagnostics,
|
||||
};
|
||||
|
||||
if !self.current_section_files.is_empty() {
|
||||
if self.current_section_files.is_some() {
|
||||
bail!(
|
||||
"Header '{}' not valid inside a test case; parent '{}' has code files.",
|
||||
section.title,
|
||||
@@ -575,7 +439,7 @@ impl<'s> Parser<'s> {
|
||||
let section_id = self.sections.push(section);
|
||||
self.stack.push(section_id);
|
||||
|
||||
self.current_section_files.clear();
|
||||
self.current_section_files = None;
|
||||
self.current_section_has_config = false;
|
||||
|
||||
Ok(())
|
||||
@@ -585,11 +449,10 @@ impl<'s> Parser<'s> {
|
||||
&mut self,
|
||||
lang: &'s str,
|
||||
code: &'s str,
|
||||
backtick_offsets: BacktickOffsets,
|
||||
backtick_offset: TextSize,
|
||||
) -> anyhow::Result<()> {
|
||||
// We never pop the implicit root section.
|
||||
let section = self.stack.top();
|
||||
let test_name = self.sections[section].title;
|
||||
|
||||
if lang == "toml" {
|
||||
return self.process_config_block(code);
|
||||
@@ -602,86 +465,52 @@ impl<'s> Parser<'s> {
|
||||
&& !explicit_path.ends_with(&format!(".{lang}"))
|
||||
{
|
||||
bail!(
|
||||
"File extension of test file path `{explicit_path}` in test `{test_name}` does not match language specified `{lang}` of code block"
|
||||
"File ending of test file path `{explicit_path}` does not match `lang={lang}` of code block"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let path = match self.explicit_path {
|
||||
Some(path) => {
|
||||
if !EmbeddedFilePath::is_allowed_explicit_path(path) {
|
||||
bail!(
|
||||
"The file name `{path}` in test `{test_name}` must not be used explicitly.",
|
||||
);
|
||||
}
|
||||
Some(path) => path.to_string(),
|
||||
None => {
|
||||
let unnamed_file_count = self.unnamed_file_count.entry(section).or_default();
|
||||
*unnamed_file_count += 1;
|
||||
|
||||
EmbeddedFilePath::Explicit(path)
|
||||
match lang {
|
||||
"py" | "pyi" => format!("mdtest_snippet__{unnamed_file_count}.{lang}"),
|
||||
"" => format!("mdtest_snippet__{unnamed_file_count}.py"),
|
||||
_ => {
|
||||
bail!(
|
||||
"Cannot generate name for `lang={}`: Unsupported extension",
|
||||
lang
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => match lang {
|
||||
"py" => EmbeddedFilePath::Autogenerated(PySourceType::Python),
|
||||
"pyi" => EmbeddedFilePath::Autogenerated(PySourceType::Stub),
|
||||
"" => {
|
||||
bail!("Cannot auto-generate file name for code block with empty language specifier in test `{test_name}`");
|
||||
}
|
||||
_ => {
|
||||
bail!(
|
||||
"Cannot auto-generate file name for code block with language `{}` in test `{test_name}`",
|
||||
lang
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let has_merged_snippets = self.current_section_has_merged_snippets();
|
||||
let has_explicit_file_paths = self.current_section_has_explicit_file_paths();
|
||||
self.files.push(EmbeddedFile {
|
||||
path: path.clone(),
|
||||
section,
|
||||
lang,
|
||||
code,
|
||||
backtick_offset,
|
||||
});
|
||||
|
||||
match self.current_section_files.entry(path.clone()) {
|
||||
Entry::Vacant(entry) => {
|
||||
if has_merged_snippets {
|
||||
bail!("Merged snippets in test `{test_name}` are not allowed in the presence of other files.");
|
||||
}
|
||||
|
||||
let index = self.files.push(EmbeddedFile {
|
||||
path: path.clone(),
|
||||
section,
|
||||
lang,
|
||||
code: Cow::Borrowed(code),
|
||||
backtick_offsets: vec![backtick_offsets],
|
||||
});
|
||||
entry.insert(index);
|
||||
}
|
||||
Entry::Occupied(entry) => {
|
||||
if path.is_explicit() {
|
||||
bail!(
|
||||
"Test `{test_name}` has duplicate files named `{}`.",
|
||||
path.as_str(),
|
||||
);
|
||||
};
|
||||
|
||||
if has_explicit_file_paths {
|
||||
bail!("Merged snippets in test `{test_name}` are not allowed in the presence of other files.");
|
||||
}
|
||||
|
||||
let index = *entry.get();
|
||||
self.files[index].append_code(backtick_offsets, code);
|
||||
}
|
||||
if let Some(current_files) = &mut self.current_section_files {
|
||||
if !current_files.insert(path.clone()) {
|
||||
bail!(
|
||||
"Test `{}` has duplicate files named `{path}`.",
|
||||
self.sections[section].title
|
||||
);
|
||||
};
|
||||
} else {
|
||||
self.current_section_files = Some(FxHashSet::from_iter([path]));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_section_has_explicit_file_paths(&self) -> bool {
|
||||
self.current_section_files
|
||||
.iter()
|
||||
.any(|(path, _)| path.is_explicit())
|
||||
}
|
||||
|
||||
fn current_section_has_merged_snippets(&self) -> bool {
|
||||
self.current_section_files
|
||||
.values()
|
||||
.any(|id| self.files[*id].backtick_offsets.len() > 1)
|
||||
}
|
||||
|
||||
fn process_config_block(&mut self, code: &str) -> anyhow::Result<()> {
|
||||
if self.current_section_has_config {
|
||||
bail!("Multiple TOML configuration blocks in the same section are not allowed.");
|
||||
@@ -702,7 +531,7 @@ impl<'s> Parser<'s> {
|
||||
everything else (including TOML configuration blocks).",
|
||||
);
|
||||
}
|
||||
if !self.current_section_files.is_empty() {
|
||||
if self.current_section_files.is_some() {
|
||||
bail!(
|
||||
"Section config to enable snapshotting diagnostics must come before \
|
||||
everything else (including embedded files).",
|
||||
@@ -726,7 +555,7 @@ impl<'s> Parser<'s> {
|
||||
self.stack.pop();
|
||||
// We would have errored before pushing a child section if there were files, so we know
|
||||
// no parent section can have files.
|
||||
self.current_section_files.clear();
|
||||
self.current_section_files = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,11 +567,8 @@ impl<'s> Parser<'s> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
|
||||
use crate::parser::EmbeddedFilePath;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let mf = super::parse("file.md", "").unwrap();
|
||||
@@ -771,10 +597,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
@@ -799,10 +622,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
@@ -825,14 +645,10 @@ mod tests {
|
||||
|
||||
# Three
|
||||
|
||||
`mod_a.pyi`:
|
||||
|
||||
```pyi
|
||||
a: int
|
||||
```
|
||||
|
||||
`mod_b.pyi`:
|
||||
|
||||
```pyi
|
||||
b: str
|
||||
```
|
||||
@@ -852,10 +668,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
|
||||
@@ -863,10 +676,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "y = 2");
|
||||
|
||||
@@ -874,11 +684,11 @@ mod tests {
|
||||
panic!("expected two files");
|
||||
};
|
||||
|
||||
assert_eq!(file_1.relative_path(), "mod_a.pyi");
|
||||
assert_eq!(file_1.path, "mdtest_snippet__1.pyi");
|
||||
assert_eq!(file_1.lang, "pyi");
|
||||
assert_eq!(file_1.code, "a: int");
|
||||
|
||||
assert_eq!(file_2.relative_path(), "mod_b.pyi");
|
||||
assert_eq!(file_2.path, "mdtest_snippet__2.pyi");
|
||||
assert_eq!(file_2.lang, "pyi");
|
||||
assert_eq!(file_2.code, "b: str");
|
||||
}
|
||||
@@ -921,11 +731,11 @@ mod tests {
|
||||
panic!("expected two files");
|
||||
};
|
||||
|
||||
assert_eq!(main.relative_path(), "main.py");
|
||||
assert_eq!(main.path, "main.py");
|
||||
assert_eq!(main.lang, "py");
|
||||
assert_eq!(main.code, "from foo import y");
|
||||
|
||||
assert_eq!(foo.relative_path(), "foo.py");
|
||||
assert_eq!(foo.path, "foo.py");
|
||||
assert_eq!(foo.lang, "py");
|
||||
assert_eq!(foo.code, "y = 2");
|
||||
|
||||
@@ -933,157 +743,11 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "y = 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merged_snippets() {
|
||||
let source = dedent(
|
||||
"
|
||||
# One
|
||||
|
||||
This is the first part of the embedded file:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
And this is the second part:
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
|
||||
And this is the third part:
|
||||
|
||||
```py
|
||||
z = 3
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1\ny = 2\nz = 3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_merged_snippets_for_explicit_paths() {
|
||||
let source = dedent(
|
||||
"
|
||||
# One
|
||||
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Test `One` has duplicate files named `foo.py`."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disallow_merged_snippets_in_presence_of_explicit_paths() {
|
||||
for source in [
|
||||
// Merged snippets first
|
||||
"
|
||||
# One
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
print('hello')
|
||||
```
|
||||
",
|
||||
// Explicit path first
|
||||
"
|
||||
# One
|
||||
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
print('hello')
|
||||
```
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
] {
|
||||
let err = super::parse("file.md", &dedent(source)).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Merged snippets in test `One` are not allowed in the presence of other files."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disallow_pyi_snippets_in_presence_of_merged_py_snippets() {
|
||||
let source = dedent(
|
||||
"
|
||||
# One
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
|
||||
```pyi
|
||||
x: int
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Merged snippets in test `One` are not allowed in the presence of other files."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_file_path() {
|
||||
let source = dedent(
|
||||
@@ -1104,7 +768,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.relative_path(), "foo.py");
|
||||
assert_eq!(file.path, "foo.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
@@ -1156,27 +820,28 @@ mod tests {
|
||||
fn no_lang() {
|
||||
let source = dedent(
|
||||
"
|
||||
# No language specifier
|
||||
|
||||
```
|
||||
x = 10
|
||||
```
|
||||
",
|
||||
);
|
||||
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Cannot auto-generate file name for code block with empty language specifier in test `No language specifier`"
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.code, "x = 10");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_generate_name_for_lang() {
|
||||
let source = dedent(
|
||||
"
|
||||
# JSON test?
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
@@ -1185,7 +850,7 @@ mod tests {
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Cannot auto-generate file name for code block with language `json` in test `JSON test?`"
|
||||
"Cannot generate name for `lang=json`: Unsupported extension"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1193,8 +858,6 @@ mod tests {
|
||||
fn mismatching_lang() {
|
||||
let source = dedent(
|
||||
"
|
||||
# Accidental stub
|
||||
|
||||
`a.py`:
|
||||
|
||||
```pyi
|
||||
@@ -1205,7 +868,7 @@ mod tests {
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"File extension of test file path `a.py` in test `Accidental stub` does not match language specified `pyi` of code block"
|
||||
"File ending of test file path `a.py` does not match `lang=pyi` of code block"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1230,7 +893,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.relative_path(), "lorem");
|
||||
assert_eq!(file.path, "lorem");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1255,7 +918,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.relative_path(), "lorem.yaml");
|
||||
assert_eq!(file.path, "lorem.yaml");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1277,13 +940,13 @@ mod tests {
|
||||
"
|
||||
## A well-fenced block
|
||||
|
||||
```py
|
||||
```
|
||||
y = 2
|
||||
```
|
||||
|
||||
## A not-so-well-fenced block
|
||||
|
||||
```py
|
||||
```
|
||||
x = 1
|
||||
",
|
||||
);
|
||||
@@ -1324,8 +987,7 @@ mod tests {
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Indented code blocks are not supported.");
|
||||
super::parse("file.md", &source).expect_err("Indented code blocks are not supported.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1371,10 +1033,7 @@ mod tests {
|
||||
};
|
||||
|
||||
assert_eq!(test.section.title, "file.md");
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1403,10 +1062,7 @@ mod tests {
|
||||
};
|
||||
|
||||
assert_eq!(test.section.title, "Foo");
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1435,12 +1091,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_usage_of_autogenerated_name() {
|
||||
fn no_duplicate_name_files_in_test_2() {
|
||||
let source = dedent(
|
||||
"
|
||||
# Name clash
|
||||
|
||||
`mdtest_snippet.py`:
|
||||
`mdtest_snippet__1.py`:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
@@ -1454,7 +1108,7 @@ mod tests {
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"The file name `mdtest_snippet.py` in test `Name clash` must not be used explicitly."
|
||||
"Test `file.md` has duplicate files named `mdtest_snippet__1.py`."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1479,7 +1133,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.relative_path(), "foo.py");
|
||||
assert_eq!(file.path, "foo.py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1504,7 +1158,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.relative_path(), "foo.py");
|
||||
assert_eq!(file.path, "foo.py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1528,7 +1182,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.relative_path(), "foo.py");
|
||||
assert_eq!(file.path, "foo.py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1553,7 +1207,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.relative_path(), "foo bar.py");
|
||||
assert_eq!(file.path, "foo bar.py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1579,10 +1233,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1607,10 +1258,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1636,10 +1284,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
@@ -1665,10 +1310,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.path,
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python)
|
||||
);
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -2218,11 +2218,13 @@ def func(t: _T) -> _T:
|
||||
return x
|
||||
"#
|
||||
),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
@r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
from typing import TypeVar
|
||||
_T = TypeVar("_T")
|
||||
|
||||
class OldStyle[T]:
|
||||
var: T
|
||||
@@ -2232,7 +2234,8 @@ def func(t: _T) -> _T:
|
||||
return x
|
||||
|
||||
----- stderr -----
|
||||
Found 7 errors (7 fixed, 0 remaining).
|
||||
"
|
||||
test.py:3:1: PYI018 Private TypeVar `_T` is never used
|
||||
Found 6 errors (5 fixed, 1 remaining).
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -6,10 +6,6 @@ class _bad:
|
||||
pass
|
||||
|
||||
|
||||
class __bad:
|
||||
pass
|
||||
|
||||
|
||||
class bad_class:
|
||||
pass
|
||||
|
||||
@@ -17,8 +13,6 @@ class bad_class:
|
||||
class Bad_Class:
|
||||
pass
|
||||
|
||||
class Bad__Class:
|
||||
pass
|
||||
|
||||
class BAD_CLASS:
|
||||
pass
|
||||
@@ -38,6 +32,3 @@ class GoodClass:
|
||||
|
||||
class GOOD:
|
||||
pass
|
||||
|
||||
class __GoodClass:
|
||||
pass
|
||||
@@ -1,7 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path += [os.path.dirname(__file__)]
|
||||
sys.path += ["../"]
|
||||
|
||||
from package import module
|
||||
@@ -1,109 +0,0 @@
|
||||
from typing import Generic, ParamSpec, TypeVar, TypeVarTuple, Unpack
|
||||
|
||||
|
||||
_A = TypeVar('_A')
|
||||
_B = TypeVar('_B', bound=int)
|
||||
_C = TypeVar('_C', str, bytes)
|
||||
_D = TypeVar('_D', default=int)
|
||||
_E = TypeVar('_E', bound=int, default=int)
|
||||
_F = TypeVar('_F', str, bytes, default=str)
|
||||
_G = TypeVar('_G', str, a := int)
|
||||
|
||||
_As = TypeVarTuple('_As')
|
||||
_Bs = TypeVarTuple('_Bs', bound=tuple[int, str])
|
||||
_Cs = TypeVarTuple('_Cs', default=tuple[int, str])
|
||||
|
||||
_P1 = ParamSpec('_P1')
|
||||
_P2 = ParamSpec('_P2', bound=[bytes, bool])
|
||||
_P3 = ParamSpec('_P3', default=[int, str])
|
||||
|
||||
|
||||
### Errors
|
||||
|
||||
class C[T](Generic[_A]): ...
|
||||
class C[T](Generic[_B], str): ...
|
||||
class C[T](int, Generic[_C]): ...
|
||||
class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
|
||||
class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
|
||||
class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
|
||||
|
||||
class C[*Ts](Generic[*_As]): ...
|
||||
class C[*Ts](Generic[Unpack[_As]]): ...
|
||||
class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
|
||||
class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
|
||||
|
||||
|
||||
class C[**P](Generic[_P1]): ...
|
||||
class C[**P](Generic[_P2]): ...
|
||||
class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
|
||||
|
||||
|
||||
class C[T](Generic[T, _A]): ...
|
||||
|
||||
|
||||
# See `is_existing_param_of_same_class`.
|
||||
# `expr_name_to_type_var` doesn't handle named expressions,
|
||||
# only simple assignments, so there is no fix.
|
||||
class C[T: (_Z := TypeVar('_Z'))](Generic[_Z]): ...
|
||||
|
||||
|
||||
class C(Generic[_B]):
|
||||
class D[T](Generic[_B, T]): ...
|
||||
|
||||
|
||||
class C[T]:
|
||||
class D[U](Generic[T, U]): ...
|
||||
|
||||
|
||||
# In a single run, only the first is reported.
|
||||
# Others will be reported/fixed in following iterations.
|
||||
class C[T](Generic[_C], Generic[_D]): ...
|
||||
class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults
|
||||
|
||||
|
||||
class C[
|
||||
T # Comment
|
||||
](Generic[_E]): ... # TODO: Type parameter defaults
|
||||
|
||||
|
||||
class C[T](Generic[Generic[_F]]): ...
|
||||
class C[T](Generic[Unpack[_A]]): ...
|
||||
class C[T](Generic[Unpack[_P1]]): ...
|
||||
class C[T](Generic[Unpack[Unpack[_P2]]]): ...
|
||||
class C[T](Generic[Unpack[*_As]]): ...
|
||||
class C[T](Generic[Unpack[_As, _Bs]]): ...
|
||||
|
||||
|
||||
class C[T](Generic[_A, _A]): ...
|
||||
class C[T](Generic[_A, Unpack[_As]]): ...
|
||||
class C[T](Generic[*_As, _A]): ...
|
||||
|
||||
|
||||
from somewhere import APublicTypeVar
|
||||
class C[T](Generic[APublicTypeVar]): ...
|
||||
class C[T](Generic[APublicTypeVar, _A]): ...
|
||||
|
||||
|
||||
# `_G` has two constraints: `str` and `a := int`.
|
||||
# The latter cannot be used as a PEP 695 constraint,
|
||||
# as named expressions are forbidden within type parameter lists.
|
||||
# See also the `_Z` example above.
|
||||
class C[T](Generic[_G]): ... # Should be moved down below eventually
|
||||
|
||||
|
||||
# Single-element constraints should not be converted to a bound.
|
||||
class C[T: (str,)](Generic[_A]): ...
|
||||
class C[T: [a]](Generic[_A]): ...
|
||||
|
||||
|
||||
# Existing bounds should not be deparenthesized.
|
||||
# class C[T: (_Y := int)](Generic[_A]): ... # TODO: Uncomment this
|
||||
# class C[T: (*a,)](Generic[_A]): ... # TODO: Uncomment this
|
||||
|
||||
|
||||
### No errors
|
||||
|
||||
class C(Generic[_A]): ...
|
||||
class C[_A]: ...
|
||||
class C[_A](list[_A]): ...
|
||||
class C[_A](list[Generic[_A]]): ...
|
||||
@@ -554,9 +554,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::NonPEP695GenericClass) {
|
||||
pyupgrade::rules::non_pep695_generic_class(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::ClassWithMixedTypeVars) {
|
||||
ruff::rules::class_with_mixed_type_vars(checker, class_def);
|
||||
}
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
if checker.enabled(Rule::MultipleImportsOnOneLine) {
|
||||
|
||||
@@ -248,11 +248,6 @@ impl<'a> Checker<'a> {
|
||||
cell_offsets: Option<&'a CellOffsets>,
|
||||
notebook_index: Option<&'a NotebookIndex>,
|
||||
) -> Checker<'a> {
|
||||
let mut semantic = SemanticModel::new(&settings.typing_modules, path, module);
|
||||
if settings.preview.is_enabled() {
|
||||
// Set the feature flag to test `TYPE_CHECKING` semantic changes
|
||||
semantic.flags |= SemanticModelFlags::NEW_TYPE_CHECKING_BLOCK_DETECTION;
|
||||
}
|
||||
Self {
|
||||
parsed,
|
||||
parsed_type_annotation: None,
|
||||
@@ -268,7 +263,7 @@ impl<'a> Checker<'a> {
|
||||
stylist,
|
||||
indexer,
|
||||
importer: Importer::new(parsed, locator, stylist),
|
||||
semantic,
|
||||
semantic: SemanticModel::new(&settings.typing_modules, path, module),
|
||||
visit: deferred::Visit::default(),
|
||||
analyze: deferred::Analyze::default(),
|
||||
diagnostics: Vec::default(),
|
||||
|
||||
@@ -1005,7 +1005,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "049") => (RuleGroup::Preview, rules::ruff::rules::DataclassEnum),
|
||||
(Ruff, "051") => (RuleGroup::Preview, rules::ruff::rules::IfKeyInDictDel),
|
||||
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
|
||||
(Ruff, "053") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars),
|
||||
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
|
||||
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
|
||||
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),
|
||||
|
||||
@@ -9,7 +9,7 @@ use anyhow::Result;
|
||||
use libcst_native::{ImportAlias, Name as cstName, NameOrAttribute};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::{self as ast, Expr, ModModule, Stmt};
|
||||
use ruff_python_ast::{self as ast, ModModule, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::{Parsed, Tokens};
|
||||
use ruff_python_semantic::{
|
||||
@@ -125,7 +125,7 @@ impl<'a> Importer<'a> {
|
||||
&self,
|
||||
import: &ImportedMembers,
|
||||
at: TextSize,
|
||||
semantic: &SemanticModel<'a>,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<TypingImportEdit> {
|
||||
// Generate the modified import statement.
|
||||
let content = fix::codemods::retain_imports(
|
||||
@@ -135,39 +135,6 @@ impl<'a> Importer<'a> {
|
||||
self.stylist,
|
||||
)?;
|
||||
|
||||
// Add the import to an existing `TYPE_CHECKING` block.
|
||||
if let Some(block) = self.preceding_type_checking_block(at) {
|
||||
// Add the import to the existing `TYPE_CHECKING` block.
|
||||
let type_checking_edit =
|
||||
if let Some(statement) = Self::type_checking_binding_statement(semantic, block) {
|
||||
if statement == import.statement {
|
||||
// Special-case: if the `TYPE_CHECKING` symbol is imported as part of the same
|
||||
// statement that we're modifying, avoid adding a no-op edit. For example, here,
|
||||
// the `TYPE_CHECKING` no-op edit would overlap with the edit to remove `Final`
|
||||
// from the import:
|
||||
// ```python
|
||||
// from __future__ import annotations
|
||||
//
|
||||
// from typing import Final, TYPE_CHECKING
|
||||
//
|
||||
// Const: Final[dict] = {}
|
||||
// ```
|
||||
None
|
||||
} else {
|
||||
Some(Edit::range_replacement(
|
||||
self.locator.slice(statement.range()).to_string(),
|
||||
statement.range(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return Ok(TypingImportEdit {
|
||||
type_checking_edit,
|
||||
add_import_edit: self.add_to_type_checking_block(&content, block.start()),
|
||||
});
|
||||
}
|
||||
|
||||
// Import the `TYPE_CHECKING` symbol from the typing module.
|
||||
let (type_checking_edit, type_checking) =
|
||||
if let Some(type_checking) = Self::find_type_checking(at, semantic)? {
|
||||
@@ -212,10 +179,13 @@ impl<'a> Importer<'a> {
|
||||
(Some(edit), name)
|
||||
};
|
||||
|
||||
// Add the import to a new `TYPE_CHECKING` block.
|
||||
Ok(TypingImportEdit {
|
||||
type_checking_edit,
|
||||
add_import_edit: self.add_type_checking_block(
|
||||
// Add the import to a `TYPE_CHECKING` block.
|
||||
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
|
||||
// Add the import to the `TYPE_CHECKING` block.
|
||||
self.add_to_type_checking_block(&content, block.start())
|
||||
} else {
|
||||
// Add the import to a new `TYPE_CHECKING` block.
|
||||
self.add_type_checking_block(
|
||||
&format!(
|
||||
"{}if {type_checking}:{}{}",
|
||||
self.stylist.line_ending().as_str(),
|
||||
@@ -223,25 +193,13 @@ impl<'a> Importer<'a> {
|
||||
indent(&content, self.stylist.indentation())
|
||||
),
|
||||
at,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn type_checking_binding_statement(
|
||||
semantic: &SemanticModel<'a>,
|
||||
type_checking_block: &Stmt,
|
||||
) -> Option<&'a Stmt> {
|
||||
let Stmt::If(ast::StmtIf { test, .. }) = type_checking_block else {
|
||||
return None;
|
||||
)?
|
||||
};
|
||||
|
||||
let mut source = test;
|
||||
while let Expr::Attribute(ast::ExprAttribute { value, .. }) = source.as_ref() {
|
||||
source = value;
|
||||
}
|
||||
semantic
|
||||
.binding(semantic.resolve_name(source.as_name_expr()?)?)
|
||||
.statement(semantic)
|
||||
Ok(TypingImportEdit {
|
||||
type_checking_edit,
|
||||
add_import_edit,
|
||||
})
|
||||
}
|
||||
|
||||
/// Find a reference to `typing.TYPE_CHECKING`.
|
||||
|
||||
@@ -195,8 +195,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))]
|
||||
#[test_case(Rule::UnusedPrivateTypeVar, Path::new("PYI018.py"))]
|
||||
#[test_case(Rule::UnusedPrivateTypeVar, Path::new("PYI018.pyi"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::helpers::map_subscript;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
@@ -6,7 +6,6 @@ use ruff_python_semantic::{Scope, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the presence of unused private `TypeVar`, `ParamSpec` or
|
||||
@@ -14,8 +13,7 @@ use crate::fix;
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// A private `TypeVar` that is defined but not used is likely a mistake. It
|
||||
/// should either be used, made public, or removed to avoid confusion. A type
|
||||
/// variable is considered "private" if its name starts with an underscore.
|
||||
/// should either be used, made public, or removed to avoid confusion.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```pyi
|
||||
@@ -25,11 +23,6 @@ use crate::fix;
|
||||
/// _T = typing.TypeVar("_T")
|
||||
/// _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety and availability
|
||||
/// This rule's fix is available when [`preview`] mode is enabled.
|
||||
/// It is always marked as unsafe, as it would break your code if the type
|
||||
/// variable is imported by another module.
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnusedPrivateTypeVar {
|
||||
type_var_like_name: String,
|
||||
@@ -37,8 +30,6 @@ pub(crate) struct UnusedPrivateTypeVar {
|
||||
}
|
||||
|
||||
impl Violation for UnusedPrivateTypeVar {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let UnusedPrivateTypeVar {
|
||||
@@ -47,16 +38,6 @@ impl Violation for UnusedPrivateTypeVar {
|
||||
} = self;
|
||||
format!("Private {type_var_like_kind} `{type_var_like_name}` is never used")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let UnusedPrivateTypeVar {
|
||||
type_var_like_name,
|
||||
type_var_like_kind,
|
||||
} = self;
|
||||
Some(format!(
|
||||
"Remove unused private {type_var_like_kind} `{type_var_like_name}`"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
@@ -197,7 +178,7 @@ pub(crate) fn unused_private_type_var(
|
||||
let Some(source) = binding.source else {
|
||||
continue;
|
||||
};
|
||||
let stmt @ Stmt::Assign(ast::StmtAssign { targets, value, .. }) =
|
||||
let Stmt::Assign(ast::StmtAssign { targets, value, .. }) =
|
||||
checker.semantic().statement(source)
|
||||
else {
|
||||
continue;
|
||||
@@ -229,20 +210,13 @@ pub(crate) fn unused_private_type_var(
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
diagnostics.push(Diagnostic::new(
|
||||
UnusedPrivateTypeVar {
|
||||
type_var_like_name: id.to_string(),
|
||||
type_var_like_kind: type_var_like_kind.to_string(),
|
||||
},
|
||||
binding.range(),
|
||||
);
|
||||
|
||||
if checker.settings.preview.is_enabled() {
|
||||
let edit = fix::edits::delete_stmt(stmt, None, checker.locator(), checker.indexer());
|
||||
diagnostic.set_fix(Fix::unsafe_edit(edit));
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ PYI018.py:6:1: PYI018 Private TypeVar `_T` is never used
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
|
|
||||
= help: Remove unused private TypeVar `_T`
|
||||
|
||||
PYI018.py:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
||||
|
|
||||
@@ -20,7 +19,6 @@ PYI018.py:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts`
|
||||
|
||||
PYI018.py:8:1: PYI018 Private ParamSpec `_P` is never used
|
||||
|
|
||||
@@ -31,7 +29,6 @@ PYI018.py:8:1: PYI018 Private ParamSpec `_P` is never used
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P`
|
||||
|
||||
PYI018.py:9:1: PYI018 Private ParamSpec `_P2` is never used
|
||||
|
|
||||
@@ -41,7 +38,6 @@ PYI018.py:9:1: PYI018 Private ParamSpec `_P2` is never used
|
||||
| ^^^ PYI018
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P2`
|
||||
|
||||
PYI018.py:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
||||
|
|
||||
@@ -52,4 +48,3 @@ PYI018.py:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts2`
|
||||
|
||||
@@ -10,7 +10,6 @@ PYI018.pyi:6:1: PYI018 Private TypeVar `_T` is never used
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
|
|
||||
= help: Remove unused private TypeVar `_T`
|
||||
|
||||
PYI018.pyi:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
||||
|
|
||||
@@ -20,7 +19,6 @@ PYI018.pyi:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts`
|
||||
|
||||
PYI018.pyi:8:1: PYI018 Private ParamSpec `_P` is never used
|
||||
|
|
||||
@@ -31,7 +29,6 @@ PYI018.pyi:8:1: PYI018 Private ParamSpec `_P` is never used
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P`
|
||||
|
||||
PYI018.pyi:9:1: PYI018 Private ParamSpec `_P2` is never used
|
||||
|
|
||||
@@ -41,7 +38,6 @@ PYI018.pyi:9:1: PYI018 Private ParamSpec `_P2` is never used
|
||||
| ^^^ PYI018
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P2`
|
||||
|
||||
PYI018.pyi:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
||||
|
|
||||
@@ -52,4 +48,3 @@ PYI018.pyi:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts2`
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI018.py:6:1: PYI018 [*] Private TypeVar `_T` is never used
|
||||
|
|
||||
4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 |
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
| ^^ PYI018
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
|
|
||||
= help: Remove unused private TypeVar `_T`
|
||||
|
||||
ℹ Unsafe fix
|
||||
3 3 | from typing import TypeVar
|
||||
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 5 |
|
||||
6 |-_T = typing.TypeVar("_T")
|
||||
7 6 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 7 | _P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
|
||||
PYI018.py:7:1: PYI018 [*] Private TypeVarTuple `_Ts` is never used
|
||||
|
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
| ^^^ PYI018
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts`
|
||||
|
||||
ℹ Unsafe fix
|
||||
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 5 |
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 |-_Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 7 | _P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
||||
PYI018.py:8:1: PYI018 [*] Private ParamSpec `_P` is never used
|
||||
|
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
| ^^ PYI018
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P`
|
||||
|
||||
ℹ Unsafe fix
|
||||
5 5 |
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 |-_P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
|
||||
PYI018.py:9:1: PYI018 [*] Private ParamSpec `_P2` is never used
|
||||
|
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
| ^^^ PYI018
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 8 | _P = ParamSpec("_P")
|
||||
9 |-_P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
12 11 | # OK
|
||||
|
||||
PYI018.py:10:1: PYI018 [*] Private TypeVarTuple `_Ts2` is never used
|
||||
|
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
| ^^^^ PYI018
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 8 | _P = ParamSpec("_P")
|
||||
9 9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 |-_Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
12 11 | # OK
|
||||
13 12 | _UsedTypeVar = TypeVar("_UsedTypeVar")
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI018.pyi:6:1: PYI018 [*] Private TypeVar `_T` is never used
|
||||
|
|
||||
4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 |
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
| ^^ PYI018
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
|
|
||||
= help: Remove unused private TypeVar `_T`
|
||||
|
||||
ℹ Unsafe fix
|
||||
3 3 | from typing import TypeVar
|
||||
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 5 |
|
||||
6 |-_T = typing.TypeVar("_T")
|
||||
7 6 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 7 | _P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
|
||||
PYI018.pyi:7:1: PYI018 [*] Private TypeVarTuple `_Ts` is never used
|
||||
|
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
| ^^^ PYI018
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts`
|
||||
|
||||
ℹ Unsafe fix
|
||||
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 5 |
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 |-_Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 7 | _P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
||||
PYI018.pyi:8:1: PYI018 [*] Private ParamSpec `_P` is never used
|
||||
|
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
| ^^ PYI018
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P`
|
||||
|
||||
ℹ Unsafe fix
|
||||
5 5 |
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 |-_P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
|
||||
PYI018.pyi:9:1: PYI018 [*] Private ParamSpec `_P2` is never used
|
||||
|
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
| ^^^ PYI018
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 8 | _P = ParamSpec("_P")
|
||||
9 |-_P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
12 11 | # OK
|
||||
|
||||
PYI018.pyi:10:1: PYI018 [*] Private TypeVarTuple `_Ts2` is never used
|
||||
|
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
| ^^^^ PYI018
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 8 | _P = ParamSpec("_P")
|
||||
9 9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 |-_Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
12 11 | # OK
|
||||
13 12 | _UsedTypeVar = TypeVar("_UsedTypeVar")
|
||||
@@ -226,33 +226,6 @@ SIM108.py:167:1: SIM108 [*] Use ternary operator `z = 1 if True else other` inst
|
||||
172 169 | if False:
|
||||
173 170 | z = 1
|
||||
|
||||
SIM108.py:172:1: SIM108 [*] Use ternary operator `z = 1 if False else other` instead of `if`-`else`-block
|
||||
|
|
||||
170 | z = other
|
||||
171 |
|
||||
172 | / if False:
|
||||
173 | | z = 1
|
||||
174 | | else:
|
||||
175 | | z = other
|
||||
| |_____________^ SIM108
|
||||
176 |
|
||||
177 | if 1:
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = 1 if False else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
169 169 | else:
|
||||
170 170 | z = other
|
||||
171 171 |
|
||||
172 |-if False:
|
||||
173 |- z = 1
|
||||
174 |-else:
|
||||
175 |- z = other
|
||||
172 |+z = 1 if False else other
|
||||
176 173 |
|
||||
177 174 | if 1:
|
||||
178 175 | z = True
|
||||
|
||||
SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block
|
||||
|
|
||||
175 | z = other
|
||||
|
||||
@@ -524,41 +524,4 @@ mod tests {
|
||||
);
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
r"
|
||||
from __future__ import annotations
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
def foo(tb: TracebackType): ...
|
||||
",
|
||||
"github_issue_15681_regression_test"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib # TC003
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
def foo(tb: TracebackType) -> pathlib.Path: ...
|
||||
",
|
||||
"github_issue_15681_fix_test"
|
||||
)]
|
||||
fn contents_preview(contents: &str, snapshot: &str) {
|
||||
let diagnostics = test_snippet(
|
||||
contents,
|
||||
&settings::LinterSettings {
|
||||
preview: settings::types::PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rules(Linter::Flake8TypeChecking.rules())
|
||||
},
|
||||
);
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
<filename>:4:8: TC003 [*] Move standard library import `pathlib` into a type-checking block
|
||||
|
|
||||
2 | from __future__ import annotations
|
||||
3 |
|
||||
4 | import pathlib # TC003
|
||||
| ^^^^^^^ TC003
|
||||
5 |
|
||||
6 | TYPE_CHECKING = False
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 1 |
|
||||
2 2 | from __future__ import annotations
|
||||
3 3 |
|
||||
4 |-import pathlib # TC003
|
||||
5 4 |
|
||||
6 5 | TYPE_CHECKING = False
|
||||
7 6 | if TYPE_CHECKING:
|
||||
7 |+ import pathlib
|
||||
8 8 | from types import TracebackType
|
||||
9 9 |
|
||||
10 10 | def foo(tb: TracebackType) -> pathlib.Path: ...
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
|
||||
@@ -54,7 +54,7 @@ pub(crate) fn invalid_class_name(
|
||||
name: &str,
|
||||
ignore_names: &IgnoreNames,
|
||||
) -> Option<Diagnostic> {
|
||||
let stripped = name.trim_start_matches('_');
|
||||
let stripped = name.strip_prefix('_').unwrap_or(name);
|
||||
if !stripped.chars().next().is_some_and(char::is_uppercase) || stripped.contains('_') {
|
||||
// Ignore any explicitly-allowed names.
|
||||
if ignore_names.matches(name) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pep8_naming/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
N801.py:1:7: N801 Class name `bad` should use CapWords convention
|
||||
|
|
||||
@@ -15,41 +16,23 @@ N801.py:5:7: N801 Class name `_bad` should use CapWords convention
|
||||
6 | pass
|
||||
|
|
||||
|
||||
N801.py:9:7: N801 Class name `__bad` should use CapWords convention
|
||||
N801.py:9:7: N801 Class name `bad_class` should use CapWords convention
|
||||
|
|
||||
9 | class __bad:
|
||||
| ^^^^^ N801
|
||||
9 | class bad_class:
|
||||
| ^^^^^^^^^ N801
|
||||
10 | pass
|
||||
|
|
||||
|
||||
N801.py:13:7: N801 Class name `bad_class` should use CapWords convention
|
||||
N801.py:13:7: N801 Class name `Bad_Class` should use CapWords convention
|
||||
|
|
||||
13 | class bad_class:
|
||||
13 | class Bad_Class:
|
||||
| ^^^^^^^^^ N801
|
||||
14 | pass
|
||||
|
|
||||
|
||||
N801.py:17:7: N801 Class name `Bad_Class` should use CapWords convention
|
||||
N801.py:17:7: N801 Class name `BAD_CLASS` should use CapWords convention
|
||||
|
|
||||
17 | class Bad_Class:
|
||||
17 | class BAD_CLASS:
|
||||
| ^^^^^^^^^ N801
|
||||
18 | pass
|
||||
|
|
||||
|
||||
N801.py:20:7: N801 Class name `Bad__Class` should use CapWords convention
|
||||
|
|
||||
18 | pass
|
||||
19 |
|
||||
20 | class Bad__Class:
|
||||
| ^^^^^^^^^^ N801
|
||||
21 | pass
|
||||
|
|
||||
|
||||
N801.py:23:7: N801 Class name `BAD_CLASS` should use CapWords convention
|
||||
|
|
||||
21 | pass
|
||||
22 |
|
||||
23 | class BAD_CLASS:
|
||||
| ^^^^^^^^^ N801
|
||||
24 | pass
|
||||
|
|
||||
|
||||
@@ -44,7 +44,6 @@ mod tests {
|
||||
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_1.py"))]
|
||||
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))]
|
||||
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_3.py"))]
|
||||
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_4.py"))]
|
||||
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.ipynb"))]
|
||||
#[test_case(Rule::MultipleImportsOnOneLine, Path::new("E40.py"))]
|
||||
#[test_case(Rule::MultipleStatementsOnOneLineColon, Path::new("E70.py"))]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
@@ -57,7 +57,7 @@ mod native_literals;
|
||||
mod open_alias;
|
||||
mod os_error_alias;
|
||||
mod outdated_version_block;
|
||||
pub(crate) mod pep695;
|
||||
mod pep695;
|
||||
mod printf_string_formatting;
|
||||
mod quoted_annotation;
|
||||
mod redundant_open_modes;
|
||||
|
||||
@@ -9,7 +9,7 @@ use ruff_python_ast::{
|
||||
self as ast,
|
||||
name::Name,
|
||||
visitor::{self, Visitor},
|
||||
Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, TypeParam,
|
||||
Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, TypeParam,
|
||||
TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
|
||||
};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
@@ -28,7 +28,7 @@ mod non_pep695_type_alias;
|
||||
mod private_type_parameter;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum TypeVarRestriction<'a> {
|
||||
enum TypeVarRestriction<'a> {
|
||||
/// A type variable with a bound, e.g., `TypeVar("T", bound=int)`.
|
||||
Bound(&'a Expr),
|
||||
/// A type variable with constraints, e.g., `TypeVar("T", int, str)`.
|
||||
@@ -39,25 +39,25 @@ pub(crate) enum TypeVarRestriction<'a> {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum TypeParamKind {
|
||||
enum TypeParamKind {
|
||||
TypeVar,
|
||||
TypeVarTuple,
|
||||
ParamSpec,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TypeVar<'a> {
|
||||
pub(crate) name: &'a str,
|
||||
pub(crate) restriction: Option<TypeVarRestriction<'a>>,
|
||||
pub(crate) kind: TypeParamKind,
|
||||
pub(crate) default: Option<&'a Expr>,
|
||||
struct TypeVar<'a> {
|
||||
name: &'a str,
|
||||
restriction: Option<TypeVarRestriction<'a>>,
|
||||
kind: TypeParamKind,
|
||||
default: Option<&'a Expr>,
|
||||
}
|
||||
|
||||
/// Wrapper for formatting a sequence of [`TypeVar`]s for use as a generic type parameter (e.g. `[T,
|
||||
/// *Ts, **P]`). See [`DisplayTypeVar`] for further details.
|
||||
pub(crate) struct DisplayTypeVars<'a> {
|
||||
pub(crate) type_vars: &'a [TypeVar<'a>],
|
||||
pub(crate) source: &'a str,
|
||||
struct DisplayTypeVars<'a> {
|
||||
type_vars: &'a [TypeVar<'a>],
|
||||
source: &'a str,
|
||||
}
|
||||
|
||||
impl Display for DisplayTypeVars<'_> {
|
||||
@@ -81,7 +81,7 @@ impl Display for DisplayTypeVars<'_> {
|
||||
|
||||
/// Used for displaying `type_var`. `source` is the whole file, which will be sliced to recover the
|
||||
/// `TypeVarRestriction` values for generic bounds and constraints.
|
||||
pub(crate) struct DisplayTypeVar<'a> {
|
||||
struct DisplayTypeVar<'a> {
|
||||
type_var: &'a TypeVar<'a>,
|
||||
source: &'a str,
|
||||
}
|
||||
@@ -192,34 +192,6 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a TypeParam> for TypeVar<'a> {
|
||||
fn from(param: &'a TypeParam) -> Self {
|
||||
let (kind, restriction) = match param {
|
||||
TypeParam::TypeVarTuple(_) => (TypeParamKind::TypeVarTuple, None),
|
||||
TypeParam::ParamSpec(_) => (TypeParamKind::ParamSpec, None),
|
||||
|
||||
TypeParam::TypeVar(param) => {
|
||||
let restriction = match param.bound.as_deref() {
|
||||
None => None,
|
||||
Some(Expr::Tuple(constraints)) => Some(TypeVarRestriction::Constraint(
|
||||
constraints.elts.iter().collect::<Vec<_>>(),
|
||||
)),
|
||||
Some(bound) => Some(TypeVarRestriction::Bound(bound)),
|
||||
};
|
||||
|
||||
(TypeParamKind::TypeVar, restriction)
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
name: param.name(),
|
||||
kind,
|
||||
restriction,
|
||||
default: param.default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TypeVarReferenceVisitor<'a> {
|
||||
vars: Vec<TypeVar<'a>>,
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
@@ -270,7 +242,7 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expr_name_to_type_var<'a>(
|
||||
fn expr_name_to_type_var<'a>(
|
||||
semantic: &'a SemanticModel,
|
||||
name: &'a ExprName,
|
||||
) -> Option<TypeVar<'a>> {
|
||||
@@ -377,18 +349,3 @@ fn check_type_vars(vars: Vec<TypeVar<'_>>) -> Option<Vec<TypeVar<'_>>> {
|
||||
== vars.len())
|
||||
.then_some(vars)
|
||||
}
|
||||
|
||||
/// Search `class_bases` for a `typing.Generic` base class. Returns the `Generic` expression (if
|
||||
/// any), along with its index in the class's bases tuple.
|
||||
pub(crate) fn find_generic<'a>(
|
||||
class_bases: &'a Arguments,
|
||||
semantic: &SemanticModel,
|
||||
) -> Option<(usize, &'a ExprSubscript)> {
|
||||
class_bases.args.iter().enumerate().find_map(|(idx, expr)| {
|
||||
expr.as_subscript_expr().and_then(|sub_expr| {
|
||||
semantic
|
||||
.match_typing_expr(&sub_expr.value, "Generic")
|
||||
.then_some((idx, sub_expr))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{ExprSubscript, StmtClassDef};
|
||||
use ruff_python_ast::{Arguments, ExprSubscript, StmtClassDef};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::{remove_argument, Parentheses};
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
use super::{
|
||||
check_type_vars, find_generic, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor,
|
||||
};
|
||||
use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor};
|
||||
|
||||
/// ## What it does
|
||||
///
|
||||
@@ -212,3 +211,18 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Search `class_bases` for a `typing.Generic` base class. Returns the `Generic` expression (if
|
||||
/// any), along with its index in the class's bases tuple.
|
||||
fn find_generic<'a>(
|
||||
class_bases: &'a Arguments,
|
||||
semantic: &SemanticModel,
|
||||
) -> Option<(usize, &'a ExprSubscript)> {
|
||||
class_bases.args.iter().enumerate().find_map(|(idx, expr)| {
|
||||
expr.as_subscript_expr().and_then(|sub_expr| {
|
||||
semantic
|
||||
.match_typing_expr(&sub_expr.value, "Generic")
|
||||
.then_some((idx, sub_expr))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -435,7 +435,6 @@ mod tests {
|
||||
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
|
||||
#[test_case(Rule::StarmapZip, Path::new("RUF058_0.py"))]
|
||||
#[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))]
|
||||
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::iter;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{
|
||||
Arguments, Expr, ExprStarred, ExprSubscript, ExprTuple, StmtClassDef, TypeParams,
|
||||
};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::{remove_argument, Parentheses};
|
||||
use crate::rules::pyupgrade::rules::pep695::{
|
||||
expr_name_to_type_var, find_generic, DisplayTypeVars, TypeParamKind, TypeVar,
|
||||
};
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for classes that have [PEP 695] [type parameter lists]
|
||||
/// while also inheriting from `typing.Generic` or `typing_extensions.Generic`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Such classes cause errors at runtime:
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import Generic, TypeVar
|
||||
///
|
||||
/// U = TypeVar("U")
|
||||
///
|
||||
/// # TypeError: Cannot inherit from Generic[...] multiple times.
|
||||
/// class C[T](Generic[U]): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import Generic, ParamSpec, TypeVar, TypeVarTuple
|
||||
///
|
||||
/// U = TypeVar("U")
|
||||
/// P = ParamSpec("P")
|
||||
/// Ts = TypeVarTuple("Ts")
|
||||
///
|
||||
///
|
||||
/// class C[T](Generic[U, P, *Ts]): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class C[T, U, **P, *Ts]: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// As the fix changes runtime behaviour, it is always marked as unsafe.
|
||||
/// Additionally, comments within the fix range will not be preserved.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: User-defined generic types](https://docs.python.org/3/library/typing.html#user-defined-generic-types)
|
||||
/// - [Python documentation: type parameter lists](https://docs.python.org/3/reference/compound_stmts.html#type-params)
|
||||
/// - [PEP 695 - Type Parameter Syntax](https://peps.python.org/pep-0695/)
|
||||
///
|
||||
/// [PEP 695]: https://peps.python.org/pep-0695/
|
||||
/// [type parameter lists]: https://docs.python.org/3/reference/compound_stmts.html#type-params
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct ClassWithMixedTypeVars;
|
||||
|
||||
impl Violation for ClassWithMixedTypeVars {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Class with type parameter list inherits from `Generic`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Remove `Generic` base class".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF053
|
||||
pub(crate) fn class_with_mixed_type_vars(checker: &mut Checker, class_def: &StmtClassDef) {
|
||||
if checker.settings.target_version < PythonVersion::Py312 {
|
||||
return;
|
||||
}
|
||||
|
||||
let StmtClassDef {
|
||||
type_params,
|
||||
arguments,
|
||||
..
|
||||
} = class_def;
|
||||
|
||||
let Some(type_params) = type_params else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(arguments) = arguments else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((generic_base, old_style_type_vars)) =
|
||||
typing_generic_base_and_arguments(arguments, checker.semantic())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(ClassWithMixedTypeVars, generic_base.range);
|
||||
|
||||
diagnostic.try_set_optional_fix(|| {
|
||||
convert_type_vars(
|
||||
generic_base,
|
||||
old_style_type_vars,
|
||||
type_params,
|
||||
arguments,
|
||||
checker,
|
||||
)
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
fn typing_generic_base_and_arguments<'a>(
|
||||
class_arguments: &'a Arguments,
|
||||
semantic: &SemanticModel,
|
||||
) -> Option<(&'a ExprSubscript, &'a Expr)> {
|
||||
let (_, base @ ExprSubscript { slice, .. }) = find_generic(class_arguments, semantic)?;
|
||||
|
||||
Some((base, slice.as_ref()))
|
||||
}
|
||||
|
||||
fn convert_type_vars(
|
||||
generic_base: &ExprSubscript,
|
||||
old_style_type_vars: &Expr,
|
||||
type_params: &TypeParams,
|
||||
class_arguments: &Arguments,
|
||||
checker: &Checker,
|
||||
) -> anyhow::Result<Option<Fix>> {
|
||||
let mut type_vars: Vec<_> = type_params.type_params.iter().map(TypeVar::from).collect();
|
||||
|
||||
let semantic = checker.semantic();
|
||||
let converted_type_vars = match old_style_type_vars {
|
||||
Expr::Tuple(ExprTuple { elts, .. }) => {
|
||||
generic_arguments_to_type_vars(elts.iter(), type_params, semantic)
|
||||
}
|
||||
expr @ (Expr::Subscript(_) | Expr::Name(_)) => {
|
||||
generic_arguments_to_type_vars(iter::once(expr), type_params, semantic)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let Some(converted_type_vars) = converted_type_vars else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
type_vars.extend(converted_type_vars);
|
||||
|
||||
let source = checker.source();
|
||||
let new_type_params = DisplayTypeVars {
|
||||
type_vars: &type_vars,
|
||||
source,
|
||||
};
|
||||
|
||||
let remove_generic_base =
|
||||
remove_argument(generic_base, class_arguments, Parentheses::Remove, source)?;
|
||||
let replace_type_params =
|
||||
Edit::range_replacement(new_type_params.to_string(), type_params.range);
|
||||
|
||||
Ok(Some(Fix::unsafe_edits(
|
||||
remove_generic_base,
|
||||
[replace_type_params],
|
||||
)))
|
||||
}
|
||||
|
||||
/// Returns the type variables `exprs` represent.
|
||||
///
|
||||
/// If at least one of them cannot be converted to [`TypeVar`],
|
||||
/// `None` is returned.
|
||||
fn generic_arguments_to_type_vars<'a>(
|
||||
exprs: impl Iterator<Item = &'a Expr>,
|
||||
existing_type_params: &TypeParams,
|
||||
semantic: &'a SemanticModel,
|
||||
) -> Option<Vec<TypeVar<'a>>> {
|
||||
let mut type_vars = vec![];
|
||||
let mut encountered: FxHashSet<&str> = existing_type_params
|
||||
.iter()
|
||||
.map(|tp| tp.name().as_str())
|
||||
.collect();
|
||||
|
||||
for expr in exprs {
|
||||
let (name, unpacked) = match expr {
|
||||
Expr::Name(name) => (name, false),
|
||||
Expr::Starred(ExprStarred { value, .. }) => (value.as_name_expr()?, true),
|
||||
|
||||
Expr::Subscript(ExprSubscript { value, slice, .. }) => {
|
||||
if !semantic.match_typing_expr(value, "Unpack") {
|
||||
return None;
|
||||
}
|
||||
|
||||
(slice.as_name_expr()?, true)
|
||||
}
|
||||
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if !encountered.insert(name.id.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let type_var = expr_name_to_type_var(semantic, name)?;
|
||||
|
||||
if !type_var_is_valid(&type_var, unpacked) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// TODO: Type parameter defaults
|
||||
if type_var.default.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
type_vars.push(type_var);
|
||||
}
|
||||
|
||||
Some(type_vars)
|
||||
}
|
||||
|
||||
/// Returns true in the following cases:
|
||||
///
|
||||
/// * If `type_var` is a `TypeVar`:
|
||||
/// * It must not be unpacked
|
||||
/// * If `type_var` is a `TypeVarTuple`:
|
||||
/// * It must be unpacked
|
||||
/// * It must not have any restrictions
|
||||
/// * If `type_var` is a `ParamSpec`:
|
||||
/// * It must not be unpacked
|
||||
/// * It must not have any restrictions
|
||||
fn type_var_is_valid(type_var: &TypeVar, unpacked: bool) -> bool {
|
||||
let is_type_var_tuple = matches!(&type_var.kind, TypeParamKind::TypeVarTuple);
|
||||
|
||||
if is_type_var_tuple && !unpacked || !is_type_var_tuple && unpacked {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !matches!(&type_var.kind, TypeParamKind::TypeVar) && type_var.restriction.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -2,7 +2,6 @@ pub(crate) use ambiguous_unicode_character::*;
|
||||
pub(crate) use assert_with_print_message::*;
|
||||
pub(crate) use assignment_in_assert::*;
|
||||
pub(crate) use asyncio_dangling_task::*;
|
||||
pub(crate) use class_with_mixed_type_vars::*;
|
||||
pub(crate) use collection_literal_concatenation::*;
|
||||
pub(crate) use dataclass_enum::*;
|
||||
pub(crate) use decimal_from_float_literal::*;
|
||||
@@ -56,7 +55,6 @@ mod ambiguous_unicode_character;
|
||||
mod assert_with_print_message;
|
||||
mod assignment_in_assert;
|
||||
mod asyncio_dangling_task;
|
||||
mod class_with_mixed_type_vars;
|
||||
mod collection_literal_concatenation;
|
||||
mod confusables;
|
||||
mod dataclass_enum;
|
||||
|
||||
@@ -24,12 +24,7 @@ use crate::rules::ruff::rules::helpers::{
|
||||
/// `typing.ClassVar`. When mutability is not required, values should be
|
||||
/// immutable types, like `tuple` or `frozenset`.
|
||||
///
|
||||
/// For mutable variables, prefer to initialize them in `__init__`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// Using `ClassVar` and imutable types:
|
||||
///
|
||||
/// ```python
|
||||
/// class A:
|
||||
/// mutable_default: list[int] = []
|
||||
@@ -37,7 +32,6 @@ use crate::rules::ruff::rules::helpers::{
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import ClassVar
|
||||
///
|
||||
@@ -46,27 +40,6 @@ use crate::rules::ruff::rules::helpers::{
|
||||
/// mutable_default: ClassVar[list[int]] = []
|
||||
/// immutable_default: tuple[int, ...] = ()
|
||||
/// ```
|
||||
///
|
||||
/// Using instance variables instead of class variables:
|
||||
///
|
||||
/// ```python
|
||||
/// class A:
|
||||
/// instance_dict: dict[str, str] = {"key": "value"}
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class A:
|
||||
/// instance_dict: ClassVar[dict[str, str]]
|
||||
///
|
||||
/// def __init__(self) -> None:
|
||||
/// self.instance_dict: dict[str, str] = {"key": "value"}
|
||||
/// ```
|
||||
///
|
||||
/// In cases where memory efficiency is a priority, `MappingProxyType`
|
||||
/// can be used to create immutable dictionaries that are shared between
|
||||
/// instances.
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct MutableClassDefault;
|
||||
|
||||
|
||||
@@ -1,475 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF053.py:23:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
21 | ### Errors
|
||||
22 |
|
||||
23 | class C[T](Generic[_A]): ...
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
24 | class C[T](Generic[_B], str): ...
|
||||
25 | class C[T](int, Generic[_C]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
20 20 |
|
||||
21 21 | ### Errors
|
||||
22 22 |
|
||||
23 |-class C[T](Generic[_A]): ...
|
||||
23 |+class C[T, _A]: ...
|
||||
24 24 | class C[T](Generic[_B], str): ...
|
||||
25 25 | class C[T](int, Generic[_C]): ...
|
||||
26 26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
|
||||
|
||||
RUF053.py:24:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
23 | class C[T](Generic[_A]): ...
|
||||
24 | class C[T](Generic[_B], str): ...
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
25 | class C[T](int, Generic[_C]): ...
|
||||
26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
21 21 | ### Errors
|
||||
22 22 |
|
||||
23 23 | class C[T](Generic[_A]): ...
|
||||
24 |-class C[T](Generic[_B], str): ...
|
||||
24 |+class C[T, _B: int](str): ...
|
||||
25 25 | class C[T](int, Generic[_C]): ...
|
||||
26 26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
|
||||
27 27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
|
||||
|
||||
RUF053.py:25:17: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
23 | class C[T](Generic[_A]): ...
|
||||
24 | class C[T](Generic[_B], str): ...
|
||||
25 | class C[T](int, Generic[_C]): ...
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
|
||||
27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
22 22 |
|
||||
23 23 | class C[T](Generic[_A]): ...
|
||||
24 24 | class C[T](Generic[_B], str): ...
|
||||
25 |-class C[T](int, Generic[_C]): ...
|
||||
25 |+class C[T, _C: (str, bytes)](int): ...
|
||||
26 26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
|
||||
27 27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
|
||||
28 28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
|
||||
|
||||
RUF053.py:26:19: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
24 | class C[T](Generic[_B], str): ...
|
||||
25 | class C[T](int, Generic[_C]): ...
|
||||
26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
|
||||
28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:27:12: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
25 | class C[T](int, Generic[_C]): ...
|
||||
26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
|
||||
27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:28:22: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
|
||||
27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
|
||||
28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
29 |
|
||||
30 | class C[*Ts](Generic[*_As]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:30:14: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
|
||||
29 |
|
||||
30 | class C[*Ts](Generic[*_As]): ...
|
||||
| ^^^^^^^^^^^^^ RUF053
|
||||
31 | class C[*Ts](Generic[Unpack[_As]]): ...
|
||||
32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
27 27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
|
||||
28 28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
|
||||
29 29 |
|
||||
30 |-class C[*Ts](Generic[*_As]): ...
|
||||
30 |+class C[*Ts, *_As]: ...
|
||||
31 31 | class C[*Ts](Generic[Unpack[_As]]): ...
|
||||
32 32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
|
||||
33 33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
|
||||
|
||||
RUF053.py:31:14: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
30 | class C[*Ts](Generic[*_As]): ...
|
||||
31 | class C[*Ts](Generic[Unpack[_As]]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
|
||||
33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
28 28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
|
||||
29 29 |
|
||||
30 30 | class C[*Ts](Generic[*_As]): ...
|
||||
31 |-class C[*Ts](Generic[Unpack[_As]]): ...
|
||||
31 |+class C[*Ts, *_As]: ...
|
||||
32 32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
|
||||
33 33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
|
||||
34 34 |
|
||||
|
||||
RUF053.py:32:14: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
30 | class C[*Ts](Generic[*_As]): ...
|
||||
31 | class C[*Ts](Generic[Unpack[_As]]): ...
|
||||
32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:33:44: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
31 | class C[*Ts](Generic[Unpack[_As]]): ...
|
||||
32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
|
||||
33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
|
||||
| ^^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:36:14: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
36 | class C[**P](Generic[_P1]): ...
|
||||
| ^^^^^^^^^^^^ RUF053
|
||||
37 | class C[**P](Generic[_P2]): ...
|
||||
38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
33 33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
|
||||
34 34 |
|
||||
35 35 |
|
||||
36 |-class C[**P](Generic[_P1]): ...
|
||||
36 |+class C[**P, **_P1]: ...
|
||||
37 37 | class C[**P](Generic[_P2]): ...
|
||||
38 38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
|
||||
39 39 |
|
||||
|
||||
RUF053.py:37:14: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
36 | class C[**P](Generic[_P1]): ...
|
||||
37 | class C[**P](Generic[_P2]): ...
|
||||
| ^^^^^^^^^^^^ RUF053
|
||||
38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:38:14: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
36 | class C[**P](Generic[_P1]): ...
|
||||
37 | class C[**P](Generic[_P2]): ...
|
||||
38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
|
||||
| ^^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:41:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
41 | class C[T](Generic[T, _A]): ...
|
||||
| ^^^^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
38 38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
|
||||
39 39 |
|
||||
40 40 |
|
||||
41 |-class C[T](Generic[T, _A]): ...
|
||||
41 |+class C[T, _A]: ...
|
||||
42 42 |
|
||||
43 43 |
|
||||
44 44 | # See `is_existing_param_of_same_class`.
|
||||
|
||||
RUF053.py:47:35: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
45 | # `expr_name_to_type_var` doesn't handle named expressions,
|
||||
46 | # only simple assignments, so there is no fix.
|
||||
47 | class C[T: (_Z := TypeVar('_Z'))](Generic[_Z]): ...
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:51:16: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
50 | class C(Generic[_B]):
|
||||
51 | class D[T](Generic[_B, T]): ...
|
||||
| ^^^^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
48 48 |
|
||||
49 49 |
|
||||
50 50 | class C(Generic[_B]):
|
||||
51 |- class D[T](Generic[_B, T]): ...
|
||||
51 |+ class D[T, _B: int]: ...
|
||||
52 52 |
|
||||
53 53 |
|
||||
54 54 | class C[T]:
|
||||
|
||||
RUF053.py:55:16: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
54 | class C[T]:
|
||||
55 | class D[U](Generic[T, U]): ...
|
||||
| ^^^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:60:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
58 | # In a single run, only the first is reported.
|
||||
59 | # Others will be reported/fixed in following iterations.
|
||||
60 | class C[T](Generic[_C], Generic[_D]): ...
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
61 | class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
57 57 |
|
||||
58 58 | # In a single run, only the first is reported.
|
||||
59 59 | # Others will be reported/fixed in following iterations.
|
||||
60 |-class C[T](Generic[_C], Generic[_D]): ...
|
||||
60 |+class C[T, _C: (str, bytes)](Generic[_D]): ...
|
||||
61 61 | class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults
|
||||
62 62 |
|
||||
63 63 |
|
||||
|
||||
RUF053.py:61:30: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
59 | # Others will be reported/fixed in following iterations.
|
||||
60 | class C[T](Generic[_C], Generic[_D]): ...
|
||||
61 | class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:66:3: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
64 | class C[
|
||||
65 | T # Comment
|
||||
66 | ](Generic[_E]): ... # TODO: Type parameter defaults
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:69:12: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
69 | class C[T](Generic[Generic[_F]]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
70 | class C[T](Generic[Unpack[_A]]): ...
|
||||
71 | class C[T](Generic[Unpack[_P1]]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:70:12: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
69 | class C[T](Generic[Generic[_F]]): ...
|
||||
70 | class C[T](Generic[Unpack[_A]]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
71 | class C[T](Generic[Unpack[_P1]]): ...
|
||||
72 | class C[T](Generic[Unpack[Unpack[_P2]]]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:71:12: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
69 | class C[T](Generic[Generic[_F]]): ...
|
||||
70 | class C[T](Generic[Unpack[_A]]): ...
|
||||
71 | class C[T](Generic[Unpack[_P1]]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
72 | class C[T](Generic[Unpack[Unpack[_P2]]]): ...
|
||||
73 | class C[T](Generic[Unpack[*_As]]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:72:12: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
70 | class C[T](Generic[Unpack[_A]]): ...
|
||||
71 | class C[T](Generic[Unpack[_P1]]): ...
|
||||
72 | class C[T](Generic[Unpack[Unpack[_P2]]]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
73 | class C[T](Generic[Unpack[*_As]]): ...
|
||||
74 | class C[T](Generic[Unpack[_As, _Bs]]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:73:12: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
71 | class C[T](Generic[Unpack[_P1]]): ...
|
||||
72 | class C[T](Generic[Unpack[Unpack[_P2]]]): ...
|
||||
73 | class C[T](Generic[Unpack[*_As]]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
74 | class C[T](Generic[Unpack[_As, _Bs]]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:74:12: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
72 | class C[T](Generic[Unpack[Unpack[_P2]]]): ...
|
||||
73 | class C[T](Generic[Unpack[*_As]]): ...
|
||||
74 | class C[T](Generic[Unpack[_As, _Bs]]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:77:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
77 | class C[T](Generic[_A, _A]): ...
|
||||
| ^^^^^^^^^^^^^^^ RUF053
|
||||
78 | class C[T](Generic[_A, Unpack[_As]]): ...
|
||||
79 | class C[T](Generic[*_As, _A]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
74 74 | class C[T](Generic[Unpack[_As, _Bs]]): ...
|
||||
75 75 |
|
||||
76 76 |
|
||||
77 |-class C[T](Generic[_A, _A]): ...
|
||||
77 |+class C[T, _A]: ...
|
||||
78 78 | class C[T](Generic[_A, Unpack[_As]]): ...
|
||||
79 79 | class C[T](Generic[*_As, _A]): ...
|
||||
80 80 |
|
||||
|
||||
RUF053.py:78:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
77 | class C[T](Generic[_A, _A]): ...
|
||||
78 | class C[T](Generic[_A, Unpack[_As]]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
79 | class C[T](Generic[*_As, _A]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
75 75 |
|
||||
76 76 |
|
||||
77 77 | class C[T](Generic[_A, _A]): ...
|
||||
78 |-class C[T](Generic[_A, Unpack[_As]]): ...
|
||||
78 |+class C[T, _A, *_As]: ...
|
||||
79 79 | class C[T](Generic[*_As, _A]): ...
|
||||
80 80 |
|
||||
81 81 |
|
||||
|
||||
RUF053.py:79:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
77 | class C[T](Generic[_A, _A]): ...
|
||||
78 | class C[T](Generic[_A, Unpack[_As]]): ...
|
||||
79 | class C[T](Generic[*_As, _A]): ...
|
||||
| ^^^^^^^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
76 76 |
|
||||
77 77 | class C[T](Generic[_A, _A]): ...
|
||||
78 78 | class C[T](Generic[_A, Unpack[_As]]): ...
|
||||
79 |-class C[T](Generic[*_As, _A]): ...
|
||||
79 |+class C[T, *_As, _A]: ...
|
||||
80 80 |
|
||||
81 81 |
|
||||
82 82 | from somewhere import APublicTypeVar
|
||||
|
||||
RUF053.py:83:12: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
82 | from somewhere import APublicTypeVar
|
||||
83 | class C[T](Generic[APublicTypeVar]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
84 | class C[T](Generic[APublicTypeVar, _A]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:84:12: RUF053 Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
82 | from somewhere import APublicTypeVar
|
||||
83 | class C[T](Generic[APublicTypeVar]): ...
|
||||
84 | class C[T](Generic[APublicTypeVar, _A]): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
RUF053.py:91:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
89 | # as named expressions are forbidden within type parameter lists.
|
||||
90 | # See also the `_Z` example above.
|
||||
91 | class C[T](Generic[_G]): ... # Should be moved down below eventually
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
88 88 | # The latter cannot be used as a PEP 695 constraint,
|
||||
89 89 | # as named expressions are forbidden within type parameter lists.
|
||||
90 90 | # See also the `_Z` example above.
|
||||
91 |-class C[T](Generic[_G]): ... # Should be moved down below eventually
|
||||
91 |+class C[T, _G: (str, a := int)]: ... # Should be moved down below eventually
|
||||
92 92 |
|
||||
93 93 |
|
||||
94 94 | # Single-element constraints should not be converted to a bound.
|
||||
|
||||
RUF053.py:95:20: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
94 | # Single-element constraints should not be converted to a bound.
|
||||
95 | class C[T: (str,)](Generic[_A]): ...
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
96 | class C[T: [a]](Generic[_A]): ...
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
92 92 |
|
||||
93 93 |
|
||||
94 94 | # Single-element constraints should not be converted to a bound.
|
||||
95 |-class C[T: (str,)](Generic[_A]): ...
|
||||
95 |+class C[T: (str), _A]: ...
|
||||
96 96 | class C[T: [a]](Generic[_A]): ...
|
||||
97 97 |
|
||||
98 98 |
|
||||
|
||||
RUF053.py:96:17: RUF053 [*] Class with type parameter list inherits from `Generic`
|
||||
|
|
||||
94 | # Single-element constraints should not be converted to a bound.
|
||||
95 | class C[T: (str,)](Generic[_A]): ...
|
||||
96 | class C[T: [a]](Generic[_A]): ...
|
||||
| ^^^^^^^^^^^ RUF053
|
||||
|
|
||||
= help: Remove `Generic` base class
|
||||
|
||||
ℹ Unsafe fix
|
||||
93 93 |
|
||||
94 94 | # Single-element constraints should not be converted to a bound.
|
||||
95 95 | class C[T: (str,)](Generic[_A]): ...
|
||||
96 |-class C[T: [a]](Generic[_A]): ...
|
||||
96 |+class C[T: [a], _A]: ...
|
||||
97 97 |
|
||||
98 98 |
|
||||
99 99 | # Existing bounds should not be deparenthesized.
|
||||
@@ -69,7 +69,7 @@ pub enum TomlSourceType {
|
||||
Unrecognized,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum PySourceType {
|
||||
/// The source is a Python file (`.py`).
|
||||
|
||||
@@ -374,15 +374,12 @@ impl fmt::Display for DocstringCode {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Eq, PartialEq, CacheKey)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(untagged, rename_all = "lowercase")
|
||||
)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
|
||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum DocstringCodeLineWidth {
|
||||
/// Wrap docstring code examples at a fixed line width.
|
||||
#[cfg_attr(feature = "schemars", schemars(schema_with = "schema::fixed"))]
|
||||
Fixed(LineWidth),
|
||||
|
||||
/// Respect the line length limit setting for the surrounding Python code.
|
||||
@@ -391,45 +388,27 @@ pub enum DocstringCodeLineWidth {
|
||||
feature = "serde",
|
||||
serde(deserialize_with = "deserialize_docstring_code_line_width_dynamic")
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", schemars(schema_with = "schema::dynamic"))]
|
||||
#[cfg_attr(feature = "schemars", schemars(with = "DynamicSchema"))]
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
/// A dummy type that is used to generate a schema for `DocstringCodeLineWidth::Dynamic`.
|
||||
#[cfg(feature = "schemars")]
|
||||
mod schema {
|
||||
use ruff_formatter::LineWidth;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{Metadata, Schema, SubschemaValidation};
|
||||
struct DynamicSchema;
|
||||
|
||||
/// A dummy type that is used to generate a schema for `DocstringCodeLineWidth::Dynamic`.
|
||||
pub(super) fn dynamic(_: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Object(schemars::schema::SchemaObject {
|
||||
const_value: Some("dynamic".to_string().into()),
|
||||
..Default::default()
|
||||
})
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for DynamicSchema {
|
||||
fn schema_name() -> String {
|
||||
"Dynamic".to_string()
|
||||
}
|
||||
|
||||
// We use a manual schema for `fixed` even thought it isn't strictly necessary according to the
|
||||
// JSON schema specification to work around a bug in Even Better TOML with `allOf`.
|
||||
// https://github.com/astral-sh/ruff/issues/15978#issuecomment-2639547101
|
||||
//
|
||||
// The only difference to the automatically derived schema is that we use `oneOf` instead of
|
||||
// `allOf`. There's no semantic difference between `allOf` and `oneOf` for single element lists.
|
||||
pub(super) fn fixed(gen: &mut SchemaGenerator) -> Schema {
|
||||
let schema = gen.subschema_for::<LineWidth>();
|
||||
Schema::Object(schemars::schema::SchemaObject {
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some(
|
||||
"Wrap docstring code examples at a fixed line width.".to_string(),
|
||||
),
|
||||
..Metadata::default()
|
||||
})),
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
one_of: Some(vec![schema]),
|
||||
..SubschemaValidation::default()
|
||||
})),
|
||||
fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
instance_type: Some(schemars::schema::InstanceType::String.into()),
|
||||
const_value: Some("dynamic".to_string().into()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,37 +8,33 @@ use crate::SemanticModel;
|
||||
/// import sys
|
||||
///
|
||||
/// sys.path.append("../")
|
||||
/// sys.path += ["../"]
|
||||
/// ```
|
||||
pub fn is_sys_path_modification(stmt: &Stmt, semantic: &SemanticModel) -> bool {
|
||||
match stmt {
|
||||
Stmt::Expr(ast::StmtExpr { value, range: _ }) => match value.as_ref() {
|
||||
Expr::Call(ast::ExprCall { func, .. }) => semantic
|
||||
.resolve_qualified_name(func.as_ref())
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
[
|
||||
"sys",
|
||||
"path",
|
||||
"append"
|
||||
| "insert"
|
||||
| "extend"
|
||||
| "remove"
|
||||
| "pop"
|
||||
| "clear"
|
||||
| "reverse"
|
||||
| "sort"
|
||||
]
|
||||
)
|
||||
}),
|
||||
_ => false,
|
||||
},
|
||||
Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => semantic
|
||||
.resolve_qualified_name(map_subscript(target))
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["sys", "path"])),
|
||||
_ => false,
|
||||
}
|
||||
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
|
||||
return false;
|
||||
};
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
semantic
|
||||
.resolve_qualified_name(func.as_ref())
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
[
|
||||
"sys",
|
||||
"path",
|
||||
"append"
|
||||
| "insert"
|
||||
| "extend"
|
||||
| "remove"
|
||||
| "pop"
|
||||
| "clear"
|
||||
| "reverse"
|
||||
| "sort"
|
||||
]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if a [`Stmt`] is an `os.environ` modification, as in:
|
||||
|
||||
@@ -382,22 +382,6 @@ pub fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool {
|
||||
let ast::StmtIf { test, .. } = stmt;
|
||||
|
||||
if semantic.use_new_type_checking_block_detection_semantics() {
|
||||
return match test.as_ref() {
|
||||
// As long as the symbol's name is "TYPE_CHECKING" we will treat it like `typing.TYPE_CHECKING`
|
||||
// for this specific check even if it's defined somewhere else, like the current module.
|
||||
// Ex) `if TYPE_CHECKING:`
|
||||
Expr::Name(ast::ExprName { id, .. }) => {
|
||||
id == "TYPE_CHECKING"
|
||||
// Ex) `if TC:` with `from typing import TYPE_CHECKING as TC`
|
||||
|| semantic.match_typing_expr(test, "TYPE_CHECKING")
|
||||
}
|
||||
// Ex) `if typing.TYPE_CHECKING:`
|
||||
Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr == "TYPE_CHECKING",
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// Ex) `if False:`
|
||||
if is_const_false(test) {
|
||||
return true;
|
||||
|
||||
@@ -2014,18 +2014,6 @@ impl<'a> SemanticModel<'a> {
|
||||
.intersects(SemanticModelFlags::DEFERRED_CLASS_BASE)
|
||||
}
|
||||
|
||||
/// Return `true` if we should use the new semantics to recognize
|
||||
/// type checking blocks. Previously we only recognized type checking
|
||||
/// blocks if `TYPE_CHECKING` was imported from a typing module.
|
||||
///
|
||||
/// With this feature flag enabled we recognize any symbol named
|
||||
/// `TYPE_CHECKING`, regardless of where it comes from to mirror
|
||||
/// what mypy and pyright do.
|
||||
pub const fn use_new_type_checking_block_detection_semantics(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::NEW_TYPE_CHECKING_BLOCK_DETECTION)
|
||||
}
|
||||
|
||||
/// Return an iterator over all bindings shadowed by the given [`BindingId`], within the
|
||||
/// containing scope, and across scopes.
|
||||
pub fn shadowed_bindings(
|
||||
@@ -2557,14 +2545,6 @@ bitflags! {
|
||||
/// [#13824]: https://github.com/astral-sh/ruff/issues/13824
|
||||
const NO_TYPE_CHECK = 1 << 30;
|
||||
|
||||
/// The model special-cases any symbol named `TYPE_CHECKING`.
|
||||
///
|
||||
/// Previously we only recognized `TYPE_CHECKING` if it was part of
|
||||
/// one of the configured `typing` modules. This flag exists to
|
||||
/// test out the semantic change only in preview. This flag will go
|
||||
/// away once this change has been stabilized.
|
||||
const NEW_TYPE_CHECKING_BLOCK_DETECTION = 1 << 31;
|
||||
|
||||
/// The context is in any type annotation.
|
||||
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -9,22 +9,22 @@ While `ruff server` supports the same feature set as [`ruff-lsp`](https://github
|
||||
settings are supported by `ruff server`. As such, this migration guide is primarily targeted at editors that lack
|
||||
explicit documentation for `ruff server` settings, such as Helix or Neovim.
|
||||
|
||||
Refer to the [setup guide](setup.md) for instructions on how to configure your editor to use `ruff server`.
|
||||
|
||||
## Unsupported Settings
|
||||
|
||||
Several `ruff-lsp` settings are not supported by `ruff server`. These are, as follows:
|
||||
|
||||
- `lint.run`: This setting is no longer relevant for the native language server, which runs on every
|
||||
keystroke by default
|
||||
- `lint.args`, `format.args`: These settings have been replaced by more granular settings in `ruff server` like [`lint.select`](settings.md#select), [`format.preview`](settings.md#format_preview),
|
||||
etc. along with the ability to provide a default configuration file using
|
||||
[`configuration`](settings.md#configuration)
|
||||
- [`path`](settings.md#path), [`interpreter`](settings.md#interpreter): These settings are no longer
|
||||
accepted by the language server but are still used by the VS Code extension. Refer to their
|
||||
respective documentation for more information on how it's being used by the extension.
|
||||
- `format.args`
|
||||
- `ignoreStandardLibrary`
|
||||
- `showNotifications`
|
||||
- `interpreter`
|
||||
- `lint.args`
|
||||
- `lint.run`
|
||||
- `path`
|
||||
|
||||
!!! note
|
||||
|
||||
Some of these settings, like `interpreter` and `path`, are still accepted by the VS Code
|
||||
extension. `path`, in particular, can be used to specify a dedicated binary to use when
|
||||
initializing `ruff server`. But the language server itself will no longer accept such settings.
|
||||
|
||||
## New Settings
|
||||
|
||||
|
||||
@@ -882,12 +882,6 @@ Whether to enable the Ruff extension. Modifying this setting requires restarting
|
||||
|
||||
### `format.args`
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
This setting is only used by [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) which is
|
||||
deprecated in favor of the native language server. Refer to the [migration
|
||||
guide](migration.md) for more information.
|
||||
|
||||
_**This setting is not used by the native language server.**_
|
||||
|
||||
Additional arguments to pass to the Ruff formatter.
|
||||
@@ -906,12 +900,6 @@ Additional arguments to pass to the Ruff formatter.
|
||||
|
||||
### `ignoreStandardLibrary`
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
This setting is only used by [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) which is
|
||||
deprecated in favor of the native language server. Refer to the [migration
|
||||
guide](migration.md) for more information.
|
||||
|
||||
_**This setting is not used by the native language server.**_
|
||||
|
||||
Whether to ignore files that are inferred to be part of the Python standard library.
|
||||
@@ -972,12 +960,6 @@ This setting depends on the [`ruff.nativeServer`](#nativeserver) setting:
|
||||
|
||||
### `lint.args`
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
This setting is only used by [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) which is
|
||||
deprecated in favor of the native language server. Refer to the [migration
|
||||
guide](migration.md) for more information.
|
||||
|
||||
_**This setting is not used by the native language server.**_
|
||||
|
||||
Additional arguments to pass to the Ruff linter.
|
||||
@@ -996,12 +978,6 @@ Additional arguments to pass to the Ruff linter.
|
||||
|
||||
### `lint.run`
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
This setting is only used by [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) which is
|
||||
deprecated in favor of the native language server. Refer to the [migration
|
||||
guide](migration.md) for more information.
|
||||
|
||||
_**This setting is not used by the native language server.**_
|
||||
|
||||
Run Ruff on every keystroke (`onType`) or on save (`onSave`).
|
||||
@@ -1020,14 +996,6 @@ Run Ruff on every keystroke (`onType`) or on save (`onSave`).
|
||||
|
||||
### `nativeServer`
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
This setting has been deprecated with the deprecation of
|
||||
[`ruff-lsp`](https://github.com/astral-sh/ruff-lsp). It was mainly used to provide a way to
|
||||
switch between the native language server and
|
||||
[`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) during the transition period. Refer to the
|
||||
[migration guide](migration.md) for more information.
|
||||
|
||||
Whether to use the native language server, [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) or
|
||||
automatically decide between the two based on the Ruff version and extension settings.
|
||||
|
||||
@@ -1078,12 +1046,6 @@ The first executable in the list which is exists is used. This setting takes pre
|
||||
|
||||
### `showNotifications`
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
This setting is only used by [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) which is
|
||||
deprecated in favor of the native language server. Refer to the [migration
|
||||
guide](migration.md) for more information.
|
||||
|
||||
Setting to control when a notification is shown.
|
||||
|
||||
**Default value**: `"off"`
|
||||
|
||||
@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
|
||||
stage: build
|
||||
interruptible: true
|
||||
image:
|
||||
name: ghcr.io/astral-sh/ruff:0.9.5-alpine
|
||||
name: ghcr.io/astral-sh/ruff:0.9.4-alpine
|
||||
before_script:
|
||||
- cd $CI_PROJECT_DIR
|
||||
- ruff --version
|
||||
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.9.5
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.9.5
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.9.5
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -365,7 +365,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.9.5
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "ruff"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
|
||||
readme = "README.md"
|
||||
|
||||
13
ruff.schema.json
generated
13
ruff.schema.json
generated
@@ -879,7 +879,7 @@
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Wrap docstring code examples at a fixed line width.",
|
||||
"oneOf": [
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/LineWidth"
|
||||
}
|
||||
@@ -887,10 +887,18 @@
|
||||
},
|
||||
{
|
||||
"description": "Respect the line length limit setting for the surrounding Python code.",
|
||||
"const": "dynamic"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Dynamic"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Dynamic": {
|
||||
"type": "string",
|
||||
"const": "dynamic"
|
||||
},
|
||||
"Flake8AnnotationsOptions": {
|
||||
"description": "Options for the `flake8-annotations` plugin.",
|
||||
"type": "object",
|
||||
@@ -3947,7 +3955,6 @@
|
||||
"RUF05",
|
||||
"RUF051",
|
||||
"RUF052",
|
||||
"RUF053",
|
||||
"RUF055",
|
||||
"RUF056",
|
||||
"RUF057",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "scripts"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
description = ""
|
||||
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user