-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[red-knot] Emit a diagnostic if a non-protocol is passed to get_protocol_members
#17551
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
Conversation
8eac7ad to
9255038
Compare
|
|
I don't know why the ruff (not red-knot!) ecosystem check is failing, but it doesn't seem related to this PR. |
It's because you're using stacked PRs, once the PR that's below this one has the ecosystem check completed, you can re-run here so that it can find the base ecosystem report which would be part of that PR. |
801147a to
abc9ff6
Compare
9255038 to
0d68ffd
Compare
Hrm, I've tried rerunning the job several times now (and the PR below this one has definitely completed its CI!). I think it's possibly because the PR below this one (#17550) had the Linux build skipped entirely in its CI run? I'm not sure why -- PRs that only change |
Ah, yeah that's probably the case. The Ruff ecosystem checks needs a base report to compare against. This is usually from |
ece0c86 to
b3a1538
Compare
...s/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap
Show resolved
Hide resolved
b3a1538 to
e34384c
Compare
carljm
left a comment
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!
| let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call) else { | ||
| return; | ||
| }; | ||
| let db = context.db(); | ||
| let mut diagnostic = builder.into_diagnostic("Invalid argument to `get_protocol_members`"); | ||
| diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); | ||
| diagnostic.info("Only protocol classes can be passed to `get_protocol_members`"); | ||
|
|
||
| let class_scope = class.body_scope(db); | ||
| let class_node = class_scope.node(db).expect_class(); | ||
| let class_name = &class_node.name; | ||
| let class_def_diagnostic_range = TextRange::new( | ||
| class_name.start(), | ||
| class_node | ||
| .arguments | ||
| .as_deref() | ||
| .map(Ranged::end) | ||
| .unwrap_or_else(|| class_name.end()), | ||
| ); | ||
| let mut class_def_diagnostic = SubDiagnostic::new( | ||
| Severity::Info, | ||
| format_args!("`{class_name}` is declared here, but it is not a protocol class:"), | ||
| ); | ||
| class_def_diagnostic.annotate(Annotation::primary( | ||
| Span::from(class_scope.file(db)).with_range(class_def_diagnostic_range), | ||
| )); | ||
| diagnostic.sub(class_def_diagnostic); | ||
|
|
||
| diagnostic.info( | ||
| "A class is only a protocol class if it directly inherits \ | ||
| from `typing.Protocol` or `typing_extensions.Protocol`", | ||
| ); | ||
| diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#"); | ||
| } |
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 a lovely diagnostic, and a great example for what we can be doing with the new diagnostic framework in other cases -- but it's also a lot of code for an extremely narrow case (misuse of one specific function)! Not really suggesting we change anything here, just not sure it will be worth investing this much in every narrow edge-case diagnostic...
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.
Yeah, I know what you mean.
I think there might be ways to make the diagnostic APIs a little less verbose? The code here isn't that complicated, just a little boilerplate-y. And I think the way you get a reputation for having "excellent error messages" like rustc is by paying attention to lots of little details like this.
e34384c to
0c975b4
Compare
* main: (28 commits) [red-knot] Make `BoundMethodType` a salsa interned (#17581) [red-knot] Emit a diagnostic if a non-protocol is passed to `get_protocol_members` (#17551) [red-knot] Add more tests for protocol members (#17550) [red-knot] Assignability for subclasses of `Any` and `Unknown` (#17557) [red-knot] mypy_primer: add strawberry, print compilation errors to stderr (#17578) [red-knot] GenericAlias instances as a base class (#17575) Remove redundant `type_to_visitor_function` entries (#17564) Fixes how the checker visits `typing.cast`/`typing.NewType` arguments (#17538) [red-knot] Class literal `__new__` function callable subtyping (#17533) [red-knot] Surround intersections with `()` in potentially ambiguous contexts (#17568) [minor] Delete outdated TODO comment (#17565) [red-knot] add regression test for fixed cycle panic (#17535) [red-knot] fix unions of literals, again (#17534) red_knot_python_semantic: remove last vestige of old diagnostics! red_knot_python_semantic: migrate `types` to new diagnostics red_knot_python_semantic: migrate `types/diagnostic` to new diagnostics red_knot_python_semantic: migrate `types/call/bind` to new diagnostics red_knot_python_semantic: migrate `types/string_annotation` to new diagnostics red_knot_python_semantic: migrate `types/infer` to new diagnostic model red_knot_python_semantic: migrate INVALID_ASSIGNMENT for inference ...
…var-instance * dcreager/generic-constructor: (29 commits) We don't need this [red-knot] Make `BoundMethodType` a salsa interned (#17581) [red-knot] Emit a diagnostic if a non-protocol is passed to `get_protocol_members` (#17551) [red-knot] Add more tests for protocol members (#17550) [red-knot] Assignability for subclasses of `Any` and `Unknown` (#17557) [red-knot] mypy_primer: add strawberry, print compilation errors to stderr (#17578) [red-knot] GenericAlias instances as a base class (#17575) Remove redundant `type_to_visitor_function` entries (#17564) Fixes how the checker visits `typing.cast`/`typing.NewType` arguments (#17538) [red-knot] Class literal `__new__` function callable subtyping (#17533) [red-knot] Surround intersections with `()` in potentially ambiguous contexts (#17568) [minor] Delete outdated TODO comment (#17565) [red-knot] add regression test for fixed cycle panic (#17535) [red-knot] fix unions of literals, again (#17534) red_knot_python_semantic: remove last vestige of old diagnostics! red_knot_python_semantic: migrate `types` to new diagnostics red_knot_python_semantic: migrate `types/diagnostic` to new diagnostics red_knot_python_semantic: migrate `types/call/bind` to new diagnostics red_knot_python_semantic: migrate `types/string_annotation` to new diagnostics red_knot_python_semantic: migrate `types/infer` to new diagnostic model ...
Summary
Passing a non-protocol class to
get_protocol_membersfails at runtime. This PR adds logic to red-knot so that we detect that error and warn the user about it.Stacked on top of #17550
Test Plan
Existing mdtests updated and extended. Snapshots added!
Screenshot of what the diagnostics look like in the terminal:
