Compare commits

..

1 Commits

Author SHA1 Message Date
Aria Desires
b291969813 proof of concept: clickable types in hover 2025-12-15 14:18:45 -05:00
25 changed files with 606 additions and 1599 deletions

View File

@@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
13100,
13030,
);
static TANJUN: Benchmark = Benchmark::new(
@@ -223,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
1100,
950,
);
#[track_caller]

View File

@@ -166,9 +166,8 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
output.push('\n');
let _ = writeln!(output, "**Type**: `{}`", field.value_type);
output.push('\n');
output.push_str("**Example usage**:\n\n");
output.push_str("**Example usage** (`pyproject.toml`):\n\n");
output.push_str(&format_example(
"pyproject.toml",
&format_header(
field.scope,
field.example,
@@ -180,11 +179,11 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
output.push('\n');
}
fn format_example(title: &str, header: &str, content: &str) -> String {
fn format_example(header: &str, content: &str) -> String {
if header.is_empty() {
format!("```toml title=\"{title}\"\n{content}\n```\n",)
format!("```toml\n{content}\n```\n",)
} else {
format!("```toml title=\"{title}\"\n{header}\n{content}\n```\n",)
format!("```toml\n{header}\n{content}\n```\n",)
}
}

View File

@@ -18,9 +18,9 @@ Valid severities are:
**Type**: `dict[RuleName, "ignore" | "warn" | "error"]`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.rules]
possibly-unresolved-reference = "warn"
division-by-zero = "ignore"
@@ -45,9 +45,9 @@ configuration setting.
**Type**: `list[str]`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.environment]
extra-paths = ["./shared/my-search-path"]
```
@@ -76,9 +76,9 @@ This option can be used to point to virtual or system Python environments.
**Type**: `str`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.environment]
python = "./custom-venv-location/.venv"
```
@@ -103,9 +103,9 @@ If no platform is specified, ty will use the current platform:
**Type**: `"win32" | "darwin" | "android" | "ios" | "linux" | "all" | str`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.environment]
# Tailor type stubs and conditionalized type definitions to windows.
python-platform = "win32"
@@ -137,9 +137,9 @@ to reflect the differing contents of the standard library across Python versions
**Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | "3.14" | <major>.<minor>`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.environment]
python-version = "3.12"
```
@@ -165,9 +165,9 @@ it will also be included in the first party search path.
**Type**: `list[str]`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.environment]
# Multiple directories (priority order)
root = ["./src", "./lib", "./vendor"]
@@ -185,9 +185,9 @@ bundled as a zip file in the binary
**Type**: `str`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.environment]
typeshed = "/path/to/custom/typeshed"
```
@@ -240,9 +240,9 @@ If not specified, defaults to `[]` (excludes no files).
**Type**: `list[str]`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[[tool.ty.overrides]]
exclude = [
"generated",
@@ -268,9 +268,9 @@ If not specified, defaults to `["**"]` (matches all files).
**Type**: `list[str]`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[[tool.ty.overrides]]
include = [
"src",
@@ -292,9 +292,9 @@ severity levels or disable them entirely.
**Type**: `dict[RuleName, "ignore" | "warn" | "error"]`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[[tool.ty.overrides]]
include = ["src"]
@@ -358,9 +358,9 @@ to re-include `dist` use `exclude = ["!dist"]`
**Type**: `list[str]`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.src]
exclude = [
"generated",
@@ -399,9 +399,9 @@ matches `<project_root>/src` and not `<project_root>/test/src`).
**Type**: `list[str]`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.src]
include = [
"src",
@@ -421,9 +421,9 @@ Enabled by default.
**Type**: `bool`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.src]
respect-ignore-files = false
```
@@ -450,9 +450,9 @@ it will also be included in the first party search path.
**Type**: `str`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.src]
root = "./app"
```
@@ -471,9 +471,9 @@ Defaults to `false`.
**Type**: `bool`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.terminal]
# Error if ty emits any warning-level diagnostics.
error-on-warning = true
@@ -491,9 +491,9 @@ Defaults to `full`.
**Type**: `full | concise`
**Example usage**:
**Example usage** (`pyproject.toml`):
```toml title="pyproject.toml"
```toml
[tool.ty.terminal]
output-format = "concise"
```

View File

