Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
18bb0a36b6 [red-knot] Separate 'infer_expression_type' query 2025-02-04 16:42:49 +01:00
171 changed files with 1419 additions and 7974 deletions

View File

@@ -280,7 +280,7 @@ jobs:
cargo-build-msrv:
name: "cargo build (msrv)"
runs-on: depot-ubuntu-latest-8
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20

View File

@@ -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

8
Cargo.lock generated
View File

@@ -2518,14 +2518,12 @@ dependencies = [
"anyhow",
"camino",
"colored 3.0.0",
"insta",
"memchr",
"red_knot_python_semantic",
"red_knot_vendored",
"regex",
"ruff_db",
"ruff_index",
"ruff_python_ast",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
@@ -2640,7 +2638,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.9.5"
version = "0.9.4"
dependencies = [
"anyhow",
"argfile",
@@ -2872,7 +2870,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.9.5"
version = "0.9.4"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3190,7 +3188,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.9.5"
version = "0.9.4"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -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

View File

@@ -103,10 +103,10 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
exit_code: 1
----- stdout -----
error: lint:unresolved-import
--> <temp_dir>/child/test.py:2:6
--> <temp_dir>/child/test.py:2:1
|
2 | from utils import add
| ^^^^^ Cannot resolve import `utils`
| ^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `utils`
3 |
4 | stat = add(10, 15)
|

View File

@@ -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),

View File

@@ -210,8 +210,6 @@ def get_str() -> str:
return "a"
class C:
z: int
def __init__(self) -> None:
self.x = get_int()
self.y: int = 1
@@ -222,14 +220,12 @@ class C:
# TODO: this redeclaration should be an error
self.y: str = "a"
# TODO: this redeclaration should be an error
self.z: str = "a"
c_instance = C()
reveal_type(c_instance.x) # revealed: Unknown | int | str
# TODO: We should probably infer `int | str` here.
reveal_type(c_instance.y) # revealed: int
reveal_type(c_instance.z) # revealed: int
```
#### Attributes defined in tuple unpackings
@@ -358,73 +354,6 @@ class C:
reveal_type(C().declared_and_bound) # revealed: Unknown
```
#### Static methods do not influence implicitly defined attributes
```py
class Other:
x: int
class C:
@staticmethod
def f(other: Other) -> None:
other.x = 1
# error: [unresolved-attribute]
reveal_type(C.x) # revealed: Unknown
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
reveal_type(C().x) # revealed: Unknown | Literal[1]
# This also works if `staticmethod` is aliased:
my_staticmethod = staticmethod
class D:
@my_staticmethod
def f(other: Other) -> None:
other.x = 1
# error: [unresolved-attribute]
reveal_type(D.x) # revealed: Unknown
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
reveal_type(D().x) # revealed: Unknown | Literal[1]
```
If `staticmethod` is something else, that should not influence the behavior:
```py
def staticmethod(f):
return f
class C:
@staticmethod
def f(self) -> None:
self.x = 1
reveal_type(C().x) # revealed: Unknown | Literal[1]
```
And if `staticmethod` is fully qualified, that should also be recognized:
```py
import builtins
class Other:
x: int
class C:
@builtins.staticmethod
def f(other: Other) -> None:
other.x = 1
# error: [unresolved-attribute]
reveal_type(C.x) # revealed: Unknown
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
reveal_type(C().x) # revealed: Unknown | Literal[1]
```
#### Attributes defined in statically-known-to-be-false branches
```py
@@ -511,12 +440,12 @@ reveal_type(C.pure_class_variable) # revealed: Unknown
C.pure_class_variable = "overwritten on class"
# TODO: should be `Unknown | Literal["value set in class method"]` or
# Literal["overwritten on class"]`, once/if we support local narrowing.
# TODO: should be `Literal["overwritten on class"]`
# error: [unresolved-attribute]
reveal_type(C.pure_class_variable) # revealed: Unknown
c_instance = C()
# TODO: should be `Literal["overwritten on class"]`
reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"]
# TODO: should raise an error.
@@ -845,6 +774,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 +785,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 +799,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 +808,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 +820,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 +829,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]

View File

@@ -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")

View File

@@ -1,199 +0,0 @@
# Descriptor protocol
[Descriptors] let objects customize attribute lookup, storage, and deletion.
A descriptor is an attribute value that has one of the methods in the descriptor protocol. Those
methods are `__get__()`, `__set__()`, and `__delete__()`. If any of those methods are defined for an
attribute, it is said to be a descriptor.
## Basic example
An introductory example, modeled after a [simple example] in the primer on descriptors, involving a
descriptor that returns a constant value:
```py
from typing import Literal
class Ten:
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
return 10
def __set__(self, instance: object, value: Literal[10]) -> None:
pass
class C:
ten = Ten()
c = C()
# TODO: this should be `Literal[10]`
reveal_type(c.ten) # revealed: Unknown | Ten
# TODO: This should `Literal[10]`
reveal_type(C.ten) # revealed: Unknown | Ten
# These are fine:
c.ten = 10
C.ten = 10
# TODO: Both of these should be errors
c.ten = 11
C.ten = 11
```
## Different types for `__get__` and `__set__`
The return type of `__get__` and the value type of `__set__` can be different:
```py
class FlexibleInt:
def __init__(self):
self._value: int | None = None
def __get__(self, instance: object, owner: type | None = None) -> int | None:
return self._value
def __set__(self, instance: object, value: int | str) -> None:
self._value = int(value)
class C:
flexible_int = FlexibleInt()
c = C()
# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
c.flexible_int = 42 # okay
c.flexible_int = "42" # also okay!
# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
# TODO: should be an error
c.flexible_int = None # not okay
# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
```
## Built-in `property` descriptor
The built-in `property` decorator creates a descriptor. The names for attribute reads/writes are
determined by the return type of the `name` method and the parameter type of the setter,
respectively.
```py
class C:
_name: str | None = None
@property
def name(self) -> str:
return self._name or "Unset"
# TODO: No diagnostic should be emitted here
# error: [unresolved-attribute] "Type `Literal[name]` has no attribute `setter`"
@name.setter
def name(self, value: str | None) -> None:
self._value = value
c = C()
reveal_type(c._name) # revealed: str | None
# Should be `str`
reveal_type(c.name) # revealed: @Todo(bound method)
# Should be `builtins.property`
reveal_type(C.name) # revealed: Literal[name]
# This is fine:
c.name = "new"
c.name = None
# TODO: this should be an error
c.name = 42
```
## Built-in `classmethod` descriptor
Similarly to `property`, `classmethod` decorator creates an implicit descriptor that binds the first
argument to the class instead of the instance.
```py
class C:
def __init__(self, value: str) -> None:
self._name: str = value
@classmethod
def factory(cls, value: str) -> "C":
return cls(value)
@classmethod
def get_name(cls) -> str:
return cls.__name__
c1 = C.factory("test") # okay
# TODO: should be `C`
reveal_type(c1) # revealed: @Todo(return type)
# TODO: should be `str`
reveal_type(C.get_name()) # revealed: @Todo(return type)
# TODO: should be `str`
reveal_type(C("42").get_name()) # revealed: @Todo(bound method)
```
## Descriptors only work when used as class variables
From the descriptor guide:
> Descriptors only work when used as class variables. When put in instances, they have no effect.
```py
from typing import Literal
class Ten:
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
return 10
class C:
def __init__(self):
self.ten = Ten()
reveal_type(C().ten) # revealed: Unknown | Ten
```
## Descriptors distinguishing between class and instance access
Overloads can be used to distinguish between when a descriptor is accessed on a class object and
when it is accessed on an instance. A real-world example of this is the `__get__` method on
`types.FunctionType`.
```py
from typing_extensions import Literal, LiteralString, overload
class Descriptor:
@overload
def __get__(self, instance: None, owner: type, /) -> Literal["called on class object"]: ...
@overload
def __get__(self, instance: object, owner: type | None = None, /) -> Literal["called on instance"]: ...
def __get__(self, instance, owner=None, /) -> LiteralString:
if instance:
return "called on instance"
else:
return "called on class object"
class C:
d = Descriptor()
# TODO: should be `Literal["called on class object"]
reveal_type(C.d) # revealed: Unknown | Descriptor
# TODO: should be `Literal["called on instance"]
reveal_type(C().d) # revealed: Unknown | Descriptor
```
[descriptors]: https://docs.python.org/3/howto/descriptor.html
[simple example]: https://docs.python.org/3/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant

View File

@@ -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]
```

View File

@@ -1,87 +0,0 @@
# Unresolved import diagnostics
<!-- snapshot-diagnostics -->
## Using `from` with an unresolvable module
This example demonstrates the diagnostic when a `from` style import is used with a module that could
not be found:
```py
from does_not_exist import add # error: [unresolved-import]
stat = add(10, 15)
```
## Using `from` with too many leading dots
This example demonstrates the diagnostic when a `from` style import is used with a presumptively
valid path, but where there are too many leading dots.
`package/__init__.py`:
```py
```
`package/foo.py`:
```py
def add(x, y):
return x + y
```
`package/subpackage/subsubpackage/__init__.py`:
```py
from ....foo import add # error: [unresolved-import]
stat = add(10, 15)
```
## Using `from` with an unknown current module
This is another case handled separately in Red Knot, where a `.` provokes relative module name
resolution, but where the module name is not resolvable.
```py
from .does_not_exist import add # error: [unresolved-import]
stat = add(10, 15)
```
## Using `from` with an unknown nested module
Like the previous test, but with sub-modules to ensure the span is correct.
```py
from .does_not_exist.foo.bar import add # error: [unresolved-import]
stat = add(10, 15)
```
## Using `from` with a resolvable module but unresolvable item
This ensures that diagnostics for an unresolvable item inside a resolvable import highlight the item
and not the entire `from ... import ...` statement.
`a.py`:
```py
does_exist1 = 1
does_exist2 = 2
```
```py
from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
```
## An unresolvable import that does not use `from`
This ensures that an unresolvable `import ...` statement highlights just the module name and not the
entire statement.
```py
import does_not_exist # error: [unresolved-import]
x = does_not_exist.foo
```

View File

@@ -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

View File

@@ -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)

View File

@@ -116,18 +116,8 @@ reveal_type(c.C) # revealed: Literal[C]
class C: ...
```
## Unresolvable module import
<!-- snapshot-diagnostics -->
```py
import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
```
## Unresolvable submodule imports
<!-- snapshot-diagnostics -->
```py
# Topmost component resolvable, submodule not resolvable:
import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"

View File

@@ -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]
```

View File

@@ -13,7 +13,7 @@ if returns_bool():
chr: int = 1
def f():
reveal_type(chr) # revealed: int | Literal[chr]
reveal_type(chr) # revealed: Literal[chr] | int
```
## Conditionally global or builtin, with annotation
@@ -28,5 +28,5 @@ if returns_bool():
chr: int = 1
def f():
reveal_type(chr) # revealed: int | Literal[chr]
reveal_type(chr) # revealed: Literal[chr] | int
```

View File

@@ -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
@@ -54,10 +56,10 @@ inside the module:
import typing
reveal_type(typing.__name__) # revealed: str
reveal_type(typing.__init__) # revealed: @Todo(bound method)
reveal_type(typing.__init__) # revealed: Literal[__init__]
# These come from `builtins.object`, not `types.ModuleType`:
reveal_type(typing.__eq__) # revealed: @Todo(bound method)
reveal_type(typing.__eq__) # revealed: Literal[__eq__]
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
@@ -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
```

View File

@@ -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(): ...

View File

@@ -1,28 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: basic.md - Structures - Unresolvable module import
mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
---
# Python source files
## mdtest_snippet.py
```
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
```
# Diagnostics
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:8
|
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
| ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq`
|
```

View File

@@ -1,51 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: basic.md - Structures - Unresolvable submodule imports
mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
---
# Python source files
## mdtest_snippet.py
```
1 | # Topmost component resolvable, submodule not resolvable:
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
3 |
4 | # Topmost component unresolvable:
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
```
## a/__init__.py
```
```
# Diagnostics
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:2:8
|
1 | # Topmost component resolvable, submodule not resolvable:
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
| ^^^^^ Cannot resolve import `a.foo`
3 |
4 | # Topmost component unresolvable:
|
```
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:5:8
|
4 | # Topmost component unresolvable:
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
| ^^^^^ Cannot resolve import `b.foo`
|
```

View File

@@ -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
|
```

View File

@@ -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)
|
```

View File

@@ -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)
|
```

View File

@@ -1,32 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unresolved_import.md - Unresolved import diagnostics - An unresolvable import that does not use `from`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
---
# Python source files
## mdtest_snippet.py
```
1 | import does_not_exist # error: [unresolved-import]
2 |
3 | x = does_not_exist.foo
```
# Diagnostics
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:8
|
1 | import does_not_exist # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
2 |
3 | x = does_not_exist.foo
|
```

View File

@@ -1,35 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with a resolvable module but unresolvable item
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
---
# Python source files
## a.py
```
1 | does_exist1 = 1
2 | does_exist2 = 2
```
## mdtest_snippet.py
```
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
```
# Diagnostics
```
error: lint:unresolved-import
--> /src/mdtest_snippet.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`
|
```

View File

@@ -1,32 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown current module
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
---
# Python source files
## mdtest_snippet.py
```
1 | from .does_not_exist import add # error: [unresolved-import]
2 |
3 | stat = add(10, 15)
```
# Diagnostics
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:7
|
1 | from .does_not_exist import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist`
2 |
3 | stat = add(10, 15)
|
```

View File

@@ -1,32 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown nested module
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
---
# Python source files
## mdtest_snippet.py
```
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
2 |
3 | stat = add(10, 15)
```
# Diagnostics
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:7
|
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist.foo.bar`
2 |
3 | stat = add(10, 15)
|
```

View File

@@ -1,32 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unresolvable module
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
---
# Python source files
## mdtest_snippet.py
```
1 | from does_not_exist import add # error: [unresolved-import]
2 |
3 | stat = add(10, 15)
```
# Diagnostics
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:6
|
1 | from does_not_exist import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
2 |
3 | stat = add(10, 15)
|
```

View File

@@ -1,44 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with too many leading dots
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
---
# Python source files
## package/__init__.py
```
```
## package/foo.py
```
1 | def add(x, y):
2 | return x + y
```
## package/subpackage/subsubpackage/__init__.py
```
1 | from ....foo import add # error: [unresolved-import]
2 |
3 | stat = add(10, 15)
```
# Diagnostics
```
error: lint:unresolved-import
--> /src/package/subpackage/subsubpackage/__init__.py:1:10
|
1 | from ....foo import add # error: [unresolved-import]
| ^^^ Cannot resolve import `....foo`
2 |
3 | stat = add(10, 15)
|
```

View File

@@ -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: ...
@@ -1502,6 +1509,37 @@ if True:
from module import symbol
```
## Known limitations
We currently have a limitation in the complexity (depth) of the visibility constraints that are
supported. This is to avoid pathological cases that would require us to recurse deeply.
```py
x = 1
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or (x := 2) # fmt: skip
# This still works fine:
reveal_type(x) # revealed: Literal[2]
y = 1
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or False or (y := 2) # fmt: skip
# TODO: This should ideally be `Literal[2]` as well:
reveal_type(y) # revealed: Literal[1, 2]
```
## Unsupported features
We do not support full unreachable code analysis yet. We also raise diagnostics from

View File

@@ -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

View File

@@ -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)

View File

@@ -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"]
```

View File

@@ -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]

View File

@@ -1,7 +1,5 @@
# Truthiness
## Literals
```py
from typing_extensions import Literal, LiteralString
from knot_extensions import AlwaysFalsy, AlwaysTruthy
@@ -47,31 +45,3 @@ def _(
reveal_type(bool(c)) # revealed: bool
reveal_type(bool(d)) # revealed: bool
```
## Instances
Checks that we don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
### __bool__ is bool
```py
class BoolIsBool:
__bool__ = bool
reveal_type(bool(BoolIsBool())) # revealed: bool
```
### Conditional __bool__ method
```py
def flag() -> bool:
return True
class Boom:
if flag():
__bool__ = bool
else:
__bool__ = int
reveal_type(bool(Boom())) # revealed: bool
```

View File

@@ -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]))

View File

@@ -141,6 +141,15 @@ class AlwaysFalse:
# revealed: Literal[True]
reveal_type(not AlwaysFalse())
# We don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
class BoolIsBool:
# TODO: The `type[bool]` declaration here is a workaround to avoid running into
# https://github.com/astral-sh/ruff/issues/15672
__bool__: type[bool] = bool
# revealed: bool
reveal_type(not BoolIsBool())
# At runtime, no `__bool__` and no `__len__` means truthy, but we can't rely on that, because
# a subclass could add a `__bool__` method.
class NoBoolMethod: ...

View File

@@ -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()
},
);

View File

@@ -5,20 +5,20 @@ use crate::db::Db;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Constraint<'db> {
pub(crate) node: ConstraintNode<'db>,
pub(crate) is_positive: bool,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ConstraintNode<'db> {
Expression(Expression<'db>),
Pattern(PatternConstraint<'db>),
}
/// Pattern kinds for which we support type narrowing and/or static visibility analysis.
#[derive(Debug, Clone, Hash, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum PatternConstraintKind<'db> {
Singleton(Singleton, Option<Expression<'db>>),
Value(Expression<'db>, Option<Expression<'db>>),

View File

@@ -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> {

View File

@@ -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);

View File

@@ -9,6 +9,15 @@ pub(crate) enum Boundness {
PossiblyUnbound,
}
impl Boundness {
pub(crate) fn or(self, other: Boundness) -> Boundness {
match (self, other) {
(Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound,
(Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound,
}
}
}
/// The result of a symbol lookup, which can either be a (possibly unbound) type
/// or a completely unbound symbol.
///
@@ -37,6 +46,13 @@ impl<'db> Symbol<'db> {
matches!(self, Symbol::Unbound)
}
pub(crate) fn possibly_unbound(&self) -> bool {
match self {
Symbol::Type(_, Boundness::PossiblyUnbound) | Symbol::Unbound => true,
Symbol::Type(_, Boundness::Bound) => false,
}
}
/// Returns the type of the symbol, ignoring possible unboundness.
///
/// If the symbol is *definitely* unbound, this function will return `None`. Otherwise,
@@ -55,32 +71,18 @@ impl<'db> Symbol<'db> {
.expect("Expected a (possibly unbound) type, not an unbound symbol")
}
/// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound.
///
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
/// 2. Else, evaluate `fallback_fn()`:
/// a. If `self` is definitely unbound, return the result of `fallback_fn()`.
/// b. Else, if `fallback` is definitely unbound, return `self`.
/// c. Else, if `self` is possibly unbound and `fallback` is definitely bound,
/// return `Symbol(<union of self-type and fallback-type>, Boundness::Bound)`
/// d. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
/// return `Symbol(<union of self-type and fallback-type>, Boundness::PossiblyUnbound)`
#[must_use]
pub(crate) fn or_fall_back_to(
self,
db: &'db dyn Db,
fallback_fn: impl FnOnce() -> Self,
) -> Self {
match self {
Symbol::Type(_, Boundness::Bound) => self,
Symbol::Unbound => fallback_fn(),
Symbol::Type(self_ty, Boundness::PossiblyUnbound) => match fallback_fn() {
Symbol::Unbound => self,
Symbol::Type(fallback_ty, fallback_boundness) => Symbol::Type(
UnionType::from_elements(db, [self_ty, fallback_ty]),
fallback_boundness,
pub(crate) fn or_fall_back_to(self, db: &'db dyn Db, fallback: &Symbol<'db>) -> Symbol<'db> {
match fallback {
Symbol::Type(fallback_ty, fallback_boundness) => match self {
Symbol::Type(_, Boundness::Bound) => self,
Symbol::Type(ty, boundness @ Boundness::PossiblyUnbound) => Symbol::Type(
UnionType::from_elements(db, [*fallback_ty, ty]),
fallback_boundness.or(boundness),
),
Symbol::Unbound => fallback.clone(),
},
Symbol::Unbound => self,
}
}
@@ -108,44 +110,44 @@ mod tests {
// Start from an unbound symbol
assert_eq!(
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Unbound),
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Unbound),
Symbol::Unbound
);
assert_eq!(
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Type(ty1, PossiblyUnbound)),
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, PossiblyUnbound)),
Symbol::Type(ty1, PossiblyUnbound)
);
assert_eq!(
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Type(ty1, Bound)),
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, Bound)),
Symbol::Type(ty1, Bound)
);
// Start from a possibly unbound symbol
assert_eq!(
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, || Symbol::Unbound),
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Unbound),
Symbol::Type(ty1, PossiblyUnbound)
);
assert_eq!(
Symbol::Type(ty1, PossiblyUnbound)
.or_fall_back_to(&db, || Symbol::Type(ty2, PossiblyUnbound)),
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound)
.or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), PossiblyUnbound)
);
assert_eq!(
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, || Symbol::Type(ty2, Bound)),
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound)
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), Bound)
);
// Start from a definitely bound symbol
assert_eq!(
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Unbound),
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Unbound),
Symbol::Type(ty1, Bound)
);
assert_eq!(
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Type(ty2, PossiblyUnbound)),
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
Symbol::Type(ty1, Bound)
);
assert_eq!(
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Type(ty2, Bound)),
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
Symbol::Type(ty1, Bound)
);
}

View File

@@ -15,7 +15,8 @@ pub(crate) use self::diagnostic::register_lints;
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
pub(crate) use self::display::TypeArrayDisplay;
pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types,
infer_scope_types,
};
pub use self::narrow::KnownConstraintFunction;
pub(crate) use self::signatures::Signature;
@@ -25,7 +26,6 @@ use crate::module_resolver::{file_to_module, resolve_module, KnownModule};
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::attribute_assignment::AttributeAssignment;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{self as symbol, ScopeId, ScopedSymbolId};
use crate::semantic_index::{
attribute_assignments, global_scope, imported_modules, semantic_index, symbol_table,
@@ -256,19 +256,26 @@ fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::N
/// Looks up a module-global symbol by name in a file.
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
let explicit_symbol = symbol(db, global_scope(db, file), name);
if !explicit_symbol.possibly_unbound() {
return explicit_symbol;
}
// Not defined explicitly in the global scope?
// All modules are instances of `types.ModuleType`;
// look it up there (with a few very special exceptions)
symbol(db, global_scope(db, file), name).or_fall_back_to(db, || {
if module_type_symbols(db)
.iter()
.any(|module_type_member| &**module_type_member == name)
{
KnownClass::ModuleType.to_instance(db).member(db, name)
} else {
Symbol::Unbound
}
})
if module_type_symbols(db)
.iter()
.any(|module_type_member| &**module_type_member == name)
{
// TODO: this should use `.to_instance(db)`. but we don't understand attribute access
// on instance types yet.
let module_type_member = KnownClass::ModuleType.to_class_literal(db).member(db, name);
return explicit_symbol.or_fall_back_to(db, &module_type_member);
}
explicit_symbol
}
/// Infer the type of a binding.
@@ -663,10 +670,6 @@ impl<'db> Type<'db> {
matches!(self, Type::ClassLiteral(..))
}
pub const fn is_instance(&self) -> bool {
matches!(self, Type::Instance(..))
}
pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: Module) -> Self {
Self::ModuleLiteral(ModuleLiteralType::new(db, importing_file, submodule))
}
@@ -1840,8 +1843,19 @@ impl<'db> Type<'db> {
return Truthiness::Ambiguous;
};
// Check if the class has `__bool__ = bool` and avoid infinite recursion, since
// `Type::call` on `bool` will call `Type::bool` on the argument.
if bool_method
.into_class_literal()
.is_some_and(|ClassLiteralType { class }| {
class.is_known(db, KnownClass::Bool)
})
{
return Truthiness::Ambiguous;
}
if let Some(Type::BooleanLiteral(bool_val)) = bool_method
.call_bound(db, instance_ty, &CallArguments::positional([]))
.call(db, &CallArguments::positional([*instance_ty]))
.return_type(db)
{
bool_val.into()
@@ -2134,52 +2148,6 @@ impl<'db> Type<'db> {
}
}
/// Return the outcome of calling an class/instance attribute of this type
/// using descriptor protocol.
///
/// `receiver_ty` must be `Type::Instance(_)` or `Type::ClassLiteral`.
///
/// TODO: handle `super()` objects properly
#[must_use]
fn call_bound(
self,
db: &'db dyn Db,
receiver_ty: &Type<'db>,
arguments: &CallArguments<'_, 'db>,
) -> CallOutcome<'db> {
debug_assert!(receiver_ty.is_instance() || receiver_ty.is_class_literal());
match self {
Type::FunctionLiteral(..) => {
// Functions are always descriptors, so this would effectively call
// the function with the instance as the first argument
self.call(db, &arguments.with_self(*receiver_ty))
}
Type::Instance(_) | Type::ClassLiteral(_) => {
// TODO descriptor protocol. For now, assume non-descriptor and call without `self` argument.
self.call(db, arguments)
}
Type::Union(union) => CallOutcome::union(
self,
union
.elements(db)
.iter()
.map(|elem| elem.call_bound(db, receiver_ty, arguments)),
),
Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_type(
todo_type!("Type::Intersection.call_bound()"),
)),
// Cases that duplicate, and thus must be kept in sync with, `Type::call()`
Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_type(self)),
_ => CallOutcome::not_callable(self),
}
}
/// Look up a dunder method on the meta type of `self` and call it.
fn call_dunder(
self,
@@ -3789,8 +3757,10 @@ impl<'db> ModuleLiteralType<'db> {
}
}
// If it's not found in the global scope, check if it's present as an instance
// on `types.ModuleType` or `builtins.object`.
let global_lookup = symbol(db, global_scope(db, self.module(db).file()), name);
// If it's unbound, check if it's present as an instance on `types.ModuleType`
// or `builtins.object`.
//
// We do a more limited version of this in `global_symbol_ty`,
// but there are two crucial differences here:
@@ -3804,13 +3774,14 @@ impl<'db> ModuleLiteralType<'db> {
// ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType`
// to help out with dynamic imports; we shouldn't use it for `ModuleLiteral` types
// where we know exactly which module we're dealing with.
symbol(db, global_scope(db, self.module(db).file()), name).or_fall_back_to(db, || {
if name == "__getattr__" {
Symbol::Unbound
} else {
KnownClass::ModuleType.to_instance(db).member(db, name)
}
})
if name != "__getattr__" && global_lookup.possibly_unbound() {
// TODO: this should use `.to_instance()`, but we don't understand instance attribute yet
let module_type_instance_member =
KnownClass::ModuleType.to_class_literal(db).member(db, name);
global_lookup.or_fall_back_to(db, &module_type_instance_member)
} else {
global_lookup
}
}
}
@@ -4176,16 +4147,6 @@ impl<'db> Class<'db> {
name: &str,
inferred_type_from_class_body: Option<Type<'db>>,
) -> Symbol<'db> {
// We use a separate salsa query here to prevent unrelated changes in the AST of an external
// file from triggering re-evaluations of downstream queries.
// See the `dependency_implicit_instance_attribute` test for more information.
#[salsa::tracked]
fn infer_expression_type<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Type<'db> {
let inference = infer_expression_types(db, expression);
let expr_scope = expression.scope(db);
inference.expression_type(expression.node_ref(db).scoped_expression_id(db, expr_scope))
}
// If we do not see any declarations of an attribute, neither in the class body nor in
// any method, we build a union of `Unknown` with the inferred types of all bindings of
// that attribute. We include `Unknown` in that union to account for the fact that the

View File

