-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[red-knot] Typed diagnostic id #14869
Conversation
|
9d47d4c
to
3792746
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code looks good!
My main question about this PR is the use of the term "lint" to encompass core type checker diagnostics. This was discussed extensively in the CLI proposal, without a clear resolution. I don't think this usage is wrong (Python type checkers really are linters, since they don't integrate with runtime), but I suspect that even if we use this terminology internally, we will need to be careful to avoid it in the UI, because I think existing mypy/pyright users will likely find it confusing to have type errors referred to as "lints".
But you said you don't want to over-focus on this now, and we can change it, so not a blocking issue.
I agree. We should use the term rule as the user-facing term and I also intend to use it in code when the context only allows rules but not other lints. For example, the configuration only allows rule names, not arbitrary lints. One possible design is to define a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
5150616
to
b012b24
Compare
## 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](#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`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense to me at this point in my journey. :-) Thank you for tagging me!
} | ||
} | ||
|
||
pub fn as_str(&self) -> Result<&str, DiagnosticAsStrError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make more sense to just return a DiagnosticAsStr
type that implements Display
? So, basically, a DiagnosticAsStrError
like you have, but for all the variants. And in this case, you'd probably rename it to display()
or some such.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another pattern I like for things like this is to bite the bullet on the alloc, but do it at construction time. Then you can have an as_str()
method that unconditionally returns &str
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I introduced it to avoid repeating the variant to name matching code for the matches
method. I think the easiest would be to just call to_string()
in matches which I'm open to do when we have other use cases for it.
Another solution is to have a macro that generates a name
method for it.
Overall, I don't have a very good understanding yet of how this type will be used, and I suggest we change it as we see fit.
Summary
This PR introduces a structured
DiagnosticId
instead of using a plain&'static str
. It is the first of three in a stack that implements a basic rules infrastructure for Red Knot.DiagnosticId
is an enum over all known diagnostic codes. A closed enum reduces the risk of accidentally introducing two identical diagnostic codes. It also opens the possibility of generating reference documentation from the enum in the future (not part of this PR).The enum isn't fully closed because it uses a
&'static str
for lint names. This is because we want the flexibility to define lints in different crates, and all names are only known inred_knot_linter
or above. Still, lower-level crates must already reference the lint names to emit diagnostics. We could define all lint-names inDiagnosticId
but I decided against it because:DiagnosticId
type between Ruff and Red Knot to avoid extra complexity in the diagnostic crate, and both tools use different lint names.In the long term, we may also want to support plugins, which would make it impossible to know all lint names at compile time. The next PR in the stack introduces extra syntax for defining lints.
A closed enum does have a few disadvantages:
ruff_db
crate to define the diagnostic ID. I consider this an acceptable trade. We may want to moveDiagnosticId
to its own crate or into a sharedred_knot_diagnostic
crate.Preventing duplicate diagnostic identifiers
One goal of this PR is to make it harder to introduce ambiguous diagnostic IDs, which is achieved by defining a closed enum. However, the enum isn't fully "closed" because it doesn't explicitly list the IDs for all lint rules. That leaves the possibility that a lint rule and a diagnostic ID share the same name.
I made the names unambiguous in this PR by separating them into different namespaces by using
lint/<rule>
for lint rule codes. I don't mind thelint
prefix in a Ruff next context, but it is a bit weird for a standalone type checker. I'd like to not overfocus on this for now because I see a few different options:lint
prefix and add a unit test in a top-level crate that iterates over all known lint rules and diagnostic IDs to ensure the names are non-overlapping.[lint]
as the error code and add a note to the diagnostic mentioning the lint rule. This is similar to clippy and has the advantage that the header line remains short (lint/some-long-rule-name
is very long ;))I think we can defer this decision for now because the
DiagnosticId
contains all the relevant information to change the rendering accordingly.Why
Lint
and notLintRule
I see three kinds of diagnostics in Red Knot:
DiagnosticId
)Our current implementation doesn't distinguish between lints and Lint rules because we aren't aware of a suppressible code-related lint that can't be configured in the configuration. The only lint that comes to my mind is maybe
division-by-zero
if we're 99.99% sure that it is always right. However, I want to keep the door open to making this distinction in the future if it proves useful.Another reason why I chose lint over lint rule (or just rule) is that I want to leave room for a future lint rule and lint phase concept:
LintRule
trait inred_knot_python_linter
that provides the necessary hooks to run as part of the linter. A lint rule produces diagnostics for exactly one lint. A lint rule differs from all lints inred_knot_python_semantic
because they don't run as "rules" in the Ruff sense. Instead, they're a side-product of type inference.LintDiagnostic
, but a few lints might need more flexibility and implement their custom diagnostic rendering (at least customDiagnostic
implementation).Test Plan
cargo test