mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
[red-knot] Emit a diagnostic if a non-protocol is passed to get_protocol_members
(#17551)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[Knot Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[Knot Playground] Release / publish (push) Waiting to run
This commit is contained in:
parent
f9c7908bb7
commit
0a1f9d090e
4 changed files with 177 additions and 12 deletions
|
@ -375,15 +375,6 @@ class Foo(Protocol):
|
|||
reveal_type(get_protocol_members(Foo)) # revealed: @Todo(specialized non-generic class)
|
||||
```
|
||||
|
||||
Calling `get_protocol_members` on a non-protocol class raises an error at runtime:
|
||||
|
||||
```py
|
||||
class NotAProtocol: ...
|
||||
|
||||
# TODO: should emit `[invalid-protocol]` error, should reveal `Unknown`
|
||||
reveal_type(get_protocol_members(NotAProtocol)) # revealed: @Todo(specialized non-generic class)
|
||||
```
|
||||
|
||||
Certain special attributes and methods are not considered protocol members at runtime, and should
|
||||
not be considered protocol members by type checkers either:
|
||||
|
||||
|
@ -423,6 +414,38 @@ class Baz2(Bar, Foo, Protocol): ...
|
|||
reveal_type(get_protocol_members(Baz2)) # revealed: @Todo(specialized non-generic class)
|
||||
```
|
||||
|
||||
## Invalid calls to `get_protocol_members()`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Calling `get_protocol_members` on a non-protocol class raises an error at runtime:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import Protocol, get_protocol_members
|
||||
|
||||
class NotAProtocol: ...
|
||||
|
||||
get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
|
||||
|
||||
class AlsoNotAProtocol(NotAProtocol, object): ...
|
||||
|
||||
get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
The original class object must be passed to the function; a specialised version of a generic version
|
||||
does not suffice:
|
||||
|
||||
```py
|
||||
class GenericProtocol[T](Protocol): ...
|
||||
|
||||
get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549)
|
||||
```
|
||||
|
||||
## Subtyping of protocols with attribute members
|
||||
|
||||
In the following example, the protocol class `HasX` defines an interface such that any other fully
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: protocols.md - Protocols - Invalid calls to `get_protocol_members()`
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import Protocol, get_protocol_members
|
||||
2 |
|
||||
3 | class NotAProtocol: ...
|
||||
4 |
|
||||
5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
|
||||
6 |
|
||||
7 | class AlsoNotAProtocol(NotAProtocol, object): ...
|
||||
8 |
|
||||
9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type]
|
||||
10 | class GenericProtocol[T](Protocol): ...
|
||||
11 |
|
||||
12 | get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type: Invalid argument to `get_protocol_members`
|
||||
--> /src/mdtest_snippet.py:5:1
|
||||
|
|
||||
3 | class NotAProtocol: ...
|
||||
4 |
|
||||
5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime
|
||||
6 |
|
||||
7 | class AlsoNotAProtocol(NotAProtocol, object): ...
|
||||
|
|
||||
info: Only protocol classes can be passed to `get_protocol_members`
|
||||
info: `NotAProtocol` is declared here, but it is not a protocol class:
|
||||
--> /src/mdtest_snippet.py:3:7
|
||||
|
|
||||
1 | from typing_extensions import Protocol, get_protocol_members
|
||||
2 |
|
||||
3 | class NotAProtocol: ...
|
||||
| ^^^^^^^^^^^^
|
||||
4 |
|
||||
5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
|
||||
|
|
||||
info: A class is only a protocol class if it directly inherits from `typing.Protocol` or `typing_extensions.Protocol`
|
||||
info: See https://typing.python.org/en/latest/spec/protocol.html#
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type: Invalid argument to `get_protocol_members`
|
||||
--> /src/mdtest_snippet.py:9:1
|
||||
|
|
||||
7 | class AlsoNotAProtocol(NotAProtocol, object): ...
|
||||
8 |
|
||||
9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime
|
||||
10 | class GenericProtocol[T](Protocol): ...
|
||||
|
|
||||
info: Only protocol classes can be passed to `get_protocol_members`
|
||||
info: `AlsoNotAProtocol` is declared here, but it is not a protocol class:
|
||||
--> /src/mdtest_snippet.py:7:7
|
||||
|
|
||||
5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
|
||||
6 |
|
||||
7 | class AlsoNotAProtocol(NotAProtocol, object): ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
8 |
|
||||
9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type]
|
||||
|
|
||||
info: A class is only a protocol class if it directly inherits from `typing.Protocol` or `typing_extensions.Protocol`
|
||||
info: See https://typing.python.org/en/latest/spec/protocol.html#
|
||||
|
||||
```
|
|
@ -1,4 +1,5 @@
|
|||
use super::context::InferContext;
|
||||
use super::ClassLiteralType;
|
||||
use crate::declare_lint;
|
||||
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
|
||||
use crate::suppression::FileSuppressionId;
|
||||
|
@ -8,9 +9,9 @@ use crate::types::string_annotation::{
|
|||
RAW_STRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use crate::types::{KnownInstanceType, Type};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Span};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
|
@ -1313,6 +1314,51 @@ pub(crate) fn report_invalid_arguments_to_annotated(
|
|||
));
|
||||
}
|
||||
|
||||
pub(crate) fn report_bad_argument_to_get_protocol_members(
|
||||
context: &InferContext,
|
||||
call: &ast::ExprCall,
|
||||
class: ClassLiteralType,
|
||||
) {
|
||||
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`",
|
||||
);
|
||||
// TODO the typing spec isn't really designed as user-facing documentation,
|
||||
// but there isn't really any user-facing documentation that covers this specific issue well
|
||||
// (it's not described well in the CPython docs; and PEP-544 is a snapshot of a decision taken
|
||||
// years ago rather than up-to-date documentation). We should either write our own docs
|
||||
// describing this well or contribute to type-checker-agnostic docs somewhere and link to those.
|
||||
diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#");
|
||||
}
|
||||
|
||||
pub(crate) fn report_invalid_arguments_to_callable(
|
||||
context: &InferContext,
|
||||
subscript: &ast::ExprSubscript,
|
||||
|
|
|
@ -96,7 +96,8 @@ use crate::Db;
|
|||
|
||||
use super::context::{InNoTypeCheck, InferContext};
|
||||
use super::diagnostic::{
|
||||
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
|
||||
report_bad_argument_to_get_protocol_members, report_index_out_of_bounds,
|
||||
report_invalid_exception_caught, report_invalid_exception_cause,
|
||||
report_invalid_exception_raised, report_invalid_type_checking_constant,
|
||||
report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero,
|
||||
report_unresolved_reference, INVALID_METACLASS, INVALID_PROTOCOL, REDUNDANT_CAST,
|
||||
|
@ -4486,6 +4487,19 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
}
|
||||
}
|
||||
}
|
||||
KnownFunction::GetProtocolMembers => {
|
||||
if let [Some(Type::ClassLiteral(class))] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
if !class.is_protocol(self.db()) {
|
||||
report_bad_argument_to_get_protocol_members(
|
||||
&self.context,
|
||||
call_expression,
|
||||
*class,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue