This commit is contained in:
Aria Desires 2025-12-13 21:38:24 -05:00
parent a16dc52990
commit ff5241d19a
2 changed files with 128 additions and 56 deletions

View File

@ -29,17 +29,11 @@ pub fn code_actions(
let mut actions = Vec::new(); let mut actions = Vec::new();
// Suggest imports for unresolved references (often ideal) // Suggest imports/qualifications for unresolved references (often ideal)
// TODO: suggest qualifying with an already imported symbol
let is_unresolved_reference = let is_unresolved_reference =
lint_id == LintId::of(&UNRESOLVED_REFERENCE) || lint_id == LintId::of(&UNDEFINED_REVEAL); lint_id == LintId::of(&UNRESOLVED_REFERENCE) || lint_id == LintId::of(&UNDEFINED_REVEAL);
if is_unresolved_reference if is_unresolved_reference
&& let Some(import_quick_fix) = create_import_symbol_quick_fix(db, file, diagnostic_range) && let Some(import_quick_fix) = unresolved_fixes(db, file, diagnostic_range)
{
actions.extend(import_quick_fix);
}
if is_unresolved_reference
&& let Some(import_quick_fix) = create_qualify_symbol_quick_fix(db, file, diagnostic_range)
{ {
actions.extend(import_quick_fix); actions.extend(import_quick_fix);
} }
@ -54,7 +48,7 @@ pub fn code_actions(
actions actions
} }
fn create_import_symbol_quick_fix( fn unresolved_fixes(
db: &dyn Db, db: &dyn Db,
file: File, file: File,
diagnostic_range: TextRange, diagnostic_range: TextRange,
@ -64,27 +58,7 @@ fn create_import_symbol_quick_fix(
let symbol = &node.expr_name()?.id; let symbol = &node.expr_name()?.id;
Some( Some(
completion::missing_imports(db, file, &parsed, symbol, node) completion::unresolved_fixes(db, file, &parsed, symbol, node)
.into_iter()
.map(|import| QuickFix {
title: import.label,
edits: vec![import.edit],
preferred: true,
}),
)
}
fn create_qualify_symbol_quick_fix(
db: &dyn Db,
file: File,
diagnostic_range: TextRange,
) -> Option<impl Iterator<Item = QuickFix>> {
let parsed = parsed_module(db, file).load(db);
let node = covering_node(parsed.syntax().into(), diagnostic_range).node();
let symbol = &node.expr_name()?.id;
Some(
completion::missing_qualifications(db, file, &parsed, symbol, node)
.into_iter() .into_iter()
.map(|import| QuickFix { .map(|import| QuickFix {
title: import.label, title: import.label,
@ -503,6 +477,108 @@ mod tests {
"); ");
} }
#[test]
fn unresolved_deprecated() {
let test = CodeActionTest::with_source(
r#"
@<START>deprecated<END>("do not use")
def my_func(): ...
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
info[code-action]: import warnings.deprecated
--> main.py:2:14
|
2 | @deprecated("do not use")
| ^^^^^^^^^^
3 | def my_func(): ...
|
help: This is a preferred code action
1 + from warnings import deprecated
2 |
3 | @deprecated("do not use")
4 | def my_func(): ...
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:2:14
|
2 | @deprecated("do not use")
| ^^^^^^^^^^
3 | def my_func(): ...
|
1 |
- @deprecated("do not use")
2 + @deprecated("do not use") # ty:ignore[unresolved-reference]
3 | def my_func(): ...
4 |
"#);
}
#[test]
fn unresolved_deprecated_warnings_imported() {
let test = CodeActionTest::with_source(
r#"
import warnings
@<START>deprecated<END>("do not use")
def my_func(): ...
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
info[code-action]: import warnings.deprecated
--> main.py:4:14
|
2 | import warnings
3 |
4 | @deprecated("do not use")
| ^^^^^^^^^^
5 | def my_func(): ...
|
help: This is a preferred code action
1 + from warnings import deprecated
2 |
3 | import warnings
4 |
info[code-action]: qualify warnings.deprecated
--> main.py:4:14
|
2 | import warnings
3 |
4 | @deprecated("do not use")
| ^^^^^^^^^^
5 | def my_func(): ...
|
help: This is a preferred code action
1 |
2 | import warnings
3 |
- @deprecated("do not use")
4 + @warnings.deprecated("do not use")
5 | def my_func(): ...
6 |
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:4:14
|
2 | import warnings
3 |
4 | @deprecated("do not use")
| ^^^^^^^^^^
5 | def my_func(): ...
|
1 |
2 | import warnings
3 |
- @deprecated("do not use")
4 + @deprecated("do not use") # ty:ignore[unresolved-reference]
5 | def my_func(): ...
6 |
"#);
}
pub(super) struct CodeActionTest { pub(super) struct CodeActionTest {
pub(super) db: ty_project::TestDb, pub(super) db: ty_project::TestDb,
pub(super) file: File, pub(super) file: File,

View File

@ -67,6 +67,7 @@ impl<'db> Completions<'db> {
self.items self.items
} }
// Convert this collection into a list of "import..." fixes
fn into_imports(mut self) -> Vec<ImportEdit> { fn into_imports(mut self) -> Vec<ImportEdit> {
self.items.sort_by(compare_suggestions); self.items.sort_by(compare_suggestions);
self.items self.items
@ -82,25 +83,24 @@ impl<'db> Completions<'db> {
.collect() .collect()
} }
fn into_qualified(mut self, range: TextRange) -> Vec<ImportEdit> { // Convert this collection into a list of "qualify..." fixes
fn into_qualifications(mut self, range: TextRange) -> Vec<ImportEdit> {
self.items.sort_by(compare_suggestions); self.items.sort_by(compare_suggestions);
self.items self.items
.dedup_by(|c1, c2| (&c1.name, c1.module_name) == (&c2.name, c2.module_name)); .dedup_by(|c1, c2| (&c1.name, c1.module_name) == (&c2.name, c2.module_name));
self.items self.items
.into_iter() .into_iter()
.filter_map(|item| { .filter_map(|item| {
if item.import.is_none() { // If we would have to actually import something, don't suggest the qualification
Some(ImportEdit { // (we could, maybe we should, but for now, we don't)
label: format!("qualify {}", item.insert.as_ref()?), if item.import.is_some() {
edit: Edit::replacement( return None;
item.insert?.into_string(),
range.start(),
range.end(),
),
})
} else {
None
} }
Some(ImportEdit {
label: format!("qualify {}", item.insert.as_ref()?),
edit: Edit::replacement(item.insert?.into_string(), range.start(), range.end()),
})
}) })
.collect() .collect()
} }
@ -578,15 +578,19 @@ pub(crate) struct ImportEdit {
pub edit: Edit, pub edit: Edit,
} }
pub(crate) fn missing_imports( /// Get fixes that would resolve an unresolved reference
pub(crate) fn unresolved_fixes(
db: &dyn Db, db: &dyn Db,
file: File, file: File,
parsed: &ParsedModuleRef, parsed: &ParsedModuleRef,
symbol: &str, symbol: &str,
node: AnyNodeRef, node: AnyNodeRef,
) -> Vec<ImportEdit> { ) -> Vec<ImportEdit> {
let mut completions = Completions::exactly(db, symbol); let mut results = Vec::new();
let scoped = ScopedTarget { node }; let scoped = ScopedTarget { node };
// Request imports we could add to put the symbol in scope
let mut completions = Completions::exactly(db, symbol);
add_unimported_completions( add_unimported_completions(
db, db,
file, file,
@ -597,19 +601,10 @@ pub(crate) fn missing_imports(
}, },
&mut completions, &mut completions,
); );
results.extend(completions.into_imports());
completions.into_imports() // Request qualifications we could apply to the symbol to make it resolve
}
pub(crate) fn missing_qualifications(
db: &dyn Db,
file: File,
parsed: &ParsedModuleRef,
symbol: &str,
node: AnyNodeRef,
) -> Vec<ImportEdit> {
let mut completions = Completions::exactly(db, symbol); let mut completions = Completions::exactly(db, symbol);
let scoped = ScopedTarget { node };
add_unimported_completions( add_unimported_completions(
db, db,
file, file,
@ -620,8 +615,9 @@ pub(crate) fn missing_qualifications(
}, },
&mut completions, &mut completions,
); );
results.extend(completions.into_qualifications(node.range()));
completions.into_qualified(node.range()) results
} }
/// Adds completions derived from keywords. /// Adds completions derived from keywords.