@@ -193,6 +193,20 @@ pub(crate) fn infer_expression_types<'db>(
TypeInferenceBuilder::new(db, InferenceRegion::Expression(expression), index).finish()
}
// Similar to `infer_expression_types` (with the same restrictions). Directly returns the
// type of the overall expression. This is a salsa query because it accesses `node_ref`,
// which is sensitive to changes in the AST. Making it a query allows downstream queries
// to short-circuit if the result type has not changed.
#[salsa::tracked]
pub(crate) fn infer_expression_type<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
) -> Type<'db> {
let inference = infer_expression_types(db, expression);
let expr_scope = expression.scope(db);
inference.expression_type(expression.node_ref(db).scoped_expression_id(db, expr_scope))
}
/// Infer the types for an [`Unpack`] operation.
///
/// This infers the expression type and performs structural match against the target expression
@@ -2513,32 +2527,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)
}
@@ -2552,12 +2552,6 @@ impl<'db> TypeInferenceBuilder<'db> {
// - Absolute `*` imports (`from collections import *`)
// - Relative `*` imports (`from ...foo import *`)
let ast::StmtImportFrom { module, level, .. } = import_from;
// For diagnostics, we want to highlight the unresolvable
// module and not the entire `from ... import ...` statement.
let module_ref = module
.as_ref()
.map(AnyNodeRef::from)
.unwrap_or_else(|| AnyNodeRef::from(import_from));
let module = module.as_deref();
let module_name = if let Some(level) = NonZeroU32::new(*level) {
@@ -2592,7 +2586,7 @@ impl<'db> TypeInferenceBuilder<'db> {
"Relative module resolution `{}` failed: too many leading dots",
format_import_from_module(*level, module),
);
report_unresolved_module(&self.context, module_ref, *level, module);
report_unresolved_module(&self.context, import_from, *level, module);
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
}
@@ -2602,14 +2596,14 @@ impl<'db> TypeInferenceBuilder<'db> {
format_import_from_module(*level, module),
self.file().path(self.db())
);
report_unresolved_module(&self.context, module_ref, *level, module);
report_unresolved_module(&self.context, import_from, *level, module);
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
}
};
let Some(module_ty) = self.module_type_from_name(&module_name) else {
report_unresolved_module(&self.context, module_ref, *level, module);
report_unresolved_module(&self.context, import_from, *level, module);
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
};
@@ -3310,9 +3304,8 @@ impl<'db> TypeInferenceBuilder<'db> {
/// Look up a name reference that isn't bound in the local scope.
fn lookup_name(&mut self, name_node: &ast::ExprName) -> Symbol<'db> {
let db = self.db();
let ast::ExprName { id: name, .. } = name_node;
let file_scope_id = self.scope().file_scope_id(db);
let file_scope_id = self.scope().file_scope_id(self.db());
let is_bound =
if let Some(symbol) = self.index.symbol_table(file_scope_id).symbol_by_name(name) {
symbol.is_bound()
@@ -3327,15 +3320,16 @@ impl<'db> TypeInferenceBuilder<'db> {
// In function-like scopes, any local variable (symbol that is bound in this scope) can
// only have a definition in this scope, or error; it never references another scope.
// (At runtime, it would use the `LOAD_FAST` opcode.)
if !is_bound || !self.scope().is_function_like(db) {
if !is_bound || !self.scope().is_function_like(self.db()) {
// Walk up parent scopes looking for a possible enclosing scope that may have a
// definition of this name visible to us (would be `LOAD_DEREF` at runtime.)
for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id) {
// Class scopes are not visible to nested scopes, and we need to handle global
// scope differently (because an unbound name there falls back to builtins), so
// check only function-like scopes.
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, self.file());
if !enclosing_scope_id.is_function_like(db) {
let enclosing_scope_id =
enclosing_scope_file_id.to_scope_id(self.db(), self.file());
if !enclosing_scope_id.is_function_like(self.db()) {
continue;
}
let enclosing_symbol_table = self.index.symbol_table(enclosing_scope_file_id);
@@ -3348,45 +3342,37 @@ impl<'db> TypeInferenceBuilder<'db> {
// runtime, it is the scope that creates the cell for our closure.) If the name
// isn't bound in that scope, we should get an unbound name, not continue
// falling back to other scopes / globals / builtins.
return symbol(db, enclosing_scope_id, name);
return symbol(self.db(), enclosing_scope_id, name);
}
}
Symbol::Unbound
// No nonlocal binding? Check the module's globals.
// Avoid infinite recursion if `self.scope` already is the module's global scope.
.or_fall_back_to(db, || {
if file_scope_id.is_global() {
Symbol::Unbound
} else {
global_symbol(db, self.file(), name)
}
})
// Not found in globals? Fallback to builtins
// (without infinite recursion if we're already in builtins.)
.or_fall_back_to(db, || {
if Some(self.scope()) == builtins_module_scope(db) {
Symbol::Unbound
} else {
builtins_symbol(db, name)
}
})
// Still not found? It might be `reveal_type`...
.or_fall_back_to(db, || {
if name == "reveal_type" {
self.context.report_lint(
&UNDEFINED_REVEAL,
name_node.into(),
format_args!(
"`reveal_type` used without importing it; \
this is allowed for debugging convenience but will fail at runtime"
),
);
typing_extensions_symbol(db, name)
} else {
Symbol::Unbound
}
})
// No nonlocal binding, check module globals. Avoid infinite recursion if `self.scope`
// already is module globals.
let global_symbol = if file_scope_id.is_global() {
Symbol::Unbound
} else {
global_symbol(self.db(), self.file(), name)
};
// Fallback to builtins (without infinite recursion if we're already in builtins.)
if global_symbol.possibly_unbound()
&& Some(self.scope()) != builtins_module_scope(self.db())
{
let mut builtins_symbol = builtins_symbol(self.db(), name);
if builtins_symbol.is_unbound() && name == "reveal_type" {
self.context.report_lint(
&UNDEFINED_REVEAL,
name_node.into(),
format_args!(
"`reveal_type` used without importing it; this is allowed for debugging convenience but will fail at runtime"),
);
builtins_symbol = typing_extensions_symbol(self.db(), name);
}
global_symbol.or_fall_back_to(self.db(), &builtins_symbol)
} else {
global_symbol
}
} else {
Symbol::Unbound
}

View File

@@ -7,7 +7,7 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::symbol::ScopeId;
use crate::types::{infer_expression_types, todo_type, Type, TypeCheckDiagnostics};
use crate::types::{infer_expression_type, todo_type, Type, TypeCheckDiagnostics};
use crate::unpack::UnpackValue;
use crate::Db;
@@ -42,8 +42,7 @@ impl<'db> Unpacker<'db> {
"Unpacking target must be a list or tuple expression"
);
let mut value_ty = infer_expression_types(self.db(), value.expression())
.expression_type(value.scoped_expression_id(self.db(), self.scope));
let mut value_ty = infer_expression_type(self.db(), value.expression());
if value.is_assign()
&& self.context.in_stub()
@@ -62,15 +61,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 +73,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 +152,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 +166,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);
}
}
_ => {}

View File

@@ -3,7 +3,6 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange};
use crate::ast_node_ref::AstNodeRef;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
use crate::Db;
@@ -88,17 +87,6 @@ impl<'db> UnpackValue<'db> {
}
}
/// Returns the [`ScopedExpressionId`] of the underlying expression.
pub(crate) fn scoped_expression_id(
self,
db: &'db dyn Db,
scope: ScopeId<'db>,
) -> ScopedExpressionId {
self.expression()
.node_ref(db)
.scoped_expression_id(db, scope)
}
/// Returns the expression as an [`AnyNodeRef`].
pub(crate) fn as_any_node_ref(self, db: &'db dyn Db) -> AnyNodeRef<'db> {
self.expression().node_ref(db).node().into()

View File

@@ -137,54 +137,33 @@
//! create a state where the `x = <unbound>` binding is always visible.
//!
//!
//! ### Representing formulas
//! ### Properties
//!
//! Given everything above, we can represent a visibility constraint as a _ternary formula_. This
//! is like a boolean formula (which maps several true/false variables to a single true/false
//! result), but which allows the third "ambiguous" value in addition to "true" and "false".
//! The ternary `AND` and `OR` operations have the property that `~a OR ~b = ~(a AND b)`. This
//! means we could, in principle, get rid of either of these two to simplify the representation.
//!
//! [_Binary decision diagrams_][bdd] (BDDs) are a common way to represent boolean formulas when
//! doing program analysis. We extend this to a _ternary decision diagram_ (TDD) to support
//! ambiguous values.
//!
//! A TDD is a graph, and a ternary formula is represented by a node in this graph. There are three
//! possible leaf nodes representing the "true", "false", and "ambiguous" constant functions.
//! Interior nodes consist of a ternary variable to evaluate, and outgoing edges for whether the
//! variable evaluates to true, false, or ambiguous.
//!
//! Our TDDs are _reduced_ and _ordered_ (as is typical for BDDs).
//!
//! An ordered TDD means that variables appear in the same order in all paths within the graph.
//!
//! A reduced TDD means two things: First, we intern the graph nodes, so that we only keep a single
//! copy of interior nodes with the same contents. Second, we eliminate any nodes that are "noops",
//! where the "true" and "false" outgoing edges lead to the same node. (This implies that it
//! doesn't matter what value that variable has when evaluating the formula, and we can leave it
//! out of the evaluation chain completely.)
//!
//! Reduced and ordered decision diagrams are _normal forms_, which means that two equivalent
//! formulas (which have the same outputs for every combination of inputs) are represented by
//! exactly the same graph node. (Because of interning, this is not _equal_ nodes, but _identical_
//! ones.) That means that we can compare formulas for equivalence in constant time, and in
//! particular, can check whether a visibility constraint is statically always true or false,
//! regardless of any Python program state, by seeing if the constraint's formula is the "true" or
//! "false" leaf node.
//! However, we already apply negative constraints `~test1` and `~test2` to the "branches not
//! taken" in the example above. This means that the tree-representation `~test1 OR ~test2` is much
//! cheaper/shallower than basically creating `~(~(~test1) AND ~(~test2))`. Similarly, if we wanted
//! to get rid of `AND`, we would also have to create additional nodes. So for performance reasons,
//! there is a small "duplication" in the code between those two constraint types.
//!
//! [Kleene]: <https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics>
//! [bdd]: https://en.wikipedia.org/wiki/Binary_decision_diagram
use std::cmp::Ordering;
use ruff_index::{newtype_index, IndexVec};
use ruff_index::{Idx, IndexVec};
use rustc_hash::FxHashMap;
use crate::semantic_index::{
ast_ids::HasScopedExpressionId,
constraint::{Constraint, ConstraintNode, PatternConstraintKind},
};
use crate::types::{infer_expression_types, Truthiness};
use crate::semantic_index::constraint::{Constraint, ConstraintNode, PatternConstraintKind};
use crate::types::{infer_expression_type, Truthiness};
use crate::Db;
/// The maximum depth of recursion when evaluating visibility constraints.
///
/// This is a performance optimization that prevents us from descending deeply in case of
/// pathological cases. The actual limit here has been derived from performance testing on
/// the `black` codebase. When increasing the limit beyond 32, we see a 5x runtime increase
/// resulting from a few files with a lot of boolean expressions and `if`-statements.
const MAX_RECURSION_DEPTH: usize = 24;
/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula
/// is just like a boolean formula, but with `Ambiguous` as a third potential result. See the
/// module documentation for more details.)
@@ -200,416 +179,211 @@ use crate::Db;
/// That means that when you are constructing a formula, you might need to create distinct atoms
/// for a particular [`Constraint`], if your formula needs to consider how a particular runtime
/// property might be different at different points in the execution of the program.
///
/// Visibility constraints are normalized, so equivalent constraints are guaranteed to have equal
/// IDs.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub(crate) struct ScopedVisibilityConstraintId(u32);
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct VisibilityConstraint<'db>(VisibilityConstraintInner<'db>);
impl std::fmt::Debug for ScopedVisibilityConstraintId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_tuple("ScopedVisibilityConstraintId");
match *self {
// We use format_args instead of rendering the strings directly so that we don't get
// any quotes in the output: ScopedVisibilityConstraintId(AlwaysTrue) instead of
// ScopedVisibilityConstraintId("AlwaysTrue").
ALWAYS_TRUE => f.field(&format_args!("AlwaysTrue")),
AMBIGUOUS => f.field(&format_args!("Ambiguous")),
ALWAYS_FALSE => f.field(&format_args!("AlwaysFalse")),
_ => f.field(&self.0),
};
f.finish()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum VisibilityConstraintInner<'db> {
AlwaysTrue,
AlwaysFalse,
Ambiguous,
VisibleIf(Constraint<'db>, u8),
VisibleIfNot(ScopedVisibilityConstraintId),
KleeneAnd(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
KleeneOr(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
}
// Internal details:
//
// There are 3 terminals, with hard-coded constraint IDs: true, ambiguous, and false.
//
// _Atoms_ are the underlying Constraints, which are the variables that are evaluated by the
// ternary function.
//
// _Interior nodes_ provide the TDD structure for the formula. Interior nodes are stored in an
// arena Vec, with the constraint ID providing an index into the arena.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct InteriorNode {
atom: Atom,
if_true: ScopedVisibilityConstraintId,
if_ambiguous: ScopedVisibilityConstraintId,
if_false: ScopedVisibilityConstraintId,
}
/// A "variable" that is evaluated as part of a TDD ternary function. For visibility constraints,
/// this is a `Constraint` that represents some runtime property of the Python code that we are
/// evaluating. We intern these constraints in an arena ([`VisibilityConstraints::constraints`]).
/// An atom is then an index into this arena.
///
/// By using a 32-bit index, we would typically allow 4 billion distinct constraints within a
/// scope. However, we sometimes have to model how a `Constraint` can have a different runtime
/// value at different points in the execution of the program. To handle this, we reserve the top
/// byte of an atom to represent a "copy number". This is just an opaque value that allows
/// different `Atom`s to evaluate the same `Constraint`. This yields a maximum of 16 million
/// distinct `Constraint`s in a scope, and 256 possible copies of each of those constraints.
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct Atom(u32);
impl Atom {
/// Deconstruct an atom into a constraint index and a copy number.
#[inline]
fn into_index_and_copy(self) -> (u32, u8) {
let copy = self.0 >> 24;
let index = self.0 & 0x00ff_ffff;
(index, copy as u8)
}
#[inline]
fn copy_of(mut self, copy: u8) -> Self {
// Clear out the previous copy number
self.0 &= 0x00ff_ffff;
// OR in the new one
self.0 |= u32::from(copy) << 24;
self
}
}
// A custom Debug implementation that prints out the constraint index and copy number as distinct
// fields.
impl std::fmt::Debug for Atom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (index, copy) = self.into_index_and_copy();
f.debug_tuple("Atom").field(&index).field(&copy).finish()
}
}
impl Idx for Atom {
#[inline]
fn new(value: usize) -> Self {
assert!(value <= 0x00ff_ffff);
#[allow(clippy::cast_possible_truncation)]
Self(value as u32)
}
#[inline]
fn index(self) -> usize {
let (index, _) = self.into_index_and_copy();
index as usize
}
}
/// A newtype-index for a visibility constraint in a particular scope.
#[newtype_index]
pub(crate) struct ScopedVisibilityConstraintId;
impl ScopedVisibilityConstraintId {
/// A special ID that is used for an "always true" / "always visible" constraint.
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
/// present at index 0.
pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId(0xffff_ffff);
/// A special ID that is used for an ambiguous constraint.
pub(crate) const AMBIGUOUS: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId(0xffff_fffe);
ScopedVisibilityConstraintId::from_u32(0);
/// A special ID that is used for an "always false" / "never visible" constraint.
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
/// present at index 1.
pub(crate) const ALWAYS_FALSE: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId(0xffff_fffd);
ScopedVisibilityConstraintId::from_u32(1);
fn is_terminal(self) -> bool {
self.0 >= SMALLEST_TERMINAL.0
}
/// A special ID that is used for an ambiguous constraint.
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
/// present at index 2.
pub(crate) const AMBIGUOUS: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId::from_u32(2);
}
impl Idx for ScopedVisibilityConstraintId {
#[inline]
fn new(value: usize) -> Self {
assert!(value <= (SMALLEST_TERMINAL.0 as usize));
#[allow(clippy::cast_possible_truncation)]
Self(value as u32)
}
#[inline]
fn index(self) -> usize {
debug_assert!(!self.is_terminal());
self.0 as usize
}
}
// Rebind some constants locally so that we don't need as many qualifiers below.
const ALWAYS_TRUE: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::ALWAYS_TRUE;
const AMBIGUOUS: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::AMBIGUOUS;
const ALWAYS_FALSE: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::ALWAYS_FALSE;
const SMALLEST_TERMINAL: ScopedVisibilityConstraintId = ALWAYS_FALSE;
/// A collection of visibility constraints. This is currently stored in `UseDefMap`, which means we
/// maintain a separate set of visibility constraints for each scope in file.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct VisibilityConstraints<'db> {
constraints: IndexVec<Atom, Constraint<'db>>,
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
constraints: IndexVec<ScopedVisibilityConstraintId, VisibilityConstraint<'db>>,
}
#[derive(Debug, Default, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct VisibilityConstraintsBuilder<'db> {
constraints: IndexVec<Atom, Constraint<'db>>,
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
constraint_cache: FxHashMap<Constraint<'db>, Atom>,
interior_cache: FxHashMap<InteriorNode, ScopedVisibilityConstraintId>,
not_cache: FxHashMap<ScopedVisibilityConstraintId, ScopedVisibilityConstraintId>,
and_cache: FxHashMap<
(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
ScopedVisibilityConstraintId,
>,
or_cache: FxHashMap<
(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
ScopedVisibilityConstraintId,
>,
constraints: IndexVec<ScopedVisibilityConstraintId, VisibilityConstraint<'db>>,
}
impl Default for VisibilityConstraintsBuilder<'_> {
fn default() -> Self {
Self {
constraints: IndexVec::from_iter([
VisibilityConstraint(VisibilityConstraintInner::AlwaysTrue),
VisibilityConstraint(VisibilityConstraintInner::AlwaysFalse),
VisibilityConstraint(VisibilityConstraintInner::Ambiguous),
]),
}
}
}
impl<'db> VisibilityConstraintsBuilder<'db> {
pub(crate) fn build(self) -> VisibilityConstraints<'db> {
VisibilityConstraints {
constraints: self.constraints,
interiors: self.interiors,
}
}
/// Returns whether `a` or `b` has a "larger" atom. TDDs are ordered such that interior nodes
/// can only have edges to "larger" nodes. Terminals are considered to have a larger atom than
/// any internal node, since they are leaf nodes.
fn cmp_atoms(
&self,
a: ScopedVisibilityConstraintId,
b: ScopedVisibilityConstraintId,
) -> Ordering {
if a == b || (a.is_terminal() && b.is_terminal()) {
Ordering::Equal
} else if a.is_terminal() {
Ordering::Greater
} else if b.is_terminal() {
Ordering::Less
} else {
self.interiors[a].atom.cmp(&self.interiors[b].atom)
}
fn add(&mut self, constraint: VisibilityConstraintInner<'db>) -> ScopedVisibilityConstraintId {
self.constraints.push(VisibilityConstraint(constraint))
}
/// Adds a constraint, ensuring that we only store any particular constraint once.
fn add_constraint(&mut self, constraint: Constraint<'db>, copy: u8) -> Atom {
self.constraint_cache
.entry(constraint)
.or_insert_with(|| self.constraints.push(constraint))
.copy_of(copy)
}
/// Adds an interior node, ensuring that we always use the same visibility constraint ID for
/// equal nodes.
fn add_interior(&mut self, node: InteriorNode) -> ScopedVisibilityConstraintId {
// If the true and false branches lead to the same node, we can override the ambiguous
// branch to go there too. And this node is then redundant and can be reduced.
if node.if_true == node.if_false {
return node.if_true;
}
*self
.interior_cache
.entry(node)
.or_insert_with(|| self.interiors.push(node))
}
/// Adds a new visibility constraint that checks a single [`Constraint`]. Provide different
/// values for `copy` if you need to model that the constraint can evaluate to different
/// results at different points in the execution of the program being modeled.
pub(crate) fn add_atom(
&mut self,
constraint: Constraint<'db>,
copy: u8,
) -> ScopedVisibilityConstraintId {
let atom = self.add_constraint(constraint, copy);
self.add_interior(InteriorNode {
atom,
if_true: ALWAYS_TRUE,
if_ambiguous: AMBIGUOUS,
if_false: ALWAYS_FALSE,
})
self.add(VisibilityConstraintInner::VisibleIf(constraint, copy))
}
/// Adds a new visibility constraint that is the ternary NOT of an existing one.
pub(crate) fn add_not_constraint(
&mut self,
a: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
if a == ALWAYS_TRUE {
return ALWAYS_FALSE;
} else if a == AMBIGUOUS {
return AMBIGUOUS;
} else if a == ALWAYS_FALSE {
return ALWAYS_TRUE;
if a == ScopedVisibilityConstraintId::ALWAYS_FALSE {
ScopedVisibilityConstraintId::ALWAYS_TRUE
} else if a == ScopedVisibilityConstraintId::ALWAYS_TRUE {
ScopedVisibilityConstraintId::ALWAYS_FALSE
} else if a == ScopedVisibilityConstraintId::AMBIGUOUS {
ScopedVisibilityConstraintId::AMBIGUOUS
} else {
self.add(VisibilityConstraintInner::VisibleIfNot(a))
}
if let Some(cached) = self.not_cache.get(&a) {
return *cached;
}
let a_node = self.interiors[a];
let if_true = self.add_not_constraint(a_node.if_true);
let if_ambiguous = self.add_not_constraint(a_node.if_ambiguous);
let if_false = self.add_not_constraint(a_node.if_false);
let result = self.add_interior(InteriorNode {
atom: a_node.atom,
if_true,
if_ambiguous,
if_false,
});
self.not_cache.insert(a, result);
result
}
/// Adds a new visibility constraint that is the ternary OR of two existing ones.
pub(crate) fn add_or_constraint(
&mut self,
a: ScopedVisibilityConstraintId,
b: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
match (a, b) {
(ALWAYS_TRUE, _) | (_, ALWAYS_TRUE) => return ALWAYS_TRUE,
(ALWAYS_FALSE, other) | (other, ALWAYS_FALSE) => return other,
(AMBIGUOUS, AMBIGUOUS) => return AMBIGUOUS,
_ => {}
if a == ScopedVisibilityConstraintId::ALWAYS_TRUE
|| b == ScopedVisibilityConstraintId::ALWAYS_TRUE
{
return ScopedVisibilityConstraintId::ALWAYS_TRUE;
} else if a == ScopedVisibilityConstraintId::ALWAYS_FALSE {
return b;
} else if b == ScopedVisibilityConstraintId::ALWAYS_FALSE {
return a;
}
// OR is commutative, which lets us halve the cache requirements
let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) };
if let Some(cached) = self.or_cache.get(&(a, b)) {
return *cached;
match (&self.constraints[a], &self.constraints[b]) {
(_, VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id))) if a == *id => {
ScopedVisibilityConstraintId::ALWAYS_TRUE
}
(VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id)), _) if *id == b => {
ScopedVisibilityConstraintId::ALWAYS_TRUE
}
_ => self.add(VisibilityConstraintInner::KleeneOr(a, b)),
}
let (atom, if_true, if_ambiguous, if_false) = match self.cmp_atoms(a, b) {
Ordering::Equal => {
let a_node = self.interiors[a];
let b_node = self.interiors[b];
let if_true = self.add_or_constraint(a_node.if_true, b_node.if_true);
let if_false = self.add_or_constraint(a_node.if_false, b_node.if_false);
let if_ambiguous = if if_true == if_false {
if_true
} else {
self.add_or_constraint(a_node.if_ambiguous, b_node.if_ambiguous)
};
(a_node.atom, if_true, if_ambiguous, if_false)
}
Ordering::Less => {
let a_node = self.interiors[a];
let if_true = self.add_or_constraint(a_node.if_true, b);
let if_false = self.add_or_constraint(a_node.if_false, b);
let if_ambiguous = if if_true == if_false {
if_true
} else {
self.add_or_constraint(a_node.if_ambiguous, b)
};
(a_node.atom, if_true, if_ambiguous, if_false)
}
Ordering::Greater => {
let b_node = self.interiors[b];
let if_true = self.add_or_constraint(a, b_node.if_true);
let if_false = self.add_or_constraint(a, b_node.if_false);
let if_ambiguous = if if_true == if_false {
if_true
} else {
self.add_or_constraint(a, b_node.if_ambiguous)
};
(b_node.atom, if_true, if_ambiguous, if_false)
}
};
let result = self.add_interior(InteriorNode {
atom,
if_true,
if_ambiguous,
if_false,
});
self.or_cache.insert((a, b), result);
result
}
/// Adds a new visibility constraint that is the ternary AND of two existing ones.
pub(crate) fn add_and_constraint(
&mut self,
a: ScopedVisibilityConstraintId,
b: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
match (a, b) {
(ALWAYS_FALSE, _) | (_, ALWAYS_FALSE) => return ALWAYS_FALSE,
(ALWAYS_TRUE, other) | (other, ALWAYS_TRUE) => return other,
(AMBIGUOUS, AMBIGUOUS) => return AMBIGUOUS,
_ => {}
if a == ScopedVisibilityConstraintId::ALWAYS_FALSE
|| b == ScopedVisibilityConstraintId::ALWAYS_FALSE
{
return ScopedVisibilityConstraintId::ALWAYS_FALSE;
} else if a == ScopedVisibilityConstraintId::ALWAYS_TRUE {
return b;
} else if b == ScopedVisibilityConstraintId::ALWAYS_TRUE {
return a;
}
// AND is commutative, which lets us halve the cache requirements
let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) };
if let Some(cached) = self.and_cache.get(&(a, b)) {
return *cached;
match (&self.constraints[a], &self.constraints[b]) {
(_, VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id))) if a == *id => {
ScopedVisibilityConstraintId::ALWAYS_FALSE
}
(VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id)), _) if *id == b => {
ScopedVisibilityConstraintId::ALWAYS_FALSE
}
_ => self.add(VisibilityConstraintInner::KleeneAnd(a, b)),
}
let (atom, if_true, if_ambiguous, if_false) = match self.cmp_atoms(a, b) {
Ordering::Equal => {
let a_node = self.interiors[a];
let b_node = self.interiors[b];
let if_true = self.add_and_constraint(a_node.if_true, b_node.if_true);
let if_false = self.add_and_constraint(a_node.if_false, b_node.if_false);
let if_ambiguous = if if_true == if_false {
if_true
} else {
self.add_and_constraint(a_node.if_ambiguous, b_node.if_ambiguous)
};
(a_node.atom, if_true, if_ambiguous, if_false)
}
Ordering::Less => {
let a_node = self.interiors[a];
let if_true = self.add_and_constraint(a_node.if_true, b);
let if_false = self.add_and_constraint(a_node.if_false, b);
let if_ambiguous = if if_true == if_false {
if_true
} else {
self.add_and_constraint(a_node.if_ambiguous, b)
};
(a_node.atom, if_true, if_ambiguous, if_false)
}
Ordering::Greater => {
let b_node = self.interiors[b];
let if_true = self.add_and_constraint(a, b_node.if_true);
let if_false = self.add_and_constraint(a, b_node.if_false);
let if_ambiguous = if if_true == if_false {
if_true
} else {
self.add_and_constraint(a, b_node.if_ambiguous)
};
(b_node.atom, if_true, if_ambiguous, if_false)
}
};
let result = self.add_interior(InteriorNode {
atom,
if_true,
if_ambiguous,
if_false,
});
self.and_cache.insert((a, b), result);
result
}
}
impl<'db> VisibilityConstraints<'db> {
/// Analyze the statically known visibility for a given visibility constraint.
pub(crate) fn evaluate(
pub(crate) fn evaluate(&self, db: &'db dyn Db, id: ScopedVisibilityConstraintId) -> Truthiness {
self.evaluate_impl(db, id, MAX_RECURSION_DEPTH)
}
fn evaluate_impl(
&self,
db: &'db dyn Db,
mut id: ScopedVisibilityConstraintId,
id: ScopedVisibilityConstraintId,
max_depth: usize,
) -> Truthiness {
loop {
let node = match id {
ALWAYS_TRUE => return Truthiness::AlwaysTrue,
AMBIGUOUS => return Truthiness::Ambiguous,
ALWAYS_FALSE => return Truthiness::AlwaysFalse,
_ => self.interiors[id],
};
let constraint = &self.constraints[node.atom];
match Self::analyze_single(db, constraint) {
Truthiness::AlwaysTrue => id = node.if_true,
Truthiness::Ambiguous => id = node.if_ambiguous,
Truthiness::AlwaysFalse => id = node.if_false,
if max_depth == 0 {
return Truthiness::Ambiguous;
}
let VisibilityConstraint(visibility_constraint) = &self.constraints[id];
match visibility_constraint {
VisibilityConstraintInner::AlwaysTrue => Truthiness::AlwaysTrue,
VisibilityConstraintInner::AlwaysFalse => Truthiness::AlwaysFalse,
VisibilityConstraintInner::Ambiguous => Truthiness::Ambiguous,
VisibilityConstraintInner::VisibleIf(constraint, _) => {
Self::analyze_single(db, constraint)
}
VisibilityConstraintInner::VisibleIfNot(negated) => {
self.evaluate_impl(db, *negated, max_depth - 1).negate()
}
VisibilityConstraintInner::KleeneAnd(lhs, rhs) => {
let lhs = self.evaluate_impl(db, *lhs, max_depth - 1);
if lhs == Truthiness::AlwaysFalse {
return Truthiness::AlwaysFalse;
}
let rhs = self.evaluate_impl(db, *rhs, max_depth - 1);
if rhs == Truthiness::AlwaysFalse {
Truthiness::AlwaysFalse
} else if lhs == Truthiness::AlwaysTrue && rhs == Truthiness::AlwaysTrue {
Truthiness::AlwaysTrue
} else {
Truthiness::Ambiguous
}
}
VisibilityConstraintInner::KleeneOr(lhs_id, rhs_id) => {
let lhs = self.evaluate_impl(db, *lhs_id, max_depth - 1);
if lhs == Truthiness::AlwaysTrue {
return Truthiness::AlwaysTrue;
}
let rhs = self.evaluate_impl(db, *rhs_id, max_depth - 1);
if rhs == Truthiness::AlwaysTrue {
Truthiness::AlwaysTrue
} else if lhs == Truthiness::AlwaysFalse && rhs == Truthiness::AlwaysFalse {
Truthiness::AlwaysFalse
} else {
Truthiness::Ambiguous
}
}
}
}
@@ -617,28 +391,15 @@ impl<'db> VisibilityConstraints<'db> {
fn analyze_single(db: &dyn Db, constraint: &Constraint) -> Truthiness {
match constraint.node {
ConstraintNode::Expression(test_expr) => {
let inference = infer_expression_types(db, test_expr);
let scope = test_expr.scope(db);
let ty = inference
.expression_type(test_expr.node_ref(db).scoped_expression_id(db, scope));
let ty = infer_expression_type(db, test_expr);
ty.bool(db).negate_if(!constraint.is_positive)
}
ConstraintNode::Pattern(inner) => match inner.kind(db) {
PatternConstraintKind::Value(value, guard) => {
let subject_expression = inner.subject(db);
let inference = infer_expression_types(db, *subject_expression);
let scope = subject_expression.scope(db);
let subject_ty = inference.expression_type(
subject_expression
.node_ref(db)
.scoped_expression_id(db, scope),
);
let inference = infer_expression_types(db, *value);
let scope = value.scope(db);
let value_ty = inference
.expression_type(value.node_ref(db).scoped_expression_id(db, scope));
let subject_ty = infer_expression_type(db, *subject_expression);
let value_ty = infer_expression_type(db, *value);
if subject_ty.is_single_valued(db) {
let truthiness =

View File

@@ -1,5 +1,6 @@
use camino::Utf8Path;
use dir_test::{dir_test, Fixture};
use std::path::Path;
/// See `crates/red_knot_test/README.md` for documentation on these tests.
#[dir_test(
@@ -8,23 +9,16 @@ use dir_test::{dir_test, Fixture};
)]
#[allow(clippy::needless_pass_by_value)]
fn mdtest(fixture: Fixture<&str>) {
let absolute_fixture_path = Utf8Path::new(fixture.path());
let crate_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR"));
let snapshot_path = crate_dir.join("resources").join("mdtest").join("snapshots");
let fixture_path = Utf8Path::new(fixture.path());
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let workspace_root = crate_dir.ancestors().nth(2).unwrap();
let relative_fixture_path = absolute_fixture_path.strip_prefix(workspace_root).unwrap();
let short_title = absolute_fixture_path.file_name().unwrap();
let long_title = fixture_path.strip_prefix(workspace_root).unwrap();
let short_title = fixture_path.file_name().unwrap();
let test_name = test_name("mdtest", absolute_fixture_path);
let test_name = test_name("mdtest", fixture_path);
red_knot_test::run(
absolute_fixture_path,
relative_fixture_path,
&snapshot_path,
short_title,
&test_name,
);
red_knot_test::run(fixture_path, long_title.as_str(), short_title, &test_name);
}
/// Constructs the test name used for individual markdown files

View File

@@ -18,12 +18,10 @@ 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 }
colored = { workspace = true }
insta = { workspace = true, features = ["filters"] }
memchr = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }

View File

@@ -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,63 +126,15 @@ 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
output of a test. This is done by adding a `<!-- snapshot-diagnostics -->` directive
in the corresponding section. For example:
````markdown
## Unresolvable module import
<!-- snapshot-diagnostics -->
```py
import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
```
````
The `snapshot-diagnostics` directive must appear before anything else in
the section.
This will use `insta` to manage an external file snapshot of all diagnostic
output generated.
Inline assertions, as described above, may be used in conjunction with diagnostic
snapshotting.
At present, there is no way to do inline snapshotting or to request more granular
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 +155,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
@@ -394,11 +345,6 @@ I/O error on read.
### Asserting on full diagnostic output
> [!NOTE]
> At present, one can opt into diagnostic snapshotting that is managed via external files. See
> the section above for more details. The feature outlined below, *inline* diagnostic snapshotting,
> is still desirable.
The inline comment diagnostic assertions are useful for making quick, readable assertions about
diagnostics in a particular location. But sometimes we will want to assert on the full diagnostic
output of checking an embedded Python file. Or sometimes (see “incremental tests” below) we will
@@ -419,7 +365,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 +373,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>`&lt;path>`:</code> label as usual, to explicitly specify the Python file for which it asserts
diagnostic output.
@@ -463,7 +409,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 +424,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

View File

@@ -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;
@@ -27,18 +27,12 @@ const MDTEST_TEST_FILTER: &str = "MDTEST_TEST_FILTER";
///
/// Panic on test failure, and print failure details.
#[allow(clippy::print_stdout)]
pub fn run(
absolute_fixture_path: &Utf8Path,
relative_fixture_path: &Utf8Path,
snapshot_path: &Utf8Path,
short_title: &str,
test_name: &str,
) {
let source = std::fs::read_to_string(absolute_fixture_path).unwrap();
pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str) {
let source = std::fs::read_to_string(path).unwrap();
let suite = match test_parser::parse(short_title, &source) {
Ok(suite) => suite,
Err(err) => {
panic!("Error parsing `{absolute_fixture_path}`: {err:?}")
panic!("Error parsing `{path}`: {err:?}")
}
};
@@ -60,23 +54,20 @@ pub fn run(
db.memory_file_system().remove_all();
Files::sync_all(&mut db);
if let Err(failures) = run_test(&mut db, relative_fixture_path, snapshot_path, &test) {
if let Err(failures) = run_test(&mut db, &test) {
any_failures = true;
println!("\n{}\n", test.name().bold().underline());
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 line_info =
format!("{relative_fixture_path}:{absolute_line_number}").cyan();
let absolute_line_number =
backtick_line.checked_add(relative_line_number).unwrap();
let line_info = format!("{long_title}:{absolute_line_number}").cyan();
println!(" {line_info} {failure}");
}
}
@@ -98,12 +89,7 @@ pub fn run(
assert!(!any_failures, "Some tests failed.");
}
fn run_test(
db: &mut db::Db,
relative_fixture_path: &Utf8Path,
snapshot_path: &Utf8Path,
test: &parser::MarkdownTest,
) -> Result<(), Failures> {
fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures> {
let project_root = db.project_root().to_path_buf();
let src_path = SystemPathBuf::from("/src");
let custom_typeshed_path = test.configuration().typeshed().map(SystemPathBuf::from);
@@ -122,7 +108,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 +124,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 +135,7 @@ fn run_test(
Some(TestFile {
file,
backtick_offsets: embedded.backtick_offsets.clone(),
backtick_offset: embedded.backtick_offset,
})
})
.collect();
@@ -186,10 +176,6 @@ fn run_test(
)
.expect("Failed to update Program settings in TestDb");
// When snapshot testing is enabled, this is populated with
// all diagnostics. Otherwise it remains empty.
let mut snapshot_diagnostics = vec![];
let failures: Failures = test_files
.into_iter()
.filter_map(|test_file| {
@@ -228,7 +214,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,
});
}
@@ -238,36 +224,16 @@ fn run_test(
diagnostic
}));
let failure =
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,
by_line: line_failures,
}),
};
if test.should_snapshot_diagnostics() {
snapshot_diagnostics.extend(diagnostics);
match matcher::match_file(db, test_file.file, diagnostics) {
Ok(()) => None,
Err(line_failures) => Some(FileFailures {
backtick_offset: test_file.backtick_offset,
by_line: line_failures,
}),
}
failure
})
.collect();
if !snapshot_diagnostics.is_empty() {
let snapshot =
create_diagnostic_snapshot(db, relative_fixture_path, test, snapshot_diagnostics);
let name = test.name().replace(' ', "_");
insta::with_settings!(
{
snapshot_path => snapshot_path,
input_file => name.clone(),
filters => vec![(r"\\", "/")],
prepend_module_to_snapshot => false,
},
{ insta::assert_snapshot!(name, snapshot) }
);
}
if failures.is_empty() {
Ok(())
} else {
@@ -279,10 +245,9 @@ type Failures = Vec<FileFailures>;
/// The failures for a single file in a test by line number.
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,58 +255,6 @@ struct FileFailures {
struct TestFile {
file: File,
/// Positional information about the code block(s) to reconstruct absolute line numbers.
backtick_offsets: Vec<BacktickOffsets>,
}
fn create_diagnostic_snapshot<D: Diagnostic>(
db: &mut db::Db,
relative_fixture_path: &Utf8Path,
test: &parser::MarkdownTest,
diagnostics: impl IntoIterator<Item = D>,
) -> String {
// TODO(ag): Do something better than requiring this
// global state to be twiddled everywhere.
colored::control::set_override(false);
let mut snapshot = String::new();
writeln!(snapshot).unwrap();
writeln!(snapshot, "---").unwrap();
writeln!(snapshot, "mdtest name: {}", test.name()).unwrap();
writeln!(snapshot, "mdtest path: {relative_fixture_path}").unwrap();
writeln!(snapshot, "---").unwrap();
writeln!(snapshot).unwrap();
writeln!(snapshot, "# Python source files").unwrap();
writeln!(snapshot).unwrap();
for file in test.files() {
writeln!(snapshot, "## {}", file.relative_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
// highlighting when you look at the snapshot on GitHub,
// but the line numbers are extremely useful for analyzing
// snapshots. So we keep them.
writeln!(snapshot, "```").unwrap();
let line_number_width = file.code.lines().count().to_string().len();
for (i, line) in file.code.lines().enumerate() {
let line_number = i + 1;
writeln!(snapshot, "{line_number:>line_number_width$} | {line}").unwrap();
}
writeln!(snapshot, "```").unwrap();
writeln!(snapshot).unwrap();
}
writeln!(snapshot, "# Diagnostics").unwrap();
writeln!(snapshot).unwrap();
for (i, diag) in diagnostics.into_iter().enumerate() {
if i > 0 {
writeln!(snapshot).unwrap();
}
writeln!(snapshot, "```").unwrap();
writeln!(snapshot, "{}", diag.display(db)).unwrap();
writeln!(snapshot, "```").unwrap();
}
snapshot
// Offset of the backticks that starts the code block in the Markdown file
backtick_offset: TextSize,
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2174,65 +2174,3 @@ fn flake8_import_convention_unused_aliased_import() {
.arg("-")
.pass_stdin("1"));
}
#[test]
fn flake8_import_convention_unused_aliased_import_no_conflict() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(r#"lint.isort.required-imports = ["import pandas as pd"]"#)
.args(["--select", "I002,ICN001,F401"])
.args(["--stdin-filename", "test.py"])
.arg("--unsafe-fixes")
.arg("--fix")
.arg("-")
.pass_stdin("1"));
}
/// Test that private, old-style `TypeVar` generics
/// 1. Get replaced with PEP 695 type parameters (UP046, UP047)
/// 2. Get renamed to remove leading underscores (UP049)
/// 3. Emit a warning that the standalone type variable is now unused (PYI018)
/// 4. Remove the now-unused `Generic` import
#[test]
fn pep695_generic_rename() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--select", "F401,PYI018,UP046,UP047,UP049"])
.args(["--stdin-filename", "test.py"])
.arg("--unsafe-fixes")
.arg("--fix")
.arg("--preview")
.arg("--target-version=py312")
.arg("-")
.pass_stdin(
r#"
from typing import Generic, TypeVar
_T = TypeVar("_T")
class OldStyle(Generic[_T]):
var: _T
def func(t: _T) -> _T:
x: _T
return x
"#
),
@r"
success: true
exit_code: 0
----- stdout -----
class OldStyle[T]:
var: T
def func[T](t: T) -> T:
x: T
return x
----- stderr -----
Found 7 errors (7 fixed, 0 remaining).
"
);
}

View File

@@ -1,27 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- concise
- "--config"
- "lint.isort.required-imports = [\"import pandas as pd\"]"
- "--select"
- "I002,ICN001,F401"
- "--stdin-filename"
- test.py
- "--unsafe-fixes"
- "--fix"
- "-"
stdin: "1"
---
success: true
exit_code: 0
----- stdout -----
import pandas as pd
1
----- stderr -----
Found 1 error (1 fixed, 0 remaining).

View File

@@ -375,50 +375,6 @@ impl Diagnostic for Box<dyn Diagnostic> {
}
}
impl Diagnostic for &'_ dyn Diagnostic {
fn id(&self) -> DiagnosticId {
(**self).id()
}
fn message(&self) -> Cow<str> {
(**self).message()
}
fn file(&self) -> Option<File> {
(**self).file()
}
fn range(&self) -> Option<TextRange> {
(**self).range()
}
fn severity(&self) -> Severity {
(**self).severity()
}
}
impl Diagnostic for std::sync::Arc<dyn Diagnostic> {
fn id(&self) -> DiagnosticId {
(**self).id()
}
fn message(&self) -> Cow<str> {
(**self).message()
}
fn file(&self) -> Option<File> {
(**self).file()
}
fn range(&self) -> Option<TextRange> {
(**self).range()
}
fn severity(&self) -> Severity {
(**self).severity()
}
}
#[derive(Debug)]
pub struct ParseDiagnostic {
file: File,

View File

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

View File

@@ -16,17 +16,6 @@ list((2 * x for x in range(3)))
list(((2 * x for x in range(3))))
list((((2 * x for x in range(3)))))
# Account for trailing comma in fix
# See https://github.com/astral-sh/ruff/issues/15852
list((0 for _ in []),)
list(
(0 for _ in [])
# some comments
,
# some more
)
# Not built-in list.
def list(*args, **kwargs):
return None

View File

@@ -26,16 +26,6 @@ set((2 * x for x in range(3)))
set(((2 * x for x in range(3))))
set((((2 * x for x in range(3)))))
# Account for trailing comma in fix
# See https://github.com/astral-sh/ruff/issues/15852
set((0 for _ in []),)
set(
(0 for _ in [])
# some comments
,
# some more
)
# Not built-in set.
def set(*args, **kwargs):
return None

View File

@@ -1,4 +1,4 @@
from typing import TypeVar, Self, Type, cast
from typing import TypeVar, Self, Type
_S = TypeVar("_S", bound=BadClass)
_S2 = TypeVar("_S2", BadClass, GoodClass)
@@ -56,7 +56,7 @@ class CustomClassMethod:
_S695 = TypeVar("_S695", bound="PEP695Fix")
# Only .pyi gets fixes, no fixes for .py
class PEP695Fix:
def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
@@ -139,38 +139,3 @@ class NoReturnAnnotations:
class MultipleBoundParameters:
def m[S: int, T: int](self: S, other: T) -> S: ...
def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
class MethodsWithBody:
def m[S](self: S, other: S) -> S:
x: S = other
return x
@classmethod
def n[S](cls: type[S], other: S) -> S:
x: type[S] = type(other)
return x()
class StringizedReferencesCanBeFixed:
def m[S](self: S) -> S:
x = cast("list[tuple[S, S]]", self)
return x
class ButStrangeStringizedReferencesCannotBeFixed:
def m[_T](self: _T) -> _T:
x = cast('list[_\x54]', self)
return x
class DeletionsAreNotTouched:
def m[S](self: S) -> S:
# `S` is not a local variable here, and `del` can only be used with local variables,
# so `del S` here is not actually a reference to the type variable `S`.
# This `del` statement is therefore not touched by the autofix (it raises `UnboundLocalError`
# both before and after the autofix)
del S
return self
class NamesShadowingTypeVarAreNotTouched:
def m[S](self: S) -> S:
type S = int
print(S) # not a reference to the type variable, so not touched by the autofix
return 42

View File

@@ -56,7 +56,7 @@ class CustomClassMethod:
_S695 = TypeVar("_S695", bound="PEP695Fix")
# Only .pyi gets fixes, no fixes for .py
class PEP695Fix:
def __new__[S: PEP695Fix](cls: type[S]) -> S: ...

View File

@@ -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

View File

@@ -9,21 +9,3 @@ class Class:
def func(_, setUp):
return _, setUp
from typing import override
class Extended(Class):
@override
def method(self, _, a, A): ...
@override # Incorrect usage
def func(_, a, A): ...
func = lambda _, a, A: ...
class Extended(Class):
method = override(lambda self, _, a, A: ...) # Incorrect usage

View File

@@ -1,7 +0,0 @@
import os
import sys
sys.path += [os.path.dirname(__file__)]
sys.path += ["../"]
from package import module

View File

@@ -1,30 +0,0 @@
# simple case, replace _T in signature and body
class Generic[_T]:
buf: list[_T]
def append(self, t: _T):
self.buf.append(t)
# simple case, replace _T in signature and body
def second[_T](var: tuple[_T]) -> _T:
y: _T = var[1]
return y
# one diagnostic for each variable, comments are preserved
def many_generics[
_T, # first generic
_U, # second generic
](args):
return args
# neither of these are currently renamed
from typing import Literal, cast
def f[_T](v):
cast("_T", v)
cast("Literal['_T']")
cast("list[_T]", v)

View File

@@ -1,56 +0,0 @@
# bound
class Foo[_T: str]:
var: _T
# constraint
class Foo[_T: (str, bytes)]:
var: _T
# python 3.13+ default
class Foo[_T = int]:
var: _T
# tuple
class Foo[*_Ts]:
var: tuple[*_Ts]
# paramspec
class C[**_P]:
var: _P
from typing import Callable
# each of these will get a separate diagnostic, but at least they'll all get
# fixed
class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
@staticmethod
def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
return None
# this should not be fixed because the new name is a keyword, but we still
# offer a diagnostic
class F[_async]: ...
# and this should not be fixed because of the conflict with the outer X, but it
# also gets a diagnostic
def f():
type X = int
class ScopeConflict[_X]:
var: _X
x: X
# these cases should be skipped entirely
def f[_](x: _) -> _: ...
def g[__](x: __) -> __: ...
def h[_T_](x: _T_) -> _T_: ...
def i[__T__](x: __T__) -> __T__: ...

View File

@@ -76,27 +76,6 @@ def _():
f.write(())
def _():
# https://github.com/astral-sh/ruff/issues/15936
with open("file", "w") as f:
for char in "a", "b":
f.write(char)
def _():
# https://github.com/astral-sh/ruff/issues/15936
with open("file", "w") as f:
for char in "a", "b":
f.write(f"{char}")
def _():
with open("file", "w") as f:
for char in (
"a", # Comment
"b"
):
f.write(f"{char}")
# OK
def _():

View File

@@ -31,20 +31,6 @@ for x in (1, 2, 3):
for x in (1, 2, 3):
s.add(x + num)
# https://github.com/astral-sh/ruff/issues/15936
for x in 1, 2, 3:
s.add(x)
for x in 1, 2, 3:
s.add(f"{x}")
for x in (
1, # Comment
2, 3
):
s.add(f"{x}")
# False negative
class C:
@@ -55,7 +41,6 @@ c = C()
for x in (1, 2, 3):
c.s.add(x)
# Ok
s.update(x for x in (1, 2, 3))

View File

@@ -26,23 +26,6 @@ type(None) != type(foo)
type(None) != type(None)
type(a.b) is type(None)
type(
a(
# Comment
)
) != type(None)
type(
a := 1
) == type(None)
type(
a for a in range(0)
) is not type(None)
# Ok.
foo is None

View File

@@ -23,14 +23,3 @@ class B:
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []
# Lint should account for deferred annotations
# See https://github.com/astral-sh/ruff/issues/15857
@dataclass
class AWithQuotes:
mutable_default: 'list[int]' = []
immutable_annotation: 'typing.Sequence[int]' = []
without_annotation = []
correct_code: 'list[int]' = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: 'list[int]' = field(default_factory=list)
class_variable: 'typing.ClassVar[list[int]]'= []

View File

@@ -1,19 +0,0 @@
# Lint should account for deferred annotations
# See https://github.com/astral-sh/ruff/issues/15857
from __future__ import annotations
import typing
from dataclasses import dataclass
@dataclass
class Example():
"""Class that uses ClassVar."""
options: ClassVar[dict[str, str]] = {}
if typing.TYPE_CHECKING:
from typing import ClassVar

View File

@@ -1,15 +0,0 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING
def default_function() ->list[int]:
return []
@dataclass()
class A:
hidden_mutable_default: list[int] = default_function()
class_variable: typing.ClassVar[list[int]] = default_function()
another_class_var: ClassVar[list[int]] = default_function()
if TYPE_CHECKING:
from typing import ClassVar

View File

@@ -103,18 +103,3 @@ class K(SQLModel):
class L(SQLModel):
id: int
i_j: list[K] = list()
# Lint should account for deferred annotations
# See https://github.com/astral-sh/ruff/issues/15857
class AWithQuotes:
__slots__ = {
"mutable_default": "A mutable default value",
}
mutable_default: 'list[int]' = []
immutable_annotation: 'Sequence[int]'= []
without_annotation = []
class_variable: 'ClassVar[list[int]]' = []
final_variable: 'Final[list[int]]' = []
class_variable_without_subscript: 'ClassVar' = []
final_variable_without_subscript: 'Final' = []

View File

@@ -1,16 +0,0 @@
# Lint should account for deferred annotations
# See https://github.com/astral-sh/ruff/issues/15857
from __future__ import annotations
import typing
class Example():
"""Class that uses ClassVar."""
options: ClassVar[dict[str, str]] = {}
if typing.TYPE_CHECKING:
from typing import ClassVar

View File

@@ -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]]): ...

View File

@@ -5,7 +5,7 @@ use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_type_checking, pyflakes,
pylint, pyupgrade, refurb, ruff,
pylint, refurb, ruff,
};
/// Run lint rules over the [`Binding`]s.
@@ -24,7 +24,6 @@ pub(crate) fn bindings(checker: &mut Checker) {
Rule::PytestUnittestRaisesAssertion,
Rule::ForLoopWrites,
Rule::CustomTypeVarForSelf,
Rule::PrivateTypeParameter,
]) {
return;
}
@@ -124,10 +123,5 @@ pub(crate) fn bindings(checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::PrivateTypeParameter) {
if let Some(diagnostic) = pyupgrade::rules::private_type_parameter(checker, binding) {
checker.diagnostics.push(diagnostic);
}
}
}
}

View File

@@ -18,14 +18,11 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::AsyncioDanglingTask,
Rule::BadStaticmethodArgument,
Rule::BuiltinAttributeShadowing,
Rule::FunctionCallInDataclassDefaultArgument,
Rule::GlobalVariableNotAssigned,
Rule::ImportPrivateName,
Rule::ImportShadowedByLoopVar,
Rule::InvalidFirstArgumentNameForClassMethod,
Rule::InvalidFirstArgumentNameForMethod,
Rule::MutableClassDefault,
Rule::MutableDataclassDefault,
Rule::NoSelfUse,
Rule::RedefinedArgumentFromLocal,
Rule::RedefinedWhileUnused,
@@ -383,19 +380,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
&mut diagnostics,
);
}
if checker.enabled(Rule::FunctionCallInDataclassDefaultArgument) {
ruff::rules::function_call_in_dataclass_default(
checker,
class_def,
&mut diagnostics,
);
}
if checker.enabled(Rule::MutableClassDefault) {
ruff::rules::mutable_class_default(checker, class_def, &mut diagnostics);
}
if checker.enabled(Rule::MutableDataclassDefault) {
ruff::rules::mutable_dataclass_default(checker, class_def, &mut diagnostics);
}
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {

View File

@@ -1746,12 +1746,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
ruff::rules::parenthesize_chained_logical_operators(checker, bool_op);
}
}
Expr::Lambda(lambda) => {
Expr::Lambda(lambda_expr) => {
if checker.enabled(Rule::ReimplementedOperator) {
refurb::rules::reimplemented_operator(checker, &lambda.into());
}
if checker.enabled(Rule::InvalidArgumentName) {
pep8_naming::rules::invalid_argument_name_lambda(checker, lambda);
refurb::rules::reimplemented_operator(checker, &lambda_expr.into());
}
}
_ => {}

View File

@@ -3,7 +3,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_builtins, pycodestyle};
use crate::rules::{flake8_builtins, pep8_naming, pycodestyle};
/// Run lint rules over a [`Parameter`] syntax node.
pub(crate) fn parameter(parameter: &Parameter, checker: &mut Checker) {
@@ -14,6 +14,15 @@ pub(crate) fn parameter(parameter: &Parameter, checker: &mut Checker) {
parameter.name.range(),
);
}
if checker.enabled(Rule::InvalidArgumentName) {
if let Some(diagnostic) = pep8_naming::rules::invalid_argument_name(
&parameter.name,
parameter,
&checker.settings.pep8_naming.ignore_names,
) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::BuiltinArgumentShadowing) {
flake8_builtins::rules::builtin_argument_shadowing(checker, parameter);
}

