Compare commits

..

5 Commits

Author SHA1 Message Date
Alex Waygood
4c86b40ee1 [ty] Always infer the submodule literal for submodule attribute-access, even when we emit a diagnostic 2025-11-24 19:34:31 +00:00
Alex Waygood
0c6d652b5f [ty] Switch the error code from unresolved-attribute to possibly-missing-attribute for submodules that may not be available (#21618) 2025-11-24 19:15:45 +00:00
Douglas Creager
03fe560164 [ty] Substitute for typing.Self when checking protocol members (#21569)
This patch updates our protocol assignability checks to substitute for
any occurrences of `typing.Self` in method signatures, replacing it with
the class being checked for assignability against the protocol.

This requires a new helper method on signatures, `apply_self`, which
substitutes occurrences of `typing.Self` _without_ binding the `self`
parameter.

We also update the `try_upcast_to_callable` method. Before, it would
return a `Type`, since certain types upcast to a _union_ of callables,
not to a single callable. However, even in that case, we know that every
element of the union is a callable. We now return a vector of
`CallableType`. (Actually a smallvec to handle the most common case of a
single callable; and wrapped in a new type so that we can provide helper
methods.) If there is more than one element in the result, it represents
a union of callables. This lets callers get at the `CallableType`
instances in a more type-safe way. (This makes it easier for our
protocol checking code to call the new `apply_self` helper.) We also
provide an `into_type` method so that callers that really do want a
`Type` can get the original result easily.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-11-24 14:05:09 -05:00
Andrew Gallant
68343e7edf [ty] Don't suggest things that aren't subclasses of BaseException after raise
This only applies to items that have a type associated with them. That
is, things that are already in scope. For items that don't have a type
associated with them (i.e., suggestions from auto-import), we still
suggest them since we can't know if they're appropriate or not. It's not
quite clear on how best to improve here for the auto-import case. (Short
of, say, asking for the type of each such symbol. But the performance
implications of that aren't known yet.)

Note that because of auto-import, we were still suggesting
`NotImplemented` even though astral-sh/ty#1262 specifically cites it as
the motivating example that we *shouldn't* suggest. This was occuring
because auto-import was including symbols from the `builtins` module,
even though those are actually already in scope. So this PR also gets
rid of those suggestions from auto-import.

Overall, this means that, at least, `raise NotImpl` won't suggest
`NotImplemented`.

Fixes astral-sh/ty#1262
2025-11-24 12:55:30 -05:00
Alex Waygood
a57e291311 [ty] Add hint about resolved Python version when a user attempts to import a member added on a newer version (#21615)
## Summary

Fixes https://github.com/astral-sh/ty/issues/1620. #20909 added hints if
you do something like this and your Python version is set to 3.10 or
lower:

```py
import typing

typing.LiteralString
```

And we also have hints if you try to do something like this and your
Python version is set too low:

```py
from stdlib_module import new_submodule
```

But we don't currently have any subdiagnostic hint if you do something
like _this_ and your Python version is set too low:

```py
from typing import LiteralString
```

This PR adds that hint!

## Test Plan

snapshots

---------

Co-authored-by: Aria Desires <aria.desires@gmail.com>
2025-11-24 15:12:01 +00:00
28 changed files with 614 additions and 281 deletions

View File

@@ -37,7 +37,8 @@ fn config_override_python_version() -> anyhow::Result<()> {
5 | print(sys.last_exc)
| ^^^^^^^^^^^^
|
info: Python 3.11 was assumed when accessing `last_exc`
info: The member may be available on other Python versions or platforms
info: Python 3.11 was assumed when resolving the `last_exc` attribute
--> pyproject.toml:3:18
|
2 | [tool.ty.environment]
@@ -1179,6 +1180,8 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
import os
os.grantpt(1) # only available on unix, Python 3.13 or newer
from typing import LiteralString # added in Python 3.11
"#,
),
])?;
@@ -1194,8 +1197,11 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
3 |
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
| ^^^^^^^^^^
5 |
6 | from typing import LiteralString # added in Python 3.11
|
info: Python 3.10 was assumed when accessing `grantpt`
info: The member may be available on other Python versions or platforms
info: Python 3.10 was assumed when resolving the `grantpt` attribute
--> ty.toml:3:18
|
2 | [environment]
@@ -1205,7 +1211,26 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
info: rule `unresolved-attribute` is enabled by default
Found 1 diagnostic
error[unresolved-import]: Module `typing` has no member `LiteralString`
--> main.py:6:20
|
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
5 |
6 | from typing import LiteralString # added in Python 3.11
| ^^^^^^^^^^^^^
|
info: The member may be available on other Python versions or platforms
info: Python 3.10 was assumed when resolving imports
--> ty.toml:3:18
|
2 | [environment]
3 | python-version = "3.10"
| ^^^^^^ Python 3.10 assumed due to this configuration setting
4 | python-platform = "linux"
|
info: rule `unresolved-import` is enabled by default
Found 2 diagnostics
----- stderr -----
"#);
@@ -1225,6 +1250,8 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
import os
os.grantpt(1) # only available on unix, Python 3.13 or newer
from typing import LiteralString # added in Python 3.11
"#,
),
])?;

View File

@@ -18,11 +18,11 @@ numpy-array,main.py,1,1
object-attr-instance-methods,main.py,0,1
object-attr-instance-methods,main.py,1,1
pass-keyword-completion,main.py,0,1
raise-uses-base-exception,main.py,0,2
raise-uses-base-exception,main.py,0,1
scope-existing-over-new-import,main.py,0,1
scope-prioritize-closer,main.py,0,2
scope-simple-long-identifier,main.py,0,1
tstring-completions,main.py,0,1
ty-extensions-lower-stdlib,main.py,0,8
type-var-typing-over-ast,main.py,0,3
type-var-typing-over-ast,main.py,1,279
type-var-typing-over-ast,main.py,1,278
1 name file index rank
18 object-attr-instance-methods main.py 0 1
19 object-attr-instance-methods main.py 1 1
20 pass-keyword-completion main.py 0 1
21 raise-uses-base-exception main.py 0 2 1
22 scope-existing-over-new-import main.py 0 1
23 scope-prioritize-closer main.py 0 2
24 scope-simple-long-identifier main.py 0 1
25 tstring-completions main.py 0 1
26 ty-extensions-lower-stdlib main.py 0 8
27 type-var-typing-over-ast main.py 0 3
28 type-var-typing-over-ast main.py 1 279 278

View File

