[ty] Ignore rejected member annotations for synthesized bindings#25427
Conversation
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 91.94%. The percentage of expected errors that received a diagnostic held steady at 87.09%. The number of fully passing files held steady at 92/134. |
Memory usage reportMemory usage unchanged ✅ |
|
76cf309 to
9a8a555
Compare
8e66497 to
80432a6
Compare
a410e49 to
e73c5f3
Compare
|
For context, this is a bit like #25340. |
|
Haven't looked at this PR closely, but I was curious about the approach and had Codex look at it; it pointed out that this panics in a stub file: # repro.pyi
x: dict[str, object]
x["a"]: int
reveal_type(x["a"])I think because we consider declarations in stub files to also be bindings. |
|
fml |
|
It's really too bad that we can't just ignore these rejected declarations entirely in semantic indexing and avoid a lot of this complexity. It seems like maybe we could for subscripts, since those are never valid? So for But for attributes we can't, since Although I'd be really tempted to just make this decision in semantic indexing based on "are we in a method and |
|
I agree that it's a pain, though I'm partial to getting it right as long as we can hide the complexity from callers... |
c411148 to
82bf02e
Compare
There was a problem hiding this comment.
[Edit: Just refreshed and saw @carljm's comment above after posting this. Looks like we had similar thoughts.]
Mostly nits about comment wording, so approving with comments. However, one high level thing I'd like to double check about the approach here: Are we sure we can't omit these bad-annotation definitions from the SemanticIndex entirely?
My guess is a lot of cases we can (e.g. x[0]: int is clearly never valid), but not all cases. IIUC the edge cases look like this:
class C:
@classmethod
def ok_method(x) -> None:
x.attr: int = 1 # valid (`x` is "self" here)
@staticmethod
def bad_method(x) -> None:
x.attr: int = 1 # invalid (staticmethods don't get a "self")We could almost analyze those cases entirely statically, except that @classmethod and @staticmethod aren't keywords, and we have to resolve them during type inference (after semantic indexing).
I think that's the rub? But I'm curious if you agree. It feels like it would be nicer to discard bad definitions early if there was a way.
| /// The inferred declaration produced by a syntactically indexed declaration. | ||
| #[derive(Debug, Clone, Copy, Eq, PartialEq, salsa::Update, get_size2::GetSize)] | ||
| pub(crate) enum InferredDeclaration<'db> { | ||
| /// A semantic declaration with an inferred declared type. |
There was a problem hiding this comment.
Unless "semantic declaration" is a term of art that's new to me(?), I think it's clearer to just call this a "valid declaration".
| pub(crate) enum InferredDeclaration<'db> { | ||
| /// A semantic declaration with an inferred declared type. | ||
| Declared(TypeAndQualifiers<'db>), | ||
| /// A syntactic declaration candidate that is not a valid semantic declaration. |
There was a problem hiding this comment.
IIUC "syntactic declaration" means "an annotation or annotated assignment that the SemanticIndex has a Definition for". Is that right? If so, using the word "syntactic" to refer to the SemanticIndex is pretty confusing, even though I see where you're coming from.
Also I think it's important to give a couple examples of invalid declarations here, so that the reader knows what we're talking about.
|
|
||
| if let Some(value) = value { | ||
| // The value remains an ordinary assignment, but normal binding resolution | ||
| // would consult this rejected annotation as a declaration. |
There was a problem hiding this comment.
The grammar/logic of this comment seems like it almost makes sense but then not quite?
|
|
||
| /// Infer the type of a declaration. | ||
| pub(crate) fn declaration_type<'db>( | ||
| /// Infer whether a syntactically indexed declaration is a semantic declaration. |
There was a problem hiding this comment.
The important thing this function is doing, in the overwhelming majority of valid cases, is still inferring the type of the declaration, correct? Changing the comment like this seems to obscure that.
| /// The other 50% have less than 10 declarations. Because of that, use a | ||
| /// slice with linear search over a hash map. | ||
| declarations: Box<[(Definition<'db>, TypeAndQualifiers<'db>)]>, | ||
| declarations: Box<[(Definition<'db>, InferredDeclaration<'db>)]>, |
There was a problem hiding this comment.
Possibly misguided q: Do we need to retain the invalid ones here? Or could we filter out just the valid ones and keep the tuple type unchanged?
|
👍 Yeah, I don't think it's possible. I also wanted to do that, but ultimately we do need semantic information to correctly determine where/when to discard because we can't robustly identify the implicit receiver rules. |
82bf02e to
b433318
Compare
b433318 to
73c2d3f
Compare
Summary
We now emit
invalid-type-formfor an invalid annotation on a non-name target (#25324)... But it turns out that we still allow that annotation to participate in downstream operations, since we create an annotated-assignment definition for it during the indexing pass.For example:
The rejected annotation should not widen the dictionary assigned to
x["inner"], nor should it constrain the later replacement. (The same issue applies to rejected attribute annotations, likeobj.inner: T.)We now represent each syntactically indexed declaration outcome as either
Declared(...)orRejected. Declaration consumers only useDeclared(...).