Compare commits
7 Commits
0.12.9
...
alex/thinn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c95cdb3d1d | ||
|
|
7c34ed3374 | ||
|
|
5b0a93b3c0 | ||
|
|
cfc46acc46 | ||
|
|
671f1358fb | ||
|
|
ce938fe205 | ||
|
|
7f8f1ab2c1 |
@@ -5588,15 +5588,15 @@ fn cookiecutter_globbing() -> Result<()> {
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--select=F811")
|
||||
.current_dir(tempdir.path()), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1: `foo` redefined here
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -254,6 +254,11 @@ impl Diagnostic {
|
||||
.find(|ann| ann.is_primary)
|
||||
}
|
||||
|
||||
/// Returns a mutable borrow of all annotations of this diagnostic.
|
||||
pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
|
||||
Arc::make_mut(&mut self.inner).annotations.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns the "primary" span of this diagnostic if one exists.
|
||||
///
|
||||
/// When there are multiple primary spans, then the first one that was
|
||||
@@ -310,6 +315,11 @@ impl Diagnostic {
|
||||
&self.inner.subs
|
||||
}
|
||||
|
||||
/// Returns a mutable borrow of the sub-diagnostics of this diagnostic.
|
||||
pub fn sub_diagnostics_mut(&mut self) -> impl Iterator<Item = &mut SubDiagnostic> {
|
||||
Arc::make_mut(&mut self.inner).subs.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns the fix for this diagnostic if it exists.
|
||||
pub fn fix(&self) -> Option<&Fix> {
|
||||
self.inner.fix.as_ref()
|
||||
@@ -621,6 +631,11 @@ impl SubDiagnostic {
|
||||
&self.inner.annotations
|
||||
}
|
||||
|
||||
/// Returns a mutable borrow of the annotations of this sub-diagnostic.
|
||||
pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
|
||||
self.inner.annotations.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns a shared borrow of the "primary" annotation of this diagnostic
|
||||
/// if one exists.
|
||||
///
|
||||
|
||||
@@ -264,7 +264,12 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
.annotations
|
||||
.iter()
|
||||
.filter_map(|ann| {
|
||||
let path = ann.span.file.path(resolver);
|
||||
let path = ann
|
||||
.span
|
||||
.file
|
||||
.relative_path(resolver)
|
||||
.to_str()
|
||||
.unwrap_or_else(|| ann.span.file.path(resolver));
|
||||
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
|
||||
ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
|
||||
})
|
||||
|
||||
@@ -87,11 +87,12 @@ impl Files {
|
||||
.system_by_path
|
||||
.entry(absolute.clone())
|
||||
.or_insert_with(|| {
|
||||
tracing::trace!("Adding file '{path}'");
|
||||
|
||||
let metadata = db.system().path_metadata(path);
|
||||
|
||||
tracing::trace!("Adding file '{absolute}'");
|
||||
|
||||
let durability = self
|
||||
.root(db, path)
|
||||
.root(db, &absolute)
|
||||
.map_or(Durability::default(), |root| root.durability(db));
|
||||
|
||||
let builder = File::builder(FilePath::System(absolute))
|
||||
|
||||
@@ -28,7 +28,7 @@ use itertools::Itertools;
|
||||
use log::debug;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, IntoDiagnosticMessage, Span};
|
||||
use ruff_diagnostics::{Applicability, Fix, IsolationLevel};
|
||||
use ruff_notebook::{CellOffsets, NotebookIndex};
|
||||
use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to_module_path};
|
||||
@@ -3305,6 +3305,17 @@ impl DiagnosticGuard<'_, '_> {
|
||||
Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a secondary annotation with the given message and range.
|
||||
pub(crate) fn secondary_annotation<'a>(
|
||||
&mut self,
|
||||
message: impl IntoDiagnosticMessage + 'a,
|
||||
range: impl Ranged,
|
||||
) {
|
||||
let span = Span::from(self.context.source_file.clone()).with_range(range.range());
|
||||
let ann = Annotation::secondary(span).message(message);
|
||||
self.diagnostic.as_mut().unwrap().annotate(ann);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for DiagnosticGuard<'_, '_> {
|
||||
|
||||
@@ -183,14 +183,24 @@ pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope
|
||||
// Create diagnostics for each statement.
|
||||
for (source, entries) in &redefinitions {
|
||||
for (shadowed, binding) in entries {
|
||||
let name = binding.name(checker.source());
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
RedefinedWhileUnused {
|
||||
name: binding.name(checker.source()).to_string(),
|
||||
name: name.to_string(),
|
||||
row: checker.compute_source_row(shadowed.start()),
|
||||
},
|
||||
binding.range(),
|
||||
);
|
||||
|
||||
diagnostic.secondary_annotation(
|
||||
format_args!("previous definition of `{name}` here"),
|
||||
shadowed,
|
||||
);
|
||||
|
||||
if let Some(ann) = diagnostic.primary_annotation_mut() {
|
||||
ann.set_message(format_args!("`{name}` redefined here"));
|
||||
}
|
||||
|
||||
if let Some(range) = binding.parent_range(checker.semantic()) {
|
||||
diagnostic.set_parent(range.start());
|
||||
}
|
||||
|
||||
@@ -5,7 +5,14 @@ F811 Redefinition of unused `bar` from line 6
|
||||
--> F811_0.py:10:5
|
||||
|
|
||||
10 | def bar():
|
||||
| ^^^
|
||||
| ^^^ `bar` redefined here
|
||||
11 | pass
|
||||
|
|
||||
::: F811_0.py:6:5
|
||||
|
|
||||
5 | @foo
|
||||
6 | def bar():
|
||||
| --- previous definition of `bar` here
|
||||
7 | pass
|
||||
|
|
||||
help: Remove definition: `bar`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `FU` from line 1
|
||||
--> F811_1.py:1:25
|
||||
--> F811_1.py:1:14
|
||||
|
|
||||
1 | import fu as FU, bar as FU
|
||||
| ^^
|
||||
| -- ^^ `FU` redefined here
|
||||
| |
|
||||
| previous definition of `FU` here
|
||||
|
|
||||
help: Remove definition: `FU`
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `mixer` from line 2
|
||||
--> F811_12.py:6:20
|
||||
--> F811_12.py:2:20
|
||||
|
|
||||
1 | try:
|
||||
2 | from aa import mixer
|
||||
| ----- previous definition of `mixer` here
|
||||
3 | except ImportError:
|
||||
4 | pass
|
||||
5 | else:
|
||||
6 | from bb import mixer
|
||||
| ^^^^^
|
||||
| ^^^^^ `mixer` redefined here
|
||||
7 | mixer(123)
|
||||
|
|
||||
help: Remove definition: `mixer`
|
||||
|
||||
@@ -5,7 +5,12 @@ F811 Redefinition of unused `fu` from line 1
|
||||
--> F811_15.py:4:5
|
||||
|
|
||||
4 | def fu():
|
||||
| ^^
|
||||
| ^^ `fu` redefined here
|
||||
5 | pass
|
||||
|
|
||||
::: F811_15.py:1:8
|
||||
|
|
||||
1 | import fu
|
||||
| -- previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -7,7 +7,14 @@ F811 Redefinition of unused `fu` from line 3
|
||||
6 | def bar():
|
||||
7 | def baz():
|
||||
8 | def fu():
|
||||
| ^^
|
||||
| ^^ `fu` redefined here
|
||||
9 | pass
|
||||
|
|
||||
::: F811_16.py:3:8
|
||||
|
|
||||
1 | """Test that shadowing a global with a nested function generates a warning."""
|
||||
2 |
|
||||
3 | import fu
|
||||
| -- previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -6,10 +6,16 @@ F811 [*] Redefinition of unused `fu` from line 2
|
||||
|
|
||||
5 | def bar():
|
||||
6 | import fu
|
||||
| ^^
|
||||
| ^^ `fu` redefined here
|
||||
7 |
|
||||
8 | def baz():
|
||||
|
|
||||
::: F811_17.py:2:8
|
||||
|
|
||||
1 | """Test that shadowing a global name with a nested function generates a warning."""
|
||||
2 | import fu
|
||||
| -- previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
ℹ Safe fix
|
||||
@@ -22,11 +28,15 @@ help: Remove definition: `fu`
|
||||
9 8 | def fu():
|
||||
|
||||
F811 Redefinition of unused `fu` from line 6
|
||||
--> F811_17.py:9:13
|
||||
--> F811_17.py:6:12
|
||||
|
|
||||
5 | def bar():
|
||||
6 | import fu
|
||||
| -- previous definition of `fu` here
|
||||
7 |
|
||||
8 | def baz():
|
||||
9 | def fu():
|
||||
| ^^
|
||||
| ^^ `fu` redefined here
|
||||
10 | pass
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `FU` from line 1
|
||||
--> F811_2.py:1:34
|
||||
--> F811_2.py:1:23
|
||||
|
|
||||
1 | from moo import fu as FU, bar as FU
|
||||
| ^^
|
||||
| -- ^^ `FU` redefined here
|
||||
| |
|
||||
| previous definition of `FU` here
|
||||
|
|
||||
help: Remove definition: `FU`
|
||||
|
||||
@@ -7,9 +7,17 @@ F811 [*] Redefinition of unused `Sequence` from line 26
|
||||
30 | from typing import (
|
||||
31 | List, # noqa: F811
|
||||
32 | Sequence,
|
||||
| ^^^^^^^^
|
||||
| ^^^^^^^^ `Sequence` redefined here
|
||||
33 | )
|
||||
|
|
||||
::: F811_21.py:26:5
|
||||
|
|
||||
24 | from typing import (
|
||||
25 | List, # noqa
|
||||
26 | Sequence, # noqa
|
||||
| -------- previous definition of `Sequence` here
|
||||
27 | )
|
||||
|
|
||||
help: Remove definition: `Sequence`
|
||||
|
||||
ℹ Safe fix
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `foo` from line 3
|
||||
--> F811_23.py:4:15
|
||||
--> F811_23.py:3:15
|
||||
|
|
||||
1 | """Test that shadowing an explicit re-export produces a warning."""
|
||||
2 |
|
||||
3 | import foo as foo
|
||||
| --- previous definition of `foo` here
|
||||
4 | import bar as foo
|
||||
| ^^^
|
||||
| ^^^ `foo` redefined here
|
||||
|
|
||||
help: Remove definition: `foo`
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `func` from line 2
|
||||
--> F811_26.py:5:9
|
||||
--> F811_26.py:2:9
|
||||
|
|
||||
1 | class Class:
|
||||
2 | def func(self):
|
||||
| ---- previous definition of `func` here
|
||||
3 | pass
|
||||
4 |
|
||||
5 | def func(self):
|
||||
| ^^^^
|
||||
| ^^^^ `func` redefined here
|
||||
6 | pass
|
||||
|
|
||||
help: Remove definition: `func`
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `datetime` from line 3
|
||||
--> F811_28.py:4:22
|
||||
--> F811_28.py:3:8
|
||||
|
|
||||
1 | """Regression test for: https://github.com/astral-sh/ruff/issues/10384"""
|
||||
2 |
|
||||
3 | import datetime
|
||||
| -------- previous definition of `datetime` here
|
||||
4 | from datetime import datetime
|
||||
| ^^^^^^^^
|
||||
| ^^^^^^^^ `datetime` redefined here
|
||||
5 |
|
||||
6 | datetime(1, 2, 3)
|
||||
|
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `Bar` from line 3
|
||||
--> F811_29.pyi:8:1
|
||||
--> F811_29.pyi:3:24
|
||||
|
|
||||
1 | """Regression test for: https://github.com/astral-sh/ruff/issues/10509"""
|
||||
2 |
|
||||
3 | from foo import Bar as Bar
|
||||
| --- previous definition of `Bar` here
|
||||
4 |
|
||||
5 | class Eggs:
|
||||
6 | Bar: int # OK
|
||||
7 |
|
||||
8 | Bar = 1 # F811
|
||||
| ^^^
|
||||
| ^^^ `Bar` redefined here
|
||||
|
|
||||
help: Remove definition: `Bar`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `fu` from line 1
|
||||
--> F811_3.py:1:12
|
||||
--> F811_3.py:1:8
|
||||
|
|
||||
1 | import fu; fu = 3
|
||||
| ^^
|
||||
| -- ^^ `fu` redefined here
|
||||
| |
|
||||
| previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -2,32 +2,43 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `bar` from line 10
|
||||
--> F811_30.py:12:9
|
||||
--> F811_30.py:10:5
|
||||
|
|
||||
8 | """Foo."""
|
||||
9 |
|
||||
10 | bar = foo
|
||||
| --- previous definition of `bar` here
|
||||
11 |
|
||||
12 | def bar(self) -> None:
|
||||
| ^^^
|
||||
| ^^^ `bar` redefined here
|
||||
13 | """Bar."""
|
||||
|
|
||||
help: Remove definition: `bar`
|
||||
|
||||
F811 Redefinition of unused `baz` from line 18
|
||||
--> F811_30.py:21:5
|
||||
--> F811_30.py:18:9
|
||||
|
|
||||
16 | class B:
|
||||
17 | """B."""
|
||||
18 | def baz(self) -> None:
|
||||
| --- previous definition of `baz` here
|
||||
19 | """Baz."""
|
||||
20 |
|
||||
21 | baz = 1
|
||||
| ^^^
|
||||
| ^^^ `baz` redefined here
|
||||
|
|
||||
help: Remove definition: `baz`
|
||||
|
||||
F811 Redefinition of unused `foo` from line 26
|
||||
--> F811_30.py:29:12
|
||||
--> F811_30.py:26:9
|
||||
|
|
||||
24 | class C:
|
||||
25 | """C."""
|
||||
26 | def foo(self) -> None:
|
||||
| --- previous definition of `foo` here
|
||||
27 | """Foo."""
|
||||
28 |
|
||||
29 | bar = (foo := 1)
|
||||
| ^^^
|
||||
| ^^^ `foo` redefined here
|
||||
|
|
||||
help: Remove definition: `foo`
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `baz` from line 17
|
||||
--> F811_31.py:19:29
|
||||
--> F811_31.py:17:5
|
||||
|
|
||||
16 | try:
|
||||
17 | baz = None
|
||||
| --- previous definition of `baz` here
|
||||
18 |
|
||||
19 | from some_module import baz
|
||||
| ^^^
|
||||
| ^^^ `baz` redefined here
|
||||
20 | except ImportError:
|
||||
21 | pass
|
||||
|
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 [*] Redefinition of unused `List` from line 4
|
||||
--> F811_32.py:5:5
|
||||
--> F811_32.py:4:5
|
||||
|
|
||||
3 | from typing import (
|
||||
4 | List,
|
||||
| ---- previous definition of `List` here
|
||||
5 | List,
|
||||
| ^^^^
|
||||
| ^^^^ `List` redefined here
|
||||
6 | )
|
||||
|
|
||||
help: Remove definition: `List`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `fu` from line 1
|
||||
--> F811_4.py:1:12
|
||||
--> F811_4.py:1:8
|
||||
|
|
||||
1 | import fu; fu, bar = 3
|
||||
| ^^
|
||||
| -- ^^ `fu` redefined here
|
||||
| |
|
||||
| previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `fu` from line 1
|
||||
--> F811_5.py:1:13
|
||||
--> F811_5.py:1:8
|
||||
|
|
||||
1 | import fu; [fu, bar] = 3
|
||||
| ^^
|
||||
| -- ^^ `fu` redefined here
|
||||
| |
|
||||
| previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 [*] Redefinition of unused `os` from line 5
|
||||
--> F811_6.py:6:12
|
||||
--> F811_6.py:5:12
|
||||
|
|
||||
3 | i = 2
|
||||
4 | if i == 1:
|
||||
5 | import os
|
||||
| -- previous definition of `os` here
|
||||
6 | import os
|
||||
| ^^
|
||||
| ^^ `os` redefined here
|
||||
7 | os.path
|
||||
|
|
||||
help: Remove definition: `os`
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 [*] Redefinition of unused `os` from line 4
|
||||
--> F811_8.py:5:12
|
||||
--> F811_8.py:4:12
|
||||
|
|
||||
3 | try:
|
||||
4 | import os
|
||||
| -- previous definition of `os` here
|
||||
5 | import os
|
||||
| ^^
|
||||
| ^^ `os` redefined here
|
||||
6 | except:
|
||||
7 | pass
|
||||
|
|
||||
|
||||
@@ -19,11 +19,14 @@ help: Remove unused import: `os`
|
||||
5 4 | import os
|
||||
|
||||
F811 [*] Redefinition of unused `os` from line 2
|
||||
--> <filename>:5:12
|
||||
--> <filename>:2:8
|
||||
|
|
||||
2 | import os
|
||||
| -- previous definition of `os` here
|
||||
3 |
|
||||
4 | def f():
|
||||
5 | import os
|
||||
| ^^
|
||||
| ^^ `os` redefined here
|
||||
6 |
|
||||
7 | # Despite this `del`, `import os` in `f` should still be flagged as shadowing an unused
|
||||
|
|
||||
|
||||
@@ -19,11 +19,14 @@ help: Remove unused import: `os`
|
||||
5 4 | os = 1
|
||||
|
||||
F811 Redefinition of unused `os` from line 2
|
||||
--> <filename>:5:5
|
||||
--> <filename>:2:8
|
||||
|
|
||||
2 | import os
|
||||
| -- previous definition of `os` here
|
||||
3 |
|
||||
4 | def f():
|
||||
5 | os = 1
|
||||
| ^^
|
||||
| ^^ `os` redefined here
|
||||
6 | print(os)
|
||||
|
|
||||
help: Remove definition: `os`
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 [*] Redefinition of unused `os` from line 3
|
||||
--> <filename>:4:12
|
||||
--> <filename>:3:12
|
||||
|
|
||||
2 | def f():
|
||||
3 | import os
|
||||
| -- previous definition of `os` here
|
||||
4 | import os
|
||||
| ^^
|
||||
| ^^ `os` redefined here
|
||||
5 |
|
||||
6 | # Despite this `del`, `import os` should still be flagged as shadowing an unused
|
||||
|
|
||||
|
||||
@@ -9,7 +9,7 @@ use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{Diagnostic, Span};
|
||||
use ruff_notebook::Notebook;
|
||||
#[cfg(not(fuzzing))]
|
||||
use ruff_notebook::NotebookError;
|
||||
@@ -281,10 +281,16 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
|
||||
// noqa offset and the source file
|
||||
let range = diagnostic.expect_range();
|
||||
diagnostic.set_noqa_offset(directives.noqa_line_for.resolve(range.start()));
|
||||
if let Some(annotation) = diagnostic.primary_annotation_mut() {
|
||||
annotation.set_span(
|
||||
ruff_db::diagnostic::Span::from(source_code.clone()).with_range(range),
|
||||
);
|
||||
// This part actually is necessary to avoid long relative paths in snapshots.
|
||||
for annotation in diagnostic.annotations_mut() {
|
||||
let range = annotation.get_span().range().unwrap();
|
||||
annotation.set_span(Span::from(source_code.clone()).with_range(range));
|
||||
}
|
||||
for sub in diagnostic.sub_diagnostics_mut() {
|
||||
for annotation in sub.annotations_mut() {
|
||||
let range = annotation.get_span().range().unwrap();
|
||||
annotation.set_span(Span::from(source_code.clone()).with_range(range));
|
||||
}
|
||||
}
|
||||
|
||||
diagnostic
|
||||
|
||||
@@ -21,6 +21,8 @@ mod changes;
|
||||
#[salsa::db]
|
||||
pub trait Db: SemanticDb {
|
||||
fn project(&self) -> Project;
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn Db>;
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
@@ -484,6 +486,10 @@ impl Db for ProjectDatabase {
|
||||
fn project(&self) -> Project {
|
||||
self.project.unwrap()
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn Db> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "format")]
|
||||
@@ -611,6 +617,10 @@ pub(crate) mod tests {
|
||||
fn project(&self) -> Project {
|
||||
self.project.unwrap()
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn Db> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_db::files::{File, system_path_to_file};
|
||||
use ruff_db::system::walk_directory::{ErrorKind, WalkDirectoryBuilder, WalkState};
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -163,20 +163,24 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
|
||||
/// Walks the project paths and collects the paths of all files that
|
||||
/// are included in the project.
|
||||
pub(crate) fn walk_paths(self) -> (Vec<SystemPathBuf>, Vec<IOErrorDiagnostic>) {
|
||||
let paths = std::sync::Mutex::new(Vec::new());
|
||||
pub(crate) fn collect_vec(self, db: &dyn Db) -> (Vec<File>, Vec<IOErrorDiagnostic>) {
|
||||
let files = std::sync::Mutex::new(Vec::new());
|
||||
let diagnostics = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
self.walker.run(|| {
|
||||
Box::new(|entry| {
|
||||
let db = db.dyn_clone();
|
||||
let filter = &self.filter;
|
||||
let files = &files;
|
||||
let diagnostics = &diagnostics;
|
||||
|
||||
Box::new(move |entry| {
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
// Skip excluded directories unless they were explicitly passed to the walker
|
||||
// (which is the case passed to `ty check <paths>`).
|
||||
if entry.file_type().is_directory() {
|
||||
if entry.depth() > 0 {
|
||||
let directory_included = self
|
||||
.filter
|
||||
let directory_included = filter
|
||||
.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown);
|
||||
return match directory_included {
|
||||
IncludeResult::Included => WalkState::Continue,
|
||||
@@ -210,8 +214,7 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
// For all files, except the ones that were explicitly passed to the walker (CLI),
|
||||
// check if they're included in the project.
|
||||
if entry.depth() > 0 {
|
||||
match self
|
||||
.filter
|
||||
match filter
|
||||
.is_file_included(entry.path(), GlobFilterCheckMode::TopDown)
|
||||
{
|
||||
IncludeResult::Included => {},
|
||||
@@ -232,8 +235,11 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut paths = paths.lock().unwrap();
|
||||
paths.push(entry.into_path());
|
||||
// If this returns `Err`, then the file was deleted between now and when the walk callback was called.
|
||||
// We can ignore this.
|
||||
if let Ok(file) = system_path_to_file(&*db, entry.path()) {
|
||||
files.lock().unwrap().push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => match error.kind() {
|
||||
@@ -274,39 +280,14 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
});
|
||||
|
||||
(
|
||||
paths.into_inner().unwrap(),
|
||||
files.into_inner().unwrap(),
|
||||
diagnostics.into_inner().unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vec(self, db: &dyn Db) -> (Vec<File>, Vec<IOErrorDiagnostic>) {
|
||||
let (paths, diagnostics) = self.walk_paths();
|
||||
|
||||
(
|
||||
paths
|
||||
.into_iter()
|
||||
.filter_map(move |path| {
|
||||
// If this returns `None`, then the file was deleted between the `walk_directory` call and now.
|
||||
// We can ignore this.
|
||||
system_path_to_file(db, &path).ok()
|
||||
})
|
||||
.collect(),
|
||||
diagnostics,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_set(self, db: &dyn Db) -> (FxHashSet<File>, Vec<IOErrorDiagnostic>) {
|
||||
let (paths, diagnostics) = self.walk_paths();
|
||||
|
||||
let mut files = FxHashSet::with_capacity_and_hasher(paths.len(), FxBuildHasher);
|
||||
|
||||
for path in paths {
|
||||
if let Ok(file) = system_path_to_file(db, &path) {
|
||||
files.insert(file);
|
||||
}
|
||||
}
|
||||
|
||||
(files, diagnostics)
|
||||
let (files, diagnostics) = self.collect_vec(db);
|
||||
(files.into_iter().collect(), diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -617,5 +617,34 @@ class C[T](C): ...
|
||||
class D[T](D[int]): ...
|
||||
```
|
||||
|
||||
## Tuple as a PEP-695 generic class
|
||||
|
||||
Our special handling for `tuple` does not break if `tuple` is defined as a PEP-695 generic class in
|
||||
typeshed:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
class tuple[T]: ...
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
reveal_type((1, 2, 3)) # revealed: tuple[Literal[1], Literal[2], Literal[3]]
|
||||
```
|
||||
|
||||
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
|
||||
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification
|
||||
|
||||
@@ -133,6 +133,17 @@ def _[T](x: A | B):
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
```
|
||||
|
||||
## Narrowing for tuple
|
||||
|
||||
An early version of <https://github.com/astral-sh/ruff/pull/19920> caused us to crash on this:
|
||||
|
||||
```py
|
||||
def _(val):
|
||||
if type(val) is tuple:
|
||||
# TODO: better would be `Unknown & tuple[object, ...]`
|
||||
reveal_type(val) # revealed: Unknown & tuple[Unknown, ...]
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
```py
|
||||
|
||||
@@ -1266,7 +1266,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
class_def_node.type_params.as_ref().map(|type_params| {
|
||||
let index = semantic_index(db, scope.file(db));
|
||||
let definition = index.expect_single_definition(class_def_node);
|
||||
GenericContext::from_type_params(db, index, definition, type_params)
|
||||
GenericContext::from_type_params(db, index, definition, type_params, self.known(db))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1290,6 +1290,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|ty| matches!(ty, Type::GenericAlias(_))),
|
||||
self.known(db),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1334,8 +1335,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> ClassType<'db> {
|
||||
self.apply_specialization(db, |generic_context| {
|
||||
specialization
|
||||
.unwrap_or_else(|| generic_context.default_specialization(db, self.known(db)))
|
||||
specialization.unwrap_or_else(|| generic_context.default_specialization(db))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1344,7 +1344,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
/// applies the default specialization to the class's typevars.
|
||||
pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
|
||||
self.apply_specialization(db, |generic_context| {
|
||||
generic_context.default_specialization(db, self.known(db))
|
||||
generic_context.default_specialization(db)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -454,7 +454,6 @@ impl Display for DisplayGenericContext<'_> {
|
||||
let variables = self.generic_context.variables(self.db);
|
||||
|
||||
let non_implicit_variables: Vec<_> = variables
|
||||
.iter()
|
||||
.filter(|bound_typevar| !bound_typevar.typevar(self.db).is_implicit(self.db))
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ impl<'db> OverloadLiteral<'db> {
|
||||
let definition = self.definition(db);
|
||||
let generic_context = function_stmt_node.type_params.as_ref().map(|type_params| {
|
||||
let index = semantic_index(db, scope.file(db));
|
||||
GenericContext::from_type_params(db, index, definition, type_params)
|
||||
GenericContext::from_type_params(db, index, definition, type_params, None)
|
||||
});
|
||||
|
||||
let index = semantic_index(db, scope.file(db));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use itertools::Either;
|
||||
use ruff_db::parsed::ParsedModuleRef;
|
||||
use ruff_python_ast as ast;
|
||||
use rustc_hash::FxHashMap;
|
||||
@@ -12,7 +13,7 @@ use crate::types::class_base::ClassBase;
|
||||
use crate::types::infer::infer_definition_types;
|
||||
use crate::types::instance::{Protocol, ProtocolInstanceType};
|
||||
use crate::types::signatures::{Parameter, Parameters, Signature};
|
||||
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
|
||||
use crate::types::tuple::{TupleSpec, TupleType};
|
||||
use crate::types::{
|
||||
ApplyTypeMappingVisitor, BoundTypeVarInstance, HasRelationToVisitor, KnownClass,
|
||||
KnownInstanceType, NormalizedVisitor, Type, TypeMapping, TypeRelation, TypeTransformer,
|
||||
@@ -82,6 +83,14 @@ pub(crate) fn bind_typevar<'db>(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum GenericContextInner<'db> {
|
||||
Tuple {
|
||||
single_typevar: BoundTypeVarInstance<'db>,
|
||||
},
|
||||
NonTuple(FxOrderSet<BoundTypeVarInstance<'db>>),
|
||||
}
|
||||
|
||||
/// A list of formal type variables for a generic function, class, or type alias.
|
||||
///
|
||||
/// TODO: Handle nested generic contexts better, with actual parent links to the lexically
|
||||
@@ -94,7 +103,7 @@ pub(crate) fn bind_typevar<'db>(
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct GenericContext<'db> {
|
||||
#[returns(ref)]
|
||||
pub(crate) variables: FxOrderSet<BoundTypeVarInstance<'db>>,
|
||||
pub(crate) inner: GenericContextInner<'db>,
|
||||
}
|
||||
|
||||
pub(super) fn walk_generic_context<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>(
|
||||
@@ -103,7 +112,7 @@ pub(super) fn walk_generic_context<'db, V: super::visitor::TypeVisitor<'db> + ?S
|
||||
visitor: &V,
|
||||
) {
|
||||
for bound_typevar in context.variables(db) {
|
||||
visitor.visit_bound_type_var_type(db, *bound_typevar);
|
||||
visitor.visit_bound_type_var_type(db, bound_typevar);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,12 +120,25 @@ pub(super) fn walk_generic_context<'db, V: super::visitor::TypeVisitor<'db> + ?S
|
||||
impl get_size2::GetSize for GenericContext<'_> {}
|
||||
|
||||
impl<'db> GenericContext<'db> {
|
||||
pub(crate) fn variables(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> impl ExactSizeIterator<Item = BoundTypeVarInstance<'db>> {
|
||||
match self.inner(db) {
|
||||
GenericContextInner::Tuple { single_typevar } => {
|
||||
Either::Left(std::iter::once(*single_typevar))
|
||||
}
|
||||
GenericContextInner::NonTuple(variables) => Either::Right(variables.iter().copied()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a generic context from a list of PEP-695 type parameters.
|
||||
pub(crate) fn from_type_params(
|
||||
db: &'db dyn Db,
|
||||
index: &'db SemanticIndex<'db>,
|
||||
binding_context: Definition<'db>,
|
||||
type_params_node: &ast::TypeParams,
|
||||
known_class: Option<KnownClass>,
|
||||
) -> Self {
|
||||
let variables: FxOrderSet<_> = type_params_node
|
||||
.iter()
|
||||
@@ -124,7 +146,29 @@ impl<'db> GenericContext<'db> {
|
||||
Self::variable_from_type_param(db, index, binding_context, type_param)
|
||||
})
|
||||
.collect();
|
||||
Self::new(db, variables)
|
||||
|
||||
match known_class {
|
||||
// As of 15/08/2025, this branch is never taken, since `tuple` in typeshed
|
||||
// does not have any PEP-695 type parameters. However, it will presumably
|
||||
// at some point have its stub definition rewritten to use PEP-695 type parameters,
|
||||
// and a custom typeshed could also reasonably define `tuple` in `builtins.pyi`
|
||||
// with PEP-695 type parameters. (We have a regression test for this in
|
||||
// `generics/pep695/classes.md`.)
|
||||
Some(KnownClass::Tuple) => {
|
||||
assert_eq!(
|
||||
variables.len(),
|
||||
1,
|
||||
"Tuple should always have exactly one typevar"
|
||||
);
|
||||
Self::new(
|
||||
db,
|
||||
GenericContextInner::Tuple {
|
||||
single_typevar: variables[0],
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => Self::new(db, GenericContextInner::NonTuple(variables)),
|
||||
}
|
||||
}
|
||||
|
||||
fn variable_from_type_param(
|
||||
@@ -174,7 +218,7 @@ impl<'db> GenericContext<'db> {
|
||||
if variables.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Self::new(db, variables))
|
||||
Some(Self::new(db, GenericContextInner::NonTuple(variables)))
|
||||
}
|
||||
|
||||
/// Creates a generic context from the legacy `TypeVar`s that appear in class's base class
|
||||
@@ -182,6 +226,7 @@ impl<'db> GenericContext<'db> {
|
||||
pub(crate) fn from_base_classes(
|
||||
db: &'db dyn Db,
|
||||
bases: impl Iterator<Item = Type<'db>>,
|
||||
known_class: Option<KnownClass>,
|
||||
) -> Option<Self> {
|
||||
let mut variables = FxOrderSet::default();
|
||||
for base in bases {
|
||||
@@ -190,7 +235,23 @@ impl<'db> GenericContext<'db> {
|
||||
if variables.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Self::new(db, variables))
|
||||
let context = match known_class {
|
||||
Some(KnownClass::Tuple) => {
|
||||
assert_eq!(
|
||||
variables.len(),
|
||||
1,
|
||||
"Tuple should always have exactly one typevar"
|
||||
);
|
||||
Self::new(
|
||||
db,
|
||||
GenericContextInner::Tuple {
|
||||
single_typevar: variables[0],
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => Self::new(db, GenericContextInner::NonTuple(variables)),
|
||||
};
|
||||
Some(context)
|
||||
}
|
||||
|
||||
pub(crate) fn len(self, db: &'db dyn Db) -> usize {
|
||||
@@ -200,8 +261,7 @@ impl<'db> GenericContext<'db> {
|
||||
pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> {
|
||||
let parameters = Parameters::new(
|
||||
self.variables(db)
|
||||
.iter()
|
||||
.map(|typevar| Self::parameter_from_typevar(db, *typevar)),
|
||||
.map(|typevar| Self::parameter_from_typevar(db, typevar)),
|
||||
);
|
||||
Signature::new(parameters, None)
|
||||
}
|
||||
@@ -231,50 +291,54 @@ impl<'db> GenericContext<'db> {
|
||||
parameter
|
||||
}
|
||||
|
||||
pub(crate) fn default_specialization(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
known_class: Option<KnownClass>,
|
||||
) -> Specialization<'db> {
|
||||
let partial = self.specialize_partial(db, &vec![None; self.variables(db).len()]);
|
||||
if known_class == Some(KnownClass::Tuple) {
|
||||
Specialization::new(
|
||||
db,
|
||||
self,
|
||||
partial.types(db),
|
||||
Some(TupleType::homogeneous(db, Type::unknown())),
|
||||
)
|
||||
} else {
|
||||
partial
|
||||
}
|
||||
pub(crate) fn default_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
|
||||
self.specialize_partial(db, &vec![None; self.variables(db).len()])
|
||||
}
|
||||
|
||||
pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
|
||||
let types = self
|
||||
.variables(db)
|
||||
.iter()
|
||||
.map(|typevar| Type::TypeVar(*typevar))
|
||||
.collect();
|
||||
let types = self.variables(db).map(Type::TypeVar).collect();
|
||||
self.specialize(db, types)
|
||||
}
|
||||
|
||||
pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
|
||||
let types = vec![Type::unknown(); self.variables(db).len()];
|
||||
self.specialize(db, types.into())
|
||||
self.specialize(db, types.into_boxed_slice())
|
||||
}
|
||||
|
||||
/// Returns a tuple type of the typevars introduced by this generic context.
|
||||
pub(crate) fn as_tuple(self, db: &'db dyn Db) -> Type<'db> {
|
||||
Type::heterogeneous_tuple(
|
||||
db,
|
||||
self.variables(db)
|
||||
.iter()
|
||||
.map(|typevar| Type::TypeVar(*typevar)),
|
||||
)
|
||||
Type::heterogeneous_tuple(db, self.variables(db).map(Type::TypeVar))
|
||||
}
|
||||
|
||||
pub(crate) fn is_subset_of(self, db: &'db dyn Db, other: GenericContext<'db>) -> bool {
|
||||
self.variables(db).is_subset(other.variables(db))
|
||||
match (self.inner(db), other.inner(db)) {
|
||||
(GenericContextInner::NonTuple(left), GenericContextInner::NonTuple(right)) => {
|
||||
left.is_subset(right)
|
||||
}
|
||||
|
||||
(
|
||||
GenericContextInner::Tuple {
|
||||
single_typevar: left,
|
||||
},
|
||||
GenericContextInner::NonTuple(right),
|
||||
) => right.contains(left),
|
||||
|
||||
(
|
||||
GenericContextInner::NonTuple(left),
|
||||
GenericContextInner::Tuple {
|
||||
single_typevar: right,
|
||||
},
|
||||
) => left.len() == 1 && left[0] == *right,
|
||||
|
||||
(
|
||||
GenericContextInner::Tuple {
|
||||
single_typevar: left,
|
||||
},
|
||||
GenericContextInner::Tuple {
|
||||
single_typevar: right,
|
||||
},
|
||||
) => left == right,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn binds_typevar(
|
||||
@@ -283,9 +347,7 @@ impl<'db> GenericContext<'db> {
|
||||
typevar: TypeVarInstance<'db>,
|
||||
) -> Option<BoundTypeVarInstance<'db>> {
|
||||
self.variables(db)
|
||||
.iter()
|
||||
.find(|self_bound_typevar| self_bound_typevar.typevar(db) == typevar)
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Creates a specialization of this generic context. Panics if the length of `types` does not
|
||||
@@ -297,18 +359,30 @@ impl<'db> GenericContext<'db> {
|
||||
db: &'db dyn Db,
|
||||
types: Box<[Type<'db>]>,
|
||||
) -> Specialization<'db> {
|
||||
assert!(self.variables(db).len() == types.len());
|
||||
Specialization::new(db, self, types, None)
|
||||
match self.inner(db) {
|
||||
GenericContextInner::Tuple { .. } => {
|
||||
assert_eq!(types.len(), 1);
|
||||
let tuple = TupleType::homogeneous(db, types[0]);
|
||||
Specialization::new(db, self, SpecializationInner::Tuple(tuple))
|
||||
}
|
||||
GenericContextInner::NonTuple(variables) => {
|
||||
assert_eq!(variables.len(), types.len());
|
||||
Specialization::new(db, self, SpecializationInner::NonTuple(types))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a specialization of this generic context for the `tuple` class.
|
||||
pub(crate) fn specialize_tuple(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
element_type: Type<'db>,
|
||||
tuple: TupleType<'db>,
|
||||
) -> Specialization<'db> {
|
||||
Specialization::new(db, self, Box::from([element_type]), Some(tuple))
|
||||
debug_assert!(
|
||||
matches!(self.inner(db), GenericContextInner::Tuple { .. }),
|
||||
"Should never call `GenericContext::specialize_tuple` on a non-tuple context"
|
||||
);
|
||||
Specialization::new(db, self, SpecializationInner::Tuple(tuple))
|
||||
}
|
||||
|
||||
/// Creates a specialization of this generic context. Panics if the length of `types` does not
|
||||
@@ -319,8 +393,14 @@ impl<'db> GenericContext<'db> {
|
||||
db: &'db dyn Db,
|
||||
types: &[Option<Type<'db>>],
|
||||
) -> Specialization<'db> {
|
||||
if let GenericContextInner::Tuple { .. } = self.inner(db) {
|
||||
assert_eq!(types.len(), 1);
|
||||
let ty = types[0].unwrap_or_else(Type::unknown);
|
||||
return self.specialize_tuple(db, TupleType::homogeneous(db, ty));
|
||||
}
|
||||
|
||||
let variables = self.variables(db);
|
||||
assert!(variables.len() == types.len());
|
||||
assert_eq!(variables.len(), types.len());
|
||||
|
||||
// Typevars can have other typevars as their default values, e.g.
|
||||
//
|
||||
@@ -353,20 +433,40 @@ impl<'db> GenericContext<'db> {
|
||||
expanded[idx] = default;
|
||||
}
|
||||
|
||||
Specialization::new(db, self, expanded.into_boxed_slice(), None)
|
||||
Specialization::new(
|
||||
db,
|
||||
self,
|
||||
SpecializationInner::NonTuple(expanded.into_boxed_slice()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
|
||||
let variables: FxOrderSet<_> = self
|
||||
.variables(db)
|
||||
.iter()
|
||||
.map(|bound_typevar| bound_typevar.normalized_impl(db, visitor))
|
||||
.collect();
|
||||
Self::new(db, variables)
|
||||
let inner = match self.inner(db) {
|
||||
GenericContextInner::Tuple { single_typevar } => {
|
||||
let single_typevar = single_typevar.normalized_impl(db, visitor);
|
||||
GenericContextInner::Tuple { single_typevar }
|
||||
}
|
||||
GenericContextInner::NonTuple(variables) => {
|
||||
let variables: FxOrderSet<_> = variables
|
||||
.into_iter()
|
||||
.map(|bound_typevar| bound_typevar.normalized_impl(db, visitor))
|
||||
.collect();
|
||||
GenericContextInner::NonTuple(variables)
|
||||
}
|
||||
};
|
||||
|
||||
Self::new(db, inner)
|
||||
}
|
||||
|
||||
fn heap_size((variables,): &(FxOrderSet<BoundTypeVarInstance<'db>>,)) -> usize {
|
||||
ruff_memory_usage::order_set_heap_size(variables)
|
||||
fn heap_size((inner,): &(GenericContextInner<'db>,)) -> usize {
|
||||
match inner {
|
||||
GenericContextInner::Tuple { single_typevar } => {
|
||||
ruff_memory_usage::heap_size(single_typevar)
|
||||
}
|
||||
GenericContextInner::NonTuple(variables) => {
|
||||
ruff_memory_usage::order_set_heap_size(variables)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +491,12 @@ impl std::fmt::Display for LegacyGenericBase {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
pub enum SpecializationInner<'db> {
|
||||
Tuple(TupleType<'db>),
|
||||
NonTuple(Box<[Type<'db>]>),
|
||||
}
|
||||
|
||||
/// An assignment of a specific type to each type variable in a generic scope.
|
||||
///
|
||||
/// TODO: Handle nested specializations better, with actual parent links to the specialization of
|
||||
@@ -398,12 +504,8 @@ impl std::fmt::Display for LegacyGenericBase {
|
||||
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
pub struct Specialization<'db> {
|
||||
pub(crate) generic_context: GenericContext<'db>,
|
||||
#[returns(deref)]
|
||||
pub(crate) types: Box<[Type<'db>]>,
|
||||
|
||||
/// For specializations of `tuple`, we also store more detailed information about the tuple's
|
||||
/// elements, above what the class's (single) typevar can represent.
|
||||
tuple_inner: Option<TupleType<'db>>,
|
||||
#[returns(ref)]
|
||||
inner: SpecializationInner<'db>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
@@ -418,15 +520,29 @@ pub(super) fn walk_specialization<'db, V: super::visitor::TypeVisitor<'db> + ?Si
|
||||
for ty in specialization.types(db) {
|
||||
visitor.visit_type(db, *ty);
|
||||
}
|
||||
if let Some(tuple) = specialization.tuple_inner(db) {
|
||||
walk_tuple_type(db, tuple, visitor);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Specialization<'db> {
|
||||
pub(crate) fn types(self, db: &'db dyn Db) -> &'db [Type<'db>] {
|
||||
#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)]
|
||||
fn homogeneous_element_type<'db>(db: &'db dyn Db, tuple: TupleType<'db>) -> Type<'db> {
|
||||
tuple.tuple(db).homogeneous_element_type(db)
|
||||
}
|
||||
|
||||
match self.inner(db) {
|
||||
SpecializationInner::Tuple(tuple) => {
|
||||
std::slice::from_ref(homogeneous_element_type(db, *tuple))
|
||||
}
|
||||
SpecializationInner::NonTuple(types) => types,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the tuple spec for a specialization of the `tuple` class.
|
||||
pub(crate) fn tuple(self, db: &'db dyn Db) -> Option<&'db TupleSpec<'db>> {
|
||||
self.tuple_inner(db).map(|tuple_type| tuple_type.tuple(db))
|
||||
match self.inner(db) {
|
||||
SpecializationInner::Tuple(tuple) => Some(tuple.tuple(db)),
|
||||
SpecializationInner::NonTuple(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this
|
||||
@@ -436,11 +552,18 @@ impl<'db> Specialization<'db> {
|
||||
db: &'db dyn Db,
|
||||
bound_typevar: BoundTypeVarInstance<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
let index = self
|
||||
.generic_context(db)
|
||||
.variables(db)
|
||||
.get_index_of(&bound_typevar)?;
|
||||
self.types(db).get(index).copied()
|
||||
match self.generic_context(db).inner(db) {
|
||||
GenericContextInner::Tuple { single_typevar } => (*single_typevar == bound_typevar)
|
||||
.then(|| {
|
||||
let types = self.types(db);
|
||||
assert_eq!(types.len(), 1);
|
||||
types[0]
|
||||
}),
|
||||
GenericContextInner::NonTuple(variables) => {
|
||||
let index = variables.get_index_of(&bound_typevar)?;
|
||||
self.types(db).get(index).copied()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a specialization to this specialization. This is used, for instance, when a generic
|
||||
@@ -474,15 +597,20 @@ impl<'db> Specialization<'db> {
|
||||
type_mapping: &TypeMapping<'a, 'db>,
|
||||
visitor: &ApplyTypeMappingVisitor<'db>,
|
||||
) -> Self {
|
||||
let types: Box<[_]> = self
|
||||
.types(db)
|
||||
.iter()
|
||||
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor))
|
||||
.collect();
|
||||
let tuple_inner = self
|
||||
.tuple_inner(db)
|
||||
.and_then(|tuple| tuple.apply_type_mapping_impl(db, type_mapping, visitor));
|
||||
Specialization::new(db, self.generic_context(db), types, tuple_inner)
|
||||
let inner = match self.inner(db) {
|
||||
SpecializationInner::Tuple(tuple) => tuple
|
||||
.apply_type_mapping_impl(db, type_mapping, visitor)
|
||||
.map(SpecializationInner::Tuple)
|
||||
.unwrap_or_else(|| SpecializationInner::NonTuple(Box::from([Type::Never]))),
|
||||
SpecializationInner::NonTuple(types) => {
|
||||
let types = types
|
||||
.iter()
|
||||
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor))
|
||||
.collect();
|
||||
SpecializationInner::NonTuple(types)
|
||||
}
|
||||
};
|
||||
Specialization::new(db, self.generic_context(db), inner)
|
||||
}
|
||||
|
||||
/// Applies an optional specialization to this specialization.
|
||||
@@ -505,7 +633,13 @@ impl<'db> Specialization<'db> {
|
||||
/// Panics if the two specializations are not for the same generic context.
|
||||
pub(crate) fn combine(self, db: &'db dyn Db, other: Self) -> Self {
|
||||
let generic_context = self.generic_context(db);
|
||||
assert!(other.generic_context(db) == generic_context);
|
||||
|
||||
assert_eq!(other.generic_context(db), generic_context);
|
||||
debug_assert!(
|
||||
matches!(self.inner(db), SpecializationInner::NonTuple(_)),
|
||||
"The tuple constructor is special-cased everywhere"
|
||||
);
|
||||
|
||||
// TODO special-casing Unknown to mean "no mapping" is not right here, and can give
|
||||
// confusing/wrong results in cases where there was a mapping found for a typevar, and it
|
||||
// was of type Unknown. We should probably add a bitset or similar to Specialization that
|
||||
@@ -519,44 +653,60 @@ impl<'db> Specialization<'db> {
|
||||
_ => UnionType::from_elements(db, [self_type, other_type]),
|
||||
})
|
||||
.collect();
|
||||
// TODO: Combine the tuple specs too
|
||||
Specialization::new(db, self.generic_context(db), types, None)
|
||||
|
||||
Specialization::new(
|
||||
db,
|
||||
self.generic_context(db),
|
||||
SpecializationInner::NonTuple(types),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
|
||||
let types: Box<[_]> = self
|
||||
.types(db)
|
||||
.iter()
|
||||
.map(|ty| ty.normalized_impl(db, visitor))
|
||||
.collect();
|
||||
let tuple_inner = self
|
||||
.tuple_inner(db)
|
||||
.and_then(|tuple| tuple.normalized_impl(db, visitor));
|
||||
let inner = match self.inner(db) {
|
||||
SpecializationInner::Tuple(tuple) => tuple
|
||||
.normalized_impl(db, visitor)
|
||||
.map(SpecializationInner::Tuple)
|
||||
.unwrap_or_else(|| SpecializationInner::NonTuple(Box::from([Type::Never]))),
|
||||
SpecializationInner::NonTuple(types) => {
|
||||
let types: Box<[_]> = types
|
||||
.iter()
|
||||
.map(|ty| ty.normalized_impl(db, visitor))
|
||||
.collect();
|
||||
SpecializationInner::NonTuple(types)
|
||||
}
|
||||
};
|
||||
|
||||
let context = self.generic_context(db).normalized_impl(db, visitor);
|
||||
Self::new(db, context, types, tuple_inner)
|
||||
Self::new(db, context, inner)
|
||||
}
|
||||
|
||||
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||
let types: Box<[_]> = self
|
||||
.generic_context(db)
|
||||
.variables(db)
|
||||
.into_iter()
|
||||
.zip(self.types(db))
|
||||
.map(|(bound_typevar, vartype)| {
|
||||
let variance = match bound_typevar.typevar(db).variance(db) {
|
||||
TypeVarVariance::Invariant => TypeVarVariance::Invariant,
|
||||
TypeVarVariance::Covariant => variance,
|
||||
TypeVarVariance::Contravariant => variance.flip(),
|
||||
TypeVarVariance::Bivariant => unreachable!(),
|
||||
};
|
||||
vartype.materialize(db, variance)
|
||||
})
|
||||
.collect();
|
||||
let tuple_inner = self.tuple_inner(db).and_then(|tuple| {
|
||||
// Tuples are immutable, so tuple element types are always in covariant position.
|
||||
tuple.materialize(db, variance)
|
||||
});
|
||||
Specialization::new(db, self.generic_context(db), types, tuple_inner)
|
||||
let inner = match self.inner(db) {
|
||||
SpecializationInner::Tuple(tuple) => SpecializationInner::Tuple(
|
||||
tuple
|
||||
.materialize(db, variance)
|
||||
.unwrap_or_else(|| TupleType::empty(db)),
|
||||
),
|
||||
SpecializationInner::NonTuple(types) => {
|
||||
let types: Box<[_]> = self
|
||||
.generic_context(db)
|
||||
.variables(db)
|
||||
.zip(types)
|
||||
.map(|(bound_typevar, vartype)| {
|
||||
let variance = match bound_typevar.typevar(db).variance(db) {
|
||||
TypeVarVariance::Invariant => TypeVarVariance::Invariant,
|
||||
TypeVarVariance::Covariant => variance,
|
||||
TypeVarVariance::Contravariant => variance.flip(),
|
||||
TypeVarVariance::Bivariant => unreachable!(),
|
||||
};
|
||||
vartype.materialize(db, variance)
|
||||
})
|
||||
.collect();
|
||||
SpecializationInner::NonTuple(types)
|
||||
}
|
||||
};
|
||||
|
||||
Specialization::new(db, self.generic_context(db), inner)
|
||||
}
|
||||
|
||||
pub(crate) fn has_relation_to_impl(
|
||||
@@ -571,12 +721,14 @@ impl<'db> Specialization<'db> {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db))
|
||||
if let (SpecializationInner::Tuple(left), SpecializationInner::Tuple(right)) =
|
||||
(self.inner(db), other.inner(db))
|
||||
{
|
||||
return self_tuple.has_relation_to_impl(db, other_tuple, relation, visitor);
|
||||
return left.has_relation_to_impl(db, *right, relation, visitor);
|
||||
}
|
||||
|
||||
for ((bound_typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
|
||||
for ((bound_typevar, self_type), other_type) in generic_context
|
||||
.variables(db)
|
||||
.zip(self.types(db))
|
||||
.zip(other.types(db))
|
||||
{
|
||||
@@ -623,7 +775,8 @@ impl<'db> Specialization<'db> {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ((bound_typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
|
||||
for ((bound_typevar, self_type), other_type) in generic_context
|
||||
.variables(db)
|
||||
.zip(self.types(db))
|
||||
.zip(other.types(db))
|
||||
{
|
||||
@@ -644,17 +797,14 @@ impl<'db> Specialization<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
match (self.tuple_inner(db), other.tuple_inner(db)) {
|
||||
(Some(_), None) | (None, Some(_)) => return false,
|
||||
(None, None) => {}
|
||||
(Some(self_tuple), Some(other_tuple)) => {
|
||||
if !self_tuple.is_equivalent_to(db, other_tuple) {
|
||||
return false;
|
||||
}
|
||||
match (self.inner(db), other.inner(db)) {
|
||||
(SpecializationInner::Tuple(_), SpecializationInner::NonTuple(_))
|
||||
| (SpecializationInner::NonTuple(_), SpecializationInner::Tuple(_)) => false,
|
||||
(SpecializationInner::NonTuple(_), SpecializationInner::NonTuple(_)) => true,
|
||||
(SpecializationInner::Tuple(self_tuple), SpecializationInner::Tuple(other_tuple)) => {
|
||||
self_tuple.is_equivalent_to(db, *other_tuple)
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn find_legacy_typevars(
|
||||
@@ -700,11 +850,19 @@ impl<'db> PartialSpecialization<'_, 'db> {
|
||||
db: &'db dyn Db,
|
||||
bound_typevar: BoundTypeVarInstance<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
let index = self
|
||||
.generic_context
|
||||
.variables(db)
|
||||
.get_index_of(&bound_typevar)?;
|
||||
self.types.get(index).copied()
|
||||
match self.generic_context.inner(db) {
|
||||
GenericContextInner::Tuple { single_typevar } => {
|
||||
if bound_typevar == *single_typevar {
|
||||
self.types.first().copied()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
GenericContextInner::NonTuple(variables) => {
|
||||
let index = variables.get_index_of(&bound_typevar)?;
|
||||
self.types.get(index).copied()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_owned(&self) -> PartialSpecialization<'db, 'db> {
|
||||
@@ -749,18 +907,30 @@ impl<'db> SpecializationBuilder<'db> {
|
||||
}
|
||||
|
||||
pub(crate) fn build(&mut self, generic_context: GenericContext<'db>) -> Specialization<'db> {
|
||||
let types: Box<[_]> = generic_context
|
||||
.variables(self.db)
|
||||
.iter()
|
||||
.map(|variable| {
|
||||
self.types
|
||||
.get(variable)
|
||||
let inner = match generic_context.inner(self.db) {
|
||||
GenericContextInner::Tuple { single_typevar } => {
|
||||
let ty = self
|
||||
.types
|
||||
.get(single_typevar)
|
||||
.copied()
|
||||
.unwrap_or(variable.default_type(self.db).unwrap_or(Type::unknown()))
|
||||
})
|
||||
.collect();
|
||||
// TODO Infer the tuple spec for a tuple type
|
||||
Specialization::new(self.db, generic_context, types, None)
|
||||
.unwrap_or_else(Type::unknown);
|
||||
SpecializationInner::Tuple(TupleType::homogeneous(self.db, ty))
|
||||
}
|
||||
GenericContextInner::NonTuple(variables) => {
|
||||
let types: Box<[_]> = variables
|
||||
.iter()
|
||||
.map(|bound_typevar| {
|
||||
self.types.get(bound_typevar).copied().unwrap_or(
|
||||
bound_typevar
|
||||
.default_type(self.db)
|
||||
.unwrap_or_else(Type::unknown),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
SpecializationInner::NonTuple(types)
|
||||
}
|
||||
};
|
||||
Specialization::new(self.db, generic_context, inner)
|
||||
}
|
||||
|
||||
fn add_type_mapping(&mut self, bound_typevar: BoundTypeVarInstance<'db>, ty: Type<'db>) {
|
||||
|
||||
@@ -110,7 +110,7 @@ use crate::types::enums::is_enum_class;
|
||||
use crate::types::function::{
|
||||
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
|
||||
};
|
||||
use crate::types::generics::{GenericContext, bind_typevar};
|
||||
use crate::types::generics::{GenericContext, GenericContextInner, bind_typevar};
|
||||
use crate::types::instance::SliceLiteral;
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::signatures::{CallableSignature, Signature};
|
||||
@@ -9061,7 +9061,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
typevars.map(|typevars| GenericContext::new(self.db(), typevars))
|
||||
typevars
|
||||
.map(|typevars| GenericContext::new(self.db(), GenericContextInner::NonTuple(typevars)))
|
||||
}
|
||||
|
||||
fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> {
|
||||
|
||||
@@ -131,16 +131,6 @@ pub struct TupleType<'db> {
|
||||
pub(crate) tuple: TupleSpec<'db>,
|
||||
}
|
||||
|
||||
pub(super) fn walk_tuple_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>(
|
||||
db: &'db dyn Db,
|
||||
tuple: TupleType<'db>,
|
||||
visitor: &V,
|
||||
) {
|
||||
for element in tuple.tuple(db).all_elements() {
|
||||
visitor.visit_type(db, *element);
|
||||
}
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
impl get_size2::GetSize for TupleType<'_> {}
|
||||
|
||||
@@ -206,10 +196,9 @@ impl<'db> TupleType<'db> {
|
||||
|
||||
tuple_class.apply_specialization(db, |generic_context| {
|
||||
if generic_context.variables(db).len() == 1 {
|
||||
let element_type = self.tuple(db).homogeneous_element_type(db);
|
||||
generic_context.specialize_tuple(db, element_type, self)
|
||||
generic_context.specialize_tuple(db, self)
|
||||
} else {
|
||||
generic_context.default_specialization(db, Some(KnownClass::Tuple))
|
||||
generic_context.default_specialization(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -290,9 +279,9 @@ fn to_class_type_cycle_initial<'db>(db: &'db dyn Db, self_: TupleType<'db>) -> C
|
||||
|
||||
tuple_class.apply_specialization(db, |generic_context| {
|
||||
if generic_context.variables(db).len() == 1 {
|
||||
generic_context.specialize_tuple(db, Type::Never, self_)
|
||||
generic_context.specialize_tuple(db, self_)
|
||||
} else {
|
||||
generic_context.default_specialization(db, Some(KnownClass::Tuple))
|
||||
generic_context.default_specialization(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user