Skip to content
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] Add a fix for runtime-string-union (TC010) #15222

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

Daverball
Copy link
Contributor

@Daverball Daverball commented Jan 2, 2025

This is still a work in progress to some degrees. I'm pretty happy with how this behaves, but there's a bit of code duplication, which could potentially be reduced by refactoring after #15201 and #15180 have been merged.

There's also a chance that the fix strategy is currently too opinionated and we should instead rely on the quote-annotations (or rather the upcoming quote-type-expressions) setting to guide us towards which fix strategy we choose and only emit a fix when we know that strategy can be applied safely and otherwise choose to not emit a fix.

I'm also not stoked about the complexity of having to parse forward references without the cache in the checker and needing to rely on lookup_symbol rather than resolve_name, which makes it more difficult to merge the two versions of quotes_are_removable back together.

Perhaps we can move this check to a different checker stage in order to address some of these problems, although it would become more difficult to merge related fixes.

Perhaps the fix should also only be available in preview because of this.

\cc @MichaReiser

Summary

This adds a fix for TC010 which removes quotes whenever they can be removed safely and otherwise extends the quotes to the entire union.

This also disables TC010 explicitly in stubs, rather than rely on the execution context, which can still be runtime in stub files for a small set of type expressions.

Test Plan

cargo nextest run

@Daverball
Copy link
Contributor Author

As a side note: I'm not quite sure why some of these fixes are marked as unsafe, even though the union expression doesn't overlap a comment. Do we perhaps have a off-by-one error here?

Copy link
Contributor

github-actions bot commented Jan 2, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Comment on lines +235 to +280
/// Traverses the type expression and checks if the expression can safely
/// be unquoted
fn quotes_are_removable(semantic: &SemanticModel, expr: &Expr, settings: &LinterSettings) -> bool {
match expr {
Expr::BinOp(ast::ExprBinOp {
left, right, op, ..
}) => {
match op {
Operator::BitOr => {
if settings.target_version < PythonVersion::Py310 {
return false;
}
quotes_are_removable(semantic, left, settings)
&& quotes_are_removable(semantic, right, settings)
}
// for now we'll treat uses of other operators as unremovable quotes
// since that would make it an invalid type expression anyways. We skip
// walking subscript
_ => false,
}
}
Expr::Starred(ast::ExprStarred {
value,
ctx: ExprContext::Load,
..
}) => quotes_are_removable(semantic, value, settings),
// Subscript or attribute accesses that are valid type expressions may fail
// at runtime, so we have to assume that they do, to keep code working.
Expr::Subscript(_) | Expr::Attribute(_) => false,
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
for elt in elts {
if !quotes_are_removable(semantic, elt, settings) {
return false;
}
}
true
}
Expr::Name(name) => {
semantic.lookup_symbol(name.id.as_str()).is_none()
|| semantic
.simulate_runtime_load(name, TypingOnlyBindingsStatus::Disallowed)
.is_some()
}
_ => true,
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While there is some code duplication between this and quotes_are_unremovable, this version is simplified for lookups from a runtime context and needs to use lookup_symbol instead of resolve_name since the forward reference has not been traversed yet by the checker.

So I'm torn about whether to merge this with quotes_are_unremovable into a common function in helpers.rs and leveraging an enum with two variants to determine whether we can use resolve_name or need to use lookup_symbol instead. Alternatively we could try passing in a closure. But both approaches seem kind of messy to me.

We could also say we're fine with the additional overhead of lookup_symbol over resolve_name and just always use that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant