diff options
| author | Lukas Wirth <lukastw97@gmail.com> | 2025-05-28 14:24:02 +0200 |
|---|---|---|
| committer | Lukas Wirth <me@lukaswirth.de> | 2025-05-29 14:55:09 +0200 |
| commit | 087cfe3c9f31b50f120d662c767ad85748192595 (patch) | |
| tree | e232a8ed3c120b3f14064aa35b077e51e4771f3e | |
| parent | f06e37674d2930bf176ebdd1be23e75fef50d6c7 (diff) | |
| download | rust-087cfe3c9f31b50f120d662c767ad85748192595.tar.gz rust-087cfe3c9f31b50f120d662c767ad85748192595.zip | |
fix: Fix import insertion not being fully cfg aware
12 files changed, 214 insertions, 128 deletions
diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/auto_import.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/auto_import.rs index d310e11011b..f3243d369a0 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/auto_import.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/auto_import.rs @@ -128,11 +128,7 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option< format!("Import `{import_name}`"), range, |builder| { - let scope = match scope.clone() { - ImportScope::File(it) => ImportScope::File(builder.make_mut(it)), - ImportScope::Module(it) => ImportScope::Module(builder.make_mut(it)), - ImportScope::Block(it) => ImportScope::Block(builder.make_mut(it)), - }; + let scope = builder.make_import_scope_mut(scope.clone()); insert_use(&scope, mod_path_to_ast(&import_path, edition), &ctx.config.insert_use); }, ); @@ -153,11 +149,7 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option< format!("Import `{import_name} as _`"), range, |builder| { - let scope = match scope.clone() { - ImportScope::File(it) => ImportScope::File(builder.make_mut(it)), - ImportScope::Module(it) => ImportScope::Module(builder.make_mut(it)), - ImportScope::Block(it) => ImportScope::Block(builder.make_mut(it)), - }; + let scope = builder.make_import_scope_mut(scope.clone()); insert_use_as_alias( &scope, mod_path_to_ast(&import_path, edition), @@ -1877,4 +1869,30 @@ fn main() { ", ); } + + #[test] + fn carries_cfg_attr() { + check_assist( + auto_import, + r#" +mod m { + pub struct S; +} + +#[cfg(test)] +fn foo(_: S$0) {} +"#, + r#" +#[cfg(test)] +use m::S; + +mod m { + pub struct S; +} + +#[cfg(test)] +fn foo(_: S) {} +"#, + ); + } } diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_bool_to_enum.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_bool_to_enum.rs index 00e9fdf124d..f73b8c4fd0f 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_bool_to_enum.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_bool_to_enum.rs @@ -312,12 +312,8 @@ fn replace_usages( } // add imports across modules where needed - if let Some((import_scope, path)) = import_data { - let scope = match import_scope { - ImportScope::File(it) => ImportScope::File(edit.make_mut(it)), - ImportScope::Module(it) => ImportScope::Module(edit.make_mut(it)), - ImportScope::Block(it) => ImportScope::Block(edit.make_mut(it)), - }; + if let Some((scope, path)) = import_data { + let scope = edit.make_import_scope_mut(scope); delayed_mutations.push((scope, path)); } }, diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs index ed8aad7b2c6..5d75e445861 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs @@ -996,7 +996,8 @@ pub struct $0Foo { } "#, r#" -pub struct Foo(#[my_custom_attr] u32); +pub struct Foo(#[my_custom_attr] +u32); "#, ); } diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs index 777e366da95..0c0b93bcfbc 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs @@ -923,7 +923,8 @@ where pub struct $0Foo(#[my_custom_attr] u32); "#, r#" -pub struct Foo { #[my_custom_attr] field1: u32 } +pub struct Foo { #[my_custom_attr] +field1: u32 } "#, ); } diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_function.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_function.rs index e977798c4fd..cf45ea0a30d 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_function.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_function.rs @@ -204,12 +204,7 @@ pub(crate) fn extract_function(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op .kind .is_some_and(|kind| matches!(kind, FlowKind::Break(_, _) | FlowKind::Continue(_))) { - let scope = match scope { - ImportScope::File(it) => ImportScope::File(builder.make_mut(it)), - ImportScope::Module(it) => ImportScope::Module(builder.make_mut(it)), - ImportScope::Block(it) => ImportScope::Block(builder.make_mut(it)), - }; - + let scope = builder.make_import_scope_mut(scope); let control_flow_enum = FamousDefs(&ctx.sema, module.krate()).core_ops_ControlFlow(); diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs index c067747bc1b..fa005a411d3 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs @@ -81,11 +81,7 @@ pub(crate) fn replace_qualified_name_with_use( |builder| { // Now that we've brought the name into scope, re-qualify all paths that could be // affected (that is, all paths inside the node we added the `use` to). - let scope = match scope { - ImportScope::File(it) => ImportScope::File(builder.make_mut(it)), - ImportScope::Module(it) => ImportScope::Module(builder.make_mut(it)), - ImportScope::Block(it) => ImportScope::Block(builder.make_mut(it)), - }; + let scope = builder.make_import_scope_mut(scope); shorten_paths(scope.as_syntax_node(), &original_path); let path = drop_generic_args(&original_path); let edition = ctx diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/unqualify_method_call.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/unqualify_method_call.rs index ebb8ef99100..1f89a3d5f17 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/unqualify_method_call.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/unqualify_method_call.rs @@ -1,4 +1,3 @@ -use ide_db::imports::insert_use::ImportScope; use syntax::{ TextRange, ast::{self, AstNode, HasArgList, prec::ExprPrecedence}, @@ -114,11 +113,7 @@ fn add_import( ); if let Some(scope) = scope { - let scope = match scope { - ImportScope::File(it) => ImportScope::File(edit.make_mut(it)), - ImportScope::Module(it) => ImportScope::Module(edit.make_mut(it)), - ImportScope::Block(it) => ImportScope::Block(edit.make_mut(it)), - }; + let scope = edit.make_import_scope_mut(scope); ide_db::imports::insert_use::insert_use(&scope, import, &ctx.config.insert_use); } } diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs index d26e5d62ced..813f38380f6 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs @@ -60,107 +60,87 @@ pub struct InsertUseConfig { } #[derive(Debug, Clone)] -pub enum ImportScope { +pub struct ImportScope { + pub kind: ImportScopeKind, + pub required_cfgs: Vec<ast::Attr>, +} + +#[derive(Debug, Clone)] +pub enum ImportScopeKind { File(ast::SourceFile), Module(ast::ItemList), Block(ast::StmtList), } impl ImportScope { - // FIXME: Remove this? - #[cfg(test)] - fn from(syntax: SyntaxNode) -> Option<Self> { - use syntax::match_ast; - fn contains_cfg_attr(attrs: &dyn HasAttrs) -> bool { - attrs.attrs().any(|attr| attr.as_simple_call().is_some_and(|(ident, _)| ident == "cfg")) - } - match_ast! { - match syntax { - ast::Module(module) => module.item_list().map(ImportScope::Module), - ast::SourceFile(file) => Some(ImportScope::File(file)), - ast::Fn(func) => contains_cfg_attr(&func).then(|| func.body().and_then(|it| it.stmt_list().map(ImportScope::Block))).flatten(), - ast::Const(konst) => contains_cfg_attr(&konst).then(|| match konst.body()? { - ast::Expr::BlockExpr(block) => Some(block), - _ => None, - }).flatten().and_then(|it| it.stmt_list().map(ImportScope::Block)), - ast::Static(statik) => contains_cfg_attr(&statik).then(|| match statik.body()? { - ast::Expr::BlockExpr(block) => Some(block), - _ => None, - }).flatten().and_then(|it| it.stmt_list().map(ImportScope::Block)), - _ => None, - - } - } - } - /// Determines the containing syntax node in which to insert a `use` statement affecting `position`. /// Returns the original source node inside attributes. pub fn find_insert_use_container( position: &SyntaxNode, sema: &Semantics<'_, RootDatabase>, ) -> Option<Self> { - fn contains_cfg_attr(attrs: &dyn HasAttrs) -> bool { - attrs.attrs().any(|attr| attr.as_simple_call().is_some_and(|(ident, _)| ident == "cfg")) - } - + // The closest block expression ancestor + let mut block = None; + let mut required_cfgs = Vec::new(); // Walk up the ancestor tree searching for a suitable node to do insertions on // with special handling on cfg-gated items, in which case we want to insert imports locally // or FIXME: annotate inserted imports with the same cfg for syntax in sema.ancestors_with_macros(position.clone()) { if let Some(file) = ast::SourceFile::cast(syntax.clone()) { - return Some(ImportScope::File(file)); - } else if let Some(item) = ast::Item::cast(syntax) { - return match item { - ast::Item::Const(konst) if contains_cfg_attr(&konst) => { - // FIXME: Instead of bailing out with None, we should note down that - // this import needs an attribute added - match sema.original_ast_node(konst)?.body()? { - ast::Expr::BlockExpr(block) => block, - _ => return None, + return Some(ImportScope { kind: ImportScopeKind::File(file), required_cfgs }); + } else if let Some(module) = ast::Module::cast(syntax.clone()) { + // early return is important here, if we can't find the original module + // in the input there is no way for us to insert an import anywhere. + return sema + .original_ast_node(module)? + .item_list() + .map(ImportScopeKind::Module) + .map(|kind| ImportScope { kind, required_cfgs }); + } else if let Some(has_attrs) = ast::AnyHasAttrs::cast(syntax) { + if block.is_none() { + if let Some(b) = ast::BlockExpr::cast(has_attrs.syntax().clone()) { + if let Some(b) = sema.original_ast_node(b) { + block = b.stmt_list(); } - .stmt_list() - .map(ImportScope::Block) } - ast::Item::Fn(func) if contains_cfg_attr(&func) => { - // FIXME: Instead of bailing out with None, we should note down that - // this import needs an attribute added - sema.original_ast_node(func)?.body()?.stmt_list().map(ImportScope::Block) - } - ast::Item::Static(statik) if contains_cfg_attr(&statik) => { - // FIXME: Instead of bailing out with None, we should note down that - // this import needs an attribute added - match sema.original_ast_node(statik)?.body()? { - ast::Expr::BlockExpr(block) => block, - _ => return None, - } - .stmt_list() - .map(ImportScope::Block) - } - ast::Item::Module(module) => { - // early return is important here, if we can't find the original module - // in the input there is no way for us to insert an import anywhere. - sema.original_ast_node(module)?.item_list().map(ImportScope::Module) + } + if has_attrs + .attrs() + .any(|attr| attr.as_simple_call().is_some_and(|(ident, _)| ident == "cfg")) + { + if let Some(b) = block { + return Some(ImportScope { + kind: ImportScopeKind::Block(b), + required_cfgs, + }); } - _ => continue, - }; + required_cfgs.extend(has_attrs.attrs().filter(|attr| { + attr.as_simple_call().is_some_and(|(ident, _)| ident == "cfg") + })); + } } } None } pub fn as_syntax_node(&self) -> &SyntaxNode { - match self { - ImportScope::File(file) => file.syntax(), - ImportScope::Module(item_list) => item_list.syntax(), - ImportScope::Block(block) => block.syntax(), + match &self.kind { + ImportScopeKind::File(file) => file.syntax(), + ImportScopeKind::Module(item_list) => item_list.syntax(), + ImportScopeKind::Block(block) => block.syntax(), } } pub fn clone_for_update(&self) -> Self { - match self { - ImportScope::File(file) => ImportScope::File(file.clone_for_update()), - ImportScope::Module(item_list) => ImportScope::Module(item_list.clone_for_update()), - ImportScope::Block(block) => ImportScope::Block(block.clone_for_update()), + Self { + kind: match &self.kind { + ImportScopeKind::File(file) => ImportScopeKind::File(file.clone_for_update()), + ImportScopeKind::Module(item_list) => { + ImportScopeKind::Module(item_list.clone_for_update()) + } + ImportScopeKind::Block(block) => ImportScopeKind::Block(block.clone_for_update()), + }, + required_cfgs: self.required_cfgs.iter().map(|attr| attr.clone_for_update()).collect(), } } } @@ -216,6 +196,11 @@ fn insert_use_with_alias_option( use_tree.wrap_in_tree_list(); } let use_item = make::use_(None, use_tree).clone_for_update(); + for attr in + scope.required_cfgs.iter().map(|attr| attr.syntax().clone_subtree().clone_for_update()) + { + ted::insert(ted::Position::first_child_of(use_item.syntax()), attr); + } // merge into existing imports if possible if let Some(mb) = mb { @@ -229,7 +214,6 @@ fn insert_use_with_alias_option( } } } - // either we weren't allowed to merge or there is no import that fits the merge conditions // so look for the place we have to insert to insert_use_(scope, use_item, cfg.group); @@ -316,10 +300,10 @@ fn guess_granularity_from_scope(scope: &ImportScope) -> ImportGranularityGuess { } _ => None, }; - let mut use_stmts = match scope { - ImportScope::File(f) => f.items(), - ImportScope::Module(m) => m.items(), - ImportScope::Block(b) => b.items(), + let mut use_stmts = match &scope.kind { + ImportScopeKind::File(f) => f.items(), + ImportScopeKind::Module(m) => m.items(), + ImportScopeKind::Block(b) => b.items(), } .filter_map(use_stmt); let mut res = ImportGranularityGuess::Unknown; @@ -463,12 +447,12 @@ fn insert_use_(scope: &ImportScope, use_item: ast::Use, group_imports: bool) { } } - let l_curly = match scope { - ImportScope::File(_) => None, + let l_curly = match &scope.kind { + ImportScopeKind::File(_) => None, // don't insert the imports before the item list/block expr's opening curly brace - ImportScope::Module(item_list) => item_list.l_curly_token(), + ImportScopeKind::Module(item_list) => item_list.l_curly_token(), // don't insert the imports before the item list's opening curly brace - ImportScope::Block(block) => block.l_curly_token(), + ImportScopeKind::Block(block) => block.l_curly_token(), }; // there are no imports in this file at all // so put the import after all inner module attributes and possible license header comments diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs index 428ba1d5118..4a00854f01a 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs @@ -23,7 +23,7 @@ struct Struct; } #[test] -fn respects_cfg_attr_fn() { +fn respects_cfg_attr_fn_body() { check( r"bar::Bar", r#" @@ -41,6 +41,25 @@ fn foo() { } #[test] +fn respects_cfg_attr_fn_sig() { + check( + r"bar::Bar", + r#" +#[cfg(test)] +fn foo($0) {} +"#, + r#" +#[cfg(test)] +use bar::Bar; + +#[cfg(test)] +fn foo() {} +"#, + ImportGranularity::Crate, + ); +} + +#[test] fn respects_cfg_attr_const() { check( r"bar::Bar", @@ -59,6 +78,51 @@ const FOO: Bar = { } #[test] +fn respects_cfg_attr_impl() { + check( + r"bar::Bar", + r#" +#[cfg(test)] +impl () {$0} +"#, + r#" +#[cfg(test)] +use bar::Bar; + +#[cfg(test)] +impl () {} +"#, + ImportGranularity::Crate, + ); +} + +#[test] +fn respects_cfg_attr_multiple_layers() { + check( + r"bar::Bar", + r#" +#[cfg(test)] +impl () { + #[cfg(test2)] + fn f($0) {} +} +"#, + r#" +#[cfg(test)] +#[cfg(test2)] +use bar::Bar; + +#[cfg(test)] +impl () { + #[cfg(test2)] + fn f() {} +} +"#, + ImportGranularity::Crate, + ); +} + +#[test] fn insert_skips_lone_glob_imports() { check( "use foo::baz::A", @@ -813,7 +877,7 @@ use {std::io};", } #[test] -fn merge_groups_skip_attributed() { +fn merge_groups_cfg_vs_no_cfg() { check_crate( "std::io", r#" @@ -837,6 +901,25 @@ use {std::io}; } #[test] +fn merge_groups_cfg_matching() { + check_crate( + "std::io", + r#" +#[cfg(feature = "gated")] use std::fmt::{Result, Display}; + +#[cfg(feature = "gated")] +fn f($0) {} +"#, + r#" +#[cfg(feature = "gated")] use std::{fmt::{Display, Result}, io}; + +#[cfg(feature = "gated")] +fn f() {} +"#, + ); +} + +#[test] fn split_out_merge() { // FIXME: This is suboptimal, we want to get `use std::fmt::{self, Result}` // instead. @@ -1259,12 +1342,14 @@ fn check_with_config( }; let sema = &Semantics::new(&db); let source_file = sema.parse(file_id); - let syntax = source_file.syntax().clone_for_update(); let file = pos - .and_then(|pos| syntax.token_at_offset(pos.expect_offset()).next()?.parent()) + .and_then(|pos| source_file.syntax().token_at_offset(pos.expect_offset()).next()?.parent()) .and_then(|it| ImportScope::find_insert_use_container(&it, sema)) - .or_else(|| ImportScope::from(syntax)) - .unwrap(); + .unwrap_or_else(|| ImportScope { + kind: ImportScopeKind::File(source_file), + required_cfgs: vec![], + }) + .clone_for_update(); let path = ast::SourceFile::parse(&format!("use {path};"), span::Edition::CURRENT) .tree() .syntax() @@ -1349,7 +1434,7 @@ fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior } fn check_guess(#[rust_analyzer::rust_fixture] ra_fixture: &str, expected: ImportGranularityGuess) { - let syntax = ast::SourceFile::parse(ra_fixture, span::Edition::CURRENT).tree().syntax().clone(); - let file = ImportScope::from(syntax).unwrap(); + let syntax = ast::SourceFile::parse(ra_fixture, span::Edition::CURRENT).tree(); + let file = ImportScope { kind: ImportScopeKind::File(syntax), required_cfgs: vec![] }; assert_eq!(super::guess_granularity_from_scope(&file), expected); } diff --git a/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs b/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs index b1b58d6568c..16c0d8d97a7 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs @@ -5,6 +5,7 @@ use std::{collections::hash_map::Entry, fmt, iter, mem}; +use crate::imports::insert_use::{ImportScope, ImportScopeKind}; use crate::text_edit::{TextEdit, TextEditBuilder}; use crate::{SnippetCap, assists::Command, syntax_helpers::tree_diff::diff}; use base_db::AnchoredPathBuf; @@ -367,6 +368,17 @@ impl SourceChangeBuilder { pub fn make_mut<N: AstNode>(&mut self, node: N) -> N { self.mutated_tree.get_or_insert_with(|| TreeMutator::new(node.syntax())).make_mut(&node) } + + pub fn make_import_scope_mut(&mut self, scope: ImportScope) -> ImportScope { + ImportScope { + kind: match scope.kind.clone() { + ImportScopeKind::File(it) => ImportScopeKind::File(self.make_mut(it)), + ImportScopeKind::Module(it) => ImportScopeKind::Module(self.make_mut(it)), + ImportScopeKind::Block(it) => ImportScopeKind::Block(self.make_mut(it)), + }, + required_cfgs: scope.required_cfgs.iter().map(|it| self.make_mut(it.clone())).collect(), + } + } /// Returns a copy of the `node`, suitable for mutation. /// /// Syntax trees in rust-analyzer are typically immutable, and mutating diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs index ac1b599c49e..87c9397fb77 100644 --- a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs +++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs @@ -137,11 +137,7 @@ pub(crate) fn json_in_items( ) .with_fixes(Some(vec