-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
[flake8-type-checking] Adds implementation for TCH007 and TCH008 #12927
base: main
Are you sure you want to change the base?
Conversation
The implementation for TCH007 is incomplete and also requires some changes to TCH004 in order to avoid conflicts between TCH004 and TCH007
|
code | total | + violation | - violation | + fix | - fix |
---|---|---|---|---|---|
TCH008 | 6 | 6 | 0 | 0 | 0 |
This comment was marked as resolved.
This comment was marked as resolved.
CodSpeed Performance ReportMerging #12927 will not alter performanceComparing Summary
|
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
Also fixes lexicographical lookup not taking shadowed bindings into account.
@charliermarsh It might be worth to move the TCH010 logic to where the TCH008 logic currently sits, so we can actually autofix TCH010 in the same cases where we would otherwise emit a TCH008 by removing the quotes when we know that the quoted expression should be entirely available at runtime. Concretely we would pass it at the stage where we parse deferred string annotations and check if the current parent expression is a The drawback would be that it may be more difficult to create an autofix in the opposite case that expands the quotes to the entire type expression, since we would need to walk the parent nodes in order to determine the root node of the type expression. |
Thanks @Daverball, sorry that I haven't had a chance to review this yet. |
@charliermarsh Just a friendly reminder in case this PR slipped through the cracks somehow |
@charliermarsh Another reminder. Please let me know if these reminders are a bother. The contribution guidelines don't really seem to contain any information about whether it's acceptable/appreciated/frowned upon to bump review requests, it tends to vary from project to project and maintainer to maintainer. |
No that's fine @Daverball -- sorry, it's just blocked on me finding time and clearly I haven't done a good job of doing so yet. |
crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs
Outdated
Show resolved
Hide resolved
/// from foo import Foo | ||
/// OptFoo: TypeAlias = "Foo | None" | ||
/// ``` | ||
/// |
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.
Why is the fix marked as unsafe? We can document this in a ## Fix safety
section,
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.
Good question, I went by the precedent set by runtime_import_in_type_checking_block
which uses the same function to quote annotations in its fix and also doesn't explain its reasoning. So I don't know what the original reasoning was. It might just be the fact that the forward reference would not be able to be resolved at runtime. Which is a problem with flake8-type-checking in general.
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.
Looks like it is at least in part because it can remove comments on multi-line expressions. Maybe this documentation gap should first be addressed in a separate PR though, since it also affects other existing rules.
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.
It would be great if we can fill the gap for new rules. But I agree, filling the gap for existing rules can be done in a separate PR
f: TypeAlias = Bar # TCH007 | ||
g: TypeAlias = Foo | Bar # TCH007 x2 | ||
h: TypeAlias = Foo[str] # TCH007 | ||
|
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.
What does the fix look like when comments are present?
example:
>>> i: TypeAlias = (int | # TCH007 x2
float)
>>> i
int | float
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.
Until I know why this fix was marked unsafe in the other rules I will continue always to mark this fix as unsafe. If the only reason was the fix potentially eating comments I can apply the same comment detection logic to TCH007.
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH008.py
Outdated
Show resolved
Hide resolved
Co-authored-by: Simon Brugman <sbrugman@users.noreply.github.com>
Marks TCH007 fix as unsafe, since it may remove comments. Adds additional test cases.
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.
Nice. This overall looks good to me, but I know very little about typing or that part of the code base. That's why it would be great to get a second opinion from either @carljm or @AlexWaygood
/// from foo import Foo | ||
/// OptFoo: TypeAlias = "Foo | None" | ||
/// ``` | ||
/// |
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.
It would be great if we can fill the gap for new rules. But I agree, filling the gap for existing rules can be done in a separate PR
crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs
Outdated
Show resolved
Hide resolved
// FIXME: This does not seem quite right, if only TC004 is enabled | ||
// then we don't need to collect the runtime imports |
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.
What's the reason we can't fix this today?
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.
We can, I just didn't want to introduce unrelated changes to this PR, so I opted for a comment as reminder to myself to clean this up in a follow-up pull request.
// FIXME: Shouldn't this happen above where `class_variables_visible` is set? | ||
seen_function |= scope.kind.is_function(); |
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.
Can you expand on why you think this should happen above?
My assumption is that it's here because it updates the state for the next iteration. The only possible issue I see is that it fails updating the seen_function
if the self.bindings[binding_id].kind
is an Annotation
. I, unfortunately, don't have a good enough understanding of this code to assess whether this is a) possible and b) a problem
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'm fairly confident it's incorrect to skip updating seen_function
if you see a BindingKind::Annotation
.
That being said, situations where this would actually lead to the wrong result should be incredibly rare, since the corresponding Python code itself would be highly suspect and probably incorrect.
/// Simulates a runtime load of a given [`ast::ExprName`]. | ||
/// | ||
/// This should not be run until after all the bindings have been visited. | ||
pub fn simulate_runtime_load(&self, name: &ast::ExprName) -> Option<BindingId> { |
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 don't think I'm the right person reviewing this. @AlexWaygood or @carljm could either of you take a look to see if that makes sense (overall)?
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 took a look and left some comments, though I think familiarity with Ruff's semantic model would make @AlexWaygood a better reviewer here.
...es/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs
Outdated
Show resolved
Hide resolved
Ensures there's no circularity issues between `TCH007` and `TCH008` by always running test cases with both rules enabled.
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH008.py
Outdated
Show resolved
Hide resolved
/// Visit an [`Expr`], and treat it as a [PEP 613] explicit type alias. | ||
/// | ||
/// [PEP 613]: https://peps.python.org/pep-0613/ | ||
fn visit_explicit_type_alias(&mut self, expr: &'a Expr) { | ||
let snapshot = self.semantic.flags; | ||
self.semantic.flags |= SemanticModelFlags::EXPLICIT_TYPE_ALIAS; | ||
self.visit_type_definition(expr); | ||
self.semantic.flags = snapshot; | ||
} | ||
|
||
/// Visit an [`Expr`], and treat it as a [PEP 695] generic type alias. | ||
/// | ||
/// [PEP 695]: https://peps.python.org/pep-0695/#generic-type-alias | ||
fn visit_generic_type_alias(&mut self, expr: &'a Expr) { | ||
let snapshot = self.semantic.flags; | ||
// even though we don't visit these nodes immediately we need to | ||
// modify the semantic flags before we push the expression and its | ||
// corresponding semantic snapshot | ||
self.semantic.flags |= SemanticModelFlags::GENERIC_TYPE_ALIAS; |
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.
Naming nit: I would append _value
to the names of both these methods to clarify that they handle the RHS or "value", not the entire type alias.
More substantive naming issue: I think that using "generic type alias" as an all-encompassing description of PEP 695 type aliases is inaccurate and confusing. (It is strange that the PEP text uses this as a header over that entire section of the PEP, but it's clear in code examples in that section that only a type
statement with type parameters is actually generic; one without type parameters is clearly described there as "non-generic".)
Using "explicit type alias" to describe PEP 613 type aliases is not unreasonable, given that's the title of PEP 613, but I still think it's ambiguous (the type
statement is equally explicit!) and based on an indirect reference that requires looking up the PEP to understand.
If we don't mind referencing PEPs, then I think we should just go all-in on that, and opt for concise and totally unambiguous, with names like visit_pep613_type_alias_value
, visit_pep695_type_alias_value
, PEP613_TYPE_ALIAS
, PEP695_TYPE_ALIAS
.
If we want to avoid using PEP numbers like this (which is totally unambiguous, but does require knowing or looking up the PEP numbers), then I think we have to be more verbose to be unambiguous: visit_typealias_annotated_type_alias_value
, visit_type_statement_type_alias_value
, TYPEALIAS_ANNOTATED_TYPE_ALIAS
, TYPE_STATEMENT_TYPE_ALIAS
.
One other note: either way, a code example in the docstring of each method could go a long way towards making it immediately obvious to the reader what form each one is handling.
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'll have a little think about what I prefer. Initially I did use the PEP numbers in their names, but it seemed too easy to accidentally read one as the other when you're just skimming the code, so I decided to switch to the names used in the PEP title/section header respectively, even if they're far from ideal.
The descriptive names are a little too tongue-twistery for my liking, but they may be the correct choice after all.
/// Simulates a runtime load of a given [`ast::ExprName`]. | ||
/// |
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.
It was very much unclear to me, until I spent quite a while reading through the code, what it meant to "simulate a runtime load" and why or how this is different from what lookup_symbol
and resolve_load
do.
It seems the differences are a) this method ignores typing-only bindings (this explains the name choice), and b) this method is supposed to run after all bindings have been collected, but still do a flow-sensitive lookup (i.e. find the applicable binding for a name at a particular point in control flow, which is not necessarily the end of the scope.) Or semi-flow-sensitive, anyway; flow-sensitive as approximated by "source order within the scope, with no understanding of branches."
Difference (a) makes sense, though taken alone it seems like it could be a flag rather than an entirely separate method.
I have a few questions/thoughts about difference (b), some of which may be due to lack of familiarity with Ruff and its semantic model.
It seems like this can be an improvement over Ruff's current model in some cases. But why is this PR the one introducing this capability? What is it about TCH007
and TCH008
that require this capability, when Ruff's many other rules involving looking up name bindings have so far gotten away without this?
My understanding is that largely Ruff handles this by doing name lookups while constructing the semantic model, so inapplicable bindings just don't exist yet. Is that approach not workable in this case? Why do we need a method that must instead be run after all bindings are collected, but then ignores the "late" ones, in inner scopes?
If this capability isn't critical to the functioning of the new rules specifically, but is really a more general improvement to Ruff's semantic model, perhaps it should be looked at more holistically in view of all Ruff rules, and not introduced as part of this PR?
If we do definitely need this new method for these new rules to work at all, then I think this also should be more clearly described in a longer doc comment on the method.
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.
My understanding is that largely Ruff handles this by doing name lookups while constructing the semantic model, so inapplicable bindings just don't exist yet. Is that approach not workable in this case? Why do we need a method that must instead be run after all bindings are collected, but then ignores the "late" ones, in inner scopes?
The main reason is that TCH007/TCH008 perform speculative lookups, rather than the ones that would actually happen at runtime, to ensure that we don't produce violations for the parts of the type alias value that need to available at runtime and vice-versa. (in fact they actually perform both the speculative and regular lookup to avoid violations/fixes going in circles)
The normal flow of the semantic model does not cover the speculative nature of questions like "what if this annotation wasn't a forward reference" or "what if this annotation was turned into a forward reference?".
There are certainly other ways to implement this check (i.e. performing the speculative lookup at the right time during semantic analysis), but this seemed like the least disruptive one (especially since at the time I wrote this there wasn't yet a cache for parsing string annotations) and matches my implementation in the flake8-plugin I wrote, so I can be more confident that it works and produces zero false positives in several large code-bases of mine.
I was also worried that speculatively walking the type definition earlier/later than it was supposed to could lead to other rules triggering or unwanted state leaking into the semantic model. It would be difficult to ensure that this excursion would have no unwanted side-effects.
Difference (a) makes sense, though taken alone it seems like it could be a flag rather than an entirely separate method.
And in fact in my own implementation in the flake8 plugin they are the same method with a flag. However the potentially incorrect placement of the seen_function
update prompted me to keep this method separate for now, in order to avoid potentially subtle regressions in lookup_symbol
.
We could either take the risk and merge these methods now or put it off until later, when we're more confident that the method is stable.
/// Simulates a runtime load of a given [`ast::ExprName`]. | ||
/// | ||
/// This should not be run until after all the bindings have been visited. | ||
pub fn simulate_runtime_load(&self, name: &ast::ExprName) -> Option<BindingId> { |
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 took a look and left some comments, though I think familiarity with Ruff's semantic model would make @AlexWaygood a better reviewer here.
Co-authored-by: Carl Meyer <carl@oddbird.net>
This is part of a series of pull requests fulfilling my promise in #9573 to port some of the enhancements and new rules from flake8-type-checking to ruff.
Summary
This adds the missing flake8-type-checking rules TC(H)007 and TC(H)008 including auto fixes.
There is some overlap between TC(H)007 and TC(H)004, currently the existing rule takes precedence, although ideally we would still emit both violations and share a fix for overlaps based on the selected strategy (if it is possible for violations of two different rules to share the same fix). We could probably move the analysis for TC(H007) into the same function as TC(H)004 in order to accomplish that. But we could defer that to a future pull request.
There is also some potential overlap between TC(H)008 and TC(H)010. Currently TC(H)010 is prioritized, but since it currently has no fix it might make more sense to either prioritize TC(H)008 or add a fix for TC(H)010, although that could be covered by a future pull request.
Test Plan
cargo nextest run