Compare commits
1 Commits
0.9.5
...
david/make
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18bb0a36b6 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -1,64 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.5
|
||||
|
||||
### Preview features
|
||||
|
||||
- Recognize all symbols named `TYPE_CHECKING` for `in_type_checking_block` ([#15719](https://github.com/astral-sh/ruff/pull/15719))
|
||||
- \[`flake8-comprehensions`\] Handle builtins at top of file correctly for `unnecessary-dict-comprehension-for-iterable` (`C420`) ([#15837](https://github.com/astral-sh/ruff/pull/15837))
|
||||
- \[`flake8-logging`\] `.exception()` and `exc_info=` outside exception handlers (`LOG004`, `LOG014`) ([#15799](https://github.com/astral-sh/ruff/pull/15799))
|
||||
- \[`flake8-pyi`\] Fix incorrect behaviour of `custom-typevar-return-type` preview-mode autofix if `typing` was already imported (`PYI019`) ([#15853](https://github.com/astral-sh/ruff/pull/15853))
|
||||
- \[`flake8-pyi`\] Fix more complex cases (`PYI019`) ([#15821](https://github.com/astral-sh/ruff/pull/15821))
|
||||
- \[`flake8-pyi`\] Make `PYI019` autofixable for `.py` files in preview mode as well as stubs ([#15889](https://github.com/astral-sh/ruff/pull/15889))
|
||||
- \[`flake8-pyi`\] Remove type parameter correctly when it is the last (`PYI019`) ([#15854](https://github.com/astral-sh/ruff/pull/15854))
|
||||
- \[`pylint`\] Fix missing parens in unsafe fix for `unnecessary-dunder-call` (`PLC2801`) ([#15762](https://github.com/astral-sh/ruff/pull/15762))
|
||||
- \[`pyupgrade`\] Better messages and diagnostic range (`UP015`) ([#15872](https://github.com/astral-sh/ruff/pull/15872))
|
||||
- \[`pyupgrade`\] Rename private type parameters in PEP 695 generics (`UP049`) ([#15862](https://github.com/astral-sh/ruff/pull/15862))
|
||||
- \[`refurb`\] Also report non-name expressions (`FURB169`) ([#15905](https://github.com/astral-sh/ruff/pull/15905))
|
||||
- \[`refurb`\] Mark fix as unsafe if there are comments (`FURB171`) ([#15832](https://github.com/astral-sh/ruff/pull/15832))
|
||||
- \[`ruff`\] Classes with mixed type variable style (`RUF053`) ([#15841](https://github.com/astral-sh/ruff/pull/15841))
|
||||
- \[`airflow`\] `BashOperator` has been moved to `airflow.providers.standard.operators.bash.BashOperator` (`AIR302`) ([#15922](https://github.com/astral-sh/ruff/pull/15922))
|
||||
- \[`flake8-pyi`\] Add autofix for unused-private-type-var (`PYI018`) ([#15999](https://github.com/astral-sh/ruff/pull/15999))
|
||||
- \[`flake8-pyi`\] Significantly improve accuracy of `PYI019` if preview mode is enabled ([#15888](https://github.com/astral-sh/ruff/pull/15888))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Preserve triple quotes and prefixes for strings ([#15818](https://github.com/astral-sh/ruff/pull/15818))
|
||||
- \[`flake8-comprehensions`\] Skip when `TypeError` present from too many (kw)args for `C410`,`C411`, and `C418` ([#15838](https://github.com/astral-sh/ruff/pull/15838))
|
||||
- \[`flake8-pyi`\] Rename `PYI019` and improve its diagnostic message ([#15885](https://github.com/astral-sh/ruff/pull/15885))
|
||||
- \[`pep8-naming`\] Ignore `@override` methods (`N803`) ([#15954](https://github.com/astral-sh/ruff/pull/15954))
|
||||
- \[`pyupgrade`\] Reuse replacement logic from `UP046` and `UP047` to preserve more comments (`UP040`) ([#15840](https://github.com/astral-sh/ruff/pull/15840))
|
||||
- \[`ruff`\] Analyze deferred annotations before enforcing `mutable-(data)class-default` and `function-call-in-dataclass-default-argument` (`RUF008`,`RUF009`,`RUF012`) ([#15921](https://github.com/astral-sh/ruff/pull/15921))
|
||||
- \[`pycodestyle`\] Exempt `sys.path += ...` calls (`E402`) ([#15980](https://github.com/astral-sh/ruff/pull/15980))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Config error only when `flake8-import-conventions` alias conflicts with `isort.required-imports` bound name ([#15918](https://github.com/astral-sh/ruff/pull/15918))
|
||||
- Workaround Even Better TOML crash related to `allOf` ([#15992](https://github.com/astral-sh/ruff/pull/15992))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-comprehensions`\] Unnecessary `list` comprehension (rewrite as a `set` comprehension) (`C403`) - Handle extraneous parentheses around list comprehension ([#15877](https://github.com/astral-sh/ruff/pull/15877))
|
||||
- \[`flake8-comprehensions`\] Handle trailing comma in fixes for `unnecessary-generator-list/set` (`C400`,`C401`) ([#15929](https://github.com/astral-sh/ruff/pull/15929))
|
||||
- \[`flake8-pyi`\] Fix several correctness issues with `custom-type-var-return-type` (`PYI019`) ([#15851](https://github.com/astral-sh/ruff/pull/15851))
|
||||
- \[`pep8-naming`\] Consider any number of leading underscore for `N801` ([#15988](https://github.com/astral-sh/ruff/pull/15988))
|
||||
- \[`pyflakes`\] Visit forward annotations in `TypeAliasType` as types (`F401`) ([#15829](https://github.com/astral-sh/ruff/pull/15829))
|
||||
- \[`pylint`\] Correct min/max auto-fix and suggestion for (`PL1730`) ([#15930](https://github.com/astral-sh/ruff/pull/15930))
|
||||
- \[`refurb`\] Handle unparenthesized tuples correctly (`FURB122`, `FURB142`) ([#15953](https://github.com/astral-sh/ruff/pull/15953))
|
||||
- \[`refurb`\] Avoid `None | None` as well as better detection and fix (`FURB168`) ([#15779](https://github.com/astral-sh/ruff/pull/15779))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add deprecation warning for `ruff-lsp` related settings ([#15850](https://github.com/astral-sh/ruff/pull/15850))
|
||||
- Docs (`linter.md`): clarify that Python files are always searched for in subdirectories ([#15882](https://github.com/astral-sh/ruff/pull/15882))
|
||||
- Fix a typo in `non_pep695_generic_class.rs` ([#15946](https://github.com/astral-sh/ruff/pull/15946))
|
||||
- Improve Docs: Pylint subcategories' codes ([#15909](https://github.com/astral-sh/ruff/pull/15909))
|
||||
- Remove non-existing `lint.extendIgnore` editor setting ([#15844](https://github.com/astral-sh/ruff/pull/15844))
|
||||
- Update black deviations ([#15928](https://github.com/astral-sh/ruff/pull/15928))
|
||||
- Mention `UP049` in `UP046` and `UP047`, add `See also` section to `UP040` ([#15956](https://github.com/astral-sh/ruff/pull/15956))
|
||||
- Add instance variable examples to `RUF012` ([#15982](https://github.com/astral-sh/ruff/pull/15982))
|
||||
- Explain precedence for `ignore` and `select` config ([#15883](https://github.com/astral-sh/ruff/pull/15883))
|
||||
|
||||
## 0.9.4
|
||||
|
||||
### Preview features
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -33,6 +33,8 @@ reveal_type(a >= b) # revealed: Literal[False]
|
||||
|
||||
Even when tuples have different lengths, comparisons should be handled appropriately.
|
||||
|
||||
`different_length.py`:
|
||||
|
||||
```py
|
||||
a = (1, 2, 3)
|
||||
b = (1, 2, 3, 4)
|
||||
@@ -102,6 +104,8 @@ reveal_type(a >= b) # revealed: bool
|
||||
However, if the lexicographic comparison completes without reaching a point where str and int are
|
||||
compared, Python will still produce a result based on the prior elements.
|
||||
|
||||
`short_circuit.py`:
|
||||
|
||||
```py
|
||||
a = (1, 2)
|
||||
b = (999999, "hello")
|
||||
|
||||
@@ -1,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
|
||||
@@ -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]
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -124,49 +124,42 @@ def _(e: Exception | type[Exception] | None):
|
||||
## Exception cause is not an exception
|
||||
|
||||
```py
|
||||
def _():
|
||||
try:
|
||||
raise EOFError() from GeneratorExit # fine
|
||||
except:
|
||||
...
|
||||
try:
|
||||
raise EOFError() from GeneratorExit # fine
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise StopIteration from MemoryError() # fine
|
||||
except:
|
||||
...
|
||||
try:
|
||||
raise StopIteration from MemoryError() # fine
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise BufferError() from None # fine
|
||||
except:
|
||||
...
|
||||
try:
|
||||
raise BufferError() from None # fine
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise ZeroDivisionError from False # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
try:
|
||||
raise ZeroDivisionError from False # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise SystemExit from bool() # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
try:
|
||||
raise SystemExit from bool() # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise
|
||||
except KeyboardInterrupt as e: # fine
|
||||
reveal_type(e) # revealed: KeyboardInterrupt
|
||||
raise LookupError from e # fine
|
||||
try:
|
||||
raise
|
||||
except KeyboardInterrupt as e: # fine
|
||||
reveal_type(e) # revealed: KeyboardInterrupt
|
||||
raise LookupError from e # fine
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise
|
||||
except int as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
raise KeyError from e
|
||||
try:
|
||||
raise
|
||||
except int as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
raise KeyError from e
|
||||
|
||||
def _(e: Exception | type[Exception]):
|
||||
raise ModuleNotFoundError from e # fine
|
||||
|
||||
@@ -29,6 +29,8 @@ completing. The type of `x` at the beginning of the `except` suite in this examp
|
||||
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
|
||||
*after* that redefinition.
|
||||
|
||||
`union_type_inferred.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
@@ -50,7 +52,12 @@ reveal_type(x) # revealed: str | Literal[2]
|
||||
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
|
||||
inferred as having a union type following the `try`/`except` block:
|
||||
|
||||
`branches_unify_to_non_union_type.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -130,6 +137,8 @@ the `except` suite:
|
||||
- At the end of `else`, `x == 3`
|
||||
- At the end of `except`, `x == 2`
|
||||
|
||||
`single_except.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
@@ -158,6 +167,9 @@ been executed in its entirety, or the `try` suite and the `else` suite must both
|
||||
in their entireties:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -186,6 +198,8 @@ A `finally` suite is *always* executed. As such, if we reach the `reveal_type` c
|
||||
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
|
||||
type of `x` at the end of the example is therefore `Literal[2]`:
|
||||
|
||||
`redef_in_finally.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
@@ -211,7 +225,12 @@ at this point than there were when we were inside the `finally` block.
|
||||
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
|
||||
still a TODO item for us.)
|
||||
|
||||
`no_redef_in_finally.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -240,6 +259,8 @@ suites:
|
||||
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
|
||||
`except` suite ran to completion
|
||||
|
||||
`redef_in_finally.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
@@ -277,7 +298,18 @@ itself. (In some control-flow possibilities, some exceptions were merely *suspen
|
||||
`finally` suite; these lead to the scope's termination following the conclusion of the `finally`
|
||||
suite.)
|
||||
|
||||
`no_redef_in_finally.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -299,7 +331,18 @@ reveal_type(x) # revealed: str | bool
|
||||
|
||||
An example with multiple `except` branches and a `finally` branch:
|
||||
|
||||
`multiple_except_branches.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
@@ -337,6 +380,8 @@ If the exception handler has an `else` branch, we must also take into account th
|
||||
control flow could have jumped to the `finally` suite from partway through the `else` suite due to
|
||||
an exception raised *there*.
|
||||
|
||||
`single_except_branch.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
@@ -380,7 +425,24 @@ reveal_type(x) # revealed: bool | float
|
||||
|
||||
The same again, this time with multiple `except` branches:
|
||||
|
||||
`multiple_except_branches.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
def could_raise_returns_float() -> float:
|
||||
return 3.14
|
||||
|
||||
def could_raise_returns_range() -> range:
|
||||
return range(42)
|
||||
|
||||
|
||||
@@ -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`"
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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(): ...
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unpacking.md - Unpacking - Right hand side not iterable
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | a, b = 1 # error: [not-iterable]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:1:8
|
||||
|
|
||||
1 | a, b = 1 # error: [not-iterable]
|
||||
| ^ Object of type `Literal[1]` is not iterable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unpacking.md - Unpacking - Too few values to unpack
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | a, b = (1,) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-assignment
|
||||
--> /src/mdtest_snippet.py:1:1
|
||||
|
|
||||
1 | a, b = (1,) # error: [invalid-assignment]
|
||||
| ^^^^ Not enough values to unpack (expected 2, got 1)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unpacking.md - Unpacking - Too many values to unpack
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-assignment
|
||||
--> /src/mdtest_snippet.py:1:1
|
||||
|
|
||||
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
|
||||
| ^^^^ Too many values to unpack (expected 2, got 3)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -37,6 +37,8 @@ child expression now suppresses errors in the outer expression.
|
||||
For example, the `type: ignore` comment in this example suppresses the error of adding `2` to
|
||||
`"test"` and adding `"other"` to the result of the cast.
|
||||
|
||||
`nested.py`:
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
from typing import cast
|
||||
|
||||
@@ -109,6 +109,8 @@ reveal_type(version_info >= (3, 9)) # revealed: bool
|
||||
|
||||
The fields of `sys.version_info` can be accessed by name:
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
@@ -120,7 +122,11 @@ reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
|
||||
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
|
||||
properties on instance types:
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo(@property)
|
||||
|
||||
@@ -452,9 +452,6 @@ def raise_in_both_branches(cond: bool):
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||
else:
|
||||
# This branch is unreachable, since all control flows in the `try` clause raise exceptions.
|
||||
# As a result, this binding should never be reachable, since new bindings are visible only
|
||||
# when they are reachable.
|
||||
x = "unreachable"
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
@@ -626,9 +623,9 @@ def return_from_nested_if(cond1: bool, cond2: bool):
|
||||
|
||||
## Statically known terminal statements
|
||||
|
||||
We model reachability using the same visibility constraints that we use to model statically known
|
||||
bounds. In this example, we see that the `return` statement is always executed, and therefore that
|
||||
the `"b"` assignment is not visible to the `reveal_type`.
|
||||
Terminal statements do not yet interact correctly with statically known bounds. In this example, we
|
||||
should see that the `return` statement is always executed, and therefore that the `"b"` assignment
|
||||
is not visible to the `reveal_type`.
|
||||
|
||||
```py
|
||||
def _(cond: bool):
|
||||
@@ -638,26 +635,6 @@ def _(cond: bool):
|
||||
if True:
|
||||
return
|
||||
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
```
|
||||
|
||||
## Bindings after a terminal statement are unreachable
|
||||
|
||||
Any bindings introduced after a terminal statement are unreachable, and are currently considered not
|
||||
visible. We [anticipate](https://github.com/astral-sh/ruff/issues/15797) that we want to provide a
|
||||
more useful analysis for code after terminal statements.
|
||||
|
||||
```py
|
||||
def f(cond: bool) -> str:
|
||||
x = "before"
|
||||
if cond:
|
||||
reveal_type(x) # revealed: Literal["before"]
|
||||
return
|
||||
x = "after-return"
|
||||
# TODO: no unresolved-reference error
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(x) # revealed: Unknown
|
||||
else:
|
||||
x = "else"
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
# TODO: Literal["a"]
|
||||
reveal_type(x) # revealed: Literal["a", "b"]
|
||||
```
|
||||
|
||||
@@ -84,11 +84,8 @@ def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: tuple[str, Unknown]
|
||||
reveal_type(z) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
`Unknown` can be subclassed, just like `Any`:
|
||||
|
||||
```py
|
||||
# Unknown can be subclassed, just like Any
|
||||
class C(Unknown): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
@@ -241,12 +238,9 @@ error_message = "A custom message "
|
||||
error_message += "constructed from multiple string literals"
|
||||
# error: "Static assertion error: A custom message constructed from multiple string literals"
|
||||
static_assert(False, error_message)
|
||||
```
|
||||
|
||||
There are limitations to what we can still infer as a string literal. In those cases, we simply fall
|
||||
back to the default message:
|
||||
|
||||
```py
|
||||
# There are limitations to what we can still infer as a string literal. In those cases,
|
||||
# we simply fall back to the default message.
|
||||
shouted_message = "A custom message".upper()
|
||||
# error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(False, shouted_message)
|
||||
@@ -377,11 +371,8 @@ static_assert(is_subtype_of(TypeOf[str], type[str]))
|
||||
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
```
|
||||
|
||||
`TypeOf` can also be used in annotations:
|
||||
|
||||
```py
|
||||
# `TypeOf` can be used in annotations:
|
||||
def type_of_annotation() -> None:
|
||||
t1: TypeOf[Base] = Base
|
||||
t2: TypeOf[Base] = Derived # error: [invalid-assignment]
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>>),
|
||||
|
||||
@@ -478,6 +478,7 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
|
||||
pub(super) struct FlowSnapshot {
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
scope_start_visibility: ScopedVisibilityConstraintId,
|
||||
reachable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -505,6 +506,8 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
|
||||
/// Currently live bindings and declarations for each symbol.
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
|
||||
reachable: bool,
|
||||
}
|
||||
|
||||
impl Default for UseDefMapBuilder<'_> {
|
||||
@@ -517,13 +520,14 @@ impl Default for UseDefMapBuilder<'_> {
|
||||
bindings_by_use: IndexVec::new(),
|
||||
definitions_by_definition: FxHashMap::default(),
|
||||
symbol_states: IndexVec::new(),
|
||||
reachable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn mark_unreachable(&mut self) {
|
||||
self.record_visibility_constraint(ScopedVisibilityConstraintId::ALWAYS_FALSE);
|
||||
self.reachable = false;
|
||||
}
|
||||
|
||||
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
||||
@@ -540,7 +544,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
binding,
|
||||
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
|
||||
);
|
||||
symbol_state.record_binding(def_id, self.scope_start_visibility);
|
||||
symbol_state.record_binding(def_id);
|
||||
}
|
||||
|
||||
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
@@ -592,11 +596,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
|
||||
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
|
||||
|
||||
// If there are any control flow paths that have become unreachable between `snapshot` and
|
||||
// now, then it's not valid to simplify any visibility constraints to `snapshot`.
|
||||
if self.scope_start_visibility != snapshot.scope_start_visibility {
|
||||
return;
|
||||
}
|
||||
self.scope_start_visibility = snapshot.scope_start_visibility;
|
||||
|
||||
// Note that this loop terminates when we reach a symbol not present in the snapshot.
|
||||
// This means we keep visibility constraints for all new symbols, which is intended,
|
||||
@@ -632,7 +632,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
let def_id = self.all_definitions.push(Some(definition));
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
symbol_state.record_declaration(def_id);
|
||||
symbol_state.record_binding(def_id, self.scope_start_visibility);
|
||||
symbol_state.record_binding(def_id);
|
||||
}
|
||||
|
||||
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
|
||||
@@ -649,6 +649,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
FlowSnapshot {
|
||||
symbol_states: self.symbol_states.clone(),
|
||||
scope_start_visibility: self.scope_start_visibility,
|
||||
reachable: self.reachable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,23 +672,21 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
num_symbols,
|
||||
SymbolState::undefined(self.scope_start_visibility),
|
||||
);
|
||||
|
||||
self.reachable = snapshot.reachable;
|
||||
}
|
||||
|
||||
/// Merge the given snapshot into the current state, reflecting that we might have taken either
|
||||
/// path to get here. The new state for each symbol should include definitions from both the
|
||||
/// prior state and the snapshot.
|
||||
pub(super) fn merge(&mut self, snapshot: FlowSnapshot) {
|
||||
// As an optimization, if we know statically that either of the snapshots is always
|
||||
// unreachable, we can leave it out of the merged result entirely. Note that we cannot
|
||||
// perform any type inference at this point, so this is largely limited to unreachability
|
||||
// via terminal statements. If a flow's reachability depends on an expression in the code,
|
||||
// we will include the flow in the merged result; the visibility constraints of its
|
||||
// bindings will include this reachability condition, so that later during type inference,
|
||||
// we can determine whether any particular binding is non-visible due to unreachability.
|
||||
if snapshot.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
|
||||
// Unreachable snapshots should not be merged: If the current snapshot is unreachable, it
|
||||
// should be completely overwritten by the snapshot we're merging in. If the other snapshot
|
||||
// is unreachable, we should return without merging.
|
||||
if !snapshot.reachable {
|
||||
return;
|
||||
}
|
||||
if self.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
|
||||
if !self.reachable {
|
||||
self.restore(snapshot);
|
||||
return;
|
||||
}
|
||||
@@ -713,6 +712,9 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
self.scope_start_visibility = self
|
||||
.visibility_constraints
|
||||
.add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);
|
||||
|
||||
// Both of the snapshots are reachable, so the merged result is too.
|
||||
self.reachable = true;
|
||||
}
|
||||
|
||||
pub(super) fn finish(mut self) -> UseDefMap<'db> {
|
||||
|
||||
@@ -237,11 +237,7 @@ impl SymbolBindings {
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(
|
||||
&mut self,
|
||||
binding_id: ScopedDefinitionId,
|
||||
visibility_constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
|
||||
// The new binding replaces all previous live bindings in this path, and has no
|
||||
// constraints.
|
||||
self.live_bindings = Bindings::with(binding_id.into());
|
||||
@@ -249,7 +245,8 @@ impl SymbolBindings {
|
||||
self.constraints.push(Constraints::default());
|
||||
|
||||
self.visibility_constraints = VisibilityConstraintPerBinding::with_capacity(1);
|
||||
self.visibility_constraints.push(visibility_constraint);
|
||||
self.visibility_constraints
|
||||
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
@@ -352,14 +349,9 @@ impl SymbolState {
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(
|
||||
&mut self,
|
||||
binding_id: ScopedDefinitionId,
|
||||
visibility_constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
|
||||
debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND);
|
||||
self.bindings
|
||||
.record_binding(binding_id, visibility_constraint);
|
||||
self.bindings.record_binding(binding_id);
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
@@ -565,10 +557,7 @@ mod tests {
|
||||
#[test]
|
||||
fn with() {
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
assert_bindings(&sym, &["1<>"]);
|
||||
}
|
||||
@@ -576,10 +565,7 @@ mod tests {
|
||||
#[test]
|
||||
fn record_constraint() {
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
assert_bindings(&sym, &["1<0>"]);
|
||||
@@ -591,17 +577,11 @@ mod tests {
|
||||
|
||||
// merging the same definition with the same constraint keeps the constraint
|
||||
let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1a.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1a.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1b.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1b.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
sym1a.merge(sym1b, &mut visibility_constraints);
|
||||
@@ -610,17 +590,11 @@ mod tests {
|
||||
|
||||
// merging the same definition with differing constraints drops all constraints
|
||||
let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym2a.record_binding(
|
||||
ScopedDefinitionId::from_u32(2),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
|
||||
sym2a.record_constraint(ScopedConstraintId::from_u32(1));
|
||||
|
||||
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1b.record_binding(
|
||||
ScopedDefinitionId::from_u32(2),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym1b.record_binding(ScopedDefinitionId::from_u32(2));
|
||||
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
|
||||
|
||||
sym2a.merge(sym1b, &mut visibility_constraints);
|
||||
@@ -629,10 +603,7 @@ mod tests {
|
||||
|
||||
// merging a constrained definition with unbound keeps both
|
||||
let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym3a.record_binding(
|
||||
ScopedDefinitionId::from_u32(3),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym3a.record_binding(ScopedDefinitionId::from_u32(3));
|
||||
sym3a.record_constraint(ScopedConstraintId::from_u32(3));
|
||||
|
||||
let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(©).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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>`<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
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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).
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path += [os.path.dirname(__file__)]
|
||||
sys.path += ["../"]
|
||||
|
||||
from package import module
|
||||
@@ -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)
|
||||
@@ -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__: ...
|
||||
@@ -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 _():
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]'= []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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' = []
|
||||
|
||||
@@ -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
|
||||
@@ -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]]): ...
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(_)) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -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(
|
||||
¶meter.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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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__{}_{}",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::helpers::map_subscript;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
@@ -6,7 +6,6 @@ use ruff_python_semantic::{Scope, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the presence of unused private `TypeVar`, `ParamSpec` or
|
||||
@@ -14,8 +13,7 @@ use crate::fix;
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// A private `TypeVar` that is defined but not used is likely a mistake. It
|
||||
/// should either be used, made public, or removed to avoid confusion. A type
|
||||
/// variable is considered "private" if its name starts with an underscore.
|
||||
/// should either be used, made public, or removed to avoid confusion.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```pyi
|
||||
@@ -25,11 +23,6 @@ use crate::fix;
|
||||
/// _T = typing.TypeVar("_T")
|
||||
/// _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety and availability
|
||||
/// This rule's fix is available when [`preview`] mode is enabled.
|
||||
/// It is always marked as unsafe, as it would break your code if the type
|
||||
/// variable is imported by another module.
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnusedPrivateTypeVar {
|
||||
type_var_like_name: String,
|
||||
@@ -37,8 +30,6 @@ pub(crate) struct UnusedPrivateTypeVar {
|
||||
}
|
||||
|
||||
impl Violation for UnusedPrivateTypeVar {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let UnusedPrivateTypeVar {
|
||||
@@ -47,16 +38,6 @@ impl Violation for UnusedPrivateTypeVar {
|
||||
} = self;
|
||||
format!("Private {type_var_like_kind} `{type_var_like_name}` is never used")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let UnusedPrivateTypeVar {
|
||||
type_var_like_name,
|
||||
type_var_like_kind,
|
||||
} = self;
|
||||
Some(format!(
|
||||
"Remove unused private {type_var_like_kind} `{type_var_like_name}`"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
@@ -197,7 +178,7 @@ pub(crate) fn unused_private_type_var(
|
||||
let Some(source) = binding.source else {
|
||||
continue;
|
||||
};
|
||||
let stmt @ Stmt::Assign(ast::StmtAssign { targets, value, .. }) =
|
||||
let Stmt::Assign(ast::StmtAssign { targets, value, .. }) =
|
||||
checker.semantic().statement(source)
|
||||
else {
|
||||
continue;
|
||||
@@ -229,20 +210,13 @@ pub(crate) fn unused_private_type_var(
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
diagnostics.push(Diagnostic::new(
|
||||
UnusedPrivateTypeVar {
|
||||
type_var_like_name: id.to_string(),
|
||||
type_var_like_kind: type_var_like_kind.to_string(),
|
||||
},
|
||||
binding.range(),
|
||||
);
|
||||
|
||||
if checker.settings.preview.is_enabled() {
|
||||
let edit = fix::edits::delete_stmt(stmt, None, checker.locator(), checker.indexer());
|
||||
diagnostic.set_fix(Fix::unsafe_edit(edit));
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ PYI018.py:6:1: PYI018 Private TypeVar `_T` is never used
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
|
|
||||
= help: Remove unused private TypeVar `_T`
|
||||
|
||||
PYI018.py:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
||||
|
|
||||
@@ -20,7 +19,6 @@ PYI018.py:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts`
|
||||
|
||||
PYI018.py:8:1: PYI018 Private ParamSpec `_P` is never used
|
||||
|
|
||||
@@ -31,7 +29,6 @@ PYI018.py:8:1: PYI018 Private ParamSpec `_P` is never used
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P`
|
||||
|
||||
PYI018.py:9:1: PYI018 Private ParamSpec `_P2` is never used
|
||||
|
|
||||
@@ -41,7 +38,6 @@ PYI018.py:9:1: PYI018 Private ParamSpec `_P2` is never used
|
||||
| ^^^ PYI018
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P2`
|
||||
|
||||
PYI018.py:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
||||
|
|
||||
@@ -52,4 +48,3 @@ PYI018.py:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts2`
|
||||
|
||||
@@ -10,7 +10,6 @@ PYI018.pyi:6:1: PYI018 Private TypeVar `_T` is never used
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
|
|
||||
= help: Remove unused private TypeVar `_T`
|
||||
|
||||
PYI018.pyi:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
||||
|
|
||||
@@ -20,7 +19,6 @@ PYI018.pyi:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts`
|
||||
|
||||
PYI018.pyi:8:1: PYI018 Private ParamSpec `_P` is never used
|
||||
|
|
||||
@@ -31,7 +29,6 @@ PYI018.pyi:8:1: PYI018 Private ParamSpec `_P` is never used
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P`
|
||||
|
||||
PYI018.pyi:9:1: PYI018 Private ParamSpec `_P2` is never used
|
||||
|
|
||||
@@ -41,7 +38,6 @@ PYI018.pyi:9:1: PYI018 Private ParamSpec `_P2` is never used
|
||||
| ^^^ PYI018
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P2`
|
||||
|
||||
PYI018.pyi:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
||||
|
|
||||
@@ -52,4 +48,3 @@ PYI018.pyi:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts2`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI018.py:6:1: PYI018 [*] Private TypeVar `_T` is never used
|
||||
|
|
||||
4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 |
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
| ^^ PYI018
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
|
|
||||
= help: Remove unused private TypeVar `_T`
|
||||
|
||||
ℹ Unsafe fix
|
||||
3 3 | from typing import TypeVar
|
||||
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 5 |
|
||||
6 |-_T = typing.TypeVar("_T")
|
||||
7 6 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 7 | _P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
|
||||
PYI018.py:7:1: PYI018 [*] Private TypeVarTuple `_Ts` is never used
|
||||
|
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
| ^^^ PYI018
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts`
|
||||
|
||||
ℹ Unsafe fix
|
||||
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 5 |
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 |-_Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 7 | _P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
||||
PYI018.py:8:1: PYI018 [*] Private ParamSpec `_P` is never used
|
||||
|
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
| ^^ PYI018
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P`
|
||||
|
||||
ℹ Unsafe fix
|
||||
5 5 |
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 |-_P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
|
||||
PYI018.py:9:1: PYI018 [*] Private ParamSpec `_P2` is never used
|
||||
|
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
| ^^^ PYI018
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 8 | _P = ParamSpec("_P")
|
||||
9 |-_P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
12 11 | # OK
|
||||
|
||||
PYI018.py:10:1: PYI018 [*] Private TypeVarTuple `_Ts2` is never used
|
||||
|
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
| ^^^^ PYI018
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 8 | _P = ParamSpec("_P")
|
||||
9 9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 |-_Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
12 11 | # OK
|
||||
13 12 | _UsedTypeVar = TypeVar("_UsedTypeVar")
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI018.pyi:6:1: PYI018 [*] Private TypeVar `_T` is never used
|
||||
|
|
||||
4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 |
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
| ^^ PYI018
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
|
|
||||
= help: Remove unused private TypeVar `_T`
|
||||
|
||||
ℹ Unsafe fix
|
||||
3 3 | from typing import TypeVar
|
||||
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 5 |
|
||||
6 |-_T = typing.TypeVar("_T")
|
||||
7 6 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 7 | _P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
|
||||
PYI018.pyi:7:1: PYI018 [*] Private TypeVarTuple `_Ts` is never used
|
||||
|
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
| ^^^ PYI018
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts`
|
||||
|
||||
ℹ Unsafe fix
|
||||
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 5 |
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 |-_Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 7 | _P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
||||
PYI018.pyi:8:1: PYI018 [*] Private ParamSpec `_P` is never used
|
||||
|
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
| ^^ PYI018
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P`
|
||||
|
||||
ℹ Unsafe fix
|
||||
5 5 |
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 |-_P = ParamSpec("_P")
|
||||
9 8 | _P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
|
||||
PYI018.pyi:9:1: PYI018 [*] Private ParamSpec `_P2` is never used
|
||||
|
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
| ^^^ PYI018
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
= help: Remove unused private ParamSpec `_P2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
6 6 | _T = typing.TypeVar("_T")
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 8 | _P = ParamSpec("_P")
|
||||
9 |-_P2 = typing.ParamSpec("_P2")
|
||||
10 9 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
12 11 | # OK
|
||||
|
||||
PYI018.pyi:10:1: PYI018 [*] Private TypeVarTuple `_Ts2` is never used
|
||||
|
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
| ^^^^ PYI018
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
= help: Remove unused private TypeVarTuple `_Ts2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 8 | _P = ParamSpec("_P")
|
||||
9 9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 |-_Ts2 = TypeVarTuple("_Ts2")
|
||||
11 10 |
|
||||
12 11 | # OK
|
||||
13 12 | _UsedTypeVar = TypeVar("_UsedTypeVar")
|
||||
@@ -226,33 +226,6 @@ SIM108.py:167:1: SIM108 [*] Use ternary operator `z = 1 if True else other` inst
|
||||
172 169 | if False:
|
||||
173 170 | z = 1
|
||||
|
||||
SIM108.py:172:1: SIM108 [*] Use ternary operator `z = 1 if False else other` instead of `if`-`else`-block
|
||||
|
|
||||
170 | z = other
|
||||
171 |
|
||||
172 | / if False:
|
||||
173 | | z = 1
|
||||
174 | | else:
|
||||
175 | | z = other
|
||||
| |_____________^ SIM108
|
||||
176 |
|
||||
177 | if 1:
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = 1 if False else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
169 169 | else:
|
||||
170 170 | z = other
|
||||
171 171 |
|
||||
172 |-if False:
|
||||
173 |- z = 1
|
||||
174 |-else:
|
||||
175 |- z = other
|
||||
172 |+z = 1 if False else other
|
||||
176 173 |
|
||||
177 174 | if 1:
|
||||
178 175 | z = True
|
||||
|
||||
SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block
|
||||
|
|
||||
175 | z = other
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user