[ty] Find member references across files (#28669)

This commit is contained in:
Micha Reiser authored and GitHub committed 2026-09-17 20:47:09 +02:00
1 parent ebd8ad3158
commit b61c8b0357
3 files changed
+139 -18

No files matched your search

+70
View File
@@ -1359,6 +1359,76 @@ def process_model():
");
}
#[test]
fn multi_file_instance_attribute_references() {
let test = CursorTest::builder()
.source(
"lib.py",
r#"
class C:
def __init__(self) -> None:
self.<CURSOR>value: int = 42
"#,
)
.source(
"main.py",
r#"
from lib import C
print(C().value)
"#,
)
.build();
assert_snapshot!(test.references(), @"
info[references]: Found 2 references
--> lib.py:4:14
|
4 | self.value: int = 42
| -----
|
::: main.py:4:11
|
4 | print(C().value)
| -----
");
}
#[test]
fn multi_file_comprehension_attribute_references() {
let test = CursorTest::builder()
.source(
"lib.py",
r#"
class C:
def __init__(self) -> None:
[None for self.<CURSOR>value in [42]]
"#,
)
.source(
"main.py",
r#"
from lib import C
print(C().value)
"#,
)
.build();
assert_snapshot!(test.references(), @"
info[references]: Found 2 references
--> lib.py:4:24
|
4 | [None for self.value in [42]]
| -----
|
::: main.py:4:11
|
4 | print(C().value)
| -----
");
}
#[test]
fn multi_file_parameter_references_include_keyword_argument_labels() {
let test = CursorTest::builder()
+28 -18
View File
@@ -4,11 +4,11 @@
//! symbol's name, this is a "semantic search" where the text and the semantic
//! meaning must match.
//!
//! Some symbols (such as parameters and local variables) are visible only
//! within their scope. All other symbols, such as those defined at the global
//! scope or within classes, are visible outside the module. Finding
//! all references to these externally-visible symbols therefore requires
//! an expensive search of all source files in the workspace.
//! Some symbols (local variables) are visible only within their scope/file.
//! Other symbols can have references in other modules, requiring an expensive cross-module
//! search.
//! * Members can be visible outside the module, even when assigned inside a function.
//! * Parameters can also have cross-file references through keyword argument labels.
use crate::goto::{Definitions, GotoTarget};
use crate::{Db, ReferenceKind, ReferenceTarget};
@@ -330,20 +330,30 @@ pub(crate) fn has_any_external_visible_definitions(
definitions: &Definitions<'_>,
) -> bool {
definitions.iter().any(|definition| match definition {
ResolvedDefinition::Definition(definition) => match definition.scope(db).scope(db).kind() {
ScopeKind::Module | ScopeKind::Class => true,
ScopeKind::Comprehension => {
matches!(definition.kind(db), DefinitionKind::NamedExpression(_))
&& definition.place(db).as_symbol().is_some_and(|symbol_id| {
ty_python_core::semantic_index(db, definition.program_file(db))
.symbol_resolves_to_global_scope(symbol_id, definition.file_scope(db))
})
ResolvedDefinition::Definition(definition) => {
// A member's accessibility does not depend on the scope of its assignment.
if definition.place(db).is_member() {
return true;
}
ScopeKind::TypeParams
| ScopeKind::Function
| ScopeKind::Lambda
| ScopeKind::TypeAlias => false,
},
match definition.scope(db).scope(db).kind() {
ScopeKind::Module | ScopeKind::Class => true,
ScopeKind::Comprehension => {
matches!(definition.kind(db), DefinitionKind::NamedExpression(_))
&& definition.place(db).as_symbol().is_some_and(|symbol_id| {
ty_python_core::semantic_index(db, definition.program_file(db))
.symbol_resolves_to_global_scope(
symbol_id,
definition.file_scope(db),
)
})
}
ScopeKind::TypeParams
| ScopeKind::Function
| ScopeKind::Lambda
| ScopeKind::TypeAlias => false,
}
}
ResolvedDefinition::Module(_) | ResolvedDefinition::FileWithRange(_) => true,
})
}
+41
View File
@@ -2760,6 +2760,47 @@ DC(f=1)
"#);
}
#[test]
fn multi_file_attribute_rename_updates_slots() {
let test = CursorTest::builder()
.source(
"lib.py",
r#"
class Box:
__slots__ = ("value",)
def __init__(self) -> None:
self.value = 42
"#,
)
.source(
"main.py",
r#"
from lib import Box
print(Box().<CURSOR>value)
"#,
)
.build();
assert_snapshot!(test.rename("count"), @r#"
info[rename]: Rename symbol (found 3 locations)
--> main.py:4:13
|
4 | print(Box().value)
| ^^^^^
|
::: lib.py:3:19
|
3 | __slots__ = ("value",)
| -----
4 |
5 | def __init__(self) -> None:
6 | self.value = 42
| -----
"#);
}
#[test]
fn rename_attribute_updates_slots_tuple() {
let test = cursor_test(