Compare commits
3 Commits
amy/missin
...
gankra/imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ef86c9637 | ||
|
|
793d0d0dd4 | ||
|
|
e84d523bcf |
@@ -459,6 +459,12 @@ impl File {
|
||||
self.source_type(db).is_stub()
|
||||
}
|
||||
|
||||
/// Returns `true` if the file is an `__init__.py(i)`
|
||||
pub fn is_init(self, db: &dyn Db) -> bool {
|
||||
let path = self.path(db).as_str();
|
||||
path.ends_with("__init__.py") || path.ends_with("__init__.pyi")
|
||||
}
|
||||
|
||||
pub fn source_type(self, db: &dyn Db) -> PySourceType {
|
||||
match self.path(db) {
|
||||
FilePath::System(path) => path
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
# Violation cases: RUF065
|
||||
|
||||
|
||||
async def test_coroutine_without_await():
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
coro() # RUF065
|
||||
|
||||
|
||||
async def test_coroutine_without_await():
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
result = coro() # RUF065
|
||||
|
||||
|
||||
async def test_coroutine_without_await():
|
||||
def not_coro():
|
||||
pass
|
||||
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
not_coro()
|
||||
coro() # RUF065
|
||||
|
||||
|
||||
async def test_coroutine_without_await():
|
||||
async def coro():
|
||||
another_coro() # RUF065
|
||||
|
||||
async def another_coro():
|
||||
pass
|
||||
|
||||
await coro()
|
||||
|
||||
|
||||
async def test_asyncio_api_without_await():
|
||||
asyncio.sleep(0.5) # RUF065
|
||||
|
||||
|
||||
async def test_asyncio_api_without_await():
|
||||
async def coro():
|
||||
asyncio.sleep(0.5) # RUF065
|
||||
|
||||
await asyncio.wait(coro)
|
||||
|
||||
|
||||
async def test_asyncio_api_without_await():
|
||||
async def coro():
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
asyncio.wait_for(coro) # RUF065
|
||||
|
||||
|
||||
async def test_asyncio_api_without_await():
|
||||
async def coro1():
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def coro2():
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
tasks = [coro1(), coro2()]
|
||||
asyncio.gather(*tasks) # RUF065
|
||||
|
||||
|
||||
# Non-violation cases: RUF065
|
||||
|
||||
|
||||
async def test_coroutine_with_await():
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
await coro() # OK
|
||||
|
||||
|
||||
async def test_coroutine_with_await():
|
||||
def not_coro():
|
||||
pass
|
||||
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
not_coro()
|
||||
await coro() # OK
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
# define an asynchronous context manager
|
||||
class AsyncContextManager:
|
||||
# enter the async context manager
|
||||
async def __aenter__(self):
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
|
||||
# define a simple coroutine
|
||||
async def custom_coroutine():
|
||||
# create and use the asynchronous context manager
|
||||
async with AsyncContextManager(): # OK
|
||||
...
|
||||
|
||||
|
||||
async def test_coroutine_in_func_arg():
|
||||
async def another_coro():
|
||||
pass
|
||||
|
||||
async def coro(cr):
|
||||
await cr
|
||||
|
||||
await coro(another_coro()) # OK
|
||||
|
||||
|
||||
async def test_coroutine_with_yield():
|
||||
async def another_coro():
|
||||
pass
|
||||
|
||||
async def coro():
|
||||
yield another_coro()
|
||||
|
||||
await coro() # OK
|
||||
|
||||
|
||||
async def test_coroutine_with_return():
|
||||
async def another_coro():
|
||||
pass
|
||||
|
||||
async def coro():
|
||||
return another_coro()
|
||||
|
||||
await coro() # OK
|
||||
|
||||
|
||||
async def test_coroutine_with_async_iterator():
|
||||
class Counter:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
pass
|
||||
|
||||
async def main():
|
||||
async for c in Counter(): # OK
|
||||
pass
|
||||
|
||||
|
||||
async def test_asyncio_api_with_await():
|
||||
async def task_coro(value):
|
||||
await asyncio.sleep(1)
|
||||
return value * 10
|
||||
|
||||
# main coroutine
|
||||
async def main():
|
||||
awaitables = [task_coro(i) for i in range(10)]
|
||||
await asyncio.gather(*awaitables) # OK
|
||||
|
||||
|
||||
async def test_coroutine_inside_collections():
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
[coro(), coro()] # OK
|
||||
(coro(), coro()) # OK
|
||||
{coro(), coro()} # OK
|
||||
{"coro": coro()} # OK
|
||||
|
||||
|
||||
async def test_func_used_in_arg_should_not_raise(func):
|
||||
func() # OK
|
||||
@@ -1297,9 +1297,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::NonOctalPermissions) {
|
||||
ruff::rules::non_octal_permissions(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::MissingAwaitForCoroutine) {
|
||||
ruff::rules::missing_await_for_coroutine(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::AssertRaisesException) {
|
||||
flake8_bugbear::rules::assert_raises_exception_call(checker, call);
|
||||
}
|
||||
|
||||
@@ -1051,7 +1051,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises),
|
||||
(Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict),
|
||||
(Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::NonOctalPermissions),
|
||||
(Ruff, "065") => (RuleGroup::Preview, rules::ruff::rules::MissingAwaitForCoroutine),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
|
||||
(Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode),
|
||||
|
||||
@@ -112,7 +112,6 @@ mod tests {
|
||||
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_warns.py"))]
|
||||
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_deprecated_call.py"))]
|
||||
#[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))]
|
||||
#[test_case(Rule::MissingAwaitForCoroutine, Path::new("RUF065.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
|
||||
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
use ruff_python_ast::{Expr, ExprCall, ExprName, Stmt, StmtAssign, StmtExpr, StmtFunctionDef};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for coroutines that are not awaited. This rule is only active in async contexts.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Coroutines are not executed until they are awaited. If a coroutine is not awaited, it will
|
||||
/// not be executed, and the program will not behave as expected. This is a common mistake when
|
||||
/// using `asyncio.sleep` instead of `await asyncio.sleep`.
|
||||
///
|
||||
/// Python's asyncio runtime will emit a warning when a coroutine is not awaited.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// async def foo():
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// async def bar():
|
||||
/// foo()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// async def foo():
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// async def bar():
|
||||
/// await foo()
|
||||
///
|
||||
/// ## Limitations
|
||||
///
|
||||
/// If the call is not a direct child of an statement expression or assignment statement
|
||||
/// then this rule may not reliably determine if await is missing. Functions that return
|
||||
/// coroutine objects or pass them as arguments might not be flagged correctly.
|
||||
///
|
||||
/// ```
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct MissingAwaitForCoroutine;
|
||||
|
||||
impl Violation for MissingAwaitForCoroutine {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Coroutine is not awaited".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Coroutine is not awaited".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF065
|
||||
pub(crate) fn missing_await_for_coroutine(checker: &Checker, call: &ExprCall) {
|
||||
// Only check for missing await in async context
|
||||
if !checker.semantic().in_async_context() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !possibly_missing_await(call, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If call does not originate from asyncio or is not an async function, then it's not awaitable
|
||||
if is_awaitable_from_asyncio(call.func.as_ref(), checker.semantic())
|
||||
|| is_awaitable_func(call.func.as_ref(), checker.semantic())
|
||||
{
|
||||
checker
|
||||
.report_diagnostic(MissingAwaitForCoroutine, call.range())
|
||||
.set_fix(Fix::unsafe_edit(Edit::insertion(
|
||||
"await ".to_string(),
|
||||
call.start(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
fn is_awaitable_from_asyncio(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
if let Some(call_path) = semantic.resolve_qualified_name(func) {
|
||||
return matches!(
|
||||
call_path.segments(),
|
||||
["asyncio", "sleep" | "wait" | "wait_for" | "gather"]
|
||||
);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_awaitable_func(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
let Expr::Name(ExprName { id, .. }) = func else {
|
||||
return false;
|
||||
};
|
||||
let Some(binding_id) = semantic.lookup_symbol(id) else {
|
||||
return false;
|
||||
};
|
||||
let binding = semantic.binding(binding_id);
|
||||
if let Some(node_id) = binding.source {
|
||||
let node = semantic.statement(node_id);
|
||||
if let Stmt::FunctionDef(StmtFunctionDef { is_async, name, .. }) = node {
|
||||
return *is_async && name.as_str() == id;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Try to detect possible scenarios where await is missing and ignore other cases
|
||||
/// If the call is not a direct child of an statement expression or assignment statement
|
||||
/// then this rule may not reliably determine if await is missing. Functions that return
|
||||
/// coroutine objects or pass them as arguments might not be flagged correctly.
|
||||
fn possibly_missing_await(call: &ExprCall, semantic: &SemanticModel) -> bool {
|
||||
if let Stmt::Expr(StmtExpr { value, .. }) = semantic.current_statement() {
|
||||
if let Expr::Call(expr_call) = value.as_ref() {
|
||||
return expr_call == call;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Stmt::Assign(StmtAssign { value, .. })) = semantic.current_statement_parent() {
|
||||
if let Expr::Call(expr_call) = value.as_ref() {
|
||||
return expr_call == call;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -24,7 +24,6 @@ pub(crate) use invalid_pyproject_toml::*;
|
||||
pub(crate) use invalid_rule_code::*;
|
||||
pub(crate) use legacy_form_pytest_raises::*;
|
||||
pub(crate) use map_int_version_parsing::*;
|
||||
pub(crate) use missing_await_for_coroutine::*;
|
||||
pub(crate) use missing_fstring_syntax::*;
|
||||
pub(crate) use mutable_class_default::*;
|
||||
pub(crate) use mutable_dataclass_default::*;
|
||||
@@ -88,7 +87,6 @@ mod invalid_pyproject_toml;
|
||||
mod invalid_rule_code;
|
||||
mod legacy_form_pytest_raises;
|
||||
mod map_int_version_parsing;
|
||||
mod missing_await_for_coroutine;
|
||||
mod missing_fstring_syntax;
|
||||
mod mutable_class_default;
|
||||
mod mutable_dataclass_default;
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:10:5
|
||||
|
|
||||
8 | pass
|
||||
9 |
|
||||
10 | coro() # RUF065
|
||||
| ^^^^^^
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
7 | async def coro():
|
||||
8 | pass
|
||||
9 |
|
||||
- coro() # RUF065
|
||||
10 + await coro() # RUF065
|
||||
11 |
|
||||
12 |
|
||||
13 | async def test_coroutine_without_await():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:28:5
|
||||
|
|
||||
27 | not_coro()
|
||||
28 | coro() # RUF065
|
||||
| ^^^^^^
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
25 | pass
|
||||
26 |
|
||||
27 | not_coro()
|
||||
- coro() # RUF065
|
||||
28 + await coro() # RUF065
|
||||
29 |
|
||||
30 |
|
||||
31 | async def test_coroutine_without_await():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:33:9
|
||||
|
|
||||
31 | async def test_coroutine_without_await():
|
||||
32 | async def coro():
|
||||
33 | another_coro() # RUF065
|
||||
| ^^^^^^^^^^^^^^
|
||||
34 |
|
||||
35 | async def another_coro():
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
30 |
|
||||
31 | async def test_coroutine_without_await():
|
||||
32 | async def coro():
|
||||
- another_coro() # RUF065
|
||||
33 + await another_coro() # RUF065
|
||||
34 |
|
||||
35 | async def another_coro():
|
||||
36 | pass
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:42:5
|
||||
|
|
||||
41 | async def test_asyncio_api_without_await():
|
||||
42 | asyncio.sleep(0.5) # RUF065
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
39 |
|
||||
40 |
|
||||
41 | async def test_asyncio_api_without_await():
|
||||
- asyncio.sleep(0.5) # RUF065
|
||||
42 + await asyncio.sleep(0.5) # RUF065
|
||||
43 |
|
||||
44 |
|
||||
45 | async def test_asyncio_api_without_await():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:47:9
|
||||
|
|
||||
45 | async def test_asyncio_api_without_await():
|
||||
46 | async def coro():
|
||||
47 | asyncio.sleep(0.5) # RUF065
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
48 |
|
||||
49 | await asyncio.wait(coro)
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
44 |
|
||||
45 | async def test_asyncio_api_without_await():
|
||||
46 | async def coro():
|
||||
- asyncio.sleep(0.5) # RUF065
|
||||
47 + await asyncio.sleep(0.5) # RUF065
|
||||
48 |
|
||||
49 | await asyncio.wait(coro)
|
||||
50 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:56:5
|
||||
|
|
||||
54 | await asyncio.sleep(0.5)
|
||||
55 |
|
||||
56 | asyncio.wait_for(coro) # RUF065
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
53 | async def coro():
|
||||
54 | await asyncio.sleep(0.5)
|
||||
55 |
|
||||
- asyncio.wait_for(coro) # RUF065
|
||||
56 + await asyncio.wait_for(coro) # RUF065
|
||||
57 |
|
||||
58 |
|
||||
59 | async def test_asyncio_api_without_await():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:67:5
|
||||
|
|
||||
66 | tasks = [coro1(), coro2()]
|
||||
67 | asyncio.gather(*tasks) # RUF065
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
64 | await asyncio.sleep(0.5)
|
||||
65 |
|
||||
66 | tasks = [coro1(), coro2()]
|
||||
- asyncio.gather(*tasks) # RUF065
|
||||
67 + await asyncio.gather(*tasks) # RUF065
|
||||
68 |
|
||||
69 |
|
||||
70 | # Non-violation cases: RUF065
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -79,6 +79,78 @@ b: tuple[int] = ("foo",)
|
||||
c: tuple[str | int, str] = ([], "foo")
|
||||
```
|
||||
|
||||
## Collection literal annotations are understood
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
a: list[int] = [1, 2, 3]
|
||||
reveal_type(a) # revealed: list[int]
|
||||
|
||||
b: list[int | str] = [1, 2, 3]
|
||||
reveal_type(b) # revealed: list[int | str]
|
||||
|
||||
c: typing.List[int] = [1, 2, 3]
|
||||
reveal_type(c) # revealed: list[int]
|
||||
|
||||
d: list[typing.Any] = []
|
||||
reveal_type(d) # revealed: list[Any]
|
||||
|
||||
e: set[int] = {1, 2, 3}
|
||||
reveal_type(e) # revealed: set[int]
|
||||
|
||||
f: set[int | str] = {1, 2, 3}
|
||||
reveal_type(f) # revealed: set[int | str]
|
||||
|
||||
g: typing.Set[int] = {1, 2, 3}
|
||||
reveal_type(g) # revealed: set[int]
|
||||
|
||||
h: list[list[int]] = [[], [42]]
|
||||
reveal_type(h) # revealed: list[list[int]]
|
||||
|
||||
i: list[typing.Any] = [1, 2, "3", ([4],)]
|
||||
reveal_type(i) # revealed: list[Any | int | str | tuple[list[Unknown | int]]]
|
||||
|
||||
j: list[tuple[str | int, ...]] = [(1, 2), ("foo", "bar"), ()]
|
||||
reveal_type(j) # revealed: list[tuple[str | int, ...]]
|
||||
|
||||
k: list[tuple[list[int], ...]] = [([],), ([1, 2], [3, 4]), ([5], [6], [7])]
|
||||
reveal_type(k) # revealed: list[tuple[list[int], ...]]
|
||||
|
||||
l: tuple[list[int], *tuple[list[typing.Any], ...], list[str]] = ([1, 2, 3], [4, 5, 6], [7, 8, 9], ["10", "11", "12"])
|
||||
reveal_type(l) # revealed: tuple[list[int], list[Any | int], list[Any | int], list[str]]
|
||||
|
||||
type IntList = list[int]
|
||||
|
||||
m: IntList = [1, 2, 3]
|
||||
reveal_type(m) # revealed: list[int]
|
||||
|
||||
# TODO: this should type-check and avoid literal promotion
|
||||
# error: [invalid-assignment] "Object of type `list[int]` is not assignable to `list[Literal[1, 2, 3]]`"
|
||||
n: list[typing.Literal[1, 2, 3]] = [1, 2, 3]
|
||||
reveal_type(n) # revealed: list[Literal[1, 2, 3]]
|
||||
|
||||
# TODO: this should type-check and avoid literal promotion
|
||||
# error: [invalid-assignment] "Object of type `list[str]` is not assignable to `list[LiteralString]`"
|
||||
o: list[typing.LiteralString] = ["a", "b", "c"]
|
||||
reveal_type(o) # revealed: list[LiteralString]
|
||||
```
|
||||
|
||||
## Incorrect collection literal assignments are complained aobut
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Object of type `list[int]` is not assignable to `list[str]`"
|
||||
a: list[str] = [1, 2, 3]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `set[int | str]` is not assignable to `set[int]`"
|
||||
b: set[int] = {1, 2, "3"}
|
||||
```
|
||||
|
||||
## PEP-604 annotations are supported
|
||||
|
||||
```py
|
||||
|
||||
@@ -46,7 +46,7 @@ def delete():
|
||||
del d # error: [unresolved-reference] "Name `d` used when not defined"
|
||||
|
||||
delete()
|
||||
reveal_type(d) # revealed: list[@Todo(list literal element type)]
|
||||
reveal_type(d) # revealed: list[Unknown | int]
|
||||
|
||||
def delete_element():
|
||||
# When the `del` target isn't a name, it doesn't force local resolution.
|
||||
@@ -62,7 +62,7 @@ def delete_global():
|
||||
|
||||
delete_global()
|
||||
# Again, the variable should have been removed, but we don't check it.
|
||||
reveal_type(d) # revealed: list[@Todo(list literal element type)]
|
||||
reveal_type(d) # revealed: list[Unknown | int]
|
||||
|
||||
def delete_nonlocal():
|
||||
e = 2
|
||||
|
||||
@@ -74,25 +74,52 @@ from typing import Any as Any, Literal as Literal
|
||||
|
||||
Here, none of the symbols are being re-exported in the stub file.
|
||||
|
||||
In this case the symbols shouldn't be available as imports or attributes.
|
||||
|
||||
```py
|
||||
# error: 15 [unresolved-import] "Module `b` has no member `foo`"
|
||||
# error: 20 [unresolved-import] "Module `b` has no member `Any`"
|
||||
# error: 25 [unresolved-import] "Module `b` has no member `Literal`"
|
||||
from b import foo, Any, Literal
|
||||
from a import b
|
||||
|
||||
# error: [unresolved-attribute] "no attribute `Any`"
|
||||
reveal_type(b.Any) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "no attribute `Literal`"
|
||||
reveal_type(b.Literal) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "no attribute `foo`"
|
||||
reveal_type(b.foo) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "no attribute `bar`"
|
||||
reveal_type(b.bar) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-import] "Module `a.b` has no member `foo`"
|
||||
# error: [unresolved-import] "Module `a.b` has no member `bar`"
|
||||
# error: [unresolved-import] "Module `a.b` has no member `Any`"
|
||||
# error: [unresolved-import] "Module `a.b` has no member `Literal`"
|
||||
from a.b import foo, bar, Any, Literal
|
||||
|
||||
reveal_type(Any) # revealed: Unknown
|
||||
reveal_type(Literal) # revealed: Unknown
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
reveal_type(bar) # revealed: Unknown
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
`a/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
import foo
|
||||
```
|
||||
|
||||
`a/b.pyi`:
|
||||
|
||||
```pyi
|
||||
import a.foo
|
||||
from . import bar
|
||||
from typing import Any, Literal
|
||||
```
|
||||
|
||||
`foo.pyi`:
|
||||
`a/foo.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
```
|
||||
|
||||
`a/bar.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
@@ -261,39 +288,93 @@ reveal_type(Foo) # revealed: Unknown
|
||||
|
||||
## Re-exports in `__init__.pyi`
|
||||
|
||||
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
|
||||
but the inference would be `Unknown`.
|
||||
Within `__init__.pyi` relative imports (`from . import xyz` or `from .pub import xyz`) are also
|
||||
treated as a re-exports.
|
||||
|
||||
We check the both the members of the module and the imports of the module as you _should_ be able to
|
||||
do `from a import priv` but the attribute `a.priv` _should not_ exist.
|
||||
|
||||
The most subtle detail here is whether `from .semipriv import Pub` should make the `a.semipriv`
|
||||
attribute exist or not. We do not currently do this, although perhaps we should.
|
||||
|
||||
```py
|
||||
# error: 15 "Module `a` has no member `Foo`"
|
||||
# error: 20 "Module `a` has no member `c`"
|
||||
from a import Foo, c, foo
|
||||
import a
|
||||
|
||||
reveal_type(Foo) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(foo) # revealed: <module 'a.foo'>
|
||||
reveal_type(a.Pub) # revealed: <class 'Pub'>
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a.Priv) # revealed: Unknown
|
||||
reveal_type(a.pub) # revealed: <module 'a.pub'>
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a.priv) # revealed: Unknown
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a.semipriv) # revealed: Unknown
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a.sub) # revealed: Unknown
|
||||
reveal_type(a.subpub) # revealed: <module 'a.sub.subpub'>
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a.subpriv) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-import] "Priv"
|
||||
from a import Pub, Priv
|
||||
|
||||
# error: [unresolved-import] "subpriv"
|
||||
from a import pub, priv, semipriv, sub, subpub, subpriv
|
||||
|
||||
reveal_type(Pub) # revealed: <class 'Pub'>
|
||||
reveal_type(Priv) # revealed: Unknown
|
||||
reveal_type(pub) # revealed: <module 'a.pub'>
|
||||
reveal_type(priv) # revealed: <module 'a.priv'>
|
||||
reveal_type(semipriv) # revealed: <module 'a.semipriv'>
|
||||
reveal_type(sub) # revealed: <module 'a.sub'>
|
||||
reveal_type(subpub) # revealed: <module 'a.sub.subpub'>
|
||||
reveal_type(subpriv) # revealed: Unknown
|
||||
```
|
||||
|
||||
`a/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
from .b import c
|
||||
from .foo import Foo
|
||||
# re-exported because they're relative
|
||||
from .sub import subpub
|
||||
from .semipriv import Pub
|
||||
from . import pub
|
||||
|
||||
# not re-exported because they're absolute
|
||||
from a.sub import subpriv
|
||||
from a.semipriv import Priv
|
||||
from a import priv
|
||||
```
|
||||
|
||||
`a/foo.pyi`:
|
||||
`a/pub.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
`a/b/__init__.pyi`:
|
||||
`a/priv.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`a/semipriv.pyi`:
|
||||
|
||||
```pyi
|
||||
class Pub: ...
|
||||
|
||||
class Priv: ...
|
||||
```
|
||||
|
||||
`a/sub/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
```
|
||||
|
||||
`a/b/c.pyi`:
|
||||
`a/sub/subpub.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
```
|
||||
|
||||
`a/sub/subpriv.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
|
||||
@@ -783,9 +783,8 @@ class A: ...
|
||||
```py
|
||||
from subexporter import *
|
||||
|
||||
# TODO: Should be `list[str]`
|
||||
# TODO: Should we avoid including `Unknown` for this case?
|
||||
reveal_type(__all__) # revealed: Unknown | list[@Todo(list literal element type)]
|
||||
reveal_type(__all__) # revealed: Unknown | list[Unknown | str]
|
||||
|
||||
__all__.append("B")
|
||||
|
||||
|
||||
@@ -3,7 +3,33 @@
|
||||
## Empty list
|
||||
|
||||
```py
|
||||
reveal_type([]) # revealed: list[@Todo(list literal element type)]
|
||||
reveal_type([]) # revealed: list[Unknown]
|
||||
```
|
||||
|
||||
## List of tuples
|
||||
|
||||
```py
|
||||
reveal_type([(1, 2), (3, 4)]) # revealed: list[Unknown | tuple[int, int]]
|
||||
```
|
||||
|
||||
## List of functions
|
||||
|
||||
```py
|
||||
def a(_: int) -> int:
|
||||
return 0
|
||||
|
||||
def b(_: int) -> int:
|
||||
return 1
|
||||
|
||||
x = [a, b]
|
||||
reveal_type(x) # revealed: list[Unknown | ((_: int) -> int)]
|
||||
```
|
||||
|
||||
## Mixed list
|
||||
|
||||
```py
|
||||
# revealed: list[Unknown | int | tuple[int, int] | tuple[int, int, int]]
|
||||
reveal_type([1, (1, 2), (1, 2, 3)])
|
||||
```
|
||||
|
||||
## List comprehensions
|
||||
|
||||
@@ -3,7 +3,33 @@
|
||||
## Basic set
|
||||
|
||||
```py
|
||||
reveal_type({1, 2}) # revealed: set[@Todo(set literal element type)]
|
||||
reveal_type({1, 2}) # revealed: set[Unknown | int]
|
||||
```
|
||||
|
||||
## Set of tuples
|
||||
|
||||
```py
|
||||
reveal_type({(1, 2), (3, 4)}) # revealed: set[Unknown | tuple[int, int]]
|
||||
```
|
||||
|
||||
## Set of functions
|
||||
|
||||
```py
|
||||
def a(_: int) -> int:
|
||||
return 0
|
||||
|
||||
def b(_: int) -> int:
|
||||
return 1
|
||||
|
||||
x = {a, b}
|
||||
reveal_type(x) # revealed: set[Unknown | ((_: int) -> int)]
|
||||
```
|
||||
|
||||
## Mixed set
|
||||
|
||||
```py
|
||||
# revealed: set[Unknown | int | tuple[int, int] | tuple[int, int, int]]
|
||||
reveal_type({1, (1, 2), (1, 2, 3)})
|
||||
```
|
||||
|
||||
## Set comprehensions
|
||||
|
||||
@@ -310,17 +310,13 @@ no longer valid in the inner lazy scope.
|
||||
def f(l: list[str | None]):
|
||||
if l[0] is not None:
|
||||
def _():
|
||||
# TODO: should be `str | None`
|
||||
reveal_type(l[0]) # revealed: str | None | @Todo(list literal element type)
|
||||
# TODO: should be of type `list[None]`
|
||||
reveal_type(l[0]) # revealed: str | None | Unknown
|
||||
l = [None]
|
||||
|
||||
def f(l: list[str | None]):
|
||||
l[0] = "a"
|
||||
def _():
|
||||
# TODO: should be `str | None`
|
||||
reveal_type(l[0]) # revealed: str | None | @Todo(list literal element type)
|
||||
# TODO: should be of type `list[None]`
|
||||
reveal_type(l[0]) # revealed: str | None | Unknown
|
||||
l = [None]
|
||||
|
||||
def f(l: list[str | None]):
|
||||
@@ -328,8 +324,7 @@ def f(l: list[str | None]):
|
||||
def _():
|
||||
l: list[str | None] = [None]
|
||||
def _():
|
||||
# TODO: should be `str | None`
|
||||
reveal_type(l[0]) # revealed: @Todo(list literal element type)
|
||||
reveal_type(l[0]) # revealed: str | None
|
||||
|
||||
def _():
|
||||
def _():
|
||||
|
||||
@@ -9,13 +9,11 @@ A list can be indexed into with:
|
||||
|
||||
```py
|
||||
x = [1, 2, 3]
|
||||
reveal_type(x) # revealed: list[@Todo(list literal element type)]
|
||||
reveal_type(x) # revealed: list[Unknown | int]
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(x[0]) # revealed: @Todo(list literal element type)
|
||||
reveal_type(x[0]) # revealed: Unknown | int
|
||||
|
||||
# TODO reveal list[int]
|
||||
reveal_type(x[0:1]) # revealed: list[@Todo(list literal element type)]
|
||||
reveal_type(x[0:1]) # revealed: list[Unknown | int]
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(x["a"]) # revealed: Unknown
|
||||
|
||||
@@ -55,8 +55,7 @@ def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never], bb: LiskovUncom
|
||||
|
||||
reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]]
|
||||
|
||||
# TODO: should be `tuple[Literal[1], ...]`
|
||||
reveal_type(tuple([1])) # revealed: tuple[@Todo(list literal element type), ...]
|
||||
reveal_type(tuple([1])) # revealed: tuple[Unknown | int, ...]
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(tuple[int]([1])) # revealed: tuple[int]
|
||||
|
||||
@@ -213,9 +213,8 @@ reveal_type(d) # revealed: Literal[2]
|
||||
|
||||
```py
|
||||
a, b = [1, 2]
|
||||
# TODO: should be `int` for both `a` and `b`
|
||||
reveal_type(a) # revealed: @Todo(list literal element type)
|
||||
reveal_type(b) # revealed: @Todo(list literal element type)
|
||||
reveal_type(a) # revealed: Unknown | int
|
||||
reveal_type(b) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
### Simple unpacking
|
||||
|
||||
@@ -331,7 +331,11 @@ pub(crate) fn imported_symbol<'db>(
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| {
|
||||
if file.is_stub(db) {
|
||||
RequiresExplicitReExport::Yes
|
||||
if file.is_init(db) {
|
||||
RequiresExplicitReExport::YesButInitIdiomAllowed
|
||||
} else {
|
||||
RequiresExplicitReExport::Yes
|
||||
}
|
||||
} else {
|
||||
RequiresExplicitReExport::No
|
||||
}
|
||||
@@ -932,7 +936,8 @@ fn place_from_bindings_impl<'db>(
|
||||
let mut bindings_with_constraints = bindings_with_constraints.peekable();
|
||||
|
||||
let is_non_exported = |binding: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !is_reexported(db, binding)
|
||||
requires_explicit_reexport.is_yes()
|
||||
&& !requires_explicit_reexport.is_satisfied(is_reexported(db, binding))
|
||||
};
|
||||
|
||||
let unbound_reachability_constraint = match bindings_with_constraints.peek() {
|
||||
@@ -1209,7 +1214,8 @@ fn place_from_declarations_impl<'db>(
|
||||
let mut exactly_one_declaration = false;
|
||||
|
||||
let is_non_exported = |declaration: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !is_reexported(db, declaration)
|
||||
requires_explicit_reexport.is_yes()
|
||||
&& !requires_explicit_reexport.is_satisfied(is_reexported(db, declaration))
|
||||
};
|
||||
|
||||
let undeclared_reachability = match declarations.peek() {
|
||||
@@ -1320,21 +1326,26 @@ fn place_from_declarations_impl<'db>(
|
||||
// This will first check if the definition is using the "redundant alias" pattern like `import foo
|
||||
// as foo` or `from foo import bar as bar`. If it's not, it will check whether the symbol is being
|
||||
// exported via `__all__`.
|
||||
fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool {
|
||||
fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> ReExportKind {
|
||||
// This information is computed by the semantic index builder.
|
||||
if definition.is_reexported(db) {
|
||||
return true;
|
||||
let reexported = definition.is_reexported(db);
|
||||
if reexported != ReExportKind::No {
|
||||
return reexported;
|
||||
}
|
||||
// At this point, the definition should either be an `import` or `from ... import` statement.
|
||||
// This is because the default value of `is_reexported` is `true` for any other kind of
|
||||
// definition.
|
||||
let Some(all_names) = dunder_all_names(db, definition.file(db)) else {
|
||||
return false;
|
||||
return ReExportKind::No;
|
||||
};
|
||||
let table = place_table(db, definition.scope(db));
|
||||
let symbol_id = definition.place(db).expect_symbol();
|
||||
let symbol_name = table.symbol(symbol_id).name();
|
||||
all_names.contains(symbol_name)
|
||||
if all_names.contains(symbol_name) {
|
||||
ReExportKind::Yes
|
||||
} else {
|
||||
ReExportKind::No
|
||||
}
|
||||
}
|
||||
|
||||
mod implicit_globals {
|
||||
@@ -1500,13 +1511,35 @@ mod implicit_globals {
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum RequiresExplicitReExport {
|
||||
Yes,
|
||||
No,
|
||||
/// This is an `__init__.pyi` and `from . import b` is considered a re-export
|
||||
YesButInitIdiomAllowed,
|
||||
Yes,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ReExportKind {
|
||||
No,
|
||||
/// `b` in `from . import b`
|
||||
InitIdiom,
|
||||
Yes,
|
||||
}
|
||||
|
||||
impl get_size2::GetSize for ReExportKind {}
|
||||
|
||||
impl RequiresExplicitReExport {
|
||||
/// Whether re-exports are necessary at all (this is really "is not No")
|
||||
const fn is_yes(self) -> bool {
|
||||
matches!(self, RequiresExplicitReExport::Yes)
|
||||
!matches!(self, RequiresExplicitReExport::No)
|
||||
}
|
||||
|
||||
/// Whether the style of re-export is sufficient for the context
|
||||
fn is_satisfied(self, reexport: ReExportKind) -> bool {
|
||||
match self {
|
||||
RequiresExplicitReExport::No => true,
|
||||
RequiresExplicitReExport::YesButInitIdiomAllowed => reexport != ReExportKind::No,
|
||||
RequiresExplicitReExport::Yes => reexport == ReExportKind::Yes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::place::ReExportKind;
|
||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::definition::{
|
||||
@@ -1436,6 +1437,12 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
(Name::new(alias.name.id.split('.').next().unwrap()), false)
|
||||
};
|
||||
|
||||
let is_reexported = if is_reexported {
|
||||
ReExportKind::Yes
|
||||
} else {
|
||||
ReExportKind::No
|
||||
};
|
||||
|
||||
let symbol = self.add_symbol(symbol_name);
|
||||
self.add_definition(
|
||||
symbol.into(),
|
||||
@@ -1562,6 +1569,15 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
(&alias.name.id, false)
|
||||
};
|
||||
|
||||
let is_reexported = if is_reexported {
|
||||
ReExportKind::Yes
|
||||
} else if node.level == 1 {
|
||||
// `from . import a`
|
||||
ReExportKind::InitIdiom
|
||||
} else {
|
||||
ReExportKind::No
|
||||
};
|
||||
|
||||
// Look for imports `from __future__ import annotations`, ignore `as ...`
|
||||
// We intentionally don't enforce the rules about location of `__future__`
|
||||
// imports here, we assume the user's intent was to apply the `__future__`
|
||||
|
||||
@@ -8,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
use crate::Db;
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::place::ReExportKind;
|
||||
use crate::semantic_index::place::ScopedPlaceId;
|
||||
use crate::semantic_index::scope::{FileScopeId, ScopeId};
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
@@ -41,7 +42,7 @@ pub struct Definition<'db> {
|
||||
pub kind: DefinitionKind<'db>,
|
||||
|
||||
/// This is a dedicated field to avoid accessing `kind` to compute this value.
|
||||
pub(crate) is_reexported: bool,
|
||||
pub(crate) is_reexported: ReExportKind,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
@@ -337,7 +338,7 @@ impl<'ast> From<StarImportDefinitionNodeRef<'ast>> for DefinitionNodeRef<'ast, '
|
||||
pub(crate) struct ImportDefinitionNodeRef<'ast> {
|
||||
pub(crate) node: &'ast ast::StmtImport,
|
||||
pub(crate) alias_index: usize,
|
||||
pub(crate) is_reexported: bool,
|
||||
pub(crate) is_reexported: ReExportKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -350,7 +351,7 @@ pub(crate) struct StarImportDefinitionNodeRef<'ast> {
|
||||
pub(crate) struct ImportFromDefinitionNodeRef<'ast> {
|
||||
pub(crate) node: &'ast ast::StmtImportFrom,
|
||||
pub(crate) alias_index: usize,
|
||||
pub(crate) is_reexported: bool,
|
||||
pub(crate) is_reexported: ReExportKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -678,11 +679,11 @@ pub enum DefinitionKind<'db> {
|
||||
}
|
||||
|
||||
impl DefinitionKind<'_> {
|
||||
pub(crate) fn is_reexported(&self) -> bool {
|
||||
pub(crate) fn is_reexported(&self) -> ReExportKind {
|
||||
match self {
|
||||
DefinitionKind::Import(import) => import.is_reexported(),
|
||||
DefinitionKind::ImportFrom(import) => import.is_reexported(),
|
||||
_ => true,
|
||||
_ => ReExportKind::Yes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,7 +957,7 @@ impl<'db> ComprehensionDefinitionKind<'db> {
|
||||
pub struct ImportDefinitionKind {
|
||||
node: AstNodeRef<ast::StmtImport>,
|
||||
alias_index: usize,
|
||||
is_reexported: bool,
|
||||
is_reexported: ReExportKind,
|
||||
}
|
||||
|
||||
impl ImportDefinitionKind {
|
||||
@@ -968,7 +969,7 @@ impl ImportDefinitionKind {
|
||||
&self.node.node(module).names[self.alias_index]
|
||||
}
|
||||
|
||||
pub(crate) fn is_reexported(&self) -> bool {
|
||||
pub(crate) fn is_reexported(&self) -> ReExportKind {
|
||||
self.is_reexported
|
||||
}
|
||||
}
|
||||
@@ -977,7 +978,7 @@ impl ImportDefinitionKind {
|
||||
pub struct ImportFromDefinitionKind {
|
||||
node: AstNodeRef<ast::StmtImportFrom>,
|
||||
alias_index: usize,
|
||||
is_reexported: bool,
|
||||
is_reexported: ReExportKind,
|
||||
}
|
||||
|
||||
impl ImportFromDefinitionKind {
|
||||
@@ -989,7 +990,7 @@ impl ImportFromDefinitionKind {
|
||||
&self.node.node(module).names[self.alias_index]
|
||||
}
|
||||
|
||||
pub(crate) fn is_reexported(&self) -> bool {
|
||||
pub(crate) fn is_reexported(&self) -> ReExportKind {
|
||||
self.is_reexported
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1130,11 +1130,30 @@ impl<'db> Type<'db> {
|
||||
Type::IntLiteral(_) => Some(KnownClass::Int.to_instance(db)),
|
||||
Type::BytesLiteral(_) => Some(KnownClass::Bytes.to_instance(db)),
|
||||
Type::ModuleLiteral(_) => Some(KnownClass::ModuleType.to_instance(db)),
|
||||
Type::FunctionLiteral(_) => Some(KnownClass::FunctionType.to_instance(db)),
|
||||
Type::EnumLiteral(literal) => Some(literal.enum_class_instance(db)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// If this type is a literal, promote it to a type that this literal is an instance of.
|
||||
///
|
||||
/// Note that this function tries to promote literals to a more user-friendly form than their
|
||||
/// fallback instance type. For example, `def _() -> int` is promoted to `Callable[[], int]`,
|
||||
/// as opposed to `FunctionType`.
|
||||
pub(crate) fn literal_promotion_type(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Type::StringLiteral(_) | Type::LiteralString => Some(KnownClass::Str.to_instance(db)),
|
||||
Type::BooleanLiteral(_) => Some(KnownClass::Bool.to_instance(db)),
|
||||
Type::IntLiteral(_) => Some(KnownClass::Int.to_instance(db)),
|
||||
Type::BytesLiteral(_) => Some(KnownClass::Bytes.to_instance(db)),
|
||||
Type::ModuleLiteral(_) => Some(KnownClass::ModuleType.to_instance(db)),
|
||||
Type::EnumLiteral(literal) => Some(literal.enum_class_instance(db)),
|
||||
Type::FunctionLiteral(literal) => Some(Type::Callable(literal.into_callable_type(db))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a "normalized" version of `self` that ensures that equivalent types have the same Salsa ID.
|
||||
///
|
||||
/// A normalized type:
|
||||
@@ -1704,18 +1723,13 @@ impl<'db> Type<'db> {
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::EnumLiteral(_),
|
||||
| Type::EnumLiteral(_)
|
||||
| Type::FunctionLiteral(_),
|
||||
_,
|
||||
) => (self.literal_fallback_instance(db)).when_some_and(|instance| {
|
||||
instance.has_relation_to_impl(db, target, relation, visitor)
|
||||
}),
|
||||
|
||||
// A `FunctionLiteral` type is a single-valued type like the other literals handled above,
|
||||
// so it also, for now, just delegates to its instance fallback.
|
||||
(Type::FunctionLiteral(_), _) => KnownClass::FunctionType
|
||||
.to_instance(db)
|
||||
.has_relation_to_impl(db, target, relation, visitor),
|
||||
|
||||
// The same reasoning applies for these special callable types:
|
||||
(Type::BoundMethod(_), _) => KnownClass::MethodType
|
||||
.to_instance(db)
|
||||
@@ -5979,8 +5993,9 @@ impl<'db> Type<'db> {
|
||||
self
|
||||
}
|
||||
}
|
||||
TypeMapping::PromoteLiterals | TypeMapping::BindLegacyTypevars(_) |
|
||||
TypeMapping::MarkTypeVarsInferable(_) => self,
|
||||
TypeMapping::PromoteLiterals
|
||||
| TypeMapping::BindLegacyTypevars(_)
|
||||
| TypeMapping::MarkTypeVarsInferable(_) => self,
|
||||
TypeMapping::Materialize(materialization_kind) => {
|
||||
Type::TypeVar(bound_typevar.materialize_impl(db, *materialization_kind, visitor))
|
||||
}
|
||||
@@ -6000,10 +6015,10 @@ impl<'db> Type<'db> {
|
||||
self
|
||||
}
|
||||
}
|
||||
TypeMapping::PromoteLiterals |
|
||||
TypeMapping::BindLegacyTypevars(_) |
|
||||
TypeMapping::BindSelf(_) |
|
||||
TypeMapping::ReplaceSelf { .. }
|
||||
TypeMapping::PromoteLiterals
|
||||
| TypeMapping::BindLegacyTypevars(_)
|
||||
| TypeMapping::BindSelf(_)
|
||||
| TypeMapping::ReplaceSelf { .. }
|
||||
=> self,
|
||||
TypeMapping::Materialize(materialization_kind) => Type::NonInferableTypeVar(bound_typevar.materialize_impl(db, *materialization_kind, visitor))
|
||||
|
||||
@@ -6023,7 +6038,13 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
|
||||
Type::FunctionLiteral(function) => {
|
||||
Type::FunctionLiteral(function.with_type_mapping(db, type_mapping))
|
||||
let function = Type::FunctionLiteral(function.with_type_mapping(db, type_mapping));
|
||||
|
||||
match type_mapping {
|
||||
TypeMapping::PromoteLiterals => function.literal_promotion_type(db)
|
||||
.expect("function literal should have a promotion type"),
|
||||
_ => function
|
||||
}
|
||||
}
|
||||
|
||||
Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new(
|
||||
@@ -6129,8 +6150,8 @@ impl<'db> Type<'db> {
|
||||
TypeMapping::ReplaceSelf { .. } |
|
||||
TypeMapping::MarkTypeVarsInferable(_) |
|
||||
TypeMapping::Materialize(_) => self,
|
||||
TypeMapping::PromoteLiterals => self.literal_fallback_instance(db)
|
||||
.expect("literal type should have fallback instance type"),
|
||||
TypeMapping::PromoteLiterals => self.literal_promotion_type(db)
|
||||
.expect("literal type should have a promotion type"),
|
||||
}
|
||||
|
||||
Type::Dynamic(_) => match type_mapping {
|
||||
@@ -6663,8 +6684,8 @@ pub enum TypeMapping<'a, 'db> {
|
||||
Specialization(Specialization<'db>),
|
||||
/// Applies a partial specialization to the type
|
||||
PartialSpecialization(PartialSpecialization<'a, 'db>),
|
||||
/// Promotes any literal types to their corresponding instance types (e.g. `Literal["string"]`
|
||||
/// to `str`)
|
||||
/// Replaces any literal types with their corresponding promoted type form (e.g. `Literal["string"]`
|
||||
/// to `str`, or `def _() -> int` to `Callable[[], int]`).
|
||||
PromoteLiterals,
|
||||
/// Binds a legacy typevar with the generic context (class, function, type alias) that it is
|
||||
/// being used in.
|
||||
|
||||
@@ -1048,7 +1048,7 @@ impl<'db> ClassType<'db> {
|
||||
|
||||
/// Return a callable type (or union of callable types) that represents the callable
|
||||
/// constructor signature of this class.
|
||||
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
|
||||
#[salsa::tracked(cycle_fn=into_callable_cycle_recover, cycle_initial=into_callable_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
|
||||
pub(super) fn into_callable(self, db: &'db dyn Db) -> Type<'db> {
|
||||
let self_ty = Type::from(self);
|
||||
let metaclass_dunder_call_function_symbol = self_ty
|
||||
@@ -1208,6 +1208,20 @@ impl<'db> ClassType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn into_callable_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Type<'db>,
|
||||
_count: u32,
|
||||
_self: ClassType<'db>,
|
||||
) -> salsa::CycleRecoveryAction<Type<'db>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn into_callable_cycle_initial<'db>(_db: &'db dyn Db, _self: ClassType<'db>) -> Type<'db> {
|
||||
Type::Never
|
||||
}
|
||||
|
||||
impl<'db> From<GenericAlias<'db>> for ClassType<'db> {
|
||||
fn from(generic: GenericAlias<'db>) -> ClassType<'db> {
|
||||
ClassType::Generic(generic)
|
||||
|
||||
@@ -2626,7 +2626,7 @@ pub(crate) fn report_undeclared_protocol_member(
|
||||
let binding_type = binding_type(db, definition);
|
||||
|
||||
let suggestion = binding_type
|
||||
.literal_fallback_instance(db)
|
||||
.literal_promotion_type(db)
|
||||
.unwrap_or(binding_type);
|
||||
|
||||
if should_give_hint(db, suggestion) {
|
||||
|
||||
@@ -1081,16 +1081,13 @@ fn is_instance_truthiness<'db>(
|
||||
| Type::StringLiteral(..)
|
||||
| Type::LiteralString
|
||||
| Type::ModuleLiteral(..)
|
||||
| Type::EnumLiteral(..) => always_true_if(
|
||||
| Type::EnumLiteral(..)
|
||||
| Type::FunctionLiteral(..) => always_true_if(
|
||||
ty.literal_fallback_instance(db)
|
||||
.as_ref()
|
||||
.is_some_and(is_instance),
|
||||
),
|
||||
|
||||
Type::FunctionLiteral(..) => {
|
||||
always_true_if(is_instance(&KnownClass::FunctionType.to_instance(db)))
|
||||
}
|
||||
|
||||
Type::ClassLiteral(..) => always_true_if(is_instance(&KnownClass::Type.to_instance(db))),
|
||||
|
||||
Type::TypeAlias(alias) => is_instance_truthiness(db, alias.value_type(db), class),
|
||||
|
||||
@@ -49,8 +49,9 @@ use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::scope::ScopeId;
|
||||
use crate::semantic_index::{SemanticIndex, semantic_index};
|
||||
use crate::types::diagnostic::TypeCheckDiagnostics;
|
||||
use crate::types::generics::Specialization;
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{ClassLiteral, Truthiness, Type, TypeAndQualifiers};
|
||||
use crate::types::{ClassLiteral, KnownClass, Truthiness, Type, TypeAndQualifiers};
|
||||
use crate::unpack::Unpack;
|
||||
use builder::TypeInferenceBuilder;
|
||||
|
||||
@@ -355,10 +356,31 @@ pub(crate) struct TypeContext<'db> {
|
||||
}
|
||||
|
||||
impl<'db> TypeContext<'db> {
|
||||
pub(crate) fn new(annotation: Type<'db>) -> Self {
|
||||
Self {
|
||||
annotation: Some(annotation),
|
||||
pub(crate) fn new(annotation: Option<Type<'db>>) -> Self {
|
||||
Self { annotation }
|
||||
}
|
||||
|
||||
// If the type annotation is a specialized instance of the given `KnownClass`, returns the
|
||||
// specialization.
|
||||
fn known_specialization(
|
||||
&self,
|
||||
known_class: KnownClass,
|
||||
db: &'db dyn Db,
|
||||
) -> Option<Specialization<'db>> {
|
||||
let class_type = match self.annotation? {
|
||||
Type::NominalInstance(instance) => instance,
|
||||
Type::TypeAlias(alias) => alias.value_type(db).into_nominal_instance()?,
|
||||
_ => return None,
|
||||
}
|
||||
.class(db);
|
||||
|
||||
if !class_type.is_known(db, known_class) {
|
||||
return None;
|
||||
}
|
||||
|
||||
class_type
|
||||
.into_generic_alias()
|
||||
.map(|generic_alias| generic_alias.specialization(db))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,13 +73,13 @@ use crate::types::diagnostic::{
|
||||
use crate::types::function::{
|
||||
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
|
||||
};
|
||||
use crate::types::generics::LegacyGenericBase;
|
||||
use crate::types::generics::{GenericContext, bind_typevar};
|
||||
use crate::types::generics::{LegacyGenericBase, SpecializationBuilder};
|
||||
use crate::types::instance::SliceLiteral;
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::signatures::Signature;
|
||||
use crate::types::subclass_of::SubclassOfInner;
|
||||
use crate::types::tuple::{Tuple, TupleSpec, TupleType};
|
||||
use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleType};
|
||||
use crate::types::typed_dict::{
|
||||
TypedDictAssignmentKind, validate_typed_dict_constructor, validate_typed_dict_dict_literal,
|
||||
validate_typed_dict_key_assignment,
|
||||
@@ -90,8 +90,9 @@ use crate::types::{
|
||||
IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, MemberLookupPolicy,
|
||||
MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType,
|
||||
SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers,
|
||||
TypeContext, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation,
|
||||
TypeVarInstance, TypeVarKind, UnionBuilder, UnionType, binding_type, todo_type,
|
||||
TypeContext, TypeMapping, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation,
|
||||
TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind, UnionBuilder, UnionType, binding_type,
|
||||
todo_type,
|
||||
};
|
||||
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
|
||||
use crate::unpack::{EvaluationMode, UnpackPosition};
|
||||
@@ -4008,7 +4009,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
if let Some(value) = value {
|
||||
self.infer_maybe_standalone_expression(
|
||||
value,
|
||||
TypeContext::new(annotated.inner_type()),
|
||||
TypeContext::new(Some(annotated.inner_type())),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4101,8 +4102,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
debug_assert!(PlaceExpr::try_from_expr(target).is_some());
|
||||
|
||||
if let Some(value) = value {
|
||||
let inferred_ty = self
|
||||
.infer_maybe_standalone_expression(value, TypeContext::new(declared.inner_type()));
|
||||
let inferred_ty = self.infer_maybe_standalone_expression(
|
||||
value,
|
||||
TypeContext::new(Some(declared.inner_type())),
|
||||
);
|
||||
let mut inferred_ty = if target
|
||||
.as_name_expr()
|
||||
.is_some_and(|name| &name.id == "TYPE_CHECKING")
|
||||
@@ -5236,7 +5239,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
fn infer_tuple_expression(
|
||||
&mut self,
|
||||
tuple: &ast::ExprTuple,
|
||||
_tcx: TypeContext<'db>,
|
||||
tcx: TypeContext<'db>,
|
||||
) -> Type<'db> {
|
||||
let ast::ExprTuple {
|
||||
range: _,
|
||||
@@ -5246,11 +5249,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
parenthesized: _,
|
||||
} = tuple;
|
||||
|
||||
let annotated_tuple = tcx
|
||||
.known_specialization(KnownClass::Tuple, self.db())
|
||||
.and_then(|specialization| {
|
||||
specialization
|
||||
.tuple(self.db())
|
||||
.expect("the specialization of `KnownClass::Tuple` must have a tuple spec")
|
||||
.resize(self.db(), TupleLength::Fixed(elts.len()))
|
||||
.ok()
|
||||
});
|
||||
|
||||
let mut annotated_elt_tys = annotated_tuple.as_ref().map(Tuple::all_elements);
|
||||
|
||||
let db = self.db();
|
||||
let divergent = Type::divergent(self.scope());
|
||||
let element_types = elts.iter().map(|element| {
|
||||
// TODO: Use the type context for more precise inference.
|
||||
let element_type = self.infer_expression(element, TypeContext::default());
|
||||
let annotated_elt_ty = annotated_elt_tys.as_mut().and_then(Iterator::next).copied();
|
||||
let element_type = self.infer_expression(element, TypeContext::new(annotated_elt_ty));
|
||||
|
||||
if element_type.has_divergent_type(self.db(), divergent) {
|
||||
divergent
|
||||
} else {
|
||||
@@ -5261,7 +5277,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
Type::heterogeneous_tuple(db, element_types)
|
||||
}
|
||||
|
||||
fn infer_list_expression(&mut self, list: &ast::ExprList, _tcx: TypeContext<'db>) -> Type<'db> {
|
||||
fn infer_list_expression(&mut self, list: &ast::ExprList, tcx: TypeContext<'db>) -> Type<'db> {
|
||||
let ast::ExprList {
|
||||
range: _,
|
||||
node_index: _,
|
||||
@@ -5269,28 +5285,102 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
ctx: _,
|
||||
} = list;
|
||||
|
||||
// TODO: Use the type context for more precise inference.
|
||||
for elt in elts {
|
||||
self.infer_expression(elt, TypeContext::default());
|
||||
}
|
||||
|
||||
KnownClass::List
|
||||
.to_specialized_instance(self.db(), [todo_type!("list literal element type")])
|
||||
self.infer_collection_literal(elts, tcx, KnownClass::List)
|
||||
.unwrap_or_else(|| {
|
||||
KnownClass::List.to_specialized_instance(self.db(), [Type::unknown()])
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_set_expression(&mut self, set: &ast::ExprSet, _tcx: TypeContext<'db>) -> Type<'db> {
|
||||
fn infer_set_expression(&mut self, set: &ast::ExprSet, tcx: TypeContext<'db>) -> Type<'db> {
|
||||
let ast::ExprSet {
|
||||
range: _,
|
||||
node_index: _,
|
||||
elts,
|
||||
} = set;
|
||||
|
||||
// TODO: Use the type context for more precise inference.
|
||||
for elt in elts {
|
||||
self.infer_expression(elt, TypeContext::default());
|
||||
self.infer_collection_literal(elts, tcx, KnownClass::Set)
|
||||
.unwrap_or_else(|| {
|
||||
KnownClass::Set.to_specialized_instance(self.db(), [Type::unknown()])
|
||||
})
|
||||
}
|
||||
|
||||
// Infer the type of a collection literal expression.
|
||||
fn infer_collection_literal(
|
||||
&mut self,
|
||||
elts: &[ast::Expr],
|
||||
tcx: TypeContext<'db>,
|
||||
collection_class: KnownClass,
|
||||
) -> Option<Type<'db>> {
|
||||
// Extract the type variable `T` from `list[T]` in typeshed.
|
||||
fn elts_ty(
|
||||
collection_class: KnownClass,
|
||||
db: &dyn Db,
|
||||
) -> Option<(ClassLiteral<'_>, Type<'_>)> {
|
||||
let class_literal = collection_class.try_to_class_literal(db)?;
|
||||
let generic_context = class_literal.generic_context(db)?;
|
||||
let variables = generic_context.variables(db);
|
||||
let elts_ty = variables.iter().exactly_one().ok()?;
|
||||
Some((class_literal, Type::TypeVar(*elts_ty)))
|
||||
}
|
||||
|
||||
KnownClass::Set.to_specialized_instance(self.db(), [todo_type!("set literal element type")])
|
||||
let annotated_elts_ty = tcx
|
||||
.known_specialization(collection_class, self.db())
|
||||
.and_then(|specialization| specialization.types(self.db()).iter().exactly_one().ok())
|
||||
.copied();
|
||||
|
||||
let (class_literal, elts_ty) = elts_ty(collection_class, self.db()).unwrap_or_else(|| {
|
||||
let name = collection_class.name(self.db());
|
||||
panic!("Typeshed should always have a `{name}` class in `builtins.pyi` with a single type variable")
|
||||
});
|
||||
|
||||
let mut elements_are_assignable = true;
|
||||
let mut inferred_elt_tys = Vec::with_capacity(elts.len());
|
||||
|
||||
// Infer the type of each element in the collection literal.
|
||||
for elt in elts {
|
||||
let inferred_elt_ty = self.infer_expression(elt, TypeContext::new(annotated_elts_ty));
|
||||
inferred_elt_tys.push(inferred_elt_ty);
|
||||
|
||||
if let Some(annotated_elts_ty) = annotated_elts_ty {
|
||||
elements_are_assignable &=
|
||||
inferred_elt_ty.is_assignable_to(self.db(), annotated_elts_ty);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a set of constraints to infer a precise type for `T`.
|
||||
let mut builder = SpecializationBuilder::new(self.db());
|
||||
|
||||
match annotated_elts_ty {
|
||||
// If the inferred type of any element is not assignable to the type annotation, we
|
||||
// ignore it, as to provide a more precise error message.
|
||||
Some(_) if !elements_are_assignable => {}
|
||||
|
||||
// Otherwise, the annotated type acts as a constraint for `T`.
|
||||
//
|
||||
// Note that we infer the annotated type _before_ the elements, to closer match the order
|
||||
// of any unions written in the type annotation.
|
||||
Some(annotated_elts_ty) => {
|
||||
builder.infer(elts_ty, annotated_elts_ty).ok()?;
|
||||
}
|
||||
|
||||
// If a valid type annotation was not provided, avoid restricting the type of the collection
|
||||
// by unioning the inferred type with `Unknown`.
|
||||
None => builder.infer(elts_ty, Type::unknown()).ok()?,
|
||||
}
|
||||
|
||||
// The inferred type of each element acts as an additional constraint on `T`.
|
||||
for inferred_elt_ty in inferred_elt_tys {
|
||||
// Convert any element literals to their promoted type form to avoid excessively large
|
||||
// unions for large nested list literals, which the constraint solver struggles with.
|
||||
let inferred_elt_ty =
|
||||
inferred_elt_ty.apply_type_mapping(self.db(), &TypeMapping::PromoteLiterals);
|
||||
builder.infer(elts_ty, inferred_elt_ty).ok()?;
|
||||
}
|
||||
|
||||
let class_type = class_literal
|
||||
.apply_specialization(self.db(), |generic_context| builder.build(generic_context));
|
||||
|
||||
Type::from(class_type).to_instance(self.db())
|
||||
}
|
||||
|
||||
fn infer_dict_expression(&mut self, dict: &ast::ExprDict, _tcx: TypeContext<'db>) -> Type<'db> {
|
||||
@@ -5314,6 +5404,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Infer the type of the `iter` expression of the first comprehension.
|
||||
fn infer_first_comprehension_iter(&mut self, comprehensions: &[ast::Comprehension]) {
|
||||
let mut comprehensions_iter = comprehensions.iter();
|
||||
|
||||
@@ -545,11 +545,15 @@ impl<T> VariableLengthTuple<T> {
|
||||
})
|
||||
}
|
||||
|
||||
fn prefix_elements(&self) -> impl DoubleEndedIterator<Item = &T> + ExactSizeIterator + '_ {
|
||||
pub(crate) fn prefix_elements(
|
||||
&self,
|
||||
) -> impl DoubleEndedIterator<Item = &T> + ExactSizeIterator + '_ {
|
||||
self.prefix.iter()
|
||||
}
|
||||
|
||||
fn suffix_elements(&self) -> impl DoubleEndedIterator<Item = &T> + ExactSizeIterator + '_ {
|
||||
pub(crate) fn suffix_elements(
|
||||
&self,
|
||||
) -> impl DoubleEndedIterator<Item = &T> + ExactSizeIterator + '_ {
|
||||
self.suffix.iter()
|
||||
}
|
||||
|
||||
|
||||
1
ruff.schema.json
generated
1
ruff.schema.json
generated
@@ -4057,7 +4057,6 @@
|
||||
"RUF061",
|
||||
"RUF063",
|
||||
"RUF064",
|
||||
"RUF065",
|
||||
"RUF1",
|
||||
"RUF10",
|
||||
"RUF100",
|
||||
|
||||
Reference in New Issue
Block a user