From cccb0bbaa41e95a1efbe997ebe8119454f9b93f9 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Thu, 4 Dec 2025 10:46:23 -0500 Subject: [PATCH] [ty] Add tests for implicit submodule references (#21793) ## Summary I realized we don't really test `DefinitionKind::ImportFromSubmodule` in the IDE at all, so here's a bunch of them, just recording our current behaviour. ## Test Plan *stares at the camera* --- crates/ty_ide/src/find_references.rs | 207 +++++++++++++++ crates/ty_ide/src/goto_declaration.rs | 292 ++++++++++++++++++++++ crates/ty_ide/src/goto_type_definition.rs | 277 ++++++++++++++++++++ crates/ty_ide/src/hover.rs | 291 +++++++++++++++++++++ crates/ty_ide/src/rename.rs | 203 +++++++++++++++ 5 files changed, 1270 insertions(+) diff --git a/crates/ty_ide/src/find_references.rs b/crates/ty_ide/src/find_references.rs index 5b5c3e4f43..48cbfaf9cf 100644 --- a/crates/ty_ide/src/find_references.rs +++ b/crates/ty_ide/src/find_references.rs @@ -1906,4 +1906,211 @@ func_alias() | "); } + + #[test] + fn references_submodule_import_from_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = subpkg + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // TODO(submodule-imports): this should light up both instances of `subpkg` + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg.submod import val + 3 | + 4 | x = subpkg + | ^^^^^^ + | + "); + } + + #[test] + fn references_submodule_import_from_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = subpkg + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // TODO(submodule-imports): this should light up both instances of `subpkg` + assert_snapshot!(test.references(), @"No references found"); + } + + #[test] + fn references_submodule_import_from_wrong_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = submod + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // No references is actually correct (or it should only see itself) + assert_snapshot!(test.references(), @"No references found"); + } + + #[test] + fn references_submodule_import_from_wrong_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = submod + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // No references is actually correct (or it should only see itself) + assert_snapshot!(test.references(), @"No references found"); + } + + #[test] + fn references_submodule_import_from_confusing_shadowed_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // No references is actually correct (or it should only see itself) + assert_snapshot!(test.references(), @"No references found"); + } + + #[test] + fn references_submodule_import_from_confusing_real_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> mypackage/__init__.py:2:21 + | + 2 | from .subpkg import subpkg + | ^^^^^^ + 3 | + 4 | x = subpkg + | + + info[references]: Reference 2 + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg import subpkg + 3 | + 4 | x = subpkg + | ^^^^^^ + | + + info[references]: Reference 3 + --> mypackage/subpkg/__init__.py:2:1 + | + 2 | subpkg: int = 10 + | ^^^^^^ + | + "); + } + + #[test] + fn references_submodule_import_from_confusing_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // TODO: this should also highlight the RHS subpkg in the import + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg import subpkg + 3 | + 4 | x = subpkg + | ^^^^^^ + | + "); + } } diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index 02b329f88e..a2f147b2d3 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -2602,6 +2602,298 @@ def ab(a: int, *, c: int): ... "); } + #[test] + fn goto_declaration_submodule_import_from_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = subpkg + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // TODO(submodule-imports): this should only highlight `subpkg` in the import statement + // This happens because DefinitionKind::ImportFromSubmodule claims the entire ImportFrom node, + // which is correct but unhelpful. Unfortunately even if it only claimed the LHS identifier it + // would highlight `subpkg.submod` which is strictly better but still isn't what we want. + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> mypackage/__init__.py:2:1 + | + 2 | from .subpkg.submod import val + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 | + 4 | x = subpkg + | + info: Source + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg.submod import val + 3 | + 4 | x = subpkg + | ^^^^^^ + | + "); + } + + #[test] + fn goto_declaration_submodule_import_from_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = subpkg + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // TODO(submodule-imports): I don't *think* this is what we want..? + // It's a bit confusing because this symbol is essentially the LHS *and* RHS of + // `subpkg = mypackage.subpkg`. As in, it's both defining a local `subpkg` and + // loading the module `mypackage.subpkg`, so, it's understandable to get confused! + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> mypackage/subpkg/__init__.py:1:1 + | + | + info: Source + --> mypackage/__init__.py:2:7 + | + 2 | from .subpkg.submod import val + | ^^^^^^ + 3 | + 4 | x = subpkg + | + "); + } + + #[test] + fn goto_declaration_submodule_import_from_wrong_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = submod + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // No result is correct! + assert_snapshot!(test.goto_declaration(), @"No goto target found"); + } + + #[test] + fn goto_declaration_submodule_import_from_wrong_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = submod + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // Going to the submod module is correct! + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> mypackage/subpkg/submod.py:1:1 + | + 1 | + | ^ + 2 | val: int = 0 + | + info: Source + --> mypackage/__init__.py:2:14 + | + 2 | from .subpkg.submod import val + | ^^^^^^ + 3 | + 4 | x = submod + | + "); + } + + #[test] + fn goto_declaration_submodule_import_from_confusing_shadowed_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // Going to the subpkg module is correct! + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> mypackage/subpkg/__init__.py:1:1 + | + 1 | + | ^ + 2 | subpkg: int = 10 + | + info: Source + --> mypackage/__init__.py:2:7 + | + 2 | from .subpkg import subpkg + | ^^^^^^ + 3 | + 4 | x = subpkg + | + "); + } + + #[test] + fn goto_declaration_submodule_import_from_confusing_real_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // Going to the subpkg `int` is correct! + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> mypackage/subpkg/__init__.py:2:1 + | + 2 | subpkg: int = 10 + | ^^^^^^ + | + info: Source + --> mypackage/__init__.py:2:21 + | + 2 | from .subpkg import subpkg + | ^^^^^^ + 3 | + 4 | x = subpkg + | + "); + } + + #[test] + fn goto_declaration_submodule_import_from_confusing_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // TODO(submodule-imports): Ok this one is FASCINATING and it's kinda right but confusing! + // + // So there's 3 relevant definitions here: + // + // * `subpkg: int = 10` in the other file is in fact the original definition + // + // * the LHS `subpkg` in the import is an instance of `subpkg = ...` + // because it's a `DefinitionKind::ImportFromSubmodle`. + // This is the span that covers the entire import. + // + // * `the RHS `subpkg` in the import is a second instance of `subpkg = ...` + // that *immediately* overwrites the `ImportFromSubmodule`'s definition + // This span seemingly doesn't appear at all!? Is it getting hidden by the LHS span? + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> mypackage/__init__.py:2:1 + | + 2 | from .subpkg import subpkg + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 | + 4 | x = subpkg + | + info: Source + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg import subpkg + 3 | + 4 | x = subpkg + | ^^^^^^ + | + + info[goto-declaration]: Declaration + --> mypackage/subpkg/__init__.py:2:1 + | + 2 | subpkg: int = 10 + | ^^^^^^ + | + info: Source + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg import subpkg + 3 | + 4 | x = subpkg + | ^^^^^^ + | + "); + } + impl CursorTest { fn goto_declaration(&self) -> String { let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset) diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index b611eac28a..fc5aa9aded 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -1672,6 +1672,283 @@ def function(): "#); } + #[test] + fn goto_type_submodule_import_from_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = subpkg + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // The module is the correct type definition + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> mypackage/subpkg/__init__.py:1:1 + | + | + info: Source + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg.submod import val + 3 | + 4 | x = subpkg + | ^^^^^^ + | + "); + } + + #[test] + fn goto_type_submodule_import_from_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = subpkg + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // The module is the correct type definition + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> mypackage/subpkg/__init__.py:1:1 + | + | + info: Source + --> mypackage/__init__.py:2:7 + | + 2 | from .subpkg.submod import val + | ^^^^^^ + 3 | + 4 | x = subpkg + | + "); + } + + #[test] + fn goto_type_submodule_import_from_wrong_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = submod + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // Unknown is correct, `submod` is not in scope + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> stdlib/ty_extensions.pyi:20:1 + | + 19 | # Types + 20 | Unknown = object() + | ^^^^^^^ + 21 | AlwaysTruthy = object() + 22 | AlwaysFalsy = object() + | + info: Source + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg.submod import val + 3 | + 4 | x = submod + | ^^^^^^ + | + "); + } + + #[test] + fn goto_type_submodule_import_from_wrong_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = submod + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // The module is correct + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> mypackage/subpkg/submod.py:1:1 + | + 1 | / + 2 | | val: int = 0 + | |_____________^ + | + info: Source + --> mypackage/__init__.py:2:14 + | + 2 | from .subpkg.submod import val + | ^^^^^^ + 3 | + 4 | x = submod + | + "); + } + + #[test] + fn goto_type_submodule_import_from_confusing_shadowed_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // The module is correct + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> mypackage/subpkg/__init__.py:1:1 + | + 1 | / + 2 | | subpkg: int = 10 + | |_________________^ + | + info: Source + --> mypackage/__init__.py:2:7 + | + 2 | from .subpkg import subpkg + | ^^^^^^ + 3 | + 4 | x = subpkg + | + "); + } + + #[test] + fn goto_type_submodule_import_from_confusing_real_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // `int` is correct + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | ^^^ + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | + info: Source + --> mypackage/__init__.py:2:21 + | + 2 | from .subpkg import subpkg + | ^^^^^^ + 3 | + 4 | x = subpkg + | + "#); + } + + #[test] + fn goto_type_submodule_import_from_confusing_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // `int` is correct + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | ^^^ + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | + info: Source + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg import subpkg + 3 | + 4 | x = subpkg + | ^^^^^^ + | + "#); + } + impl CursorTest { fn goto_type_definition(&self) -> String { let Some(targets) = diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index e9d913570c..affa6054e2 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -3321,6 +3321,297 @@ def function(): "); } + #[test] + fn hover_submodule_import_from_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = subpkg + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // The module is correct + assert_snapshot!(test.hover(), @r" + + --------------------------------------------- + ```python + + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg.submod import val + 3 | + 4 | x = subpkg + | ^^^-^^ + | | | + | | Cursor offset + | source + | + "); + } + + #[test] + fn hover_submodule_import_from_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = subpkg + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // The module is correct + assert_snapshot!(test.hover(), @r" + + --------------------------------------------- + ```python + + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> mypackage/__init__.py:2:7 + | + 2 | from .subpkg.submod import val + | ^^^-^^ + | | | + | | Cursor offset + | source + 3 | + 4 | x = subpkg + | + "); + } + + #[test] + fn hover_submodule_import_from_wrong_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = submod + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // Unknown is correct + assert_snapshot!(test.hover(), @r" + Unknown + --------------------------------------------- + ```python + Unknown + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg.submod import val + 3 | + 4 | x = submod + | ^^^-^^ + | | | + | | Cursor offset + | source + | + "); + } + + #[test] + fn hover_submodule_import_from_wrong_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = submod + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // The submodule is correct + assert_snapshot!(test.hover(), @r" + + --------------------------------------------- + ```python + + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> mypackage/__init__.py:2:14 + | + 2 | from .subpkg.submod import val + | ^^^-^^ + | | | + | | Cursor offset + | source + 3 | + 4 | x = submod + | + "); + } + + #[test] + fn hover_submodule_import_from_confusing_shadowed_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // The module is correct + assert_snapshot!(test.hover(), @r" + + --------------------------------------------- + ```python + + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> mypackage/__init__.py:2:7 + | + 2 | from .subpkg import subpkg + | ^^^-^^ + | | | + | | Cursor offset + | source + 3 | + 4 | x = subpkg + | + "); + } + + #[test] + fn hover_submodule_import_from_confusing_real_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // int is correct + assert_snapshot!(test.hover(), @r" + int + --------------------------------------------- + ```python + int + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> mypackage/__init__.py:2:21 + | + 2 | from .subpkg import subpkg + | ^^^-^^ + | | | + | | Cursor offset + | source + 3 | + 4 | x = subpkg + | + "); + } + + #[test] + fn hover_submodule_import_from_confusing_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // int is correct + assert_snapshot!(test.hover(), @r" + int + --------------------------------------------- + ```python + int + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg import subpkg + 3 | + 4 | x = subpkg + | ^^^-^^ + | | | + | | Cursor offset + | source + | + "); + } + impl CursorTest { fn hover(&self) -> String { use std::fmt::Write; diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index 62f1c9f1e4..10ef9c197f 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -1223,4 +1223,207 @@ result = func(10, y=20) assert_snapshot!(test.rename("z"), @"Cannot rename"); } + + #[test] + fn rename_submodule_import_from_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = subpkg + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // TODO(submodule-imports): we should refuse to rename this (it's the name of a module) + assert_snapshot!(test.rename("mypkg"), @r" + info[rename]: Rename symbol (found 1 locations) + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg.submod import val + 3 | + 4 | x = subpkg + | ^^^^^^ + | + "); + } + + #[test] + fn rename_submodule_import_from_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = subpkg + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // Refusing to rename is correct + assert_snapshot!(test.rename("mypkg"), @"Cannot rename"); + } + + #[test] + fn rename_submodule_import_from_wrong_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = submod + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // Refusing to rename is good/fine here, it's an undefined reference + assert_snapshot!(test.rename("mypkg"), @"Cannot rename"); + } + + #[test] + fn rename_submodule_import_from_wrong_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg.submod import val + + x = submod + "#, + ) + .source("mypackage/subpkg/__init__.py", r#""#) + .source( + "mypackage/subpkg/submod.py", + r#" + val: int = 0 + "#, + ) + .build(); + + // Refusing to rename is good here, it's a module name + assert_snapshot!(test.rename("mypkg"), @"Cannot rename"); + } + + #[test] + fn rename_submodule_import_from_confusing_shadowed_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // Refusing to rename is good here, it's the name of a module + assert_snapshot!(test.rename("mypkg"), @"Cannot rename"); + } + + #[test] + fn rename_submodule_import_from_confusing_real_def() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // Renaming the integer is correct + assert_snapshot!(test.rename("mypkg"), @r" + info[rename]: Rename symbol (found 3 locations) + --> mypackage/__init__.py:2:21 + | + 2 | from .subpkg import subpkg + | ^^^^^^ + 3 | + 4 | x = subpkg + | ------ + | + ::: mypackage/subpkg/__init__.py:2:1 + | + 2 | subpkg: int = 10 + | ------ + | + "); + } + + #[test] + fn rename_submodule_import_from_confusing_use() { + let test = CursorTest::builder() + .source( + "mypackage/__init__.py", + r#" + from .subpkg import subpkg + + x = subpkg + "#, + ) + .source( + "mypackage/subpkg/__init__.py", + r#" + subpkg: int = 10 + "#, + ) + .build(); + + // TODO(submodule-imports): this is incorrect, we should rename the `subpkg` int + // and the RHS of the import statement (but *not* rename the LHS). + // + // However us being cautious here *would* be good as the rename will actually + // result in a `subpkg` variable still existing in this code, as the import's LHS + // `DefinitionKind::ImportFromSubmodule` would stop being overwritten by the RHS! + assert_snapshot!(test.rename("mypkg"), @r" + info[rename]: Rename symbol (found 1 locations) + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg import subpkg + 3 | + 4 | x = subpkg + | ^^^^^^ + | + "); + } }