@@ -9,9 +9,10 @@ use ruff_python_ast::name::Name;
use ruff_python_codegen::Stylist;
use ruff_python_parser::{Token, TokenAt, TokenKind, Tokens};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ty_python_semantic::types::UnionType;
use ty_python_semantic::{
Completion as SemanticCompletion, ModuleName, NameKind, SemanticModel,
types::{CycleDetector, Type},
Completion as SemanticCompletion, KnownModule, ModuleName, NameKind, SemanticModel,
types::{CycleDetector, KnownClass, Type},
};
use crate::docstring::Docstring;
@@ -82,6 +83,31 @@ impl<'db> Completions<'db> {
fn force_add(&mut self, completion: Completion<'db>) {
self.items.push(completion);
}
/// Tags completions with whether they are known to be usable in
/// a `raise` context.
///
/// It's possible that some completions are usable in a `raise`
/// but aren't marked by this method. That is, false negatives are
/// possible but false positives are not.
fn tag_raisable(&mut self) {
let raisable_type = UnionType::from_elements(
self.db,
[
KnownClass::BaseException.to_subclass_of(self.db),
KnownClass::BaseException.to_instance(self.db),
],
);
for item in &mut self.items {
let Some(ty) = item.ty else { continue };
item.is_definitively_raisable = ty.is_assignable_to(self.db, raisable_type);
}
}
/// Removes any completion that doesn't satisfy the given predicate.
fn retain(&mut self, predicate: impl FnMut(&Completion<'_>) -> bool) {
self.items.retain(predicate);
}
}
impl<'db> Extend<SemanticCompletion<'db>> for Completions<'db> {
@@ -153,6 +179,13 @@ pub struct Completion<'db> {
/// Whether this item only exists for type checking purposes and
/// will be missing at runtime
pub is_type_check_only: bool,
/// Whether this item can definitively be used in a `raise` context.
///
/// Note that this may not always be computed. (i.e., Only computed
/// when we are in a `raise` context.) And also note that if this
/// is `true`, then it's definitively usable in `raise`, but if
/// it's `false`, it _may_ still be usable in `raise`.
pub is_definitively_raisable: bool,
/// The documentation associated with this item, if
/// available.
pub documentation: Option<Docstring>,
@@ -177,6 +210,7 @@ impl<'db> Completion<'db> {
import: None,
builtin: semantic.builtin,
is_type_check_only,
is_definitively_raisable: false,
documentation,
}
}
@@ -257,6 +291,7 @@ impl<'db> Completion<'db> {
import: None,
builtin: false,
is_type_check_only: false,
is_definitively_raisable: false,
documentation: None,
}
}
@@ -271,6 +306,7 @@ impl<'db> Completion<'db> {
import: None,
builtin: true,
is_type_check_only: false,
is_definitively_raisable: false,
documentation: None,
}
}
@@ -364,6 +400,20 @@ pub fn completion<'db>(
}
}
if is_raising_exception(tokens) {
completions.tag_raisable();
// As a special case, and because it's a common footgun, we
// specifically disallow `NotImplemented` in this context.
// `NotImplementedError` should be used instead. So if we can
// definitively detect `NotImplemented`, then we can safely
// omit it from suggestions.
completions.retain(|item| {
let Some(ty) = item.ty else { return true };
!ty.is_notimplemented(db)
});
}
completions.into_completions()
}
@@ -427,7 +477,8 @@ fn add_unimported_completions<'db>(
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
for symbol in all_symbols(db, &completions.query) {
if symbol.module.file(db) == Some(file) {
if symbol.module.file(db) == Some(file) || symbol.module.is_known(db, KnownModule::Builtins)
{
continue;
}
@@ -450,6 +501,7 @@ fn add_unimported_completions<'db>(
builtin: false,
// TODO: `is_type_check_only` requires inferring the type of the symbol
is_type_check_only: false,
is_definitively_raisable: false,
documentation: None,
});
}
@@ -1358,6 +1410,30 @@ fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Opt
})
}
/// Returns true when the cursor is after a `raise` keyword.
fn is_raising_exception(tokens: &[Token]) -> bool {
/// The maximum number of tokens we're willing to
/// look-behind to find a `raise` keyword.
const LIMIT: usize = 10;
// This only looks for things like `raise foo.bar.baz.qu<CURSOR>`.
// Technically, any kind of expression is allowed after `raise`.
// But we may not always want to treat it specially. So we're
// rather conservative about what we consider "raising an
// exception" to be for the purposes of completions. The failure
// mode here is that we may wind up suggesting things that
// shouldn't be raised. The benefit is that when this heuristic
// does work, we won't suggest things that shouldn't be raised.
for token in tokens.iter().rev().take(LIMIT) {
match token.kind() {
TokenKind::Name | TokenKind::Dot => continue,
TokenKind::Raise => return true,
_ => return false,
}
}
false
}
/// Order completions according to the following rules:
///
/// 1) Names with no underscore prefix
@@ -1370,8 +1446,16 @@ fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Opt
/// This has the effect of putting all dunder attributes after "normal"
/// attributes, and all single-underscore attributes after dunder attributes.
fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering {
fn key<'a>(completion: &'a Completion) -> (bool, bool, bool, NameKind, bool, &'a Name) {
fn key<'a>(completion: &'a Completion) -> (bool, bool, bool, bool, NameKind, bool, &'a Name) {
(
// This is only true when we are both in a `raise` context
// *and* we know this suggestion is definitively usable
// in a `raise` context. So we should sort these before
// anything else.
!completion.is_definitively_raisable,
// When `None`, a completion is for something in the
// current module, which we should generally prefer over
// something from outside the module.
completion.module_name.is_some(),
// At time of writing (2025-11-11), keyword completions
// are classified as builtins, which makes them sort after

View File

@@ -2711,10 +2711,10 @@ We give special diagnostics for this common case too:
import foo
import baz
# error: [unresolved-attribute]
reveal_type(foo.bar) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(baz.bar) # revealed: Unknown
# error: [possibly-missing-attribute]
reveal_type(foo.bar) # revealed: <module 'foo.bar'>
# error: [possibly-missing-attribute]
reveal_type(baz.bar) # revealed: <module 'baz.bar'>
```
## References

View File

@@ -124,6 +124,10 @@ def match_singletons_error(obj: Literal[1, "a"] | None):
case None:
pass
case _ as obj:
# TODO: We should emit an error here: `Literal["a"]` is not `Never`.
# TODO: We should emit an error here, but the message should
# show the type `Literal["a"]` instead of `@Todo(…)`. We only
# assert on the first part of the message because the `@Todo`
# message is not available in release mode builds.
# error: [type-assertion-failure] "Type `@Todo"
assert_never(obj)
```

View File

@@ -60,8 +60,8 @@ Y: int = 47
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: [unresolved-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: int
```
### In Non-Stub
@@ -90,8 +90,8 @@ Y: int = 47
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: [unresolved-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: int
```
## Absolute `from` Import of Direct Submodule in `__init__`
@@ -125,8 +125,8 @@ Y: int = 47
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: [unresolved-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: int
```
### In Non-Stub
@@ -155,8 +155,8 @@ Y: int = 47
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: [unresolved-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: int
```
## Import of Direct Submodule in `__init__`
@@ -184,8 +184,8 @@ X: int = 42
import mypackage
# TODO: this could work and would be nice to have?
# error: [unresolved-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: int
```
### In Non-Stub
@@ -208,8 +208,8 @@ X: int = 42
import mypackage
# TODO: this could work and would be nice to have
# error: [unresolved-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: int
```
## Relative `from` Import of Nested Submodule in `__init__`
@@ -242,10 +242,10 @@ X: int = 42
import mypackage
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
# error: [unresolved-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: [unresolved-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: int
# error: [unresolved-attribute] "has no member `nested`"
reveal_type(mypackage.nested) # revealed: Unknown
# error: [unresolved-attribute] "has no member `nested`"
@@ -280,10 +280,10 @@ import mypackage
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
# TODO: this would be nice to support
# error: [unresolved-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: [unresolved-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: int
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
reveal_type(mypackage.nested.X) # revealed: int
```
@@ -318,10 +318,10 @@ X: int = 42
import mypackage
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
# error: [unresolved-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: [unresolved-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: int
# error: [unresolved-attribute] "has no member `nested`"
reveal_type(mypackage.nested) # revealed: Unknown
# error: [unresolved-attribute] "has no member `nested`"
@@ -356,10 +356,10 @@ import mypackage
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
# TODO: this would be nice to support
# error: [unresolved-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: [unresolved-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: int
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
reveal_type(mypackage.nested.X) # revealed: int
```
@@ -393,12 +393,14 @@ X: int = 42
```py
import mypackage
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: int
```
### In Non-Stub
@@ -429,12 +431,14 @@ X: int = 42
import mypackage
# TODO: this would be nice to support
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: int
```
## Relative `from` Import of Direct Submodule in `__init__`, Mismatched Alias
@@ -460,8 +464,8 @@ X: int = 42
```py
import mypackage
# error: [unresolved-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: int
# error: [unresolved-attribute] "has no member `imported_m`"
reveal_type(mypackage.imported_m.X) # revealed: Unknown
```
@@ -486,8 +490,8 @@ X: int = 42
import mypackage
# TODO: this would be nice to support, as it works at runtime
# error: [unresolved-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: int
reveal_type(mypackage.imported_m.X) # revealed: int
```
@@ -673,8 +677,8 @@ reveal_type(imported.X) # revealed: int
# TODO: this would be nice to support, but it's dangerous with available_submodule_attributes
# for details, see: https://github.com/astral-sh/ty/issues/1488
# error: [unresolved-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: int
```
### In Non-Stub
@@ -699,8 +703,8 @@ from mypackage import imported
reveal_type(imported.X) # revealed: int
# TODO: this would be nice to support, as it works at runtime
# error: [unresolved-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: int
```
## `from` Import of Sibling Module
@@ -737,8 +741,8 @@ from mypackage import imported
reveal_type(imported.X) # revealed: int
# error: [unresolved-attribute] "has no member `fails`"
reveal_type(imported.fails.Y) # revealed: Unknown
# error: [unresolved-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: int
```
### In Non-Stub
@@ -770,8 +774,8 @@ from mypackage import imported
reveal_type(imported.X) # revealed: int
reveal_type(imported.fails.Y) # revealed: int
# error: [unresolved-attribute] "Submodule `fails`"
reveal_type(mypackage.fails.Y) # revealed: Unknown
# error: [possibly-missing-attribute] "Submodule `fails`"
reveal_type(mypackage.fails.Y) # revealed: int
```
## Fractal Re-export Nameclash Problems

View File

@@ -247,8 +247,8 @@ X: int = 42
from . import foo
import package
# error: [unresolved-attribute]
reveal_type(package.foo.X) # revealed: Unknown
# error: [possibly-missing-attribute]
reveal_type(package.foo.X) # revealed: int
```
## Relative imports at the top of a search path

View File

@@ -2003,6 +2003,7 @@ python-version = "3.12"
```
```py
from typing import final
from typing_extensions import TypeVar, Self, Protocol
from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of
@@ -2094,6 +2095,13 @@ class NominalReturningSelfNotGeneric:
def g(self) -> "NominalReturningSelfNotGeneric":
return self
@final
class Other: ...
class NominalReturningOtherClass:
def g(self) -> Other:
raise NotImplementedError
# TODO: should pass
static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error]
@@ -2112,8 +2120,7 @@ static_assert(not is_assignable_to(NominalLegacy, UsesSelf))
static_assert(not is_assignable_to(NominalWithSelf, NewStyleFunctionScoped))
static_assert(not is_assignable_to(NominalWithSelf, LegacyFunctionScoped))
static_assert(is_assignable_to(NominalWithSelf, UsesSelf))
# TODO: should pass
static_assert(is_subtype_of(NominalWithSelf, UsesSelf)) # error: [static-assert-error]
static_assert(is_subtype_of(NominalWithSelf, UsesSelf))
# TODO: these should pass
static_assert(not is_assignable_to(NominalNotGeneric, NewStyleFunctionScoped)) # error: [static-assert-error]
@@ -2126,6 +2133,8 @@ static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, LegacyFunctio
# TODO: should pass
static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, UsesSelf)) # error: [static-assert-error]
static_assert(not is_assignable_to(NominalReturningOtherClass, UsesSelf))
# These test cases are taken from the typing conformance suite:
class ShapeProtocolImplicitSelf(Protocol):
def set_scale(self, scale: float) -> Self: ...

View File

@@ -32,7 +32,8 @@ error[unresolved-attribute]: Module `datetime` has no member `UTC`
5 | # error: [unresolved-attribute]
6 | reveal_type(datetime.fakenotreal) # revealed: Unknown
|
info: Python 3.10 was assumed when accessing `UTC` because it was specified on the command line
info: The member may be available on other Python versions or platforms
info: Python 3.10 was assumed when resolving the `UTC` attribute because it was specified on the command line
info: rule `unresolved-attribute` is enabled by default
```

View File

@@ -30,39 +30,39 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
1 | import foo
2 | import baz
3 |
4 | # error: [unresolved-attribute]
5 | reveal_type(foo.bar) # revealed: Unknown
6 | # error: [unresolved-attribute]
7 | reveal_type(baz.bar) # revealed: Unknown
4 | # error: [possibly-missing-attribute]
5 | reveal_type(foo.bar) # revealed: <module 'foo.bar'>
6 | # error: [possibly-missing-attribute]
7 | reveal_type(baz.bar) # revealed: <module 'baz.bar'>
```
# Diagnostics
```
error[unresolved-attribute]: Submodule `bar` may not be available as an attribute on module `foo`
warning[possibly-missing-attribute]: Submodule `bar` may not be available as an attribute on module `foo`
--> src/main.py:5:13
|
4 | # error: [unresolved-attribute]
5 | reveal_type(foo.bar) # revealed: Unknown
4 | # error: [possibly-missing-attribute]
5 | reveal_type(foo.bar) # revealed: <module 'foo.bar'>
| ^^^^^^^
6 | # error: [unresolved-attribute]
7 | reveal_type(baz.bar) # revealed: Unknown
6 | # error: [possibly-missing-attribute]
7 | reveal_type(baz.bar) # revealed: <module 'baz.bar'>
|
help: Consider explicitly importing `foo.bar`
info: rule `unresolved-attribute` is enabled by default
info: rule `possibly-missing-attribute` is enabled by default
```
```
error[unresolved-attribute]: Submodule `bar` may not be available as an attribute on module `baz`
warning[possibly-missing-attribute]: Submodule `bar` may not be available as an attribute on module `baz`
--> src/main.py:7:13
|
5 | reveal_type(foo.bar) # revealed: Unknown
6 | # error: [unresolved-attribute]
7 | reveal_type(baz.bar) # revealed: Unknown
5 | reveal_type(foo.bar) # revealed: <module 'foo.bar'>
6 | # error: [possibly-missing-attribute]
7 | reveal_type(baz.bar) # revealed: <module 'baz.bar'>
| ^^^^^^^
|
help: Consider explicitly importing `baz.bar`
info: rule `unresolved-attribute` is enabled by default
info: rule `possibly-missing-attribute` is enabled by default
```

View File

@@ -628,8 +628,8 @@ import imported
from module2 import imported as other_imported
from ty_extensions import TypeOf, static_assert, is_equivalent_to
# error: [unresolved-attribute]
reveal_type(imported.abc) # revealed: Unknown
# error: [possibly-missing-attribute]
reveal_type(imported.abc) # revealed: <module 'imported.abc'>
reveal_type(other_imported.abc) # revealed: <module 'imported.abc'>

View File

@@ -12,8 +12,8 @@ pub use db::Db;
pub use diagnostic::add_inferred_python_version_hint_to_diagnostic;
pub use module_name::{ModuleName, ModuleNameResolutionError};
pub use module_resolver::{
Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules, list_modules,
resolve_module, resolve_real_module, system_module_search_paths,
KnownModule, Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules,
list_modules, resolve_module, resolve_real_module, system_module_search_paths,
};
pub use program::{
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,

View File

@@ -1,7 +1,7 @@
use std::iter::FusedIterator;
pub use list::{all_modules, list_modules};
pub(crate) use module::KnownModule;
pub use module::KnownModule;
pub use module::Module;
pub use path::{SearchPath, SearchPathValidationError};
pub use resolver::SearchPaths;

View File

@@ -67,7 +67,7 @@ impl<'db> Module<'db> {
}
/// Does this module represent the given known module?
pub(crate) fn is_known(self, db: &'db dyn Database, known_module: KnownModule) -> bool {
pub fn is_known(self, db: &'db dyn Database, known_module: KnownModule) -> bool {
self.known(db) == Some(known_module)
}

View File

@@ -1376,9 +1376,7 @@ mod implicit_globals {
use crate::place::{Definedness, PlaceAndQualifiers, TypeOrigin};
use crate::semantic_index::symbol::Symbol;
use crate::semantic_index::{place_table, use_def_map};
use crate::types::{
CallableType, KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type,
};
use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type};
use ruff_python_ast::PythonVersion;
use super::{Place, place_from_declarations};
@@ -1461,7 +1459,7 @@ mod implicit_globals {
)),
);
Place::Defined(
CallableType::function_like(db, signature),
Type::function_like_callable(db, signature),
TypeOrigin::Inferred,
Definedness::PossiblyUndefined,
)

View File

@@ -208,7 +208,7 @@ use crate::semantic_index::predicate::{
Predicates, ScopedPredicateId,
};
use crate::types::{
IntersectionBuilder, Truthiness, Type, TypeContext, UnionBuilder, UnionType,
CallableTypes, IntersectionBuilder, Truthiness, Type, TypeContext, UnionBuilder, UnionType,
infer_expression_type, static_expression_truthiness,
};
@@ -871,12 +871,14 @@ impl ReachabilityConstraints {
return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive);
}
let overloads_iterator =
if let Some(Type::Callable(callable)) = ty.try_upcast_to_callable(db) {
callable.signatures(db).overloads.iter()
} else {
return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive);
};
let overloads_iterator = if let Some(callable) = ty
.try_upcast_to_callable(db)
.and_then(CallableTypes::exactly_one)
{
callable.signatures(db).overloads.iter()
} else {
return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive);
};
let (no_overloads_return_never, all_overloads_return_never) = overloads_iterator
.fold((true, true), |(none, all), overload| {

View File

@@ -16,6 +16,7 @@ use ruff_db::files::File;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_text_size::{Ranged, TextRange};
use smallvec::{SmallVec, smallvec};
use type_ordering::union_or_intersection_elements_ordering;
@@ -74,7 +75,8 @@ use crate::types::variance::VarianceInferable;
use crate::types::visitor::{any_over_type, exceeds_max_specialization_depth};
use crate::unpack::EvaluationMode;
use crate::{Db, FxOrderSet, Module, Program};
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
pub use class::KnownClass;
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias};
use instance::Protocol;
pub use instance::{NominalInstanceType, ProtocolInstanceType};
pub use special_form::SpecialFormType;
@@ -892,7 +894,7 @@ impl<'db> Type<'db> {
!(check_dunder("__eq__", true) && check_dunder("__ne__", false))
}
pub(crate) fn is_notimplemented(&self, db: &'db dyn Db) -> bool {
pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool {
self.is_instance_of(db, KnownClass::NotImplementedType)
}
@@ -1532,17 +1534,20 @@ impl<'db> Type<'db> {
}
}
pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option<Type<'db>> {
pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option<CallableTypes<'db>> {
match self {
Type::Callable(_) => Some(self),
Type::Callable(callable) => Some(CallableTypes::one(callable)),
Type::Dynamic(_) => Some(CallableType::function_like(db, Signature::dynamic(self))),
Type::Dynamic(_) => Some(CallableTypes::one(CallableType::function_like(
db,
Signature::dynamic(self),
))),
Type::FunctionLiteral(function_literal) => {
Some(Type::Callable(function_literal.into_callable_type(db)))
Some(CallableTypes::one(function_literal.into_callable_type(db)))
}
Type::BoundMethod(bound_method) => {
Some(Type::Callable(bound_method.into_callable_type(db)))
Some(CallableTypes::one(bound_method.into_callable_type(db)))
}
Type::NominalInstance(_) | Type::ProtocolInstance(_) => {
@@ -1573,13 +1578,22 @@ impl<'db> Type<'db> {
// TODO: This is unsound so in future we can consider an opt-in option to disable it.
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
SubclassOfInner::Class(class) => Some(class.into_callable(db)),
SubclassOfInner::Dynamic(dynamic) => Some(CallableType::single(
db,
Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))),
)),
SubclassOfInner::Dynamic(dynamic) => {
Some(CallableTypes::one(CallableType::single(
db,
Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))),
)))
}
},
Type::Union(union) => union.try_map(db, |element| element.try_upcast_to_callable(db)),
Type::Union(union) => {
let mut callables = SmallVec::new();
for element in union.elements(db) {
let element_callable = element.try_upcast_to_callable(db)?;
callables.extend(element_callable.into_inner());
}
Some(CallableTypes(callables))
}
Type::EnumLiteral(enum_literal) => enum_literal
.enum_class_instance(db)
@@ -1587,26 +1601,30 @@ impl<'db> Type<'db> {
Type::TypeAlias(alias) => alias.value_type(db).try_upcast_to_callable(db),
Type::KnownBoundMethod(method) => Some(Type::Callable(CallableType::new(
Type::KnownBoundMethod(method) => Some(CallableTypes::one(CallableType::new(
db,
CallableSignature::from_overloads(method.signatures(db)),
false,
))),
Type::WrapperDescriptor(wrapper_descriptor) => Some(Type::Callable(CallableType::new(
db,
CallableSignature::from_overloads(wrapper_descriptor.signatures(db)),
false,
))),
Type::WrapperDescriptor(wrapper_descriptor) => {
Some(CallableTypes::one(CallableType::new(
db,
CallableSignature::from_overloads(wrapper_descriptor.signatures(db)),
false,
)))
}
Type::KnownInstance(KnownInstanceType::NewType(newtype)) => Some(CallableType::single(
db,
Signature::new(
Parameters::new([Parameter::positional_only(None)
.with_annotated_type(newtype.base(db).instance_type(db))]),
Some(Type::NewTypeInstance(newtype)),
),
)),
Type::KnownInstance(KnownInstanceType::NewType(newtype)) => {
Some(CallableTypes::one(CallableType::single(
db,
Signature::new(
Parameters::new([Parameter::positional_only(None)
.with_annotated_type(newtype.base(db).instance_type(db))]),
Some(Type::NewTypeInstance(newtype)),
),
)))
}
Type::Never
| Type::DataclassTransformer(_)
@@ -1670,8 +1688,8 @@ impl<'db> Type<'db> {
/// Return true if this type is assignable to type `target`.
///
/// See [`TypeRelation::Assignability`] for more details.
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
/// See `TypeRelation::Assignability` for more details.
pub fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
self.when_assignable_to(db, target, InferableTypeVars::None)
.is_always_satisfied(db)
}
@@ -2182,18 +2200,20 @@ impl<'db> Type<'db> {
)
}),
(_, Type::Callable(_)) => relation_visitor.visit((self, target, relation), || {
self.try_upcast_to_callable(db).when_some_and(|callable| {
callable.has_relation_to_impl(
db,
target,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
(_, Type::Callable(other_callable)) => {
relation_visitor.visit((self, target, relation), || {
self.try_upcast_to_callable(db).when_some_and(|callables| {
callables.has_relation_to_impl(
db,
other_callable,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
})
}),
}
(_, Type::ProtocolInstance(protocol)) => {
relation_visitor.visit((self, target, relation), || {
@@ -4092,7 +4112,7 @@ impl<'db> Type<'db> {
Some((self, AttributeKind::NormalOrNonDataDescriptor))
} else {
Some((
Type::Callable(callable.bind_self(db)),
Type::Callable(callable.bind_self(db, None)),
AttributeKind::NormalOrNonDataDescriptor,
))
};
@@ -5626,7 +5646,7 @@ impl<'db> Type<'db> {
.with_annotated_type(UnionType::from_elements(
db,
[
CallableType::single(db, getter_signature),
Type::single_callable(db, getter_signature),
Type::none(db),
],
))
@@ -5635,7 +5655,7 @@ impl<'db> Type<'db> {
.with_annotated_type(UnionType::from_elements(
db,
[
CallableType::single(db, setter_signature),
Type::single_callable(db, setter_signature),
Type::none(db),
],
))
@@ -5644,7 +5664,7 @@ impl<'db> Type<'db> {
.with_annotated_type(UnionType::from_elements(
db,
[
CallableType::single(db, deleter_signature),
Type::single_callable(db, deleter_signature),
Type::none(db),
],
))
@@ -7138,6 +7158,15 @@ impl<'db> Type<'db> {
tcx: TypeContext<'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Type<'db> {
// If we are binding `typing.Self`, and this type is what we are binding `Self` to, return
// early. This is not just an optimization, it also prevents us from infinitely expanding
// the type, if it's something that can contain a `Self` reference.
if let TypeMapping::BindSelf(self_type) = type_mapping
&& self == *self_type
{
return self;
}
match self {
Type::TypeVar(bound_typevar) => match type_mapping {
TypeMapping::Specialization(specialization) => {
@@ -10956,34 +10985,44 @@ pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
// The Salsa heap is tracked separately.
impl get_size2::GetSize for CallableType<'_> {}
impl<'db> CallableType<'db> {
impl<'db> Type<'db> {
/// Create a callable type with a single non-overloaded signature.
pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> {
Type::Callable(CallableType::new(
db,
CallableSignature::single(signature),
false,
))
pub(crate) fn single_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> {
Type::Callable(CallableType::single(db, signature))
}
/// Create a non-overloaded, function-like callable type with a single signature.
///
/// A function-like callable will bind `self` when accessed as an attribute on an instance.
pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> {
Type::Callable(CallableType::new(
db,
CallableSignature::single(signature),
true,
))
pub(crate) fn function_like_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> {
Type::Callable(CallableType::function_like(db, signature))
}
}
impl<'db> CallableType<'db> {
pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> {
CallableType::new(db, CallableSignature::single(signature), false)
}
pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> {
CallableType::new(db, CallableSignature::single(signature), true)
}
/// Create a callable type which accepts any parameters and returns an `Unknown` type.
pub(crate) fn unknown(db: &'db dyn Db) -> Type<'db> {
Self::single(db, Signature::unknown())
Type::Callable(Self::single(db, Signature::unknown()))
}
pub(crate) fn bind_self(self, db: &'db dyn Db) -> CallableType<'db> {
CallableType::new(db, self.signatures(db).bind_self(db, None), false)
pub(crate) fn bind_self(
self,
db: &'db dyn Db,
self_type: Option<Type<'db>>,
) -> CallableType<'db> {
CallableType::new(db, self.signatures(db).bind_self(db, self_type), false)
}
pub(crate) fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> {
CallableType::new(db, self.signatures(db).apply_self(db, self_type), false)
}
/// Create a callable type which represents a fully-static "bottom" callable.
@@ -11077,6 +11116,72 @@ impl<'db> CallableType<'db> {
}
}
/// Converting a type "into a callable" can possibly return a _union_ of callables. Eventually,
/// when coercing that result to a single type, you'll get a `UnionType`. But this lets you handle
/// that result as a list of `CallableType`s before merging them into a `UnionType` should that be
/// helpful.
///
/// Note that this type is guaranteed to contain at least one callable. If you need to support "no
/// callables" as a possibility, use `Option<CallableTypes>`.
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update)]
pub(crate) struct CallableTypes<'db>(SmallVec<[CallableType<'db>; 1]>);
impl<'db> CallableTypes<'db> {
pub(crate) fn one(callable: CallableType<'db>) -> Self {
CallableTypes(smallvec![callable])
}
pub(crate) fn from_elements(callables: impl IntoIterator<Item = CallableType<'db>>) -> Self {
let callables: SmallVec<_> = callables.into_iter().collect();
assert!(!callables.is_empty(), "CallableTypes should not be empty");
CallableTypes(callables)
}
pub(crate) fn exactly_one(self) -> Option<CallableType<'db>> {
match self.0.as_slice() {
[single] => Some(*single),
_ => None,
}
}
fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> {
self.0
}
pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> {
match self.0.as_slice() {
[] => unreachable!("CallableTypes should not be empty"),
[single] => Type::Callable(*single),
slice => UnionType::from_elements(db, slice.iter().copied().map(Type::Callable)),
}
}
pub(crate) fn map(self, mut f: impl FnMut(CallableType<'db>) -> CallableType<'db>) -> Self {
Self::from_elements(self.0.iter().map(|element| f(*element)))
}
pub(crate) fn has_relation_to_impl(
self,
db: &'db dyn Db,
other: CallableType<'db>,
inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> {
self.0.iter().when_all(db, |element| {
element.has_relation_to_impl(
db,
other,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
}
}
/// Represents a specific instance of a bound method type for a builtin class.
///
/// Unlike bound methods of user-defined classes, these are not generally instances
@@ -12108,7 +12213,7 @@ impl get_size2::GetSize for UnionType<'_> {}
impl<'db> UnionType<'db> {
/// Create a union from a list of elements
/// (which may be eagerly simplified into a different variant of [`Type`] altogether).
pub(crate) fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
pub fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
where
I: IntoIterator<Item = T>,
T: Into<Type<'db>>,

View File

@@ -32,7 +32,7 @@ use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::typed_dict::typed_dict_params_from_class_def;
use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard};
use crate::types::{
ApplyTypeMappingVisitor, Binding, BoundSuperType, CallableType, DATACLASS_FLAGS,
ApplyTypeMappingVisitor, Binding, BoundSuperType, CallableType, CallableTypes, DATACLASS_FLAGS,
DataclassFlags, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor,
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownInstanceType,
ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType,
@@ -791,7 +791,7 @@ impl<'db> ClassType<'db> {
.with_annotated_type(Type::instance(db, self))]);
let synthesized_dunder_method =
CallableType::function_like(db, Signature::new(parameters, Some(return_type)));
Type::function_like_callable(db, Signature::new(parameters, Some(return_type)));
Member::definitely_declared(synthesized_dunder_method)
}
@@ -1013,7 +1013,7 @@ impl<'db> ClassType<'db> {
iterable_parameter,
]);
let synthesized_dunder = CallableType::function_like(
let synthesized_dunder = Type::function_like_callable(
db,
Signature::new_generic(inherited_generic_context, parameters, None),
);
@@ -1052,7 +1052,7 @@ impl<'db> ClassType<'db> {
/// Return a callable type (or union of callable types) that represents the callable
/// constructor signature of this class.
#[salsa::tracked(cycle_initial=into_callable_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
pub(super) fn into_callable(self, db: &'db dyn Db) -> Type<'db> {
pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> {
let self_ty = Type::from(self);
let metaclass_dunder_call_function_symbol = self_ty
.member_lookup_with_policy(
@@ -1070,7 +1070,7 @@ impl<'db> ClassType<'db> {
// https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable
// by always respecting the signature of the metaclass `__call__`, rather than
// using a heuristic which makes unwarranted assumptions to sometimes ignore it.
return Type::Callable(metaclass_dunder_call_function.into_callable_type(db));
return CallableTypes::one(metaclass_dunder_call_function.into_callable_type(db));
}
let dunder_new_function_symbol = self_ty.lookup_dunder_new(db);
@@ -1098,14 +1098,14 @@ impl<'db> ClassType<'db> {
});
let instance_ty = Type::instance(db, self);
let dunder_new_bound_method = Type::Callable(CallableType::new(
let dunder_new_bound_method = CallableType::new(
db,
dunder_new_signature.bind_self(db, Some(instance_ty)),
true,
));
);
if returns_non_subclass {
return dunder_new_bound_method;
return CallableTypes::one(dunder_new_bound_method);
}
Some(dunder_new_bound_method)
} else {
@@ -1148,11 +1148,11 @@ impl<'db> ClassType<'db> {
signature.overloads.iter().map(synthesized_signature),
);
Some(Type::Callable(CallableType::new(
Some(CallableType::new(
db,
synthesized_dunder_init_signature,
true,
)))
))
} else {
None
}
@@ -1162,12 +1162,14 @@ impl<'db> ClassType<'db> {
match (dunder_new_function, synthesized_dunder_init_callable) {
(Some(dunder_new_function), Some(synthesized_dunder_init_callable)) => {
UnionType::from_elements(
db,
vec![dunder_new_function, synthesized_dunder_init_callable],
)
CallableTypes::from_elements([
dunder_new_function,
synthesized_dunder_init_callable,
])
}
(Some(constructor), None) | (None, Some(constructor)) => {
CallableTypes::one(constructor)
}
(Some(constructor), None) | (None, Some(constructor)) => constructor,
(None, None) => {
// If no `__new__` or `__init__` method is found, then we fall back to looking for
// an `object.__new__` method.
@@ -1182,17 +1184,17 @@ impl<'db> ClassType<'db> {
if let Place::Defined(Type::FunctionLiteral(new_function), _, _) =
new_function_symbol
{
Type::Callable(
CallableTypes::one(
new_function
.into_bound_method_type(db, correct_return_type)
.into_callable_type(db),
)
} else {
// Fallback if no `object.__new__` is found.
CallableType::single(
CallableTypes::one(CallableType::single(
db,
Signature::new(Parameters::empty(), Some(correct_return_type)),
)
))
}
}
}
@@ -1208,11 +1210,11 @@ impl<'db> ClassType<'db> {
}
fn into_callable_cycle_initial<'db>(
_db: &'db dyn Db,
db: &'db dyn Db,
_id: salsa::Id,
_self: ClassType<'db>,
) -> Type<'db> {
Type::Never
) -> CallableTypes<'db> {
CallableTypes::one(CallableType::bottom(db))
}
impl<'db> From<GenericAlias<'db>> for ClassType<'db> {
@@ -2156,7 +2158,7 @@ impl<'db> ClassLiteral<'db> {
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]),
Some(field.declared_ty),
);
let property_getter = CallableType::single(db, property_getter_signature);
let property_getter = Type::single_callable(db, property_getter_signature);
let property = PropertyInstanceType::new(db, Some(property_getter), None);
return Member::definitely_declared(Type::PropertyInstance(property));
}
@@ -2370,7 +2372,7 @@ impl<'db> ClassLiteral<'db> {
),
_ => Signature::new(Parameters::new(parameters), return_ty),
};
Some(CallableType::function_like(db, signature))
Some(Type::function_like_callable(db, signature))
};
match (field_policy, name) {
@@ -2406,7 +2408,7 @@ impl<'db> ClassLiteral<'db> {
Some(KnownClass::Bool.to_instance(db)),
);
Some(CallableType::function_like(db, signature))
Some(Type::function_like_callable(db, signature))
}
(CodeGeneratorKind::DataclassLike(_), "__hash__") => {
let unsafe_hash = has_dataclass_param(DataclassFlags::UNSAFE_HASH);
@@ -2422,7 +2424,7 @@ impl<'db> ClassLiteral<'db> {
Some(KnownClass::Int.to_instance(db)),
);
Some(CallableType::function_like(db, signature))
Some(Type::function_like_callable(db, signature))
} else if eq && !frozen {
Some(Type::none(db))
} else {
@@ -2509,7 +2511,7 @@ impl<'db> ClassLiteral<'db> {
Some(Type::Never),
);
return Some(CallableType::function_like(db, signature));
return Some(Type::function_like_callable(db, signature));
}
None
}
@@ -2787,7 +2789,7 @@ impl<'db> ClassLiteral<'db> {
Some(Type::none(db)),
);
Some(CallableType::function_like(db, signature))
Some(Type::function_like_callable(db, signature))
}
_ => None,
}
@@ -4743,7 +4745,7 @@ impl KnownClass {
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
#[track_caller]
pub(crate) fn to_instance(self, db: &dyn Db) -> Type<'_> {
pub fn to_instance(self, db: &dyn Db) -> Type<'_> {
debug_assert_ne!(
self,
KnownClass::Tuple,
@@ -4896,7 +4898,7 @@ impl KnownClass {
/// representing that class and all possible subclasses of the class.
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_subclass_of(self, db: &dyn Db) -> Type<'_> {
pub fn to_subclass_of(self, db: &dyn Db) -> Type<'_> {
self.to_class_literal(db)
.to_class_type(db)
.map(|class| SubclassOfType::from(db, class))

View File

@@ -1790,7 +1790,11 @@ impl<'db> InteriorNode<'db> {
/// Returns a sequent map for this BDD, which records the relationships between the constraints
/// that appear in the BDD.
#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)]
#[salsa::tracked(
returns(ref),
cycle_initial=sequent_map_cycle_initial,
heap_size=ruff_memory_usage::heap_size,
)]
fn sequent_map(self, db: &'db dyn Db) -> SequentMap<'db> {
let mut map = SequentMap::default();
Node::Interior(self).for_each_constraint(db, &mut |constraint| {
@@ -2109,6 +2113,14 @@ impl<'db> InteriorNode<'db> {
}
}
fn sequent_map_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
_self: InteriorNode<'db>,
) -> SequentMap<'db> {
SequentMap::default()
}
/// An assignment of one BDD variable to either `true` or `false`. (When evaluating a BDD, we
/// must provide an assignment for each variable present in the BDD.)
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]

View File

@@ -3630,30 +3630,32 @@ pub(super) fn report_invalid_method_override<'db>(
/// *does* exist as a submodule in the standard library on *other* Python
/// versions, we add a hint to the diagnostic that the user may have
/// misconfigured their Python version.
///
/// The function returns `true` if a hint was added, `false` otherwise.
pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
db: &dyn Db,
mut diagnostic: LintDiagnosticGuard,
diagnostic: &mut Diagnostic,
full_submodule_name: &ModuleName,
parent_module: Module,
) {
) -> bool {
let Some(search_path) = parent_module.search_path(db) else {
return;
return false;
};
if !search_path.is_standard_library() {
return;
return false;
}
let program = Program::get(db);
let typeshed_versions = program.search_paths(db).typeshed_versions();
let Some(version_range) = typeshed_versions.exact(full_submodule_name) else {
return;
return false;
};
let python_version = program.python_version(db);
if version_range.contains(python_version) {
return;
return false;
}
diagnostic.info(format_args!(
@@ -3667,7 +3669,9 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
version_range = version_range.diagnostic_display(),
));
add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, "resolving modules");
add_inferred_python_version_hint_to_diagnostic(db, diagnostic, "resolving modules");
true
}
/// This function receives an unresolved `foo.bar` attribute access,
@@ -3681,8 +3685,9 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions(
db: &dyn Db,
mut diagnostic: LintDiagnosticGuard,
value_type: &Type,
value_type: Type,
attr: &str,
action: &str,
) {
// Currently we limit this analysis to attributes of stdlib modules,
// as this covers the most important cases while not being too noisy
@@ -3705,17 +3710,19 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions(
// so if this lookup succeeds then we know that this lookup *could* succeed with possible
// configuration changes.
let symbol_table = place_table(db, global_scope(db, file));
if symbol_table.symbol_by_name(attr).is_none() {
let Some(symbol) = symbol_table.symbol_by_name(attr) else {
return;
};
if !symbol.is_bound() {
return;
}
diagnostic.info("The member may be available on other Python versions or platforms");
// For now, we just mention the current version they're on, and hope that's enough of a nudge.
// TODO: determine what version they need to be on
// TODO: also mention the platform we're assuming
// TODO: determine what platform they need to be on
add_inferred_python_version_hint_to_diagnostic(
db,
&mut diagnostic,
&format!("accessing `{attr}`"),
);
add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, action);
}

View File

@@ -1358,8 +1358,6 @@ impl KnownFunction {
let db = context.db();
let parameter_types = overload.parameter_types();
let is_todo_type = |ty: Type<'db>| ty.is_todo();
match self {
KnownFunction::RevealType => {
let revealed_type = overload
@@ -1393,10 +1391,7 @@ impl KnownFunction {
let [Some(actual_ty), Some(asserted_ty)] = parameter_types else {
return;
};
if actual_ty.is_equivalent_to(db, *asserted_ty)
|| any_over_type(db, *actual_ty, &is_todo_type, true)
|| any_over_type(db, *asserted_ty, &is_todo_type, true)
{
if actual_ty.is_equivalent_to(db, *asserted_ty) {
return;
}
if let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)
@@ -1432,9 +1427,7 @@ impl KnownFunction {
let [Some(actual_ty)] = parameter_types else {
return;
};
if actual_ty.is_equivalent_to(db, Type::Never)
|| any_over_type(db, *actual_ty, &is_todo_type, true)
{
if actual_ty.is_equivalent_to(db, Type::Never) {
return;
}
if let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)

View File

@@ -15,7 +15,7 @@ use crate::types::generics::Specialization;
use crate::types::signatures::Signature;
use crate::types::{CallDunderError, UnionType};
use crate::types::{
ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext,
CallableTypes, ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext,
TypeVarBoundOrConstraints, class::CodeGeneratorKind,
};
use crate::{Db, DisplaySettings, HasType, NameKind, SemanticModel};
@@ -876,7 +876,10 @@ pub fn definitions_for_keyword_argument<'db>(
let mut resolved_definitions = Vec::new();
if let Some(Type::Callable(callable_type)) = func_type.try_upcast_to_callable(db) {
if let Some(callable_type) = func_type
.try_upcast_to_callable(db)
.and_then(CallableTypes::exactly_one)
{
let signatures = callable_type.signatures(db);
// For each signature, find the parameter with the matching name
@@ -987,7 +990,10 @@ pub fn call_signature_details<'db>(
let func_type = call_expr.func.inferred_type(model);
// Use into_callable to handle all the complex type conversions
if let Some(callable_type) = func_type.try_upcast_to_callable(db) {
if let Some(callable_type) = func_type
.try_upcast_to_callable(db)
.map(|callables| callables.into_type(db))
{
let call_arguments =
CallArguments::from_arguments(&call_expr.arguments, |_, splatted_value| {
splatted_value.inferred_type(model)
@@ -1042,7 +1048,7 @@ pub fn call_type_simplified_by_overloads<'db>(
let func_type = call_expr.func.inferred_type(model);
// Use into_callable to handle all the complex type conversions
let callable_type = func_type.try_upcast_to_callable(db)?;
let callable_type = func_type.try_upcast_to_callable(db)?.into_type(db);
let bindings = callable_type.bindings(db);
// If the callable is trivial this analysis is useless, bail out

View File

@@ -62,9 +62,9 @@ use crate::types::diagnostic::{
INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT,
INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE,
POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
@@ -101,8 +101,8 @@ use crate::types::typed_dict::{
};
use crate::types::visitor::any_over_type;
use crate::types::{
CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass,
CallDunderError, CallableBinding, CallableType, CallableTypes, ClassLiteral, ClassType,
DataclassParams, DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass,
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType,
TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext,
@@ -2291,7 +2291,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let is_input_function_like = inferred_ty
.try_upcast_to_callable(self.db())
.and_then(Type::as_callable)
.and_then(CallableTypes::exactly_one)
.is_some_and(|callable| callable.is_function_like(self.db()));
if is_input_function_like
@@ -3284,7 +3284,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// specified in this context.
match default {
ast::Expr::EllipsisLiteral(_) => {
CallableType::single(self.db(), Signature::new(Parameters::gradual_form(), None))
Type::single_callable(self.db(), Signature::new(Parameters::gradual_form(), None))
}
ast::Expr::List(ast::ExprList { elts, .. }) => {
let mut parameter_types = Vec::with_capacity(elts.len());
@@ -3312,7 +3312,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}))
};
CallableType::single(self.db(), Signature::new(parameters, None))
Type::single_callable(self.db(), Signature::new(parameters, None))
}
ast::Expr::Name(name) => {
let name_ty = self.infer_name_load(name);
@@ -6240,18 +6240,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
return;
};
let diagnostic = builder.into_diagnostic(format_args!(
let mut diagnostic = builder.into_diagnostic(format_args!(
"Module `{module_name}` has no member `{name}`"
));
let mut submodule_hint_added = false;
if let Some(full_submodule_name) = full_submodule_name {
hint_if_stdlib_submodule_exists_on_other_versions(
submodule_hint_added = hint_if_stdlib_submodule_exists_on_other_versions(
self.db(),
diagnostic,
&mut diagnostic,
&full_submodule_name,
module,
);
}
if !submodule_hint_added {
hint_if_stdlib_attribute_exists_on_other_versions(
self.db(),
diagnostic,
module_ty,
name,
"resolving imports",
);
}
}
/// Infer the implicit local definition `x = <module 'whatever.thispackage.x'>` that
@@ -6335,13 +6347,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
return;
};
let diagnostic = builder.into_diagnostic(format_args!(
let mut diagnostic = builder.into_diagnostic(format_args!(
"Module `{thispackage_name}` has no submodule `{final_part}`"
));
hint_if_stdlib_submodule_exists_on_other_versions(
self.db(),
diagnostic,
&mut diagnostic,
&full_submodule_name,
module,
);
@@ -7933,7 +7945,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// TODO: Useful inference of a lambda's return type will require a different approach,
// which does the inference of the body expression based on arguments at each call site,
// rather than eagerly computing a return type without knowing the argument types.
CallableType::function_like(self.db(), Signature::new(parameters, Some(Type::unknown())))
Type::function_like_callable(self.db(), Signature::new(parameters, Some(Type::unknown())))
}
fn infer_call_expression(
@@ -9071,6 +9083,36 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
_ => false,
};
if let Type::ModuleLiteral(module) = value_type {
let module = module.module(db);
let module_name = module.name(db);
if module.kind(db).is_package()
&& let Some(relative_submodule) = ModuleName::new(attr_name)
{
let mut maybe_submodule_name = module_name.clone();
maybe_submodule_name.extend(&relative_submodule);
if let Some(submodule) = resolve_module(db, &maybe_submodule_name) {
if let Some(builder) = self
.context
.report_lint(&POSSIBLY_MISSING_ATTRIBUTE, attribute)
{
let mut diag = builder.into_diagnostic(format_args!(
"Submodule `{attr_name}` may not be available as an attribute \
on module `{module_name}`"
));
diag.help(format_args!(
"Consider explicitly importing `{maybe_submodule_name}`"
));
}
return TypeAndQualifiers::new(
Type::module_literal(db, self.file(), submodule),
TypeOrigin::Inferred,
TypeQualifiers::empty(),
);
}
}
}
let Some(builder) = self.context.report_lint(&UNRESOLVED_ATTRIBUTE, attribute)
else {
return fallback();
@@ -9086,30 +9128,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
let diagnostic = match value_type {
Type::ModuleLiteral(module) => {
let module = module.module(db);
let module_name = module.name(db);
if module.kind(db).is_package()
&& let Some(relative_submodule) = ModuleName::new(attr_name)
{
let mut maybe_submodule_name = module_name.clone();
maybe_submodule_name.extend(&relative_submodule);
if resolve_module(db, &maybe_submodule_name).is_some() {
let mut diag = builder.into_diagnostic(format_args!(
"Submodule `{attr_name}` may not be available as an attribute \
on module `{module_name}`"
));
diag.help(format_args!(
"Consider explicitly importing `{maybe_submodule_name}`"
));
return fallback();
}
}
builder.into_diagnostic(format_args!(
"Module `{module_name}` has no member `{attr_name}`",
))
}
Type::ModuleLiteral(module) => builder.into_diagnostic(format_args!(
"Module `{module_name}` has no member `{attr_name}`",
module_name = module.module(db).name(db),
)),
Type::ClassLiteral(class) => builder.into_diagnostic(format_args!(
"Class `{}` has no attribute `{attr_name}`",
class.name(db),
@@ -9131,8 +9153,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
hint_if_stdlib_attribute_exists_on_other_versions(
db,
diagnostic,
&value_type,
value_type,
attr_name,
&format!("resolving the `{attr_name}` attribute"),
);
fallback()

View File

@@ -1011,7 +1011,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let callable_type = if let (Some(parameters), Some(return_type), true) =
(parameters, return_type, correct_argument_number)
{
CallableType::single(db, Signature::new(parameters, Some(return_type)))
Type::single_callable(db, Signature::new(parameters, Some(return_type)))
} else {
CallableType::unknown(db)
};
@@ -1227,7 +1227,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let argument_type = self.infer_expression(&arguments[0], TypeContext::default());
let Some(callable_type) = argument_type.try_upcast_to_callable(db) else {
let Some(callable_type) = argument_type
.try_upcast_to_callable(db)
.map(|callables| callables.into_type(self.db()))
else {
if let Some(builder) = self
.context
.report_lint(&INVALID_TYPE_FORM, arguments_slice)

View File

@@ -126,7 +126,10 @@ fn check_class_declaration<'db>(
break;
};
let Some(superclass_type_as_callable) = superclass_type.try_upcast_to_callable(db) else {
let Some(superclass_type_as_callable) = superclass_type
.try_upcast_to_callable(db)
.map(|callables| callables.into_type(db))
else {
continue;
};

View File

@@ -3,8 +3,8 @@ use crate::place::{builtins_symbol, known_module_symbol};
use crate::types::enums::is_single_member_enum;
use crate::types::tuple::TupleType;
use crate::types::{
BoundMethodType, CallableType, EnumLiteralType, IntersectionBuilder, KnownClass, Parameter,
Parameters, Signature, SpecialFormType, SubclassOfType, Type, UnionType,
BoundMethodType, EnumLiteralType, IntersectionBuilder, KnownClass, Parameter, Parameters,
Signature, SpecialFormType, SubclassOfType, Type, UnionType,
};
use crate::{Db, module_resolver::KnownModule};
use quickcheck::{Arbitrary, Gen};
@@ -229,7 +229,7 @@ impl Ty {
create_bound_method(db, function, builtins_class)
}
Ty::Callable { params, returns } => CallableType::single(
Ty::Callable { params, returns } => Type::single_callable(
db,
Signature::new(
params.into_parameters(db),

View File

@@ -202,7 +202,7 @@ impl<'db> ProtocolInterface<'db> {
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]),
Some(ty.normalized(db)),
);
let property_getter = CallableType::single(db, property_getter_signature);
let property_getter = Type::single_callable(db, property_getter_signature);
let property = PropertyInstanceType::new(db, Some(property_getter), None);
(
Name::new(name),
@@ -300,7 +300,7 @@ impl<'db> ProtocolInterface<'db> {
.and(db, || {
our_type.has_relation_to_impl(
db,
Type::Callable(other_type.bind_self(db)),
Type::Callable(other_type.bind_self(db, None)),
inferable,
relation,
relation_visitor,
@@ -311,9 +311,9 @@ impl<'db> ProtocolInterface<'db> {
(
ProtocolMemberKind::Method(our_method),
ProtocolMemberKind::Method(other_method),
) => our_method.bind_self(db).has_relation_to_impl(
) => our_method.bind_self(db, None).has_relation_to_impl(
db,
other_method.bind_self(db),
other_method.bind_self(db, None),
inferable,
relation,
relation_visitor,
@@ -676,10 +676,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
// unfortunately not sufficient to obtain the `Callable` supertypes of these types, due to the
// complex interaction between `__new__`, `__init__` and metaclass `__call__`.
let attribute_type = if self.name == "__call__" {
let Some(attribute_type) = other.try_upcast_to_callable(db) else {
return ConstraintSet::from(false);
};
attribute_type
other
} else {
let Place::Defined(attribute_type, _, Definedness::AlwaysDefined) = other
.invoke_descriptor_protocol(
@@ -696,14 +693,32 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
attribute_type
};
attribute_type.has_relation_to_impl(
db,
Type::Callable(method.bind_self(db)),
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
// TODO: Instances of `typing.Self` in the protocol member should specialize to the
// type that we are checking. Without this, we will treat `Self` as an inferable
// typevar, and allow it to match against _any_ type.
//
// It's not very principled, but we also use the literal fallback type, instead of
// `other` directly. This lets us check whether things like `Literal[0]` satisfy a
// protocol that includes methods that have `typing.Self` annotations, without
// overly constraining `Self` to that specific literal.
//
// With the new solver, we should be to replace all of this with an additional
// constraint that enforces what `Self` can specialize to.
let fallback_other = other.literal_fallback_instance(db).unwrap_or(other);
attribute_type
.try_upcast_to_callable(db)
.when_some_and(|callables| {
callables
.map(|callable| callable.apply_self(db, fallback_other))
.has_relation_to_impl(
db,
method.bind_self(db, Some(fallback_other)),
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
}
// TODO: consider the types of the attribute on `other` for property members
ProtocolMemberKind::Property(_) => ConstraintSet::from(matches!(

View File

@@ -213,6 +213,19 @@ impl<'db> CallableSignature<'db> {
}
}
/// Replaces any occurrences of `typing.Self` in the parameter and return annotations with the
/// given type. (Does not bind the `self` parameter; to do that, use
/// [`bind_self`][Self::bind_self].)
pub(crate) fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self {
Self {
overloads: self
.overloads
.iter()
.map(|signature| signature.apply_self(db, self_type))
.collect(),
}
}
fn is_subtype_of_impl(
&self,
db: &'db dyn Db,
@@ -628,6 +641,28 @@ impl<'db> Signature<'db> {
}
}
pub(crate) fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self {
let parameters = self.parameters.apply_type_mapping_impl(
db,
&TypeMapping::BindSelf(self_type),
TypeContext::default(),
&ApplyTypeMappingVisitor::default(),
);
let return_ty = self.return_ty.map(|ty| {
ty.apply_type_mapping(
db,
&TypeMapping::BindSelf(self_type),
TypeContext::default(),
)
});
Self {
generic_context: self.generic_context,
definition: self.definition,
parameters,
return_ty,
}
}
fn inferable_typevars(&self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> {
match self.generic_context {
Some(generic_context) => generic_context.inferable_typevars(db),