mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:24 +00:00
[red-knot] Add infrastructure to declare lints (#14873)
## Summary This is the second PR out of three that adds support for enabling/disabling lint rules in Red Knot. You may want to take a look at the [first PR](https://github.com/astral-sh/ruff/pull/14869) in this stack to familiarize yourself with the used terminology. This PR adds a new syntax to define a lint: ```rust declare_lint! { /// ## What it does /// Checks for references to names that are not defined. /// /// ## Why is this bad? /// Using an undefined variable will raise a `NameError` at runtime. /// /// ## Example /// /// ```python /// print(x) # NameError: name 'x' is not defined /// ``` pub(crate) static UNRESOLVED_REFERENCE = { summary: "detects references to names that are not defined", status: LintStatus::preview("1.0.0"), default_level: Level::Warn, } } ``` A lint has a name and metadata about its status (preview, stable, removed, deprecated), the default diagnostic level (unless the configuration changes), and documentation. I use a macro here to derive the kebab-case name and extract the documentation automatically. This PR doesn't yet add any mechanism to discover all known lints. This will be added in the next and last PR in this stack. ## Documentation I documented some rules but then decided that it's probably not my best use of time if I document all of them now (it also means that I play catch-up with all of you forever). That's why I left some rules undocumented (marked with TODO) ## Where is the best place to define all lints? I'm not sure. I think what I have in this PR is fine but I also don't love it because most lints are in a single place but not all of them. If you have ideas, let me know. ## Why is the message not part of the lint, unlike Ruff's `Violation` I understand that the main motivation for defining `message` on `Violation` in Ruff is to remove the need to repeat the same message over and over again. I'm not sure if this is an actual problem. Most rules only emit a diagnostic in a single place and they commonly use different messages if they emit diagnostics in different code paths, requiring extra fields on the `Violation` struct. That's why I'm not convinced that there's an actual need for it and there are alternatives that can reduce the repetition when creating a diagnostic: * Create a helper function. We already do this in red knot with the `add_xy` methods * Create a custom `Diagnostic` implementation that tailors the entire diagnostic and pre-codes e.g. the message Avoiding an extra field on the `Violation` also removes the need to allocate intermediate strings as it is commonly the place in Ruff. Instead, Red Knot can use a borrowed string with `format_args` ## Test Plan `cargo test`
This commit is contained in:
parent
5f548072d9
commit
5fc8e5d80e
17 changed files with 999 additions and 186 deletions
|
@ -71,21 +71,21 @@ class Foo: ...
|
|||
|
||||
```py
|
||||
def f1(
|
||||
# error: [annotation-raw-string] "Type expressions cannot use raw string literal"
|
||||
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
|
||||
a: r"int",
|
||||
# error: [annotation-f-string] "Type expressions cannot use f-strings"
|
||||
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
|
||||
b: f"int",
|
||||
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
c: b"int",
|
||||
d: "int",
|
||||
# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals"
|
||||
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
|
||||
e: "in" "t",
|
||||
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
f: "\N{LATIN SMALL LETTER I}nt",
|
||||
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
g: "\x69nt",
|
||||
h: """int""",
|
||||
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
i: "b'int'",
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
|
@ -164,9 +164,9 @@ i: "{i for i in range(5)}"
|
|||
j: "{i: i for i in range(5)}"
|
||||
k: "(i for i in range(5))"
|
||||
l: "await 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
m: "yield 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
n: "yield from 1"
|
||||
o: "1 < 2"
|
||||
p: "call()"
|
||||
|
|
|
@ -62,14 +62,14 @@ def foo(
|
|||
```py
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
except 3 as e:
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
# error: [invalid-exception] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
except (ValueError, OSError, "foo", b"bar") as e:
|
||||
reveal_type(e) # revealed: ValueError | OSError | Unknown
|
||||
|
||||
|
@ -80,10 +80,10 @@ def foo(
|
|||
):
|
||||
try:
|
||||
help()
|
||||
# error: [invalid-exception]
|
||||
# error: [invalid-exception-caught]
|
||||
except x as e:
|
||||
reveal_type(e) # revealed: Unknown
|
||||
# error: [invalid-exception]
|
||||
# error: [invalid-exception-caught]
|
||||
except y as f:
|
||||
reveal_type(f) # revealed: OSError | RuntimeError | Unknown
|
||||
except z as g:
|
||||
|
|
|
@ -47,13 +47,13 @@ except* (KeyboardInterrupt, AttributeError) as e:
|
|||
```py
|
||||
try:
|
||||
help()
|
||||
except* 3 as e: # error: [invalid-exception]
|
||||
except* 3 as e: # error: [invalid-exception-caught]
|
||||
# TODO: Should be `BaseExceptionGroup[Unknown]` --Alex
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
|
||||
try:
|
||||
help()
|
||||
except* (AttributeError, 42) as e: # error: [invalid-exception]
|
||||
except* (AttributeError, 42) as e: # error: [invalid-exception-caught]
|
||||
# TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
|
|
@ -73,7 +73,7 @@ def f[T]():
|
|||
A typevar with less than two constraints emits a diagnostic:
|
||||
|
||||
```py
|
||||
# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types"
|
||||
# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types"
|
||||
def f[T: (int,)]():
|
||||
pass
|
||||
```
|
||||
|
|
|
@ -179,9 +179,9 @@ reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
|
|||
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
|
||||
|
||||
```py path=a.pyi
|
||||
class A(B): ... # error: [cyclic-class-def]
|
||||
class B(C): ... # error: [cyclic-class-def]
|
||||
class C(A): ... # error: [cyclic-class-def]
|
||||
class A(B): ... # error: [cyclic-class-definition]
|
||||
class B(C): ... # error: [cyclic-class-definition]
|
||||
class C(A): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(A.__class__) # revealed: Unknown
|
||||
```
|
||||
|
|
|
@ -348,14 +348,14 @@ reveal_type(unknown_object.__mro__) # revealed: Unknown
|
|||
These are invalid, but we need to be able to handle them gracefully without panicking.
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Foo): ... # error: [cyclic-class-def]
|
||||
class Foo(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Bar: ...
|
||||
class Baz: ...
|
||||
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-def]
|
||||
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Boz) # revealed: Literal[Boz]
|
||||
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
|
||||
|
@ -366,9 +366,9 @@ reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[objec
|
|||
These are similarly unlikely, but we still shouldn't crash:
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo): ... # error: [cyclic-class-def]
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
|
@ -379,9 +379,9 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
|
|||
|
||||
```py path=a.pyi
|
||||
class Spam: ...
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-def]
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
|
@ -391,16 +391,16 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
|
|||
## Classes with cycles in their MRO, and a sub-graph
|
||||
|
||||
```py path=a.pyi
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-def]
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Foo: ...
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-def]
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
|
||||
class Bar(Foo): ...
|
||||
|
||||
# TODO: can we avoid emitting the errors for these?
|
||||
# The classes have cyclic superclasses,
|
||||
# but are not themselves cyclic...
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-def]
|
||||
class Spam(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Spam(Baz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
||||
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue