-
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
Move reveal_type
, assert_type
handling out of CallOutcome
#16116
base: main
Are you sure you want to change the base?
Conversation
@@ -239,24 +182,12 @@ impl<'db> CallOutcome<'db> { | |||
} => { | |||
let mut not_callable = vec![]; | |||
let mut union_builder = UnionBuilder::new(context.db()); | |||
let mut revealed = false; |
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 know how relevant this is and if it's worth supporting. I think the following would require the union handling:
if coinflip():
a = reveal_type
else:
a = something_else
a(a)
We, obviously could still support this by moving the union case handling into infer_call_expression
too, but I honestly don't think it's worth the complexity (and there's no test)
Also note that we didn't support this for static assertions... so it was sort of inconsistent already
0555c8b
to
daf19cc
Compare
I don't plan on merging this immediately even if people think this is a good idea. I mainly want to get feedback on the direction and if, deemed acceptable, continue in the direction of reducing |
To extend a bit on 3. I think it's useful if The reason I'm leaning in this direction is that I currently find it hard to decide which of the Will this work, I don't know. |
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.
This is an excellent simplification. Thank you.
if coinflip(): a = reveal_type else: a = something_else a(a)
Yeah, I don't think we need to support this. Neither mypy or pyright does, and it doesn't seem worth the extra complexity. There's no plausible use-case.
073c2d9
to
302b64b
Compare
I'm looking into moving out |
if !truthiness.is_always_true() { | ||
if let Some(message) = | ||
message.into_string_literal().map(|s| &**s.value(self.db())) | ||
{ | ||
self.context.report_lint( | ||
&STATIC_ASSERT_ERROR, | ||
call_expression.into(), | ||
format_args!("Static assertion error: {message}"), | ||
); | ||
} else if parameter_ty == Type::BooleanLiteral(false) { | ||
self.context.report_lint( | ||
&STATIC_ASSERT_ERROR, | ||
call_expression.into(), | ||
format_args!("Static assertion error: argument evaluates to `False`"), | ||
); | ||
} else if truthiness.is_always_false() { | ||
self.context.report_lint( | ||
&STATIC_ASSERT_ERROR, | ||
call_expression.into(), | ||
format_args!( | ||
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy", | ||
parameter_ty=parameter_ty.display(self.db()) | ||
), | ||
); | ||
} else { | ||
self.context.report_lint( | ||
&STATIC_ASSERT_ERROR, | ||
call_expression.into(), | ||
format_args!( | ||
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness", | ||
parameter_ty=parameter_ty.display(self.db()) | ||
), | ||
); | ||
}; | ||
} | ||
} |
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.
if !truthiness.is_always_true() { | |
if let Some(message) = | |
message.into_string_literal().map(|s| &**s.value(self.db())) | |
{ | |
self.context.report_lint( | |
&STATIC_ASSERT_ERROR, | |
call_expression.into(), | |
format_args!("Static assertion error: {message}"), | |
); | |
} else if parameter_ty == Type::BooleanLiteral(false) { | |
self.context.report_lint( | |
&STATIC_ASSERT_ERROR, | |
call_expression.into(), | |
format_args!("Static assertion error: argument evaluates to `False`"), | |
); | |
} else if truthiness.is_always_false() { | |
self.context.report_lint( | |
&STATIC_ASSERT_ERROR, | |
call_expression.into(), | |
format_args!( | |
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy", | |
parameter_ty=parameter_ty.display(self.db()) | |
), | |
); | |
} else { | |
self.context.report_lint( | |
&STATIC_ASSERT_ERROR, | |
call_expression.into(), | |
format_args!( | |
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness", | |
parameter_ty=parameter_ty.display(self.db()) | |
), | |
); | |
}; | |
} | |
} | |
if truthiness.is_always_true() { | |
return call; | |
} | |
if let Some(message) = message.into_string_literal().map(|s| &**s.value(self.db())) | |
{ | |
self.context.report_lint( | |
&STATIC_ASSERT_ERROR, | |
call_expression.into(), | |
format_args!("Static assertion error: {message}"), | |
); | |
} else if parameter_ty == Type::BooleanLiteral(false) { | |
self.context.report_lint( | |
&STATIC_ASSERT_ERROR, | |
call_expression.into(), | |
format_args!("Static assertion error: argument evaluates to `False`"), | |
); | |
} else if truthiness.is_always_false() { | |
self.context.report_lint( | |
&STATIC_ASSERT_ERROR, | |
call_expression.into(), | |
format_args!( | |
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy", | |
parameter_ty=parameter_ty.display(self.db()) | |
), | |
); | |
} else { | |
self.context.report_lint( | |
&STATIC_ASSERT_ERROR, | |
call_expression.into(), | |
format_args!( | |
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness", | |
parameter_ty=parameter_ty.display(self.db()) | |
), | |
); | |
} | |
} |
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 OK with this change as a standalone change (because it pulls some complexity out of CallOutcome
and Type::call
, in exchange for losing some precision that doesn't matter at all), but I don't see it as clarifying a broader direction for how we handle call checking or outcomes of type operations, which seems like maybe the opposite of your intent with the PR :)
Strictly speaking, this is a semantic regression, because it means we no longer "recognize" reveal_type
and friends wherever we might happen to call them, but only if they are directly called, not as part of a union, and not in an implicit call. I don't think this matters at all in practice (there is no reasonable use case either for reveal_type
to be in a union, or for something like __bool__ = reveal_type
that would result in an implicit call to it), but it makes me question whether this generalizes at all beyond a few "special" functions where we are OK with being more limited.
I think this semantic "regression" corresponds to the lack of clarity for me about how the code structure is supposed to correlate to the Python semantics, and therefore also what we should name the new method. Before it was IMO more clear: Type::call
and its return value should encapsulate all of the results, both in terms of needed diagnostics and in terms of return type, of calling any Python object. If we are going to split that up in some way, I don't yet understand the semantics of the intended split.
call_outcome.unwrap_with_diagnostic(&self.context, call_expression.into()) | ||
} | ||
|
||
fn check_call( |
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.
The name check_call
doesn't seem right here, as most of the important "call checking" happens outside this method, this method just handles special cases for a few particular known callables. I'm not sure what better name to suggest, which I think raises some question about whether this change adds clarity to the structure of the code. The right name also depends partly on what all we intend to ultimately try to move out here.
That's actually why I kinda like the PR. Figuring out a Big Picture for how to solve the overall problem is Hard. Breaking out somewhat isolated simplifications like this where they make sense makes it easier to analyse the rest of the problem and think about it in isolation. |
I'm also not sure where this goes long term but the second half of your comment already gives me some signal. So I still think this was useful :) |
Summary
Let's measure the temperture on how people feel about this ;)
Something I noticed about
CallOutcome
or more generally aboutType::call
is that we have to differentiate between three use cases:a += 1
where this calls__add__
)This PR moves the code only relevant to 1 out of
CallOutcome
and intoinfer_call_expression
instead.It simplifies the
CallOutcome
struct overall and I, as a reader, find it easier to reason about which logicapplies where.
Test Plan
cargo test