@@ -1,12 +1,12 @@
use crate::docstring::Docstring;
use crate::goto::{GotoTarget, find_goto_target};
use crate::{Db, MarkupKind, RangedValue};
use crate::{Db, HasNavigationTargets, MarkupKind, NavigationTarget, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize};
use std::fmt;
use std::fmt::Formatter;
use ty_python_semantic::types::{KnownInstanceType, Type, TypeVarVariance};
use ty_python_semantic::types::{KnownInstanceType, Type, TypeDetail, TypeVarVariance};
use ty_python_semantic::{DisplaySettings, SemanticModel};
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover<'_>>> {
@@ -59,13 +59,21 @@ pub struct Hover<'db> {
contents: Vec<HoverContent<'db>>,
}
type Linkify<'a> = &'a dyn Fn(&NavigationTarget) -> Option<String>;
impl<'db> Hover<'db> {
/// Renders the hover to a string using the specified markup kind.
pub const fn display<'a>(&'a self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHover<'db, 'a> {
pub const fn display<'a>(
&'a self,
db: &'db dyn Db,
kind: MarkupKind,
linkify: Linkify<'a>,
) -> DisplayHover<'db, 'a> {
DisplayHover {
db,
hover: self,
kind,
linkify,
}
}
@@ -96,6 +104,7 @@ pub struct DisplayHover<'db, 'a> {
db: &'db dyn Db,
hover: &'a Hover<'db>,
kind: MarkupKind,
linkify: Linkify<'a>,
}
impl fmt::Display for DisplayHover<'_, '_> {
@@ -106,7 +115,7 @@ impl fmt::Display for DisplayHover<'_, '_> {
self.kind.horizontal_line().fmt(f)?;
}
content.display(self.db, self.kind).fmt(f)?;
content.display(self.db, self.kind, self.linkify).fmt(f)?;
first = false;
}
@@ -122,11 +131,17 @@ pub enum HoverContent<'db> {
}
impl<'db> HoverContent<'db> {
fn display(&self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHoverContent<'_, 'db> {
fn display<'a>(
&'a self,
db: &'db dyn Db,
kind: MarkupKind,
linkify: Linkify<'a>,
) -> DisplayHoverContent<'a, 'db> {
DisplayHoverContent {
db,
content: self,
kind,
linkify,
}
}
}
@@ -135,6 +150,7 @@ pub(crate) struct DisplayHoverContent<'a, 'db> {
db: &'db dyn Db,
content: &'a HoverContent<'db>,
kind: MarkupKind,
linkify: Linkify<'a>,
}
impl fmt::Display for DisplayHoverContent<'_, '_> {
@@ -151,21 +167,84 @@ impl fmt::Display for DisplayHoverContent<'_, '_> {
Some(TypeVarVariance::Bivariant) => " (bivariant)",
None => "",
};
self.kind
.fenced_code_block(
format!(
"{}{variance}",
ty.display_with(self.db, DisplaySettings::default().multiline())
),
"python",
)
.fmt(f)
if self.kind == MarkupKind::Markdown {
let details = ty.display(self.db).to_string_parts();
// Ok so the idea here is that we potentially have a random soup of spans here,
// and each byte of the string can have at most one target associate with it.
// Thankfully, they were generally pushed in print order, with the inner smaller types
// appearing before the outer bigger ones.
//
// So we record where we are in the string, and every time we find a type, we
// check if it's further along in the string. If it is, great, we give it the
// span for its range, and then advance where we are.
let mut offset = 0;
for (target, detail) in details.targets.iter().zip(&details.details) {
match detail {
TypeDetail::Type(ty) => {
let start = target.start().to_usize();
let end = target.end().to_usize();
// If we skipped over some bytes, push them with no target
if start > offset {
write!(f, "{}", escape(&details.label[offset..start]))?;
}
// Ok, this is the first type that claimed these bytes, give it the target
if start >= offset {
if let Some(target) =
ty.navigation_targets(self.db).into_iter().next()
&& let Some(uri) = (self.linkify)(&target)
{
write!(
f,
"[{}]({})",
escape(&details.label[start..end]),
uri
)?;
} else {
write!(f, "{}", escape(&details.label[start..end]))?;
}
offset = end;
}
}
TypeDetail::SignatureStart
| TypeDetail::SignatureEnd
| TypeDetail::Parameter(_) => {
// Don't care about these
}
}
}
// "flush" the rest of the label without any target
if offset < details.label.len() {
write!(f, "{}", escape(&details.label[offset..details.label.len()]))?;
}
write!(f, "{}", escape(variance))
} else {
self.kind
.fenced_code_block(
format!(
"{}{variance}",
ty.display_with(self.db, DisplaySettings::default().multiline())
),
"python",
)
.fmt(f)
}
}
HoverContent::Docstring(docstring) => docstring.render(self.kind).fmt(f),
}
}
}
fn escape(x: &str) -> String {
x.replace('[', "\\[")
.replace(']', "\\]")
.replace('(', "\\(")
.replace(')', "\\)")
.replace('`', "\\`")
.replace('#', "\\#")
}
#[cfg(test)]
mod tests {
use crate::tests::{CursorTest, cursor_test};
@@ -3670,9 +3749,9 @@ def function():
write!(
&mut buf,
"{plaintext}{line}{markdown}{line}",
plaintext = hover.display(&self.db, MarkupKind::PlainText),
plaintext = hover.display(&self.db, MarkupKind::PlainText, &|_| None),
line = MarkupKind::PlainText.horizontal_line(),
markdown = hover.display(&self.db, MarkupKind::Markdown),
markdown = hover.display(&self.db, MarkupKind::Markdown, &|_| None),
)
.unwrap();

View File

@@ -194,7 +194,7 @@ reveal_type(B().name_does_not_matter()) # revealed: B
reveal_type(B().positional_only(1)) # revealed: B
reveal_type(B().keyword_only(x=1)) # revealed: B
# TODO: This should deally be `B`
reveal_type(B().decorated_method()) # revealed: Self@decorated_method
reveal_type(B().decorated_method()) # revealed: Unknown
reveal_type(B().a_property) # revealed: B

View File

@@ -43,7 +43,9 @@ async def main():
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor() as pool:
result = await loop.run_in_executor(pool, blocking_function)
reveal_type(result) # revealed: int
# TODO: should be `int`
reveal_type(result) # revealed: Unknown
```
### `asyncio.Task`

View File

@@ -82,7 +82,8 @@ def get_default() -> str:
reveal_type(field(default=1)) # revealed: dataclasses.Field[Literal[1]]
reveal_type(field(default=None)) # revealed: dataclasses.Field[None]
reveal_type(field(default_factory=get_default)) # revealed: dataclasses.Field[str]
# TODO: this could ideally be `dataclasses.Field[str]` with a better generics solver
reveal_type(field(default_factory=get_default)) # revealed: dataclasses.Field[Unknown]
```
## dataclass_transform field_specifiers

View File

@@ -144,10 +144,11 @@ from functools import cache
def f(x: int) -> int:
return x**2
# revealed: _lru_cache_wrapper[int]
reveal_type(f)
# revealed: int
reveal_type(f(1))
# TODO: Should be `_lru_cache_wrapper[int]`
reveal_type(f) # revealed: _lru_cache_wrapper[Unknown]
# TODO: Should be `int`
reveal_type(f(1)) # revealed: Unknown
```
## Lambdas as decorators

View File

@@ -11,9 +11,9 @@ classes. Uses of these items should subsequently produce a warning.
from typing_extensions import deprecated
@deprecated("use OtherClass")
def myfunc(x: int): ...
def myfunc(): ...
myfunc(1) # error: [deprecated] "use OtherClass"
myfunc() # error: [deprecated] "use OtherClass"
```
```py

View File

@@ -555,7 +555,8 @@ def identity(x: T) -> T:
def head(xs: list[T]) -> T:
return xs[0]
reveal_type(invoke(identity, 1)) # revealed: Literal[1]
# TODO: this should be `Literal[1]`
reveal_type(invoke(identity, 1)) # revealed: Unknown
# TODO: this should be `Unknown | int`
reveal_type(invoke(head, [1, 2, 3])) # revealed: Unknown

View File

@@ -518,7 +518,8 @@ V = TypeVar("V", default="V")
class D(Generic[V]):
x: V
reveal_type(D().x) # revealed: Unknown
# TODO: we shouldn't leak a typevar like this in type inference
reveal_type(D().x) # revealed: V@D
```
## Regression

View File

@@ -493,7 +493,8 @@ def identity[T](x: T) -> T:
def head[T](xs: list[T]) -> T:
return xs[0]
reveal_type(invoke(identity, 1)) # revealed: Literal[1]
# TODO: this should be `Literal[1]`
reveal_type(invoke(identity, 1)) # revealed: Unknown
# TODO: this should be `Unknown | int`
reveal_type(invoke(head, [1, 2, 3])) # revealed: Unknown
@@ -735,159 +736,3 @@ def f[T](x: T, y: Not[T]) -> T:
y = x # error: [invalid-assignment]
return x
```
## `Callable` parameters
We can recurse into the parameters and return values of `Callable` parameters to infer
specializations of a generic function.
```py
from typing import Any, Callable, NoReturn, overload, Self
from ty_extensions import generic_context, into_callable
def accepts_callable[**P, R](callable: Callable[P, R]) -> Callable[P, R]:
return callable
def returns_int() -> int:
raise NotImplementedError
# revealed: () -> int
reveal_type(into_callable(returns_int))
# revealed: () -> int
reveal_type(accepts_callable(returns_int))
# revealed: int
reveal_type(accepts_callable(returns_int)())
class ClassWithoutConstructor: ...
# revealed: () -> ClassWithoutConstructor
reveal_type(into_callable(ClassWithoutConstructor))
# revealed: () -> ClassWithoutConstructor
reveal_type(accepts_callable(ClassWithoutConstructor))
# revealed: ClassWithoutConstructor
reveal_type(accepts_callable(ClassWithoutConstructor)())
class ClassWithNew:
def __new__(cls, *args, **kwargs) -> Self:
raise NotImplementedError
# revealed: (...) -> ClassWithNew
reveal_type(into_callable(ClassWithNew))
# revealed: (...) -> ClassWithNew
reveal_type(accepts_callable(ClassWithNew))
# revealed: ClassWithNew
reveal_type(accepts_callable(ClassWithNew)())
class ClassWithInit:
def __init__(self) -> None: ...
# revealed: () -> ClassWithInit
reveal_type(into_callable(ClassWithInit))
# revealed: () -> ClassWithInit
reveal_type(accepts_callable(ClassWithInit))
# revealed: ClassWithInit
reveal_type(accepts_callable(ClassWithInit)())
class ClassWithNewAndInit:
def __new__(cls, *args, **kwargs) -> Self:
raise NotImplementedError
def __init__(self, x: int) -> None: ...
# TODO: We do not currently solve a common behavioral supertype for the two solutions of P.
# revealed: ((...) -> ClassWithNewAndInit) | ((x: int) -> ClassWithNewAndInit)
reveal_type(into_callable(ClassWithNewAndInit))
# TODO: revealed: ((...) -> ClassWithNewAndInit) | ((x: int) -> ClassWithNewAndInit)
# revealed: (...) -> ClassWithNewAndInit
reveal_type(accepts_callable(ClassWithNewAndInit))
# revealed: ClassWithNewAndInit
reveal_type(accepts_callable(ClassWithNewAndInit)())
class Meta(type):
def __call__(cls, *args: Any, **kwargs: Any) -> NoReturn:
raise NotImplementedError
class ClassWithNoReturnMetatype(metaclass=Meta):
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
raise NotImplementedError
# TODO: The return types here are wrong, because we end up creating a constraint (Never ≤ R), which
# we confuse with "R has no lower bound".
# revealed: (...) -> Never
reveal_type(into_callable(ClassWithNoReturnMetatype))
# TODO: revealed: (...) -> Never
# revealed: (...) -> Unknown
reveal_type(accepts_callable(ClassWithNoReturnMetatype))
# TODO: revealed: Never
# revealed: Unknown
reveal_type(accepts_callable(ClassWithNoReturnMetatype)())
class Proxy: ...
class ClassWithIgnoredInit:
def __new__(cls) -> Proxy:
return Proxy()
def __init__(self, x: int) -> None: ...
# revealed: () -> Proxy
reveal_type(into_callable(ClassWithIgnoredInit))
# revealed: () -> Proxy
reveal_type(accepts_callable(ClassWithIgnoredInit))
# revealed: Proxy
reveal_type(accepts_callable(ClassWithIgnoredInit)())
class ClassWithOverloadedInit[T]:
t: T # invariant
@overload
def __init__(self: "ClassWithOverloadedInit[int]", x: int) -> None: ...
@overload
def __init__(self: "ClassWithOverloadedInit[str]", x: str) -> None: ...
def __init__(self, x: int | str) -> None: ...
# TODO: The old solver cannot handle this overloaded constructor. The ideal solution is that we
# would solve **P once, and map it to the entire overloaded signature of the constructor. This
# mapping would have to include the return types, since there are different return types for each
# overload. We would then also have to determine that R must be equal to the return type of **P's
# solution.
# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]]
reveal_type(into_callable(ClassWithOverloadedInit))
# TODO: revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]]
# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str], (x: str) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str]]
reveal_type(accepts_callable(ClassWithOverloadedInit))
# TODO: revealed: ClassWithOverloadedInit[int]
# revealed: ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str]
reveal_type(accepts_callable(ClassWithOverloadedInit)(0))
# TODO: revealed: ClassWithOverloadedInit[str]
# revealed: ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str]
reveal_type(accepts_callable(ClassWithOverloadedInit)(""))
class GenericClass[T]:
t: T # invariant
def __new__(cls, x: list[T], y: list[T]) -> Self:
raise NotImplementedError
def _(x: list[str]):
# TODO: This fails because we are not propagating GenericClass's generic context into the
# Callable that we create for it.
# revealed: (x: list[T@GenericClass], y: list[T@GenericClass]) -> GenericClass[T@GenericClass]
reveal_type(into_callable(GenericClass))
# revealed: ty_extensions.GenericContext[T@GenericClass]
reveal_type(generic_context(into_callable(GenericClass)))
# revealed: (x: list[T@GenericClass], y: list[T@GenericClass]) -> GenericClass[T@GenericClass]
reveal_type(accepts_callable(GenericClass))
# TODO: revealed: ty_extensions.GenericContext[T@GenericClass]
# revealed: None
reveal_type(generic_context(accepts_callable(GenericClass)))
# TODO: revealed: GenericClass[str]
# TODO: no errors
# revealed: GenericClass[T@GenericClass]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(accepts_callable(GenericClass)(x, x))
```

View File

@@ -503,8 +503,7 @@ class C[**P]:
def __init__(self, f: Callable[P, int]) -> None:
self.f = f
# Note that the return type must match exactly, since C is invariant on the return type of C.f.
def f(x: int, y: str) -> int:
def f(x: int, y: str) -> bool:
return True
c = C(f)
@@ -619,22 +618,6 @@ reveal_type(foo.method) # revealed: bound method Foo[(int, str, /)].method(int,
reveal_type(foo.method(1, "a")) # revealed: str
```
### Gradual types propagate through `ParamSpec` inference
```py
from typing import Callable
def callable_identity[**P, R](func: Callable[P, R]) -> Callable[P, R]:
return func
@callable_identity
def f(env: dict) -> None:
pass
# revealed: (env: dict[Unknown, Unknown]) -> None
reveal_type(f)
```
### Overloads
`overloaded.pyi`:
@@ -679,7 +662,7 @@ reveal_type(change_return_type(int_int)) # revealed: Overload[(x: int) -> str,
reveal_type(change_return_type(int_str)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
# error: [invalid-argument-type]
reveal_type(change_return_type(str_str)) # revealed: (...) -> str
reveal_type(change_return_type(str_str)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
# TODO: Both of these shouldn't raise an error
# error: [invalid-argument-type]

View File

@@ -883,7 +883,7 @@ reveal_type(C[int]().y) # revealed: int
class D[T = T]:
x: T
reveal_type(D().x) # revealed: Unknown
reveal_type(D().x) # revealed: T@D
```
[pep 695]: https://peps.python.org/pep-0695/

View File

@@ -426,8 +426,7 @@ from ty_extensions import ConstraintSet, generic_context
def mentions[T, U]():
# (T@mentions ≤ int) ∧ (U@mentions = list[T@mentions])
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(list[T], U, list[T])
# TODO: revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = list[int]]
# revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = Unknown]
# revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = list[int]]
reveal_type(generic_context(mentions).specialize_constrained(constraints))
```

View File

@@ -304,7 +304,7 @@ x11: list[Literal[1] | Literal[2] | Literal[3]] = [1, 2, 3]
reveal_type(x11) # revealed: list[Literal[1, 2, 3]]
x12: Y[Y[Literal[1]]] = [[1]]
reveal_type(x12) # revealed: list[list[Literal[1]]]
reveal_type(x12) # revealed: list[Y[Literal[1]]]
x13: list[tuple[Literal[1], Literal[2], Literal[3]]] = [(1, 2, 3)]
reveal_type(x13) # revealed: list[tuple[Literal[1], Literal[2], Literal[3]]]

View File

@@ -15,9 +15,9 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/deprecated.md
1 | from typing_extensions import deprecated
2 |
3 | @deprecated("use OtherClass")
4 | def myfunc(x: int): ...
4 | def myfunc(): ...
5 |
6 | myfunc(1) # error: [deprecated] "use OtherClass"
6 | myfunc() # error: [deprecated] "use OtherClass"
7 | from typing_extensions import deprecated
8 |
9 | @deprecated("use BetterClass")
@@ -42,9 +42,9 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/deprecated.md
warning[deprecated]: The function `myfunc` is deprecated
--> src/mdtest_snippet.py:6:1
|
4 | def myfunc(x: int): ...
4 | def myfunc(): ...
5 |
6 | myfunc(1) # error: [deprecated] "use OtherClass"
6 | myfunc() # error: [deprecated] "use OtherClass"
| ^^^^^^ use OtherClass
7 | from typing_extensions import deprecated
|

View File

@@ -918,33 +918,16 @@ impl<'db> Type<'db> {
previous: Self,
cycle: &salsa::Cycle,
) -> Self {
// When we encounter a salsa cycle, we want to avoid oscillating between two or more types
// without converging on a fixed-point result. Most of the time, we union together the
// types from each cycle iteration to ensure that our result is monotonic, even if we
// encounter oscillation.
//
// However, there are a couple of cases where we don't want to do that, and want to use the
// later cycle iteration's result directly. This introduces the theoretical possibility of
// cycle oscillation involving such types (because we are not strictly widening the type on
// each iteration), but so far we have not seen an example of that.
// Avoid unioning two generic aliases of the same class together; this union will never
// simplify and is likely to cause downstream problems. This introduces the theoretical
// possibility of cycle oscillation involving such types (because we are not strictly
// widening the type on each iteration), but so far we have not seen an example of that.
match (previous, self) {
// Avoid unioning two generic aliases of the same class together; this union will never
// simplify and is likely to cause downstream problems.
(Type::GenericAlias(prev_alias), Type::GenericAlias(curr_alias))
if prev_alias.origin(db) == curr_alias.origin(db) =>
{
self
}
// Similarly, don't union together two function literals, since there are several parts
// of our type inference machinery that assume that we infer a single FunctionLiteral
// type for each overload of each function definition.
(Type::FunctionLiteral(prev_function), Type::FunctionLiteral(curr_function))
if prev_function.definition(db) == curr_function.definition(db) =>
{
self
}
_ => {
// Also avoid unioning in a previous type which contains a Divergent from the
// current cycle, if the most-recent type does not. This cannot cause an
@@ -1860,7 +1843,7 @@ impl<'db> Type<'db> {
}
}
Type::ClassLiteral(class_literal) => {
Some(class_literal.identity_specialization(db).into_callable(db))
Some(class_literal.default_specialization(db).into_callable(db))
}
Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)),
@@ -1992,16 +1975,6 @@ impl<'db> Type<'db> {
.is_always_satisfied(db)
}
/// Return true if this type is assignable to type `target` using constraint-set assignability.
///
/// This uses `TypeRelation::ConstraintSetAssignability`, which encodes typevar relations into
/// a constraint set and lets `satisfied_by_all_typevars` perform existential vs universal
/// reasoning depending on inferable typevars.
pub fn is_constraint_set_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
self.when_constraint_set_assignable_to(db, target, InferableTypeVars::None)
.is_always_satisfied(db)
}
fn when_assignable_to(
self,
db: &'db dyn Db,
@@ -2011,20 +1984,6 @@ impl<'db> Type<'db> {
self.has_relation_to(db, target, inferable, TypeRelation::Assignability)
}
fn when_constraint_set_assignable_to(
self,
db: &'db dyn Db,
target: Type<'db>,
inferable: InferableTypeVars<'_, 'db>,
) -> ConstraintSet<'db> {
self.has_relation_to(
db,
target,
inferable,
TypeRelation::ConstraintSetAssignability,
)
}
/// Return `true` if it would be redundant to add `self` to a union that already contains `other`.
///
/// See [`TypeRelation::Redundancy`] for more details.
@@ -2090,21 +2049,6 @@ impl<'db> Type<'db> {
return constraints.implies_subtype_of(db, self, target);
}
// Handle the new constraint-set-based assignability relation next. Comparisons with a
// typevar are translated directly into a constraint set.
if relation.is_constraint_set_assignability() {
// A typevar satisfies a relation when...it satisfies the relation. Yes that's a
// tautology! We're moving the caller's subtyping/assignability requirement into a
// constraint set. If the typevar has an upper bound or constraints, then the relation
// only has to hold when the typevar has a valid specialization (i.e., one that
// satisfies the upper bound/constraints).
if let Type::TypeVar(bound_typevar) = self {
return ConstraintSet::constrain_typevar(db, bound_typevar, Type::Never, target);
} else if let Type::TypeVar(bound_typevar) = target {
return ConstraintSet::constrain_typevar(db, bound_typevar, self, Type::object());
}
}
match (self, target) {
// Everything is a subtype of `object`.
(_, Type::NominalInstance(instance)) if instance.is_object() => {
@@ -2185,7 +2129,7 @@ impl<'db> Type<'db> {
);
ConstraintSet::from(match relation {
TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => false,
TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => true,
TypeRelation::Assignability => true,
TypeRelation::Redundancy => match target {
Type::Dynamic(_) => true,
Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic),
@@ -2195,7 +2139,7 @@ impl<'db> Type<'db> {
}
(_, Type::Dynamic(_)) => ConstraintSet::from(match relation {
TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => false,
TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => true,
TypeRelation::Assignability => true,
TypeRelation::Redundancy => match self {
Type::Dynamic(_) => true,
Type::Intersection(intersection) => {
@@ -2459,19 +2403,14 @@ impl<'db> Type<'db> {
TypeRelation::Subtyping
| TypeRelation::Redundancy
| TypeRelation::SubtypingAssuming(_) => self,
TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => {
self.bottom_materialization(db)
}
TypeRelation::Assignability => self.bottom_materialization(db),
};
intersection.negative(db).iter().when_all(db, |&neg_ty| {
let neg_ty = match relation {
TypeRelation::Subtyping
| TypeRelation::Redundancy
| TypeRelation::SubtypingAssuming(_) => neg_ty,
TypeRelation::Assignability
| TypeRelation::ConstraintSetAssignability => {
neg_ty.bottom_materialization(db)
}
TypeRelation::Assignability => neg_ty.bottom_materialization(db),
};
self_ty.is_disjoint_from_impl(
db,
@@ -9838,22 +9777,6 @@ impl<'db> TypeVarInstance<'db> {
))
}
fn type_is_self_referential(self, db: &'db dyn Db, ty: Type<'db>) -> bool {
let identity = self.identity(db);
any_over_type(
db,
ty,
&|ty| match ty {
Type::TypeVar(bound_typevar) => identity == bound_typevar.typevar(db).identity(db),
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => {
identity == typevar.identity(db)
}
_ => false,
},
false,
)
}
#[salsa::tracked(
cycle_fn=lazy_bound_or_constraints_cycle_recover,
cycle_initial=lazy_bound_or_constraints_cycle_initial,
@@ -9876,11 +9799,6 @@ impl<'db> TypeVarInstance<'db> {
}
_ => return None,
};
if self.type_is_self_referential(db, ty) {
return None;
}
Some(TypeVarBoundOrConstraints::UpperBound(ty))
}
@@ -9928,15 +9846,6 @@ impl<'db> TypeVarInstance<'db> {
}
_ => return None,
};
if constraints
.elements(db)
.iter()
.any(|ty| self.type_is_self_referential(db, *ty))
{
return None;
}
Some(TypeVarBoundOrConstraints::Constraints(constraints))
}
@@ -9983,11 +9892,15 @@ impl<'db> TypeVarInstance<'db> {
let definition = self.definition(db)?;
let module = parsed_module(db, definition.file(db)).load(db);
let ty = match definition.kind(db) {
match definition.kind(db) {
// PEP 695 typevar
DefinitionKind::TypeVar(typevar) => {
let typevar_node = typevar.node(&module);
definition_expression_type(db, definition, typevar_node.default.as_ref()?)
Some(definition_expression_type(
db,
definition,
typevar_node.default.as_ref()?,
))
}
// legacy typevar / ParamSpec
DefinitionKind::Assignment(assignment) => {
@@ -9997,9 +9910,9 @@ impl<'db> TypeVarInstance<'db> {
let expr = &call_expr.arguments.find_keyword("default")?.value;
let default_type = definition_expression_type(db, definition, expr);
if known_class == Some(KnownClass::ParamSpec) {
convert_type_to_paramspec_value(db, default_type)
Some(convert_type_to_paramspec_value(db, default_type))
} else {
default_type
Some(default_type)
}
}
// PEP 695 ParamSpec
@@ -10007,16 +9920,10 @@ impl<'db> TypeVarInstance<'db> {
let paramspec_node = paramspec.node(&module);
let default_ty =
definition_expression_type(db, definition, paramspec_node.default.as_ref()?);
convert_type_to_paramspec_value(db, default_ty)
Some(convert_type_to_paramspec_value(db, default_ty))
}
_ => return None,
};
if self.type_is_self_referential(db, ty) {
return None;
_ => None,
}
Some(ty)
}
pub fn bind_pep695(self, db: &'db dyn Db) -> Option<BoundTypeVarInstance<'db>> {
@@ -12093,11 +12000,6 @@ pub(crate) enum TypeRelation<'db> {
/// are not actually subtypes of each other. (That is, `implies_subtype_of(false, int, str)`
/// will return true!)
SubtypingAssuming(ConstraintSet<'db>),
/// A placeholder for the new assignability relation that uses constraint sets to encode
/// relationships with a typevar. This will eventually replace `Assignability`, but allows us
/// to start using the new relation in a controlled manner in some places.
ConstraintSetAssignability,
}
impl TypeRelation<'_> {
@@ -12105,10 +12007,6 @@ impl TypeRelation<'_> {
matches!(self, TypeRelation::Assignability)
}
pub(crate) const fn is_constraint_set_assignability(self) -> bool {
matches!(self, TypeRelation::ConstraintSetAssignability)
}
pub(crate) const fn is_subtyping(self) -> bool {
matches!(self, TypeRelation::Subtyping)
}
@@ -12594,10 +12492,6 @@ impl<'db> CallableTypes<'db> {
}
}
fn as_slice(&self) -> &[CallableType<'db>] {
&self.0
}
fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> {
self.0
}

View File

@@ -689,10 +689,20 @@ impl<'db> IntersectionBuilder<'db> {
}
}
pub(crate) fn order_elements(mut self, val: bool) -> Self {
self.order_elements = val;
self
}
pub(crate) fn add_positive(self, ty: Type<'db>) -> Self {
self.add_positive_impl(ty, &mut vec![])
}
pub(crate) fn add_positive_in_place(&mut self, ty: Type<'db>) {
let updated = std::mem::replace(self, Self::empty(self.db)).add_positive(ty);
*self = updated;
}
pub(crate) fn add_positive_impl(
mut self,
ty: Type<'db>,

View File

@@ -637,9 +637,7 @@ impl<'db> ClassType<'db> {
| TypeRelation::SubtypingAssuming(_) => {
ConstraintSet::from(other.is_object(db))
}
TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => {
ConstraintSet::from(!other.is_final(db))
}
TypeRelation::Assignability => ConstraintSet::from(!other.is_final(db)),
},
// Protocol, Generic, and TypedDict are not represented by a ClassType.

File diff suppressed because it is too large Load Diff

View File

@@ -13,15 +13,15 @@ use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::instance::{Protocol, ProtocolInstanceType};
use crate::types::signatures::Parameters;
use crate::types::signatures::{Parameters, ParametersKind};
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
use crate::types::variance::VarianceInferable;
use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard};
use crate::types::{
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance,
ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor,
IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor,
Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity,
CallableSignature, CallableType, CallableTypeKind, CallableTypes, ClassLiteral,
FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor,
KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Signature, Type,
TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity,
TypeVarInstance, TypeVarKind, TypeVarVariance, UnionType, declaration_type,
walk_type_var_bounds,
};
@@ -571,14 +571,6 @@ impl<'db> GenericContext<'db> {
let partial = PartialSpecialization {
generic_context: self,
types: &types,
// Don't recursively substitute type[i] in itself. Ideally, we could instead
// check if the result is self-referential after we're done applying the
// partial specialization. But when we apply a paramspec, we don't use the
// callable that it maps to directly; we create a new callable that reuses
// parts of it. That means we can't look for the previous type directly.
// Instead we use this to skip specializing the type in itself in the first
// place.
skip: Some(i),
};
let updated = types[i].apply_type_mapping(
db,
@@ -649,7 +641,6 @@ impl<'db> GenericContext<'db> {
let partial = PartialSpecialization {
generic_context: self,
types: &expanded[0..idx],
skip: None,
};
let default = default.apply_type_mapping(
db,
@@ -926,11 +917,7 @@ fn has_relation_in_invariant_position<'db>(
disjointness_visitor,
),
// And A <~ B (assignability) is Bottom[A] <: Top[B]
(
None,
Some(base_mat),
TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability,
) => is_subtype_in_invariant_position(
(None, Some(base_mat), TypeRelation::Assignability) => is_subtype_in_invariant_position(
db,
derived_type,
MaterializationKind::Bottom,
@@ -940,11 +927,7 @@ fn has_relation_in_invariant_position<'db>(
relation_visitor,
disjointness_visitor,
),
(
Some(derived_mat),
None,
TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability,
) => is_subtype_in_invariant_position(
(Some(derived_mat), None, TypeRelation::Assignability) => is_subtype_in_invariant_position(
db,
derived_type,
derived_mat,
@@ -1455,9 +1438,6 @@ impl<'db> Specialization<'db> {
pub struct PartialSpecialization<'a, 'db> {
generic_context: GenericContext<'db>,
types: &'a [Type<'db>],
/// An optional typevar to _not_ substitute when applying the specialization. We use this to
/// avoid recursively substituting a type inside of itself.
skip: Option<usize>,
}
impl<'db> PartialSpecialization<'_, 'db> {
@@ -1472,9 +1452,6 @@ impl<'db> PartialSpecialization<'_, 'db> {
.generic_context
.variables_inner(db)
.get_index_of(&bound_typevar.identity(db))?;
if self.skip.is_some_and(|skip| skip == index) {
return Some(Type::Never);
}
self.types.get(index).copied()
}
}
@@ -1532,7 +1509,7 @@ impl<'db> SpecializationBuilder<'db> {
.map(|(identity, _)| self.types.get(identity).copied());
// TODO Infer the tuple spec for a tuple type
generic_context.specialize_recursive(self.db, types)
generic_context.specialize_partial(self.db, types)
}
fn add_type_mapping(
@@ -1566,59 +1543,6 @@ impl<'db> SpecializationBuilder<'db> {
}
}
/// Finds all of the valid specializations of a constraint set, and adds their type mappings to
/// the specialization that this builder is building up.
///
/// `formal` should be the top-level formal parameter type that we are inferring. This is used
/// by our literal promotion logic, which needs to know which typevars are affected by each
/// argument, and the variance of those typevars in the corresponding parameter.
///
/// TODO: This is a stopgap! Eventually, the builder will maintain a single constraint set for
/// the main specialization that we are building, and [`build`][Self::build] will build the
/// specialization directly from that constraint set. This method lets us migrate to that brave
/// new world incrementally, by using the new constraint set mechanism piecemeal for certain
/// type comparisons.
fn add_type_mappings_from_constraint_set(
&mut self,
formal: Type<'db>,
constraints: ConstraintSet<'db>,
mut f: impl FnMut(TypeVarAssignment<'db>) -> Option<Type<'db>>,
) {
let constraints = constraints.limit_to_valid_specializations(self.db);
constraints.for_each_path(self.db, |path| {
for (constraint, _) in path.positive_constraints() {
let typevar = constraint.typevar(self.db);
let lower = constraint.lower(self.db);
let upper = constraint.upper(self.db);
if !upper.is_object() {
let variance = formal.variance_of(self.db, typevar);
self.add_type_mapping(typevar, upper, variance, &mut f);
} else if !lower.is_never() {
let variance = formal.variance_of(self.db, typevar);
self.add_type_mapping(typevar, lower, variance, &mut f);
}
if let Type::TypeVar(lower_bound_typevar) = lower {
let variance = formal.variance_of(self.db, lower_bound_typevar);
self.add_type_mapping(
lower_bound_typevar,
Type::TypeVar(typevar),
variance,
&mut f,
);
}
if let Type::TypeVar(upper_bound_typevar) = upper {
let variance = formal.variance_of(self.db, upper_bound_typevar);
self.add_type_mapping(
upper_bound_typevar,
Type::TypeVar(typevar),
variance,
&mut f,
);
}
}
});
}
/// Infer type mappings for the specialization based on a given type and its declared type.
pub(crate) fn infer(
&mut self,
@@ -1648,15 +1572,6 @@ impl<'db> SpecializationBuilder<'db> {
polarity: TypeVarVariance,
mut f: &mut dyn FnMut(TypeVarAssignment<'db>) -> Option<Type<'db>>,
) -> Result<(), SpecializationError<'db>> {
// TODO: Eventually, the builder will maintain a constraint set, instead of a hash-map of
// type mappings, to represent the specialization that we are building up. At that point,
// this method will just need to compare `actual ≤ formal`, using constraint set
// assignability, and AND the result into the constraint set we are building.
//
// To make progress on that migration, we use constraint set assignability whenever
// possible when adding any new heuristics here. See the `Callable` clause below for an
// example.
if formal == actual {
return Ok(());
}
@@ -1912,19 +1827,43 @@ impl<'db> SpecializationBuilder<'db> {
}
(Type::Callable(formal_callable), _) => {
let Some(actual_callables) = actual.try_upcast_to_callable(self.db) else {
return Ok(());
};
for actual_callable in actual_callables.as_slice() {
let when = actual_callable
.signatures(self.db)
.when_constraint_set_assignable_to(
if let Some(actual_callable) = actual
.try_upcast_to_callable(self.db)
.and_then(CallableTypes::exactly_one)
{
// We're only interested in a formal callable of the form `Callable[P, ...]` for
// now where `P` is a `ParamSpec`.
// TODO: This would need to be updated once we support `Concatenate`
// TODO: What to do for overloaded callables?
let [signature] = formal_callable.signatures(self.db).as_slice() else {
return Ok(());
};
let formal_parameters = signature.parameters();
let ParametersKind::ParamSpec(typevar) = formal_parameters.kind() else {
return Ok(());
};
let paramspec_value = match actual_callable.signatures(self.db).as_slice() {
[] => return Ok(()),
[actual_signature] => match actual_signature.parameters().kind() {
ParametersKind::ParamSpec(typevar) => Type::TypeVar(typevar),
_ => Type::Callable(CallableType::new(
self.db,
CallableSignature::single(Signature::new(
actual_signature.parameters().clone(),
None,
)),
CallableTypeKind::ParamSpecValue,
)),
},
actual_signatures => Type::Callable(CallableType::new(
self.db,
formal_callable.signatures(self.db),
self.inferable,
);
self.add_type_mappings_from_constraint_set(formal, when, &mut f);
CallableSignature::from_overloads(actual_signatures.iter().map(
|signature| Signature::new(signature.parameters().clone(), None),
)),
CallableTypeKind::ParamSpecValue,
)),
};
self.add_type_mapping(typevar, paramspec_value, polarity, &mut f);
}
}

View File

@@ -17,13 +17,11 @@ use smallvec::{SmallVec, smallvec_inline};
use super::{DynamicType, Type, TypeVarVariance, definition_expression_type, semantic_index};
use crate::semantic_index::definition::Definition;
use crate::types::constraints::{
ConstraintSet, IteratorConstraintsExtension, OptionConstraintsExtension,
};
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::generics::{GenericContext, InferableTypeVars, walk_generic_context};
use crate::types::infer::{infer_deferred_types, infer_scope_types};
use crate::types::{
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, CallableTypeKind,
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableTypeKind,
FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor,
KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, TypeContext,
TypeMapping, TypeRelation, VarianceInferable, todo_type,
@@ -92,6 +90,10 @@ impl<'db> CallableSignature<'db> {
self.overloads.iter()
}
pub(crate) fn as_slice(&self) -> &[Signature<'db>] {
&self.overloads
}
pub(crate) fn with_inherited_generic_context(
&self,
db: &'db dyn Db,
@@ -335,38 +337,6 @@ impl<'db> CallableSignature<'db> {
)
}
/// Checks whether the given slice contains a single signature, and that signature is a
/// `ParamSpec` signature. If so, returns the [`BoundTypeVarInstance`] for the `ParamSpec`,
/// along with the return type of the signature.
fn signatures_is_single_paramspec(
signatures: &[Signature<'db>],
) -> Option<(BoundTypeVarInstance<'db>, Option<Type<'db>>)> {
// TODO: This might need updating once we support `Concatenate`
let [signature] = signatures else {
return None;
};
signature
.parameters
.as_paramspec()
.map(|bound_typevar| (bound_typevar, signature.return_ty))
}
pub(crate) fn when_constraint_set_assignable_to(
&self,
db: &'db dyn Db,
other: &Self,
inferable: InferableTypeVars<'_, 'db>,
) -> ConstraintSet<'db> {
self.has_relation_to_impl(
db,
other,
inferable,
TypeRelation::ConstraintSetAssignability,
&HasRelationToVisitor::default(),
&IsDisjointVisitor::default(),
)
}
/// Implementation of subtyping and assignability between two, possible overloaded, callable
/// types.
fn has_relation_to_inner(
@@ -378,116 +348,6 @@ impl<'db> CallableSignature<'db> {
relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> {
if relation.is_constraint_set_assignability() {
// TODO: Oof, maybe ParamSpec needs to live at CallableSignature, not Signature?
let self_is_single_paramspec = Self::signatures_is_single_paramspec(self_signatures);
let other_is_single_paramspec = Self::signatures_is_single_paramspec(other_signatures);
// If either callable is a ParamSpec, the constraint set should bind the ParamSpec to
// the other callable's signature. We also need to compare the return types — for
// instance, to verify in `Callable[P, int]` that the return type is assignable to
// `int`, or in `Callable[P, T]` to bind `T` to the return type of the other callable.
//
// TODO: This logic might need to move down into `Signature`, if we need paramspecs to
// be able to match a _subset_ of an overloaded callable. (In that case, we need to
// check each signature individually, and combine together the ones that match into the
// overloaded callable that the paramspec binds to.)
match (self_is_single_paramspec, other_is_single_paramspec) {
(
Some((self_bound_typevar, self_return_type)),
Some((other_bound_typevar, other_return_type)),
) => {
let param_spec_matches = ConstraintSet::constrain_typevar(
db,
self_bound_typevar,
Type::TypeVar(other_bound_typevar),
Type::TypeVar(other_bound_typevar),
);
let return_types_match = self_return_type.zip(other_return_type).when_some_and(
|(self_return_type, other_return_type)| {
self_return_type.has_relation_to_impl(
db,
other_return_type,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
},
);
return param_spec_matches.and(db, || return_types_match);
}
(Some((self_bound_typevar, self_return_type)), None) => {
let upper =
Type::Callable(CallableType::new(
db,
CallableSignature::from_overloads(other_signatures.iter().map(
|signature| Signature::new(signature.parameters().clone(), None),
)),
CallableTypeKind::ParamSpecValue,
));
let param_spec_matches = ConstraintSet::constrain_typevar(
db,
self_bound_typevar,
Type::Never,
upper,
);
let return_types_match = self_return_type.when_some_and(|self_return_type| {
other_signatures
.iter()
.filter_map(|signature| signature.return_ty)
.when_any(db, |other_return_type| {
self_return_type.has_relation_to_impl(
db,
other_return_type,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
});
return param_spec_matches.and(db, || return_types_match);
}
(None, Some((other_bound_typevar, other_return_type))) => {
let lower =
Type::Callable(CallableType::new(
db,
CallableSignature::from_overloads(self_signatures.iter().map(
|signature| Signature::new(signature.parameters().clone(), None),
)),
CallableTypeKind::ParamSpecValue,
));
let param_spec_matches = ConstraintSet::constrain_typevar(
db,
other_bound_typevar,
lower,
Type::object(),
);
let return_types_match = other_return_type.when_some_and(|other_return_type| {
self_signatures
.iter()
.filter_map(|signature| signature.return_ty)
.when_any(db, |self_return_type| {
self_return_type.has_relation_to_impl(
db,
other_return_type,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
});
return param_spec_matches.and(db, || return_types_match);
}
(None, None) => {}
}
}
match (self_signatures, other_signatures) {
([self_signature], [other_signature]) => {
// Base case: both callable types contain a single signature.
@@ -1273,13 +1133,7 @@ impl<'db> Signature<'db> {
// If either of the parameter lists is gradual (`...`), then it is assignable to and from
// any other parameter list, but not a subtype or supertype of any other parameter list.
if self.parameters.is_gradual() || other.parameters.is_gradual() {
result.intersect(
db,
ConstraintSet::from(
relation.is_assignability() || relation.is_constraint_set_assignability(),
),
);
return result;
return ConstraintSet::from(relation.is_assignability());
}
let mut parameters = ParametersZip {
@@ -1699,13 +1553,6 @@ impl<'db> Parameters<'db> {
matches!(self.kind, ParametersKind::Gradual)
}
pub(crate) const fn as_paramspec(&self) -> Option<BoundTypeVarInstance<'db>> {
match self.kind {
ParametersKind::ParamSpec(bound_typevar) => Some(bound_typevar),
_ => None,
}
}
/// Return todo parameters: (*args: Todo, **kwargs: Todo)
pub(crate) fn todo() -> Self {
Self {

View File

@@ -1,6 +1,6 @@
use std::borrow::Cow;
use crate::document::{FileRangeExt, PositionExt};
use crate::document::{FileRangeExt, PositionExt, ToRangeExt};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
@@ -8,7 +8,7 @@ use crate::session::DocumentSnapshot;
use crate::session::client::Client;
use lsp_types::request::HoverRequest;
use lsp_types::{HoverContents, HoverParams, MarkupContent, Url};
use ty_ide::{MarkupKind, hover};
use ty_ide::{MarkupKind, NavigationTarget, hover};
use ty_project::ProjectDatabase;
pub(crate) struct HoverRequestHandler;
@@ -61,7 +61,23 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler {
(MarkupKind::PlainText, lsp_types::MarkupKind::PlainText)
};
let contents = range_info.display(db, markup_kind).to_string();
let to_lsp_link = |target: &NavigationTarget| -> Option<String> {
let file = target.file();
let location = target
.focus_range()
.to_lsp_range(db, file, snapshot.encoding())?
.to_location()?;
let uri = location.uri;
let line1 = location.range.start.line;
let char1 = location.range.start.character;
let line2 = location.range.end.line;
let char2 = location.range.end.character;
Some(format!("{uri}#L{line1}C{char1}-L{line2}C{char2}"))
};
let contents = range_info
.display(db, markup_kind, &to_lsp_link)
.to_string();
Ok(Some(lsp_types::Hover {
contents: HoverContents::Markup(MarkupContent {

View File

@@ -399,7 +399,7 @@ impl Workspace {
Ok(Some(Hover {
markdown: range_info
.display(&self.db, MarkupKind::Markdown)
.display(&self.db, MarkupKind::Markdown, &|_| None)
.to_string(),
range: source_range,
}))