View File

@@ -379,9 +379,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NonPEP695GenericFunction) {
pyupgrade::rules::non_pep695_generic_function(checker, function_def);
}
if checker.enabled(Rule::InvalidArgumentName) {
pep8_naming::rules::invalid_argument_name_function(checker, function_def);
}
}
Stmt::Return(_) => {
if checker.enabled(Rule::ReturnOutsideFunction) {
@@ -515,6 +512,15 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NonUniqueEnums) {
flake8_pie::rules::non_unique_enums(checker, stmt, body);
}
if checker.enabled(Rule::MutableClassDefault) {
ruff::rules::mutable_class_default(checker, class_def);
}
if checker.enabled(Rule::MutableDataclassDefault) {
ruff::rules::mutable_dataclass_default(checker, class_def);
}
if checker.enabled(Rule::FunctionCallInDataclassDefaultArgument) {
ruff::rules::function_call_in_dataclass_default(checker, class_def);
}
if checker.enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, body);
}
@@ -554,9 +560,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) {

View File

@@ -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(),

View File

@@ -542,7 +542,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyupgrade, "045") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP604AnnotationOptional),
(Pyupgrade, "046") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericClass),
(Pyupgrade, "047") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericFunction),
(Pyupgrade, "049") => (RuleGroup::Preview, rules::pyupgrade::rules::PrivateTypeParameter),
// pydocstyle
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),
@@ -1005,7 +1004,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),

View File

@@ -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`.

View File

@@ -7,11 +7,8 @@ use ruff_diagnostics::Edit;
use ruff_python_ast as ast;
use ruff_python_codegen::Stylist;
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
use ruff_python_stdlib::{builtins::is_python_builtin, keyword::is_keyword};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
pub(crate) struct Renamer;
impl Renamer {
@@ -372,52 +369,3 @@ impl Renamer {
}
}
}
/// Enumeration of various ways in which a binding can shadow other variables
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum ShadowedKind {
/// The variable shadows a global, nonlocal or local symbol
Some,
/// The variable shadows a builtin symbol
BuiltIn,
/// The variable shadows a keyword
Keyword,
/// The variable does not shadow any other symbols
None,
}
impl ShadowedKind {
/// Determines the kind of shadowing or conflict for a given variable name.
///
/// This function is useful for checking whether or not the `target` of a [`Rename::rename`]
/// will shadow another binding.
pub(crate) fn new(name: &str, checker: &Checker, scope_id: ScopeId) -> ShadowedKind {
// Check the kind in order of precedence
if is_keyword(name) {
return ShadowedKind::Keyword;
}
if is_python_builtin(
name,
checker.settings.target_version.minor(),
checker.source_type.is_ipynb(),
) {
return ShadowedKind::BuiltIn;
}
if !checker.semantic().is_available_in_scope(name, scope_id) {
return ShadowedKind::Some;
}
// Default to no shadowing
ShadowedKind::None
}
/// Returns `true` if `self` shadows any global, nonlocal, or local symbol, keyword, or builtin.
pub(crate) const fn shadows_any(self) -> bool {
matches!(
self,
ShadowedKind::Some | ShadowedKind::BuiltIn | ShadowedKind::Keyword
)
}
}

View File

@@ -4,8 +4,7 @@ use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::ExprGenerator;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
@@ -124,14 +123,11 @@ pub(crate) fn unnecessary_generator_list(checker: &mut Checker, call: &ast::Expr
);
// Replace `)` with `]`.
// Place `]` at argument's end or at trailing comma if present
let mut tokenizer =
SimpleTokenizer::new(checker.source(), TextRange::new(argument.end(), call.end()));
let right_bracket_loc = tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.map_or(call.arguments.end(), |comma| comma.end())
- TextSize::from(1);
let call_end = Edit::replacement("]".to_string(), right_bracket_loc, call.end());
let call_end = Edit::replacement(
"]".to_string(),
call.arguments.end() - TextSize::from(1),
call.end(),
);
// Remove the inner parentheses, if the expression is a generator. The easiest way to do
// this reliably is to use the printer.

View File

@@ -4,8 +4,7 @@ use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::ExprGenerator;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
@@ -127,16 +126,9 @@ pub(crate) fn unnecessary_generator_set(checker: &mut Checker, call: &ast::ExprC
);
// Replace `)` with `}`.
// Place `}` at argument's end or at trailing comma if present
let mut tokenizer =
SimpleTokenizer::new(checker.source(), TextRange::new(argument.end(), call.end()));
let right_brace_loc = tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.map_or(call.arguments.end(), |comma| comma.end())
- TextSize::from(1);
let call_end = Edit::replacement(
pad_end("}", call.range(), checker.locator(), checker.semantic()),
right_brace_loc,
call.arguments.end() - TextSize::from(1),
call.end(),
);

View File

@@ -127,7 +127,7 @@ C400.py:16:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
16 |+[2 * x for x in range(3)]
17 17 | list((((2 * x for x in range(3)))))
18 18 |
19 19 | # Account for trailing comma in fix
19 19 | # Not built-in list.
C400.py:17:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
|
@@ -136,7 +136,7 @@ C400.py:17:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
17 | list((((2 * x for x in range(3)))))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C400
18 |
19 | # Account for trailing comma in fix
19 | # Not built-in list.
|
= help: Rewrite as a list comprehension
@@ -147,57 +147,5 @@ C400.py:17:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
17 |-list((((2 * x for x in range(3)))))
17 |+[2 * x for x in range(3)]
18 18 |
19 19 | # Account for trailing comma in fix
20 20 | # See https://github.com/astral-sh/ruff/issues/15852
C400.py:21:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
|
19 | # Account for trailing comma in fix
20 | # See https://github.com/astral-sh/ruff/issues/15852
21 | list((0 for _ in []),)
| ^^^^^^^^^^^^^^^^^^^^^^ C400
22 | list(
23 | (0 for _ in [])
|
= help: Rewrite as a list comprehension
Unsafe fix
18 18 |
19 19 | # Account for trailing comma in fix
20 20 | # See https://github.com/astral-sh/ruff/issues/15852
21 |-list((0 for _ in []),)
21 |+[0 for _ in []]
22 22 | list(
23 23 | (0 for _ in [])
24 24 | # some comments
C400.py:22:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
|
20 | # See https://github.com/astral-sh/ruff/issues/15852
21 | list((0 for _ in []),)
22 | / list(
23 | | (0 for _ in [])
24 | | # some comments
25 | | ,
26 | | # some more
27 | | )
| |__^ C400
|
= help: Rewrite as a list comprehension
Unsafe fix
19 19 | # Account for trailing comma in fix
20 20 | # See https://github.com/astral-sh/ruff/issues/15852
21 21 | list((0 for _ in []),)
22 |-list(
23 |- (0 for _ in [])
22 |+[
23 |+ 0 for _ in []
24 24 | # some comments
25 |- ,
26 |- # some more
27 |- )
25 |+ ]
28 26 |
29 27 |
30 28 | # Not built-in list.
19 19 | # Not built-in list.
20 20 | def list(*args, **kwargs):

View File

@@ -290,7 +290,7 @@ C401.py:26:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
26 |+{2 * x for x in range(3)}
27 27 | set((((2 * x for x in range(3)))))
28 28 |
29 29 | # Account for trailing comma in fix
29 29 | # Not built-in set.
C401.py:27:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
|
@@ -299,7 +299,7 @@ C401.py:27:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
27 | set((((2 * x for x in range(3)))))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C401
28 |
29 | # Account for trailing comma in fix
29 | # Not built-in set.
|
= help: Rewrite as a set comprehension
@@ -310,59 +310,5 @@ C401.py:27:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
27 |-set((((2 * x for x in range(3)))))
27 |+{2 * x for x in range(3)}
28 28 |
29 29 | # Account for trailing comma in fix
30 30 | # See https://github.com/astral-sh/ruff/issues/15852
C401.py:31:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
|
29 | # Account for trailing comma in fix
30 | # See https://github.com/astral-sh/ruff/issues/15852
31 | set((0 for _ in []),)
| ^^^^^^^^^^^^^^^^^^^^^ C401
32 | set(
33 | (0 for _ in [])
|
= help: Rewrite as a set comprehension
Unsafe fix
28 28 |
29 29 | # Account for trailing comma in fix
30 30 | # See https://github.com/astral-sh/ruff/issues/15852
31 |-set((0 for _ in []),)
31 |+{0 for _ in []}
32 32 | set(
33 33 | (0 for _ in [])
34 34 | # some comments
C401.py:32:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
|
30 | # See https://github.com/astral-sh/ruff/issues/15852
31 | set((0 for _ in []),)
32 | / set(
33 | | (0 for _ in [])
34 | | # some comments
35 | | ,
36 | | # some more
37 | | )
| |_^ C401
38 |
39 | # Not built-in set.
|
= help: Rewrite as a set comprehension
Unsafe fix
29 29 | # Account for trailing comma in fix
30 30 | # See https://github.com/astral-sh/ruff/issues/15852
31 31 | set((0 for _ in []),)
32 |-set(
33 |- (0 for _ in [])
32 |+{
33 |+ 0 for _ in []
34 34 | # some comments
35 |- ,
36 |- # some more
37 |-)
35 |+ }
38 36 |
39 37 | # Not built-in set.
40 38 | def set(*args, **kwargs):
29 29 | # Not built-in set.
30 30 | def set(*args, **kwargs):

View File

@@ -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__{}_{}",

View File

@@ -14,8 +14,8 @@ use crate::importer::{ImportRequest, ResolutionError};
use crate::settings::types::PythonVersion;
/// ## What it does
/// Checks for methods that use custom [`TypeVar`s][typing_TypeVar] in their
/// annotations when they could use [`Self`][Self] instead.
/// Checks for methods that use custom `TypeVar`s in their annotations
/// when they could use `Self` instead.
///
/// ## Why is this bad?
/// While the semantics are often identical, using `Self` is more intuitive
@@ -49,11 +49,10 @@ use crate::settings::types::PythonVersion;
/// def bar(cls, arg: int) -> Self: ...
/// ```
///
/// ## Fix behaviour and safety
/// The fix removes all usages and declarations of the custom type variable.
/// [PEP-695]-style `TypeVar` declarations are also removed from the [type parameter list];
/// however, old-style `TypeVar`s do not have their declarations removed. See
/// [`unused-private-type-var`][PYI018] for a rule to clean up unused private type variables.
/// ## Fix safety
/// The fix is only available in stub files.
/// It will try to remove all usages and declarations of the custom type variable.
/// Pre-[PEP-695]-style declarations will not be removed.
///
/// If there are any comments within the fix ranges, it will be marked as unsafe.
/// Otherwise, it will be marked as safe.
@@ -72,10 +71,6 @@ use crate::settings::types::PythonVersion;
///
/// [PEP 673]: https://peps.python.org/pep-0673/#motivation
/// [PEP 695]: https://peps.python.org/pep-0695/
/// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/
/// [type parameter list]: https://docs.python.org/3/reference/compound_stmts.html#type-params
/// [Self]: https://docs.python.org/3/library/typing.html#typing.Self
/// [typing_TypeVar]: https://docs.python.org/3/library/typing.html#typing.TypeVar
#[derive(ViolationMetadata)]
pub(crate) struct CustomTypeVarForSelf {
typevar_name: String,
@@ -204,7 +199,7 @@ pub(crate) fn custom_type_var_instead_of_self(
let mut diagnostic = Diagnostic::new(
CustomTypeVarForSelf {
typevar_name: custom_typevar.name(checker.source()).to_string(),
typevar_name: custom_typevar.name(checker).to_string(),
},
diagnostic_range,
);
@@ -216,6 +211,7 @@ pub(crate) fn custom_type_var_instead_of_self(
custom_typevar,
self_or_cls_parameter,
self_or_cls_annotation,
function_header_end,
)
});
@@ -470,7 +466,7 @@ fn custom_typevar_preview<'a>(
///
/// * Import `Self` if necessary
/// * Remove the first parameter's annotation
/// * Replace other uses of the original type variable elsewhere in the function with `Self`
/// * Replace other uses of the original type variable elsewhere in the signature with `Self`
/// * If it was a PEP-695 type variable, removes that `TypeVar` from the PEP-695 type-parameter list
fn replace_custom_typevar_with_self(
checker: &Checker,
@@ -478,11 +474,19 @@ fn replace_custom_typevar_with_self(
custom_typevar: TypeVar,
self_or_cls_parameter: &ast::ParameterWithDefault,
self_or_cls_annotation: &ast::Expr,
function_header_end: TextSize,
) -> anyhow::Result<Option<Fix>> {
if checker.settings.preview.is_disabled() {
return Ok(None);
}
// This fix cannot be suggested for non-stubs,
// as a non-stub fix would have to deal with references in body/at runtime as well,
// which is substantially harder and requires a type-aware backend.
if !checker.source_type.is_stub() {
return Ok(None);
}
// (1) Import `Self` (if necessary)
let (import_edit, self_symbol_binding) = import_self(checker, function_def.start())?;
@@ -502,18 +506,18 @@ fn replace_custom_typevar_with_self(
other_edits.push(deletion_edit);
}
// (4) Replace all other references to the original type variable elsewhere in the function with `Self`
let replace_references_range = TextRange::new(self_or_cls_annotation.end(), function_def.end());
// (4) Replace all other references to the original type variable elsewhere in the function's header
// with `Self`
let replace_references_range =
TextRange::new(self_or_cls_annotation.end(), function_header_end);
replace_typevar_usages_with_self(
other_edits.extend(replace_typevar_usages_with_self(
custom_typevar,
checker.source(),
self_or_cls_annotation.range(),
&self_symbol_binding,
replace_references_range,
checker.semantic(),
&mut other_edits,
)?;
));
// (5) Determine the safety of the fixes as a whole
let comment_ranges = checker.comment_ranges();
@@ -558,35 +562,21 @@ fn import_self(checker: &Checker, position: TextSize) -> Result<(Edit, String),
/// This ensures that no edit in this series will overlap with other edits.
fn replace_typevar_usages_with_self<'a>(
typevar: TypeVar<'a>,
source: &'a str,
self_or_cls_annotation_range: TextRange,
self_symbol_binding: &'a str,
editable_range: TextRange,
semantic: &'a SemanticModel<'a>,
edits: &mut Vec<Edit>,
) -> anyhow::Result<()> {
let tvar_name = typevar.name(source);
for reference in typevar.references(semantic) {
let reference_range = reference.range();
if &source[reference_range] != tvar_name {
bail!(
"Cannot autofix: feference in the source code (`{}`) is not equal to the typevar name (`{}`)",
&source[reference_range],
tvar_name
);
}
if !editable_range.contains_range(reference_range) {
continue;
}
if self_or_cls_annotation_range.contains_range(reference_range) {
continue;
}
edits.push(Edit::range_replacement(
self_symbol_binding.to_string(),
reference_range,
));
}
Ok(())
) -> impl Iterator<Item = Edit> + 'a {
typevar
.references(semantic)
.map(Ranged::range)
.filter(move |reference_range| editable_range.contains_range(*reference_range))
.filter(move |reference_range| {
!self_or_cls_annotation_range.contains_range(*reference_range)
})
.map(|reference_range| {
Edit::range_replacement(self_symbol_binding.to_string(), reference_range)
})
}
/// Create an [`Edit`] removing the `TypeVar` binding from the PEP 695 type parameter list.
@@ -634,8 +624,8 @@ impl<'a> TypeVar<'a> {
self.0.kind.is_type_param()
}
fn name(self, source: &'a str) -> &'a str {
self.0.name(source)
fn name(self, checker: &'a Checker) -> &'a str {
self.0.name(checker.source())
}
fn references(

View File

@@ -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);
));
}
}

View File

@@ -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`

View File

@@ -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`

View File

@@ -59,6 +59,7 @@ PYI019_0.py:54:32: PYI019 Use `Self` instead of custom TypeVar `S`
PYI019_0.py:61:48: PYI019 Use `Self` instead of custom TypeVar `S`
|
59 | # Only .pyi gets fixes, no fixes for .py
60 | class PEP695Fix:
61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
| ^ PYI019
@@ -250,67 +251,5 @@ PYI019_0.py:141:63: PYI019 Use `Self` instead of custom TypeVar `S`
140 | def m[S: int, T: int](self: S, other: T) -> S: ...
141 | def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
| ^ PYI019
142 |
143 | class MethodsWithBody:
|
= help: Replace TypeVar `S` with `Self`
PYI019_0.py:144:36: PYI019 Use `Self` instead of custom TypeVar `S`
|
143 | class MethodsWithBody:
144 | def m[S](self: S, other: S) -> S:
| ^ PYI019
145 | x: S = other
146 | return x
|
= help: Replace TypeVar `S` with `Self`
PYI019_0.py:149:41: PYI019 Use `Self` instead of custom TypeVar `S`
|
148 | @classmethod
149 | def n[S](cls: type[S], other: S) -> S:
| ^ PYI019
150 | x: type[S] = type(other)
151 | return x()
|
= help: Replace TypeVar `S` with `Self`
PYI019_0.py:154:26: PYI019 Use `Self` instead of custom TypeVar `S`
|
153 | class StringizedReferencesCanBeFixed:
154 | def m[S](self: S) -> S:
| ^ PYI019
155 | x = cast("list[tuple[S, S]]", self)
156 | return x
|
= help: Replace TypeVar `S` with `Self`
PYI019_0.py:159:28: PYI019 Use `Self` instead of custom TypeVar `_T`
|
158 | class ButStrangeStringizedReferencesCannotBeFixed:
159 | def m[_T](self: _T) -> _T:
| ^^ PYI019
160 | x = cast('list[_\x54]', self)
161 | return x
|
= help: Replace TypeVar `_T` with `Self`
PYI019_0.py:164:26: PYI019 Use `Self` instead of custom TypeVar `S`
|
163 | class DeletionsAreNotTouched:
164 | def m[S](self: S) -> S:
| ^ PYI019
165 | # `S` is not a local variable here, and `del` can only be used with local variables,
166 | # so `del S` here is not actually a reference to the type variable `S`.
|
= help: Replace TypeVar `S` with `Self`
PYI019_0.py:173:26: PYI019 Use `Self` instead of custom TypeVar `S`
|
172 | class NamesShadowingTypeVarAreNotTouched:
173 | def m[S](self: S) -> S:
| ^ PYI019
174 | type S = int
175 | print(S) # not a reference to the type variable, so not touched by the autofix
|
= help: Replace TypeVar `S` with `Self`

View File

