Compare commits
1 Commits
cjm/callab
...
gankra/cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b291969813 |
@@ -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]
|
||||
|
||||
@@ -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",)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
64
crates/ty/docs/configuration.md
generated
64
crates/ty/docs/configuration.md
generated
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
```
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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))
|
||||
```
|
||||
|
||||
|
||||
@@ -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]]]
|
||||
|
||||
@@ -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
|
||||
|
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user