@@ -59,6 +59,7 @@ PYI019_0.pyi:54:32: PYI019 Use `Self` instead of custom TypeVar `S`
PYI019_0.pyi:61:48: PYI019 Use `Self` instead of custom TypeVar `S`
|
59 | # Only .pyi gets fixes, no fixes for .py
60 | class PEP695Fix:
61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
| ^ PYI019

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI019_0.py:7:16: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
PYI019_0.py:7:16: PYI019 Use `Self` instead of custom TypeVar `_S`
|
6 | class BadClass:
7 | def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ... # PYI019
@@ -9,34 +9,14 @@ PYI019_0.py:7:16: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
|
= help: Replace TypeVar `_S` with `Self`
Safe fix
4 4 | _S2 = TypeVar("_S2", BadClass, GoodClass)
5 5 |
6 6 | class BadClass:
7 |- def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ... # PYI019
7 |+ def __new__(cls, *args: str, **kwargs: int) -> Self: ... # PYI019
8 8 |
9 9 |
10 10 | def bad_instance_method(self: _S, arg: bytes) -> _S: ... # PYI019
PYI019_0.py:10:28: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
PYI019_0.py:10:28: PYI019 Use `Self` instead of custom TypeVar `_S`
|
10 | def bad_instance_method(self: _S, arg: bytes) -> _S: ... # PYI019
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
|
= help: Replace TypeVar `_S` with `Self`
Safe fix
7 7 | def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ... # PYI019
8 8 |
9 9 |
10 |- def bad_instance_method(self: _S, arg: bytes) -> _S: ... # PYI019
10 |+ def bad_instance_method(self, arg: bytes) -> Self: ... # PYI019
11 11 |
12 12 |
13 13 | @classmethod
PYI019_0.py:14:25: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
PYI019_0.py:14:25: PYI019 Use `Self` instead of custom TypeVar `_S`
|
13 | @classmethod
14 | def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019
@@ -44,17 +24,7 @@ PYI019_0.py:14:25: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
|
= help: Replace TypeVar `_S` with `Self`
Safe fix
11 11 |
12 12 |
13 13 | @classmethod
14 |- def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019
14 |+ def bad_class_method(cls, arg: int) -> Self: ... # PYI019
15 15 |
16 16 |
17 17 | @classmethod
PYI019_0.py:18:33: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
PYI019_0.py:18:33: PYI019 Use `Self` instead of custom TypeVar `_S`
|
17 | @classmethod
18 | def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
@@ -62,17 +32,7 @@ PYI019_0.py:18:33: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
|
= help: Replace TypeVar `_S` with `Self`
Safe fix
15 15 |
16 16 |
17 17 | @classmethod
18 |- def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
18 |+ def bad_posonly_class_method(cls, /) -> Self: ... # PYI019
19 19 |
20 20 |
21 21 | @classmethod
PYI019_0.py:39:14: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:39:14: PYI019 Use `Self` instead of custom TypeVar `S`
|
37 | # Python > 3.12
38 | class PEP695BadDunderNew[T]:
@@ -81,34 +41,14 @@ PYI019_0.py:39:14: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
36 36 |
37 37 | # Python > 3.12
38 38 | class PEP695BadDunderNew[T]:
39 |- def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019
39 |+ def __new__(cls, *args: Any, ** kwargs: Any) -> Self: ... # PYI019
40 40 |
41 41 |
42 42 | def generic_instance_method[S](self: S) -> S: ... # PYI019
PYI019_0.py:42:30: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:42:30: PYI019 Use `Self` instead of custom TypeVar `S`
|
42 | def generic_instance_method[S](self: S) -> S: ... # PYI019
| ^^^^^^^^^^^^^^^^^ PYI019
|
= help: Replace TypeVar `S` with `Self`
Safe fix
39 39 | def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019
40 40 |
41 41 |
42 |- def generic_instance_method[S](self: S) -> S: ... # PYI019
42 |+ def generic_instance_method(self) -> Self: ... # PYI019
43 43 |
44 44 |
45 45 | class PEP695GoodDunderNew[T]:
PYI019_0.py:54:11: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:54:11: PYI019 Use `Self` instead of custom TypeVar `S`
|
52 | # in the settings for this test:
53 | @foo_classmethod
@@ -117,18 +57,9 @@ PYI019_0.py:54:11: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
51 51 | # due to `foo_classmethod being listed in `pep8_naming.classmethod-decorators`
52 52 | # in the settings for this test:
53 53 | @foo_classmethod
54 |- def foo[S](cls: type[S]) -> S: ... # PYI019
54 |+ def foo(cls) -> Self: ... # PYI019
55 55 |
56 56 |
57 57 | _S695 = TypeVar("_S695", bound="PEP695Fix")
PYI019_0.py:61:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:61:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
59 | # Only .pyi gets fixes, no fixes for .py
60 | class PEP695Fix:
61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
@@ -137,17 +68,7 @@ PYI019_0.py:61:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
58 58 |
59 59 |
60 60 | class PEP695Fix:
61 |- def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
61 |+ def __new__(cls) -> Self: ...
62 62 |
63 63 | def __init_subclass__[S](cls: type[S]) -> S: ...
64 64 |
PYI019_0.py:63:26: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:63:26: PYI019 Use `Self` instead of custom TypeVar `S`
|
61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
62 |
@@ -158,17 +79,7 @@ PYI019_0.py:63:26: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
60 60 | class PEP695Fix:
61 61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
62 62 |
63 |- def __init_subclass__[S](cls: type[S]) -> S: ...
63 |+ def __init_subclass__(cls) -> Self: ...
64 64 |
65 65 | def __neg__[S: PEP695Fix](self: S) -> S: ...
66 66 |
PYI019_0.py:65:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:65:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
63 | def __init_subclass__[S](cls: type[S]) -> S: ...
64 |
@@ -179,17 +90,7 @@ PYI019_0.py:65:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
62 62 |
63 63 | def __init_subclass__[S](cls: type[S]) -> S: ...
64 64 |
65 |- def __neg__[S: PEP695Fix](self: S) -> S: ...
65 |+ def __neg__(self) -> Self: ...
66 66 |
67 67 | def __pos__[S](self: S) -> S: ...
68 68 |
PYI019_0.py:67:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:67:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
65 | def __neg__[S: PEP695Fix](self: S) -> S: ...
66 |
@@ -200,17 +101,7 @@ PYI019_0.py:67:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
64 64 |
65 65 | def __neg__[S: PEP695Fix](self: S) -> S: ...
66 66 |
67 |- def __pos__[S](self: S) -> S: ...
67 |+ def __pos__(self) -> Self: ...
68 68 |
69 69 | def __add__[S: PEP695Fix](self: S, other: S) -> S: ...
70 70 |
PYI019_0.py:69:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:69:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
67 | def __pos__[S](self: S) -> S: ...
68 |
@@ -221,17 +112,7 @@ PYI019_0.py:69:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
66 66 |
67 67 | def __pos__[S](self: S) -> S: ...
68 68 |
69 |- def __add__[S: PEP695Fix](self: S, other: S) -> S: ...
69 |+ def __add__(self, other: Self) -> Self: ...
70 70 |
71 71 | def __sub__[S](self: S, other: S) -> S: ...
72 72 |
PYI019_0.py:71:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:71:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
69 | def __add__[S: PEP695Fix](self: S, other: S) -> S: ...
70 |
@@ -242,17 +123,7 @@ PYI019_0.py:71:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
68 68 |
69 69 | def __add__[S: PEP695Fix](self: S, other: S) -> S: ...
70 70 |
71 |- def __sub__[S](self: S, other: S) -> S: ...
71 |+ def __sub__(self, other: Self) -> Self: ...
72 72 |
73 73 | @classmethod
74 74 | def class_method_bound[S: PEP695Fix](cls: type[S]) -> S: ...
PYI019_0.py:74:27: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:74:27: PYI019 Use `Self` instead of custom TypeVar `S`
|
73 | @classmethod
74 | def class_method_bound[S: PEP695Fix](cls: type[S]) -> S: ...
@@ -262,17 +133,7 @@ PYI019_0.py:74:27: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
71 71 | def __sub__[S](self: S, other: S) -> S: ...
72 72 |
73 73 | @classmethod
74 |- def class_method_bound[S: PEP695Fix](cls: type[S]) -> S: ...
74 |+ def class_method_bound(cls) -> Self: ...
75 75 |
76 76 | @classmethod
77 77 | def class_method_unbound[S](cls: type[S]) -> S: ...
PYI019_0.py:77:29: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:77:29: PYI019 Use `Self` instead of custom TypeVar `S`
|
76 | @classmethod
77 | def class_method_unbound[S](cls: type[S]) -> S: ...
@@ -282,17 +143,7 @@ PYI019_0.py:77:29: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
74 74 | def class_method_bound[S: PEP695Fix](cls: type[S]) -> S: ...
75 75 |
76 76 | @classmethod
77 |- def class_method_unbound[S](cls: type[S]) -> S: ...
77 |+ def class_method_unbound(cls) -> Self: ...
78 78 |
79 79 | def instance_method_bound[S: PEP695Fix](self: S) -> S: ...
80 80 |
PYI019_0.py:79:30: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:79:30: PYI019 Use `Self` instead of custom TypeVar `S`
|
77 | def class_method_unbound[S](cls: type[S]) -> S: ...
78 |
@@ -303,17 +154,7 @@ PYI019_0.py:79:30: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
76 76 | @classmethod
77 77 | def class_method_unbound[S](cls: type[S]) -> S: ...
78 78 |
79 |- def instance_method_bound[S: PEP695Fix](self: S) -> S: ...
79 |+ def instance_method_bound(self) -> Self: ...
80 80 |
81 81 | def instance_method_unbound[S](self: S) -> S: ...
82 82 |
PYI019_0.py:81:32: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:81:32: PYI019 Use `Self` instead of custom TypeVar `S`
|
79 | def instance_method_bound[S: PEP695Fix](self: S) -> S: ...
80 |
@@ -324,17 +165,7 @@ PYI019_0.py:81:32: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
78 78 |
79 79 | def instance_method_bound[S: PEP695Fix](self: S) -> S: ...
80 80 |
81 |- def instance_method_unbound[S](self: S) -> S: ...
81 |+ def instance_method_unbound(self) -> Self: ...
82 82 |
83 83 | def instance_method_bound_with_another_parameter[S: PEP695Fix](self: S, other: S) -> S: ...
84 84 |
PYI019_0.py:83:53: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:83:53: PYI019 Use `Self` instead of custom TypeVar `S`
|
81 | def instance_method_unbound[S](self: S) -> S: ...
82 |
@@ -345,17 +176,7 @@ PYI019_0.py:83:53: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
80 80 |
81 81 | def instance_method_unbound[S](self: S) -> S: ...
82 82 |
83 |- def instance_method_bound_with_another_parameter[S: PEP695Fix](self: S, other: S) -> S: ...
83 |+ def instance_method_bound_with_another_parameter(self, other: Self) -> Self: ...
84 84 |
85 85 | def instance_method_unbound_with_another_parameter[S](self: S, other: S) -> S: ...
86 86 |
PYI019_0.py:85:55: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:85:55: PYI019 Use `Self` instead of custom TypeVar `S`
|
83 | def instance_method_bound_with_another_parameter[S: PEP695Fix](self: S, other: S) -> S: ...
84 |
@@ -366,17 +187,7 @@ PYI019_0.py:85:55: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
82 82 |
83 83 | def instance_method_bound_with_another_parameter[S: PEP695Fix](self: S, other: S) -> S: ...
84 84 |
85 |- def instance_method_unbound_with_another_parameter[S](self: S, other: S) -> S: ...
85 |+ def instance_method_unbound_with_another_parameter(self, other: Self) -> Self: ...
86 86 |
87 87 | def multiple_type_vars[S, *Ts, T](self: S, other: S, /, *args: *Ts, a: T, b: list[T]) -> S: ...
88 88 |
PYI019_0.py:87:27: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:87:27: PYI019 Use `Self` instead of custom TypeVar `S`
|
85 | def instance_method_unbound_with_another_parameter[S](self: S, other: S) -> S: ...
86 |
@@ -387,17 +198,7 @@ PYI019_0.py:87:27: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
84 84 |
85 85 | def instance_method_unbound_with_another_parameter[S](self: S, other: S) -> S: ...
86 86 |
87 |- def multiple_type_vars[S, *Ts, T](self: S, other: S, /, *args: *Ts, a: T, b: list[T]) -> S: ...
87 |+ def multiple_type_vars[*Ts, T](self, other: Self, /, *args: *Ts, a: T, b: list[T]) -> Self: ...
88 88 |
89 89 | def mixing_old_and_new_style_type_vars[T](self: _S695, a: T, b: T) -> _S695: ...
90 90 |
PYI019_0.py:89:43: PYI019 [*] Use `Self` instead of custom TypeVar `_S695`
PYI019_0.py:89:43: PYI019 Use `Self` instead of custom TypeVar `_S695`
|
87 | def multiple_type_vars[S, *Ts, T](self: S, other: S, /, *args: *Ts, a: T, b: list[T]) -> S: ...
88 |
@@ -406,17 +207,7 @@ PYI019_0.py:89:43: PYI019 [*] Use `Self` instead of custom TypeVar `_S695`
|
= help: Replace TypeVar `_S695` with `Self`
Safe fix
86 86 |
87 87 | def multiple_type_vars[S, *Ts, T](self: S, other: S, /, *args: *Ts, a: T, b: list[T]) -> S: ...
88 88 |
89 |- def mixing_old_and_new_style_type_vars[T](self: _S695, a: T, b: T) -> _S695: ...
89 |+ def mixing_old_and_new_style_type_vars[T](self, a: T, b: T) -> Self: ...
90 90 |
91 91 |
92 92 | class InvalidButWeDoNotPanic:
PYI019_0.py:94:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:94:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
92 | class InvalidButWeDoNotPanic:
93 | @classmethod
@@ -426,17 +217,7 @@ PYI019_0.py:94:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
91 91 |
92 92 | class InvalidButWeDoNotPanic:
93 93 | @classmethod
94 |- def m[S](cls: type[S], /) -> S[int]: ...
94 |+ def m(cls, /) -> Self[int]: ...
95 95 | def n(self: S) -> S[int]: ...
96 96 |
97 97 |
PYI019_0.py:114:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:114:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
112 | class SubscriptReturnType:
113 | @classmethod
@@ -445,17 +226,7 @@ PYI019_0.py:114:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
111 111 |
112 112 | class SubscriptReturnType:
113 113 | @classmethod
114 |- def m[S](cls: type[S]) -> type[S]: ... # PYI019
114 |+ def m(cls) -> type[Self]: ... # PYI019
115 115 |
116 116 |
117 117 | class SelfNotUsedInReturnAnnotation:
PYI019_0.py:118:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:118:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
117 | class SelfNotUsedInReturnAnnotation:
118 | def m[S](self: S, other: S) -> int: ...
@@ -465,17 +236,7 @@ PYI019_0.py:118:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
115 115 |
116 116 |
117 117 | class SelfNotUsedInReturnAnnotation:
118 |- def m[S](self: S, other: S) -> int: ...
118 |+ def m(self, other: Self) -> int: ...
119 119 | @classmethod
120 120 | def n[S](cls: type[S], other: S) -> int: ...
121 121 |
PYI019_0.py:120:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:120:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
118 | def m[S](self: S, other: S) -> int: ...
119 | @classmethod
@@ -484,17 +245,7 @@ PYI019_0.py:120:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
117 117 | class SelfNotUsedInReturnAnnotation:
118 118 | def m[S](self: S, other: S) -> int: ...
119 119 | @classmethod
120 |- def n[S](cls: type[S], other: S) -> int: ...
120 |+ def n(cls, other: Self) -> int: ...
121 121 |
122 122 |
123 123 | class _NotATypeVar: ...
PYI019_0.py:135:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:135:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
134 | class NoReturnAnnotations:
135 | def m[S](self: S, other: S): ...
@@ -504,17 +255,7 @@ PYI019_0.py:135:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
132 132 |
133 133 |
134 134 | class NoReturnAnnotations:
135 |- def m[S](self: S, other: S): ...
135 |+ def m(self, other: Self): ...
136 136 | @classmethod
137 137 | def n[S](cls: type[S], other: S): ...
138 138 |
PYI019_0.py:137:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:137:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
135 | def m[S](self: S, other: S): ...
136 | @classmethod
@@ -525,17 +266,7 @@ PYI019_0.py:137:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
134 134 | class NoReturnAnnotations:
135 135 | def m[S](self: S, other: S): ...
136 136 | @classmethod
137 |- def n[S](cls: type[S], other: S): ...
137 |+ def n(cls, other: Self): ...
138 138 |
139 139 | class MultipleBoundParameters:
140 140 | def m[S: int, T: int](self: S, other: T) -> S: ...
PYI019_0.py:140:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:140:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
139 | class MultipleBoundParameters:
140 | def m[S: int, T: int](self: S, other: T) -> S: ...
@@ -544,149 +275,11 @@ PYI019_0.py:140:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
= help: Replace TypeVar `S` with `Self`
Safe fix
137 137 | def n[S](cls: type[S], other: S): ...
138 138 |
139 139 | class MultipleBoundParameters:
140 |- def m[S: int, T: int](self: S, other: T) -> S: ...
140 |+ def m[T: int](self, other: T) -> Self: ...
141 141 | def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
142 142 |
143 143 | class MethodsWithBody:
PYI019_0.py:141:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.py:141:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
139 | class MultipleBoundParameters:
140 | def m[S: int, T: int](self: S, other: T) -> S: ...
141 | def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
142 |
143 | class MethodsWithBody:
|
= help: Replace TypeVar `S` with `Self`
Safe fix
138 138 |
139 139 | class MultipleBoundParameters:
140 140 | def m[S: int, T: int](self: S, other: T) -> S: ...
141 |- def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
141 |+ def n[T: (int, str)](self, other: T) -> Self: ...
142 142 |
143 143 | class MethodsWithBody:
144 144 | def m[S](self: S, other: S) -> S:
PYI019_0.py:144:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
143 | class MethodsWithBody:
144 | def m[S](self: S, other: S) -> S:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
145 | x: S = other
146 | return x
|
= help: Replace TypeVar `S` with `Self`
Safe fix
141 141 | def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
142 142 |
143 143 | class MethodsWithBody:
144 |- def m[S](self: S, other: S) -> S:
145 |- x: S = other
144 |+ def m(self, other: Self) -> Self:
145 |+ x: Self = other
146 146 | return x
147 147 |
148 148 | @classmethod
PYI019_0.py:149:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
148 | @classmethod
149 | def n[S](cls: type[S], other: S) -> S:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
150 | x: type[S] = type(other)
151 | return x()
|
= help: Replace TypeVar `S` with `Self`
Safe fix
146 146 | return x
147 147 |
148 148 | @classmethod
149 |- def n[S](cls: type[S], other: S) -> S:
150 |- x: type[S] = type(other)
149 |+ def n(cls, other: Self) -> Self:
150 |+ x: type[Self] = type(other)
151 151 | return x()
152 152 |
153 153 | class StringizedReferencesCanBeFixed:
PYI019_0.py:154:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
153 | class StringizedReferencesCanBeFixed:
154 | def m[S](self: S) -> S:
| ^^^^^^^^^^^^^^^^^ PYI019
155 | x = cast("list[tuple[S, S]]", self)
156 | return x
|
= help: Replace TypeVar `S` with `Self`
Safe fix
151 151 | return x()
152 152 |
153 153 | class StringizedReferencesCanBeFixed:
154 |- def m[S](self: S) -> S:
155 |- x = cast("list[tuple[S, S]]", self)
154 |+ def m(self) -> Self:
155 |+ x = cast("list[tuple[Self, Self]]", self)
156 156 | return x
157 157 |
158 158 | class ButStrangeStringizedReferencesCannotBeFixed:
PYI019_0.py:159:10: PYI019 Use `Self` instead of custom TypeVar `_T`
|
158 | class ButStrangeStringizedReferencesCannotBeFixed:
159 | def m[_T](self: _T) -> _T:
| ^^^^^^^^^^^^^^^^^^^^ PYI019
160 | x = cast('list[_\x54]', self)
161 | return x
|
= help: Replace TypeVar `_T` with `Self`
PYI019_0.py:164:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
163 | class DeletionsAreNotTouched:
164 | def m[S](self: S) -> S:
| ^^^^^^^^^^^^^^^^^ PYI019
165 | # `S` is not a local variable here, and `del` can only be used with local variables,
166 | # so `del S` here is not actually a reference to the type variable `S`.
|
= help: Replace TypeVar `S` with `Self`
Safe fix
161 161 | return x
162 162 |
163 163 | class DeletionsAreNotTouched:
164 |- def m[S](self: S) -> S:
164 |+ def m(self) -> Self:
165 165 | # `S` is not a local variable here, and `del` can only be used with local variables,
166 166 | # so `del S` here is not actually a reference to the type variable `S`.
167 167 | # This `del` statement is therefore not touched by the autofix (it raises `UnboundLocalError`
PYI019_0.py:173:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
172 | class NamesShadowingTypeVarAreNotTouched:
173 | def m[S](self: S) -> S:
| ^^^^^^^^^^^^^^^^^ PYI019
174 | type S = int
175 | print(S) # not a reference to the type variable, so not touched by the autofix
|
= help: Replace TypeVar `S` with `Self`
Safe fix
170 170 | return self
171 171 |
172 172 | class NamesShadowingTypeVarAreNotTouched:
173 |- def m[S](self: S) -> S:
173 |+ def m(self) -> Self:
174 174 | type S = int
175 175 | print(S) # not a reference to the type variable, so not touched by the autofix
176 176 | return 42

View File

@@ -129,6 +129,7 @@ PYI019_0.pyi:54:11: PYI019 [*] Use `Self` instead of custom TypeVar `S`
PYI019_0.pyi:61:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
59 | # Only .pyi gets fixes, no fixes for .py
60 | class PEP695Fix:
61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
@@ -139,7 +140,7 @@ PYI019_0.pyi:61:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
Safe fix
58 58 |
59 59 |
59 59 | # Only .pyi gets fixes, no fixes for .py
60 60 | class PEP695Fix:
61 |- def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
61 |+ def __new__(cls) -> Self: ...

View File

@@ -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")

View File

@@ -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")

View File

@@ -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

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