aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-06-14 17:46:25 +0100
committerGitHub <[email protected]>2021-06-14 17:46:25 +0100
commit38ae18b7592f97a7058d97928307bccbd881a582 (patch)
tree5f6f59f48f05999495654bf2e4250e029e6f010f /crates
parent401d79ac0674ec62689949c3a531836420cb9beb (diff)
parent4768e5fb23c058eba90f0a1dcd6e9d5c0ecdee1b (diff)
Merge #9272
9272: internal: move diagnostics to a dedicated crate r=matklad a=matklad bors r+ Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates')
-rw-r--r--crates/base_db/src/fixture.rs8
-rw-r--r--crates/ide/Cargo.toml1
-rw-r--r--crates/ide/src/fixture.rs8
-rw-r--r--crates/ide/src/lib.rs21
-rw-r--r--crates/ide/src/references.rs2
-rw-r--r--crates/ide/src/rename.rs (renamed from crates/ide/src/references/rename.rs)459
-rw-r--r--crates/ide_assists/src/lib.rs163
-rw-r--r--crates/ide_assists/src/tests.rs26
-rw-r--r--crates/ide_db/src/assists.rs136
-rw-r--r--crates/ide_db/src/lib.rs5
-rw-r--r--crates/ide_db/src/rename.rs468
-rw-r--r--crates/ide_diagnostics/Cargo.toml29
-rw-r--r--crates/ide_diagnostics/src/field_shorthand.rs (renamed from crates/ide/src/diagnostics/field_shorthand.rs)4
-rw-r--r--crates/ide_diagnostics/src/handlers/break_outside_of_loop.rs (renamed from crates/ide/src/diagnostics/break_outside_of_loop.rs)6
-rw-r--r--crates/ide_diagnostics/src/handlers/inactive_code.rs (renamed from crates/ide/src/diagnostics/inactive_code.rs)9
-rw-r--r--crates/ide_diagnostics/src/handlers/incorrect_case.rs (renamed from crates/ide/src/diagnostics/incorrect_case.rs)43
-rw-r--r--crates/ide_diagnostics/src/handlers/macro_error.rs (renamed from crates/ide/src/diagnostics/macro_error.rs)6
-rw-r--r--crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs (renamed from crates/ide/src/diagnostics/mismatched_arg_count.rs)6
-rw-r--r--crates/ide_diagnostics/src/handlers/missing_fields.rs (renamed from crates/ide/src/diagnostics/missing_fields.rs)9
-rw-r--r--crates/ide_diagnostics/src/handlers/missing_match_arms.rs (renamed from crates/ide/src/diagnostics/missing_match_arms.rs)10
-rw-r--r--crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs (renamed from crates/ide/src/diagnostics/missing_ok_or_some_in_tail_expr.rs)9
-rw-r--r--crates/ide_diagnostics/src/handlers/missing_unsafe.rs (renamed from crates/ide/src/diagnostics/missing_unsafe.rs)6
-rw-r--r--crates/ide_diagnostics/src/handlers/no_such_field.rs (renamed from crates/ide/src/diagnostics/no_such_field.rs)9
-rw-r--r--crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs (renamed from crates/ide/src/diagnostics/remove_this_semicolon.rs)9
-rw-r--r--crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs (renamed from crates/ide/src/diagnostics/replace_filter_map_next_with_find_map.rs)11
-rw-r--r--crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs (renamed from crates/ide/src/diagnostics/unimplemented_builtin_macro.rs)7
-rw-r--r--crates/ide_diagnostics/src/handlers/unlinked_file.rs (renamed from crates/ide/src/diagnostics/unlinked_file.rs)9
-rw-r--r--crates/ide_diagnostics/src/handlers/unresolved_extern_crate.rs (renamed from crates/ide/src/diagnostics/unresolved_extern_crate.rs)6
-rw-r--r--crates/ide_diagnostics/src/handlers/unresolved_import.rs (renamed from crates/ide/src/diagnostics/unresolved_import.rs)6
-rw-r--r--crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs (renamed from crates/ide/src/diagnostics/unresolved_macro_call.rs)6
-rw-r--r--crates/ide_diagnostics/src/handlers/unresolved_module.rs (renamed from crates/ide/src/diagnostics/unresolved_module.rs)9
-rw-r--r--crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs (renamed from crates/ide/src/diagnostics/unresolved_proc_macro.rs)7
-rw-r--r--crates/ide_diagnostics/src/lib.rs (renamed from crates/ide/src/diagnostics.rs)202
33 files changed, 917 insertions, 798 deletions
diff --git a/crates/base_db/src/fixture.rs b/crates/base_db/src/fixture.rs
index da4afb5eb..1b17db102 100644
--- a/crates/base_db/src/fixture.rs
+++ b/crates/base_db/src/fixture.rs
@@ -24,6 +24,14 @@ pub trait WithFixture: Default + SourceDatabaseExt + 'static {
24 (db, fixture.files[0]) 24 (db, fixture.files[0])
25 } 25 }
26 26
27 fn with_many_files(ra_fixture: &str) -> (Self, Vec<FileId>) {
28 let fixture = ChangeFixture::parse(ra_fixture);
29 let mut db = Self::default();
30 fixture.change.apply(&mut db);
31 assert!(fixture.file_position.is_none());
32 (db, fixture.files)
33 }
34
27 fn with_files(ra_fixture: &str) -> Self { 35 fn with_files(ra_fixture: &str) -> Self {
28 let fixture = ChangeFixture::parse(ra_fixture); 36 let fixture = ChangeFixture::parse(ra_fixture);
29 let mut db = Self::default(); 37 let mut db = Self::default();
diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml
index f12928225..0e8447394 100644
--- a/crates/ide/Cargo.toml
+++ b/crates/ide/Cargo.toml
@@ -29,6 +29,7 @@ ide_db = { path = "../ide_db", version = "0.0.0" }
29cfg = { path = "../cfg", version = "0.0.0" } 29cfg = { path = "../cfg", version = "0.0.0" }
30profile = { path = "../profile", version = "0.0.0" } 30profile = { path = "../profile", version = "0.0.0" }
31ide_assists = { path = "../ide_assists", version = "0.0.0" } 31ide_assists = { path = "../ide_assists", version = "0.0.0" }
32ide_diagnostics = { path = "../ide_diagnostics", version = "0.0.0" }
32ide_ssr = { path = "../ide_ssr", version = "0.0.0" } 33ide_ssr = { path = "../ide_ssr", version = "0.0.0" }
33ide_completion = { path = "../ide_completion", version = "0.0.0" } 34ide_completion = { path = "../ide_completion", version = "0.0.0" }
34 35
diff --git a/crates/ide/src/fixture.rs b/crates/ide/src/fixture.rs
index 38e2e866b..cf679edd3 100644
--- a/crates/ide/src/fixture.rs
+++ b/crates/ide/src/fixture.rs
@@ -12,14 +12,6 @@ pub(crate) fn file(ra_fixture: &str) -> (Analysis, FileId) {
12 (host.analysis(), change_fixture.files[0]) 12 (host.analysis(), change_fixture.files[0])
13} 13}
14 14
15/// Creates analysis for many files.
16pub(crate) fn files(ra_fixture: &str) -> (Analysis, Vec<FileId>) {
17 let mut host = AnalysisHost::default();
18 let change_fixture = ChangeFixture::parse(ra_fixture);
19 host.db.apply_change(change_fixture.change);
20 (host.analysis(), change_fixture.files)
21}
22
23/// Creates analysis from a multi-file fixture, returns positions marked with $0. 15/// Creates analysis from a multi-file fixture, returns positions marked with $0.
24pub(crate) fn position(ra_fixture: &str) -> (Analysis, FilePosition) { 16pub(crate) fn position(ra_fixture: &str) -> (Analysis, FilePosition) {
25 let mut host = AnalysisHost::default(); 17 let mut host = AnalysisHost::default();
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 0511efae3..9db387d26 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -24,7 +24,6 @@ mod display;
24 24
25mod annotations; 25mod annotations;
26mod call_hierarchy; 26mod call_hierarchy;
27mod diagnostics;
28mod expand_macro; 27mod expand_macro;
29mod extend_selection; 28mod extend_selection;
30mod file_structure; 29mod file_structure;
@@ -40,6 +39,7 @@ mod matching_brace;
40mod move_item; 39mod move_item;
41mod parent_module; 40mod parent_module;
42mod references; 41mod references;
42mod rename;
43mod fn_references; 43mod fn_references;
44mod runnables; 44mod runnables;
45mod ssr; 45mod ssr;
@@ -71,7 +71,6 @@ use crate::display::ToNav;
71pub use crate::{ 71pub use crate::{
72 annotations::{Annotation, AnnotationConfig, AnnotationKind}, 72 annotations::{Annotation, AnnotationConfig, AnnotationKind},
73 call_hierarchy::CallItem, 73 call_hierarchy::CallItem,
74 diagnostics::{Diagnostic, DiagnosticsConfig, Severity},
75 display::navigation_target::NavigationTarget, 74 display::navigation_target::NavigationTarget,
76 expand_macro::ExpandedMacro, 75 expand_macro::ExpandedMacro,
77 file_structure::{StructureNode, StructureNodeKind}, 76 file_structure::{StructureNode, StructureNodeKind},
@@ -81,7 +80,8 @@ pub use crate::{
81 markup::Markup, 80 markup::Markup,
82 move_item::Direction, 81 move_item::Direction,
83 prime_caches::PrimeCachesProgress, 82 prime_caches::PrimeCachesProgress,
84 references::{rename::RenameError, ReferenceSearchResult}, 83 references::ReferenceSearchResult,
84 rename::RenameError,
85 runnables::{Runnable, RunnableKind, TestId}, 85 runnables::{Runnable, RunnableKind, TestId},
86 syntax_highlighting::{ 86 syntax_highlighting::{
87 tags::{Highlight, HlMod, HlMods, HlOperator, HlPunct, HlTag}, 87 tags::{Highlight, HlMod, HlMods, HlOperator, HlPunct, HlTag},
@@ -109,6 +109,7 @@ pub use ide_db::{
109 symbol_index::Query, 109 symbol_index::Query,
110 RootDatabase, SymbolKind, 110 RootDatabase, SymbolKind,
111}; 111};
112pub use ide_diagnostics::{Diagnostic, DiagnosticsConfig, Severity};
112pub use ide_ssr::SsrError; 113pub use ide_ssr::SsrError;
113pub use syntax::{TextRange, TextSize}; 114pub use syntax::{TextRange, TextSize};
114pub use text_edit::{Indel, TextEdit}; 115pub use text_edit::{Indel, TextEdit};
@@ -536,7 +537,7 @@ impl Analysis {
536 ) -> Cancellable<Vec<Assist>> { 537 ) -> Cancellable<Vec<Assist>> {
537 self.with_db(|db| { 538 self.with_db(|db| {
538 let ssr_assists = ssr::ssr_assists(db, &resolve, frange); 539 let ssr_assists = ssr::ssr_assists(db, &resolve, frange);
539 let mut acc = Assist::get(db, config, resolve, frange); 540 let mut acc = ide_assists::assists(db, config, resolve, frange);
540 acc.extend(ssr_assists.into_iter()); 541 acc.extend(ssr_assists.into_iter());
541 acc 542 acc
542 }) 543 })
@@ -549,7 +550,7 @@ impl Analysis {
549 resolve: AssistResolveStrategy, 550 resolve: AssistResolveStrategy,
550 file_id: FileId, 551 file_id: FileId,
551 ) -> Cancellable<Vec<Diagnostic>> { 552 ) -> Cancellable<Vec<Diagnostic>> {
552 self.with_db(|db| diagnostics::diagnostics(db, config, &resolve, file_id)) 553 self.with_db(|db| ide_diagnostics::diagnostics(db, config, &resolve, file_id))
553 } 554 }
554 555
555 /// Convenience function to return assists + quick fixes for diagnostics 556 /// Convenience function to return assists + quick fixes for diagnostics
@@ -568,7 +569,7 @@ impl Analysis {
568 self.with_db(|db| { 569 self.with_db(|db| {
569 let ssr_assists = ssr::ssr_assists(db, &resolve, frange); 570 let ssr_assists = ssr::ssr_assists(db, &resolve, frange);
570 let diagnostic_assists = if include_fixes { 571 let diagnostic_assists = if include_fixes {
571 diagnostics::diagnostics(db, diagnostics_config, &resolve, frange.file_id) 572 ide_diagnostics::diagnostics(db, diagnostics_config, &resolve, frange.file_id)
572 .into_iter() 573 .into_iter()
573 .flat_map(|it| it.fixes.unwrap_or_default()) 574 .flat_map(|it| it.fixes.unwrap_or_default())
574 .filter(|it| it.target.intersect(frange.range).is_some()) 575 .filter(|it| it.target.intersect(frange.range).is_some())
@@ -577,7 +578,7 @@ impl Analysis {
577 Vec::new() 578 Vec::new()
578 }; 579 };
579 580
580 let mut res = Assist::get(db, assist_config, resolve, frange); 581 let mut res = ide_assists::assists(db, assist_config, resolve, frange);
581 res.extend(ssr_assists.into_iter()); 582 res.extend(ssr_assists.into_iter());
582 res.extend(diagnostic_assists.into_iter()); 583 res.extend(diagnostic_assists.into_iter());
583 584
@@ -592,14 +593,14 @@ impl Analysis {
592 position: FilePosition, 593 position: FilePosition,
593 new_name: &str, 594 new_name: &str,
594 ) -> Cancellable<Result<SourceChange, RenameError>> { 595 ) -> Cancellable<Result<SourceChange, RenameError>> {
595 self.with_db(|db| references::rename::rename(db, position, new_name)) 596 self.with_db(|db| rename::rename(db, position, new_name))
596 } 597 }
597 598
598 pub fn prepare_rename( 599 pub fn prepare_rename(
599 &self, 600 &self,
600 position: FilePosition, 601 position: FilePosition,
601 ) -> Cancellable<Result<RangeInfo<()>, RenameError>> { 602 ) -> Cancellable<Result<RangeInfo<()>, RenameError>> {
602 self.with_db(|db| references::rename::prepare_rename(db, position)) 603 self.with_db(|db| rename::prepare_rename(db, position))
603 } 604 }
604 605
605 pub fn will_rename_file( 606 pub fn will_rename_file(
@@ -607,7 +608,7 @@ impl Analysis {
607 file_id: FileId, 608 file_id: FileId,
608 new_name_stem: &str, 609 new_name_stem: &str,
609 ) -> Cancellable<Option<SourceChange>> { 610 ) -> Cancellable<Option<SourceChange>> {
610 self.with_db(|db| references::rename::will_rename_file(db, file_id, new_name_stem)) 611 self.with_db(|db| rename::will_rename_file(db, file_id, new_name_stem))
611 } 612 }
612 613
613 pub fn structural_search_replace( 614 pub fn structural_search_replace(
diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs
index a0fdead2c..945c9b9e1 100644
--- a/crates/ide/src/references.rs
+++ b/crates/ide/src/references.rs
@@ -9,8 +9,6 @@
9//! at the index that the match starts at and its tree parent is 9//! at the index that the match starts at and its tree parent is
10//! resolved to the search element definition, we get a reference. 10//! resolved to the search element definition, we get a reference.
11 11
12pub(crate) mod rename;
13
14use hir::{PathResolution, Semantics}; 12use hir::{PathResolution, Semantics};
15use ide_db::{ 13use ide_db::{
16 base_db::FileId, 14 base_db::FileId,
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/rename.rs
index 6b3d02bf4..8096dfa0e 100644
--- a/crates/ide/src/references/rename.rs
+++ b/crates/ide/src/rename.rs
@@ -1,45 +1,25 @@
1//! Renaming functionality 1//! Renaming functionality.
2//! 2//!
3//! All reference and file rename requests go through here where the corresponding [`SourceChange`]s 3//! This is mostly front-end for [`ide_db::rename`], but it also includes the
4//! will be calculated. 4//! tests. This module also implements a couple of magic tricks, like renaming
5use std::fmt::{self, Display}; 5//! `self` and to `self` (to switch between associated function and method).
6 6use hir::{AsAssocItem, InFile, Semantics};
7use either::Either;
8use hir::{AsAssocItem, FieldSource, HasSource, InFile, ModuleSource, Semantics};
9use ide_db::{ 7use ide_db::{
10 base_db::{AnchoredPathBuf, FileId, FileRange}, 8 base_db::FileId,
11 defs::{Definition, NameClass, NameRefClass}, 9 defs::{Definition, NameClass, NameRefClass},
12 search::FileReference, 10 rename::{bail, format_err, source_edit_from_references, IdentifierKind},
13 RootDatabase, 11 RootDatabase,
14}; 12};
15use stdx::never; 13use stdx::never;
16use syntax::{ 14use syntax::{ast, AstNode, SyntaxNode};
17 ast::{self, NameOwner},
18 lex_single_syntax_kind, AstNode, SyntaxKind, SyntaxNode, T,
19};
20 15
21use text_edit::TextEdit; 16use text_edit::TextEdit;
22 17
23use crate::{FilePosition, FileSystemEdit, RangeInfo, SourceChange, TextRange}; 18use crate::{FilePosition, RangeInfo, SourceChange};
24
25type RenameResult<T> = Result<T, RenameError>;
26#[derive(Debug)]
27pub struct RenameError(String);
28
29impl fmt::Display for RenameError {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 Display::fmt(&self.0, f)
32 }
33}
34 19
35macro_rules! format_err { 20pub use ide_db::rename::RenameError;
36 ($fmt:expr) => {RenameError(format!($fmt))};
37 ($fmt:expr, $($arg:tt)+) => {RenameError(format!($fmt, $($arg)+))}
38}
39 21
40macro_rules! bail { 22type RenameResult<T> = Result<T, RenameError>;
41 ($($tokens:tt)*) => {return Err(format_err!($($tokens)*))}
42}
43 23
44/// Prepares a rename. The sole job of this function is to return the TextRange of the thing that is 24/// Prepares a rename. The sole job of this function is to return the TextRange of the thing that is
45/// being targeted for a rename. 25/// being targeted for a rename.
@@ -52,7 +32,8 @@ pub(crate) fn prepare_rename(
52 let syntax = source_file.syntax(); 32 let syntax = source_file.syntax();
53 33
54 let def = find_definition(&sema, syntax, position)?; 34 let def = find_definition(&sema, syntax, position)?;
55 let frange = def_name_range(&&sema, def) 35 let frange = def
36 .range_for_rename(&sema)
56 .ok_or_else(|| format_err!("No references found at position"))?; 37 .ok_or_else(|| format_err!("No references found at position"))?;
57 Ok(RangeInfo::new(frange.range, ())) 38 Ok(RangeInfo::new(frange.range, ()))
58} 39}
@@ -98,14 +79,7 @@ pub(crate) fn rename_with_semantics(
98 } 79 }
99 } 80 }
100 81
101 match def { 82 def.rename(sema, new_name)
102 Definition::ModuleDef(hir::ModuleDef::Module(module)) => rename_mod(sema, module, new_name),
103 Definition::SelfType(_) => bail!("Cannot rename `Self`"),
104 Definition::ModuleDef(hir::ModuleDef::BuiltinType(_)) => {
105 bail!("Cannot rename builtin type")
106 }
107 def => rename_reference(sema, def, new_name),
108 }
109} 83}
110 84
111/// Called by the client when it is about to rename a file. 85/// Called by the client when it is about to rename a file.
@@ -116,38 +90,12 @@ pub(crate) fn will_rename_file(
116) -> Option<SourceChange> { 90) -> Option<SourceChange> {
117 let sema = Semantics::new(db); 91 let sema = Semantics::new(db);
118 let module = sema.to_module_def(file_id)?; 92 let module = sema.to_module_def(file_id)?;
119 let mut change = rename_mod(&sema, module, new_name_stem).ok()?; 93 let def = Definition::ModuleDef(module.into());
94 let mut change = def.rename(&sema, new_name_stem).ok()?;
120 change.file_system_edits.clear(); 95 change.file_system_edits.clear();
121 Some(change) 96 Some(change)
122} 97}
123 98
124#[derive(Copy, Clone, Debug, PartialEq)]
125enum IdentifierKind {
126 Ident,
127 Lifetime,
128 Underscore,
129}
130
131impl IdentifierKind {
132 fn classify(new_name: &str) -> RenameResult<IdentifierKind> {
133 match lex_single_syntax_kind(new_name) {
134 Some(res) => match res {
135 (SyntaxKind::IDENT, _) => Ok(IdentifierKind::Ident),
136 (T![_], _) => Ok(IdentifierKind::Underscore),
137 (SyntaxKind::LIFETIME_IDENT, _) if new_name != "'static" && new_name != "'_" => {
138 Ok(IdentifierKind::Lifetime)
139 }
140 (SyntaxKind::LIFETIME_IDENT, _) => {
141 bail!("Invalid name `{}`: not a lifetime identifier", new_name)
142 }
143 (_, Some(syntax_error)) => bail!("Invalid name `{}`: {}", new_name, syntax_error),
144 (_, None) => bail!("Invalid name `{}`: not an identifier", new_name),
145 },
146 None => bail!("Invalid name `{}`: not an identifier", new_name),
147 }
148 }
149}
150
151fn find_definition( 99fn find_definition(
152 sema: &Semantics<RootDatabase>, 100 sema: &Semantics<RootDatabase>,
153 syntax: &SyntaxNode, 101 syntax: &SyntaxNode,
@@ -189,126 +137,6 @@ fn find_definition(
189 .ok_or_else(|| format_err!("No references found at position")) 137 .ok_or_else(|| format_err!("No references found at position"))
190} 138}
191 139
192fn rename_mod(
193 sema: &Semantics<RootDatabase>,
194 module: hir::Module,
195 new_name: &str,
196) -> RenameResult<SourceChange> {
197 if IdentifierKind::classify(new_name)? != IdentifierKind::Ident {
198 bail!("Invalid name `{0}`: cannot rename module to {0}", new_name);
199 }
200
201 let mut source_change = SourceChange::default();
202
203 let InFile { file_id, value: def_source } = module.definition_source(sema.db);
204 let file_id = file_id.original_file(sema.db);
205 if let ModuleSource::SourceFile(..) = def_source {
206 // mod is defined in path/to/dir/mod.rs
207 let path = if module.is_mod_rs(sema.db) {
208 format!("../{}/mod.rs", new_name)
209 } else {
210 format!("{}.rs", new_name)
211 };
212 let dst = AnchoredPathBuf { anchor: file_id, path };
213 let move_file = FileSystemEdit::MoveFile { src: file_id, dst };
214 source_change.push_file_system_edit(move_file);
215 }
216
217 if let Some(InFile { file_id, value: decl_source }) = module.declaration_source(sema.db) {
218 let file_id = file_id.original_file(sema.db);
219 match decl_source.name() {
220 Some(name) => source_change.insert_source_edit(
221 file_id,
222 TextEdit::replace(name.syntax().text_range(), new_name.to_string()),
223 ),
224 _ => never!("Module source node is missing a name"),
225 }
226 }
227 let def = Definition::ModuleDef(hir::ModuleDef::Module(module));
228 let usages = def.usages(sema).all();
229 let ref_edits = usages.iter().map(|(&file_id, references)| {
230 (file_id, source_edit_from_references(references, def, new_name))
231 });
232 source_change.extend(ref_edits);
233
234 Ok(source_change)
235}
236
237fn rename_reference(
238 sema: &Semantics<RootDatabase>,
239 mut def: Definition,
240 new_name: &str,
241) -> RenameResult<SourceChange> {
242 let ident_kind = IdentifierKind::classify(new_name)?;
243
244 if matches!(
245 def, // is target a lifetime?
246 Definition::GenericParam(hir::GenericParam::LifetimeParam(_)) | Definition::Label(_)
247 ) {
248 match ident_kind {
249 IdentifierKind::Ident | IdentifierKind::Underscore => {
250 cov_mark::hit!(rename_not_a_lifetime_ident_ref);
251 bail!("Invalid name `{}`: not a lifetime identifier", new_name);
252 }
253 IdentifierKind::Lifetime => cov_mark::hit!(rename_lifetime),
254 }
255 } else {
256 match (ident_kind, def) {
257 (IdentifierKind::Lifetime, _) => {
258 cov_mark::hit!(rename_not_an_ident_ref);
259 bail!("Invalid name `{}`: not an identifier", new_name);
260 }
261 (IdentifierKind::Ident, _) => cov_mark::hit!(rename_non_local),
262 (IdentifierKind::Underscore, _) => (),
263 }
264 }
265
266 def = match def {
267 // HACK: resolve trait impl items to the item def of the trait definition
268 // so that we properly resolve all trait item references
269 Definition::ModuleDef(mod_def) => mod_def
270 .as_assoc_item(sema.db)
271 .and_then(|it| it.containing_trait_impl(sema.db))
272 .and_then(|it| {
273 it.items(sema.db).into_iter().find_map(|it| match (it, mod_def) {
274 (hir::AssocItem::Function(trait_func), hir::ModuleDef::Function(func))
275 if trait_func.name(sema.db) == func.name(sema.db) =>
276 {
277 Some(Definition::ModuleDef(hir::ModuleDef::Function(trait_func)))
278 }
279 (hir::AssocItem::Const(trait_konst), hir::ModuleDef::Const(konst))
280 if trait_konst.name(sema.db) == konst.name(sema.db) =>
281 {
282 Some(Definition::ModuleDef(hir::ModuleDef::Const(trait_konst)))
283 }
284 (
285 hir::AssocItem::TypeAlias(trait_type_alias),
286 hir::ModuleDef::TypeAlias(type_alias),
287 ) if trait_type_alias.name(sema.db) == type_alias.name(sema.db) => {
288 Some(Definition::ModuleDef(hir::ModuleDef::TypeAlias(trait_type_alias)))
289 }
290 _ => None,
291 })
292 })
293 .unwrap_or(def),
294 _ => def,
295 };
296 let usages = def.usages(sema).all();
297
298 if !usages.is_empty() && ident_kind == IdentifierKind::Underscore {
299 cov_mark::hit!(rename_underscore_multiple);
300 bail!("Cannot rename reference to `_` as it is being referenced multiple times");
301 }
302 let mut source_change = SourceChange::default();
303 source_change.extend(usages.iter().map(|(&file_id, references)| {
304 (file_id, source_edit_from_references(references, def, new_name))
305 }));
306
307 let (file_id, edit) = source_edit_from_def(sema, def, new_name)?;
308 source_change.insert_source_edit(file_id, edit);
309 Ok(source_change)
310}
311
312fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> { 140fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> {
313 if never!(local.is_self(sema.db)) { 141 if never!(local.is_self(sema.db)) {
314 bail!("rename_to_self invoked on self"); 142 bail!("rename_to_self invoked on self");
@@ -426,243 +254,6 @@ fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Opt
426 Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text)) 254 Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text))
427} 255}
428 256
429fn source_edit_from_references(
430 references: &[FileReference],
431 def: Definition,
432 new_name: &str,
433) -> TextEdit {
434 let mut edit = TextEdit::builder();
435 for reference in references {
436 let (range, replacement) = match &reference.name {
437 // if the ranges differ then the node is inside a macro call, we can't really attempt
438 // to make special rewrites like shorthand syntax and such, so just rename the node in
439 // the macro input
440 ast::NameLike::NameRef(name_ref)
441 if name_ref.syntax().text_range() == reference.range =>
442 {
443 source_edit_from_name_ref(name_ref, new_name, def)
444 }
445 ast::NameLike::Name(name) if name.syntax().text_range() == reference.range => {
446 source_edit_from_name(name, new_name)
447 }
448 _ => None,
449 }
450 .unwrap_or_else(|| (reference.range, new_name.to_string()));
451 edit.replace(range, replacement);
452 }
453 edit.finish()
454}
455
456fn source_edit_from_name(name: &ast::Name, new_name: &str) -> Option<(TextRange, String)> {
457 if let Some(_) = ast::RecordPatField::for_field_name(name) {
458 if let Some(ident_pat) = name.syntax().parent().and_then(ast::IdentPat::cast) {
459 return Some((
460 TextRange::empty(ident_pat.syntax().text_range().start()),
461 [new_name, ": "].concat(),
462 ));
463 }
464 }
465 None
466}
467
468fn source_edit_from_name_ref(
469 name_ref: &ast::NameRef,
470 new_name: &str,
471 def: Definition,
472) -> Option<(TextRange, String)> {
473 if let Some(record_field) = ast::RecordExprField::for_name_ref(name_ref) {
474 let rcf_name_ref = record_field.name_ref();
475 let rcf_expr = record_field.expr();
476 match (rcf_name_ref, rcf_expr.and_then(|it| it.name_ref())) {
477 // field: init-expr, check if we can use a field init shorthand
478 (Some(field_name), Some(init)) => {
479 if field_name == *name_ref {
480 if init.text() == new_name {
481 cov_mark::hit!(test_rename_field_put_init_shorthand);
482 // same names, we can use a shorthand here instead.
483 // we do not want to erase attributes hence this range start
484 let s = field_name.syntax().text_range().start();
485 let e = record_field.syntax().text_range().end();
486 return Some((TextRange::new(s, e), new_name.to_owned()));
487 }
488 } else if init == *name_ref {
489 if field_name.text() == new_name {
490 cov_mark::hit!(test_rename_local_put_init_shorthand);
491 // same names, we can use a shorthand here instead.
492 // we do not want to erase attributes hence this range start
493 let s = field_name.syntax().text_range().start();
494 let e = record_field.syntax().text_range().end();
495 return Some((TextRange::new(s, e), new_name.to_owned()));
496 }
497 }
498 None
499 }
500 // init shorthand
501 // FIXME: instead of splitting the shorthand, recursively trigger a rename of the
502 // other name https://github.com/rust-analyzer/rust-analyzer/issues/6547
503 (None, Some(_)) if matches!(def, Definition::Field(_)) => {
504 cov_mark::hit!(test_rename_field_in_field_shorthand);
505 let s = name_ref.syntax().text_range().start();
506 Some((TextRange::empty(s), format!("{}: ", new_name)))
507 }
508 (None, Some(_)) if matches!(def, Definition::Local(_)) => {
509 cov_mark::hit!(test_rename_local_in_field_shorthand);
510 let s = name_ref.syntax().text_range().end();
511 Some((TextRange::empty(s), format!(": {}", new_name)))
512 }
513 _ => None,
514 }
515 } else if let Some(record_field) = ast::RecordPatField::for_field_name_ref(name_ref) {
516 let rcf_name_ref = record_field.name_ref();
517 let rcf_pat = record_field.pat();
518 match (rcf_name_ref, rcf_pat) {
519 // field: rename
520 (Some(field_name), Some(ast::Pat::IdentPat(pat))) if field_name == *name_ref => {
521 // field name is being renamed
522 if pat.name().map_or(false, |it| it.text() == new_name) {
523 cov_mark::hit!(test_rename_field_put_init_shorthand_pat);
524 // same names, we can use a shorthand here instead/
525 // we do not want to erase attributes hence this range start
526 let s = field_name.syntax().text_range().start();
527 let e = record_field.syntax().text_range().end();
528 Some((TextRange::new(s, e), pat.to_string()))
529 } else {
530 None
531 }
532 }
533 _ => None,
534 }
535 } else {
536 None
537 }
538}
539
540fn source_edit_from_def(
541 sema: &Semantics<RootDatabase>,
542 def: Definition,
543 new_name: &str,
544) -> RenameResult<(FileId, TextEdit)> {
545 let frange: FileRange = def_name_range(sema, def)
546 .ok_or_else(|| format_err!("No identifier available to rename"))?;
547
548 let mut replacement_text = String::new();
549 let mut repl_range = frange.range;
550 if let Definition::Local(local) = def {
551 if let Either::Left(pat) = local.source(sema.db).value {
552 if matches!(
553 pat.syntax().parent().and_then(ast::RecordPatField::cast),
554 Some(pat_field) if pat_field.name_ref().is_none()
555 ) {
556 replacement_text.push_str(": ");
557 replacement_text.push_str(new_name);
558 repl_range = TextRange::new(
559 pat.syntax().text_range().end(),
560 pat.syntax().text_range().end(),
561 );
562 }
563 }
564 }
565 if replacement_text.is_empty() {
566 replacement_text.push_str(new_name);
567 }
568 let edit = TextEdit::replace(repl_range, replacement_text);
569 Ok((frange.file_id, edit))
570}
571
572fn def_name_range(sema: &Semantics<RootDatabase>, def: Definition) -> Option<FileRange> {
573 // FIXME: the `original_file_range` calls here are wrong -- they never fail,
574 // and _fall back_ to the entirety of the macro call. Such fall back is
575 // incorrect for renames. The safe behavior would be to return an error for
576 // such cases. The correct behavior would be to return an auxiliary list of
577 // "can't rename these occurrences in macros" items, and then show some kind
578 // of a dialog to the user.
579
580 let res = match def {
581 Definition::Macro(mac) => {
582 let src = mac.source(sema.db)?;
583 let name = match &src.value {
584 Either::Left(it) => it.name()?,
585 Either::Right(it) => it.name()?,
586 };
587 src.with_value(name.syntax()).original_file_range(sema.db)
588 }
589 Definition::Field(field) => {
590 let src = field.source(sema.db)?;
591
592 match &src.value {
593 FieldSource::Named(record_field) => {
594 let name = record_field.name()?;
595 src.with_value(name.syntax()).original_file_range(sema.db)
596 }
597 FieldSource::Pos(_) => {
598 return None;
599 }
600 }
601 }
602 Definition::ModuleDef(module_def) => match module_def {
603 hir::ModuleDef::Module(module) => {
604 let src = module.declaration_source(sema.db)?;
605 let name = src.value.name()?;
606 src.with_value(name.syntax()).original_file_range(sema.db)
607 }
608 hir::ModuleDef::Function(it) => name_range(it, sema)?,
609 hir::ModuleDef::Adt(adt) => match adt {
610 hir::Adt::Struct(it) => name_range(it, sema)?,
611 hir::Adt::Union(it) => name_range(it, sema)?,
612 hir::Adt::Enum(it) => name_range(it, sema)?,
613 },
614 hir::ModuleDef::Variant(it) => name_range(it, sema)?,
615 hir::ModuleDef::Const(it) => name_range(it, sema)?,
616 hir::ModuleDef::Static(it) => name_range(it, sema)?,
617 hir::ModuleDef::Trait(it) => name_range(it, sema)?,
618 hir::ModuleDef::TypeAlias(it) => name_range(it, sema)?,
619 hir::ModuleDef::BuiltinType(_) => return None,
620 },
621 Definition::SelfType(_) => return None,
622 Definition::Local(local) => {
623 let src = local.source(sema.db);
624 let name = match &src.value {
625 Either::Left(bind_pat) => bind_pat.name()?,
626 Either::Right(_) => return None,
627 };
628 src.with_value(name.syntax()).original_file_range(sema.db)
629 }
630 Definition::GenericParam(generic_param) => match generic_param {
631 hir::GenericParam::TypeParam(type_param) => {
632 let src = type_param.source(sema.db)?;
633 let name = match &src.value {
634 Either::Left(_) => return None,
635 Either::Right(type_param) => type_param.name()?,
636 };
637 src.with_value(name.syntax()).original_file_range(sema.db)
638 }
639 hir::GenericParam::LifetimeParam(lifetime_param) => {
640 let src = lifetime_param.source(sema.db)?;
641 let lifetime = src.value.lifetime()?;
642 src.with_value(lifetime.syntax()).original_file_range(sema.db)
643 }
644 hir::GenericParam::ConstParam(it) => name_range(it, sema)?,
645 },
646 Definition::Label(label) => {
647 let src = label.source(sema.db);
648 let lifetime = src.value.lifetime()?;
649 src.with_value(lifetime.syntax()).original_file_range(sema.db)
650 }
651 };
652 return Some(res);
653
654 fn name_range<D>(def: D, sema: &Semantics<RootDatabase>) -> Option<FileRange>
655 where
656 D: HasSource,
657 D::Ast: ast::NameOwner,
658 {
659 let src = def.source(sema.db)?;
660 let name = src.value.name()?;
661 let res = src.with_value(name.syntax()).original_file_range(sema.db);
662 Some(res)
663 }
664}
665
666#[cfg(test)] 257#[cfg(test)]
667mod tests { 258mod tests {
668 use expect_test::{expect, Expect}; 259 use expect_test::{expect, Expect};
@@ -2178,4 +1769,22 @@ fn f() { <()>::BAR$0; }"#,
2178 res, 1769 res,
2179 ); 1770 );
2180 } 1771 }
1772
1773 #[test]
1774 fn macros_are_broken_lol() {
1775 cov_mark::check!(macros_are_broken_lol);
1776 check(
1777 "lol",
1778 r#"
1779macro_rules! m { () => { fn f() {} } }
1780m!();
1781fn main() { f$0() }
1782"#,
1783 r#"
1784macro_rules! m { () => { fn f() {} } }
1785lol
1786fn main() { lol() }
1787"#,
1788 )
1789 }
2181} 1790}
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index 331a6df2b..fa378a622 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -17,156 +17,31 @@ mod tests;
17pub mod utils; 17pub mod utils;
18pub mod path_transform; 18pub mod path_transform;
19 19
20use std::str::FromStr;
21
22use hir::Semantics; 20use hir::Semantics;
23use ide_db::{base_db::FileRange, label::Label, source_change::SourceChange, RootDatabase}; 21use ide_db::{base_db::FileRange, RootDatabase};
24use syntax::TextRange; 22use syntax::TextRange;
25 23
26pub(crate) use crate::assist_context::{AssistContext, Assists}; 24pub(crate) use crate::assist_context::{AssistContext, Assists};
27 25
28pub use assist_config::AssistConfig; 26pub use assist_config::AssistConfig;
29 27pub use ide_db::assists::{
30#[derive(Debug, Clone, Copy, PartialEq, Eq)] 28 Assist, AssistId, AssistKind, AssistResolveStrategy, GroupLabel, SingleResolve,
31pub enum AssistKind { 29};
32 // FIXME: does the None variant make sense? Probably not. 30
33 None, 31/// Return all the assists applicable at the given position.
34 32pub fn assists(
35 QuickFix, 33 db: &RootDatabase,
36 Generate, 34 config: &AssistConfig,
37 Refactor, 35 resolve: AssistResolveStrategy,
38 RefactorExtract, 36 range: FileRange,
39 RefactorInline, 37) -> Vec<Assist> {
40 RefactorRewrite, 38 let sema = Semantics::new(db);
41} 39 let ctx = AssistContext::new(sema, config, range);
42 40 let mut acc = Assists::new(&ctx, resolve);
43impl AssistKind { 41 handlers::all().iter().for_each(|handler| {
44 pub fn contains(self, other: AssistKind) -> bool { 42 handler(&mut acc, &ctx);
45 if self == other { 43 });
46 return true; 44 acc.finish()
47 }
48
49 match self {
50 AssistKind::None | AssistKind::Generate => true,
51 AssistKind::Refactor => match other {
52 AssistKind::RefactorExtract
53 | AssistKind::RefactorInline
54 | AssistKind::RefactorRewrite => true,
55 _ => false,
56 },
57 _ => false,
58 }
59 }
60
61 pub fn name(&self) -> &str {
62 match self {
63 AssistKind::None => "None",
64 AssistKind::QuickFix => "QuickFix",
65 AssistKind::Generate => "Generate",
66 AssistKind::Refactor => "Refactor",
67 AssistKind::RefactorExtract => "RefactorExtract",
68 AssistKind::RefactorInline => "RefactorInline",
69 AssistKind::RefactorRewrite => "RefactorRewrite",
70 }
71 }
72}
73
74impl FromStr for AssistKind {
75 type Err = String;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 match s {
79 "None" => Ok(AssistKind::None),
80 "QuickFix" => Ok(AssistKind::QuickFix),
81 "Generate" => Ok(AssistKind::Generate),
82 "Refactor" => Ok(AssistKind::Refactor),
83 "RefactorExtract" => Ok(AssistKind::RefactorExtract),
84 "RefactorInline" => Ok(AssistKind::RefactorInline),
85 "RefactorRewrite" => Ok(AssistKind::RefactorRewrite),
86 unknown => Err(format!("Unknown AssistKind: '{}'", unknown)),
87 }
88 }
89}
90
91/// Unique identifier of the assist, should not be shown to the user
92/// directly.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub struct AssistId(pub &'static str, pub AssistKind);
95
96/// A way to control how many asssist to resolve during the assist resolution.
97/// When an assist is resolved, its edits are calculated that might be costly to always do by default.
98#[derive(Debug)]
99pub enum AssistResolveStrategy {
100 /// No assists should be resolved.
101 None,
102 /// All assists should be resolved.
103 All,
104 /// Only a certain assist should be resolved.
105 Single(SingleResolve),
106}
107
108/// Hold the [`AssistId`] data of a certain assist to resolve.
109/// The original id object cannot be used due to a `'static` lifetime
110/// and the requirement to construct this struct dynamically during the resolve handling.
111#[derive(Debug)]
112pub struct SingleResolve {
113 /// The id of the assist.
114 pub assist_id: String,
115 // The kind of the assist.
116 pub assist_kind: AssistKind,
117}
118
119impl AssistResolveStrategy {
120 pub fn should_resolve(&self, id: &AssistId) -> bool {
121 match self {
122 AssistResolveStrategy::None => false,
123 AssistResolveStrategy::All => true,
124 AssistResolveStrategy::Single(single_resolve) => {
125 single_resolve.assist_id == id.0 && single_resolve.assist_kind == id.1
126 }
127 }
128 }
129}
130
131#[derive(Clone, Debug)]
132pub struct GroupLabel(pub String);
133
134#[derive(Debug, Clone)]
135pub struct Assist {
136 pub id: AssistId,
137 /// Short description of the assist, as shown in the UI.
138 pub label: Label,
139 pub group: Option<GroupLabel>,
140 /// Target ranges are used to sort assists: the smaller the target range,
141 /// the more specific assist is, and so it should be sorted first.
142 pub target: TextRange,
143 /// Computing source change sometimes is much more costly then computing the
144 /// other fields. Additionally, the actual change is not required to show
145 /// the lightbulb UI, it only is needed when the user tries to apply an
146 /// assist. So, we compute it lazily: the API allow requesting assists with
147 /// or without source change. We could (and in fact, used to) distinguish
148 /// between resolved and unresolved assists at the type level, but this is
149 /// cumbersome, especially if you want to embed an assist into another data
150 /// structure, such as a diagnostic.
151 pub source_change: Option<SourceChange>,
152}
153
154impl Assist {
155 /// Return all the assists applicable at the given position.
156 pub fn get(
157 db: &RootDatabase,
158 config: &AssistConfig,
159 resolve: AssistResolveStrategy,
160 range: FileRange,
161 ) -> Vec<Assist> {
162 let sema = Semantics::new(db);
163 let ctx = AssistContext::new(sema, config, range);
164 let mut acc = Assists::new(&ctx, resolve);
165 handlers::all().iter().for_each(|handler| {
166 handler(&mut acc, &ctx);
167 });
168 acc.finish()
169 }
170} 45}
171 46
172mod handlers { 47mod handlers {
diff --git a/crates/ide_assists/src/tests.rs b/crates/ide_assists/src/tests.rs
index bdf9cb71c..60cecd94c 100644
--- a/crates/ide_assists/src/tests.rs
+++ b/crates/ide_assists/src/tests.rs
@@ -16,8 +16,8 @@ use syntax::TextRange;
16use test_utils::{assert_eq_text, extract_offset}; 16use test_utils::{assert_eq_text, extract_offset};
17 17
18use crate::{ 18use crate::{
19 handlers::Handler, Assist, AssistConfig, AssistContext, AssistKind, AssistResolveStrategy, 19 assists, handlers::Handler, Assist, AssistConfig, AssistContext, AssistKind,
20 Assists, SingleResolve, 20 AssistResolveStrategy, Assists, SingleResolve,
21}; 21};
22 22
23pub(crate) const TEST_CONFIG: AssistConfig = AssistConfig { 23pub(crate) const TEST_CONFIG: AssistConfig = AssistConfig {
@@ -78,14 +78,14 @@ fn check_doc_test(assist_id: &str, before: &str, after: &str) {
78 let before = db.file_text(file_id).to_string(); 78 let before = db.file_text(file_id).to_string();
79 let frange = FileRange { file_id, range: selection.into() }; 79 let frange = FileRange { file_id, range: selection.into() };
80 80
81 let assist = Assist::get(&db, &TEST_CONFIG, AssistResolveStrategy::All, frange) 81 let assist = assists(&db, &TEST_CONFIG, AssistResolveStrategy::All, frange)
82 .into_iter() 82 .into_iter()
83 .find(|assist| assist.id.0 == assist_id) 83 .find(|assist| assist.id.0 == assist_id)
84 .unwrap_or_else(|| { 84 .unwrap_or_else(|| {
85 panic!( 85 panic!(
86 "\n\nAssist is not applicable: {}\nAvailable assists: {}", 86 "\n\nAssist is not applicable: {}\nAvailable assists: {}",
87 assist_id, 87 assist_id,
88 Assist::get(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange) 88 assists(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange)
89 .into_iter() 89 .into_iter()
90 .map(|assist| assist.id.0) 90 .map(|assist| assist.id.0)
91 .collect::<Vec<_>>() 91 .collect::<Vec<_>>()
@@ -210,7 +210,7 @@ fn assist_order_field_struct() {
210 let (before_cursor_pos, before) = extract_offset(before); 210 let (before_cursor_pos, before) = extract_offset(before);
211 let (db, file_id) = with_single_file(&before); 211 let (db, file_id) = with_single_file(&before);
212 let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) }; 212 let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) };
213 let assists = Assist::get(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange); 213 let assists = assists(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange);
214 let mut assists = assists.iter(); 214 let mut assists = assists.iter();
215 215
216 assert_eq!(assists.next().expect("expected assist").label, "Change visibility to pub(crate)"); 216 assert_eq!(assists.next().expect("expected assist").label, "Change visibility to pub(crate)");
@@ -235,7 +235,7 @@ pub fn test_some_range(a: int) -> bool {
235"#, 235"#,
236 ); 236 );
237 237
238 let assists = Assist::get(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange); 238 let assists = assists(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange);
239 let expected = labels(&assists); 239 let expected = labels(&assists);
240 240
241 expect![[r#" 241 expect![[r#"
@@ -264,7 +264,7 @@ pub fn test_some_range(a: int) -> bool {
264 let mut cfg = TEST_CONFIG; 264 let mut cfg = TEST_CONFIG;
265 cfg.allowed = Some(vec![AssistKind::Refactor]); 265 cfg.allowed = Some(vec![AssistKind::Refactor]);
266 266
267 let assists = Assist::get(&db, &cfg, AssistResolveStrategy::None, frange); 267 let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange);
268 let expected = labels(&assists); 268 let expected = labels(&assists);
269 269
270 expect![[r#" 270 expect![[r#"
@@ -279,7 +279,7 @@ pub fn test_some_range(a: int) -> bool {
279 { 279 {
280 let mut cfg = TEST_CONFIG; 280 let mut cfg = TEST_CONFIG;
281 cfg.allowed = Some(vec![AssistKind::RefactorExtract]); 281 cfg.allowed = Some(vec![AssistKind::RefactorExtract]);
282 let assists = Assist::get(&db, &cfg, AssistResolveStrategy::None, frange); 282 let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange);
283 let expected = labels(&assists); 283 let expected = labels(&assists);
284 284
285 expect![[r#" 285 expect![[r#"
@@ -292,7 +292,7 @@ pub fn test_some_range(a: int) -> bool {
292 { 292 {
293 let mut cfg = TEST_CONFIG; 293 let mut cfg = TEST_CONFIG;
294 cfg.allowed = Some(vec![AssistKind::QuickFix]); 294 cfg.allowed = Some(vec![AssistKind::QuickFix]);
295 let assists = Assist::get(&db, &cfg, AssistResolveStrategy::None, frange); 295 let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange);
296 let expected = labels(&assists); 296 let expected = labels(&assists);
297 297
298 expect![[r#""#]].assert_eq(&expected); 298 expect![[r#""#]].assert_eq(&expected);
@@ -317,7 +317,7 @@ pub fn test_some_range(a: int) -> bool {
317 cfg.allowed = Some(vec![AssistKind::RefactorExtract]); 317 cfg.allowed = Some(vec![AssistKind::RefactorExtract]);
318 318
319 { 319 {
320 let assists = Assist::get(&db, &cfg, AssistResolveStrategy::None, frange); 320 let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange);
321 assert_eq!(2, assists.len()); 321 assert_eq!(2, assists.len());
322 let mut assists = assists.into_iter(); 322 let mut assists = assists.into_iter();
323 323
@@ -353,7 +353,7 @@ pub fn test_some_range(a: int) -> bool {
353 } 353 }
354 354
355 { 355 {
356 let assists = Assist::get( 356 let assists = assists(
357 &db, 357 &db,
358 &cfg, 358 &cfg,
359 AssistResolveStrategy::Single(SingleResolve { 359 AssistResolveStrategy::Single(SingleResolve {
@@ -397,7 +397,7 @@ pub fn test_some_range(a: int) -> bool {
397 } 397 }
398 398
399 { 399 {
400 let assists = Assist::get( 400 let assists = assists(
401 &db, 401 &db,
402 &cfg, 402 &cfg,
403 AssistResolveStrategy::Single(SingleResolve { 403 AssistResolveStrategy::Single(SingleResolve {
@@ -462,7 +462,7 @@ pub fn test_some_range(a: int) -> bool {
462 } 462 }
463 463
464 { 464 {
465 let assists = Assist::get(&db, &cfg, AssistResolveStrategy::All, frange); 465 let assists = assists(&db, &cfg, AssistResolveStrategy::All, frange);
466 assert_eq!(2, assists.len()); 466 assert_eq!(2, assists.len());
467 let mut assists = assists.into_iter(); 467 let mut assists = assists.into_iter();
468 468
diff --git a/crates/ide_db/src/assists.rs b/crates/ide_db/src/assists.rs
new file mode 100644
index 000000000..7881d8369
--- /dev/null
+++ b/crates/ide_db/src/assists.rs
@@ -0,0 +1,136 @@
1//! This module defines the `Assist` data structure. The actual assist live in
2//! the `ide_assists` downstream crate. We want to define the data structures in
3//! this low-level crate though, because `ide_diagnostics` also need them
4//! (fixits for diagnostics and assists are the same thing under the hood). We
5//! want to compile `ide_assists` and `ide_diagnostics` in parallel though, so
6//! we pull the common definitions upstream, to this crate.
7
8use std::str::FromStr;
9
10use syntax::TextRange;
11
12use crate::{label::Label, source_change::SourceChange};
13
14#[derive(Debug, Clone)]
15pub struct Assist {
16 pub id: AssistId,
17 /// Short description of the assist, as shown in the UI.
18 pub label: Label,
19 pub group: Option<GroupLabel>,
20 /// Target ranges are used to sort assists: the smaller the target range,
21 /// the more specific assist is, and so it should be sorted first.
22 pub target: TextRange,
23 /// Computing source change sometimes is much more costly then computing the
24 /// other fields. Additionally, the actual change is not required to show
25 /// the lightbulb UI, it only is needed when the user tries to apply an
26 /// assist. So, we compute it lazily: the API allow requesting assists with
27 /// or without source change. We could (and in fact, used to) distinguish
28 /// between resolved and unresolved assists at the type level, but this is
29 /// cumbersome, especially if you want to embed an assist into another data
30 /// structure, such as a diagnostic.
31 pub source_change: Option<SourceChange>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum AssistKind {
36 // FIXME: does the None variant make sense? Probably not.
37 None,
38
39 QuickFix,
40 Generate,
41 Refactor,
42 RefactorExtract,
43 RefactorInline,
44 RefactorRewrite,
45}
46
47impl AssistKind {
48 pub fn contains(self, other: AssistKind) -> bool {
49 if self == other {
50 return true;
51 }
52
53 match self {
54 AssistKind::None | AssistKind::Generate => true,
55 AssistKind::Refactor => match other {
56 AssistKind::RefactorExtract
57 | AssistKind::RefactorInline
58 | AssistKind::RefactorRewrite => true,
59 _ => false,
60 },
61 _ => false,
62 }
63 }
64
65 pub fn name(&self) -> &str {
66 match self {
67 AssistKind::None => "None",
68 AssistKind::QuickFix => "QuickFix",
69 AssistKind::Generate => "Generate",
70 AssistKind::Refactor => "Refactor",
71 AssistKind::RefactorExtract => "RefactorExtract",
72 AssistKind::RefactorInline => "RefactorInline",
73 AssistKind::RefactorRewrite => "RefactorRewrite",
74 }
75 }
76}
77
78impl FromStr for AssistKind {
79 type Err = String;
80
81 fn from_str(s: &str) -> Result<Self, Self::Err> {
82 match s {
83 "None" => Ok(AssistKind::None),
84 "QuickFix" => Ok(AssistKind::QuickFix),
85 "Generate" => Ok(AssistKind::Generate),
86 "Refactor" => Ok(AssistKind::Refactor),
87 "RefactorExtract" => Ok(AssistKind::RefactorExtract),
88 "RefactorInline" => Ok(AssistKind::RefactorInline),
89 "RefactorRewrite" => Ok(AssistKind::RefactorRewrite),
90 unknown => Err(format!("Unknown AssistKind: '{}'", unknown)),
91 }
92 }
93}
94
95/// Unique identifier of the assist, should not be shown to the user
96/// directly.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct AssistId(pub &'static str, pub AssistKind);
99
100/// A way to control how many asssist to resolve during the assist resolution.
101/// When an assist is resolved, its edits are calculated that might be costly to always do by default.
102#[derive(Debug)]
103pub enum AssistResolveStrategy {
104 /// No assists should be resolved.
105 None,
106 /// All assists should be resolved.
107 All,
108 /// Only a certain assist should be resolved.
109 Single(SingleResolve),
110}
111
112/// Hold the [`AssistId`] data of a certain assist to resolve.
113/// The original id object cannot be used due to a `'static` lifetime
114/// and the requirement to construct this struct dynamically during the resolve handling.
115#[derive(Debug)]
116pub struct SingleResolve {
117 /// The id of the assist.
118 pub assist_id: String,
119 // The kind of the assist.
120 pub assist_kind: AssistKind,
121}
122
123impl AssistResolveStrategy {
124 pub fn should_resolve(&self, id: &AssistId) -> bool {
125 match self {
126 AssistResolveStrategy::None => false,
127 AssistResolveStrategy::All => true,
128 AssistResolveStrategy::Single(single_resolve) => {
129 single_resolve.assist_id == id.0 && single_resolve.assist_kind == id.1
130 }
131 }
132 }
133}
134
135#[derive(Clone, Debug)]
136pub struct GroupLabel(pub String);
diff --git a/crates/ide_db/src/lib.rs b/crates/ide_db/src/lib.rs
index 105607dca..7bbd08d6f 100644
--- a/crates/ide_db/src/lib.rs
+++ b/crates/ide_db/src/lib.rs
@@ -3,11 +3,11 @@
3//! It is mainly a `HirDatabase` for semantic analysis, plus a `SymbolsDatabase`, for fuzzy search. 3//! It is mainly a `HirDatabase` for semantic analysis, plus a `SymbolsDatabase`, for fuzzy search.
4 4
5mod apply_change; 5mod apply_change;
6pub mod assists;
6pub mod label; 7pub mod label;
7pub mod line_index; 8pub mod line_index;
8pub mod symbol_index; 9pub mod symbol_index;
9pub mod defs; 10pub mod defs;
10pub mod search;
11pub mod items_locator; 11pub mod items_locator;
12pub mod source_change; 12pub mod source_change;
13pub mod ty_filter; 13pub mod ty_filter;
@@ -15,6 +15,9 @@ pub mod traits;
15pub mod call_info; 15pub mod call_info;
16pub mod helpers; 16pub mod helpers;
17 17
18pub mod search;
19pub mod rename;
20
18use std::{fmt, sync::Arc}; 21use std::{fmt, sync::Arc};
19 22
20use base_db::{ 23use base_db::{
diff --git a/crates/ide_db/src/rename.rs b/crates/ide_db/src/rename.rs
new file mode 100644
index 000000000..82855725f
--- /dev/null
+++ b/crates/ide_db/src/rename.rs
@@ -0,0 +1,468 @@
1//! Rename infrastructure for rust-analyzer. It is used primarily for the
2//! literal "rename" in the ide (look for tests there), but it is also available
3//! as a general-purpose service. For example, it is used by the fix for the
4//! "incorrect case" diagnostic.
5//!
6//! It leverages the [`crate::search`] functionality to find what needs to be
7//! renamed. The actual renames are tricky -- field shorthands need special
8//! attention, and, when renaming modules, you also want to rename files on the
9//! file system.
10//!
11//! Another can of worms are macros:
12//!
13//! ```
14//! macro_rules! m { () => { fn f() {} } }
15//! m!();
16//! fn main() {
17//! f() // <- rename me
18//! }
19//! ```
20//!
21//! The correct behavior in such cases is probably to show a dialog to the user.
22//! Our current behavior is ¯\_(ツ)_/¯.
23use std::fmt;
24
25use base_db::{AnchoredPathBuf, FileId, FileRange};
26use either::Either;
27use hir::{AsAssocItem, FieldSource, HasSource, InFile, ModuleSource, Semantics};
28use stdx::never;
29use syntax::{
30 ast::{self, NameOwner},
31 lex_single_syntax_kind, AstNode, SyntaxKind, TextRange, T,
32};
33use text_edit::TextEdit;
34
35use crate::{
36 defs::Definition,
37 search::FileReference,
38 source_change::{FileSystemEdit, SourceChange},
39 RootDatabase,
40};
41
42pub type Result<T, E = RenameError> = std::result::Result<T, E>;
43
44#[derive(Debug)]
45pub struct RenameError(pub String);
46
47impl fmt::Display for RenameError {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 fmt::Display::fmt(&self.0, f)
50 }
51}
52
53#[macro_export]
54macro_rules! _format_err {
55 ($fmt:expr) => { RenameError(format!($fmt)) };
56 ($fmt:expr, $($arg:tt)+) => { RenameError(format!($fmt, $($arg)+)) }
57}
58pub use _format_err as format_err;
59
60#[macro_export]
61macro_rules! _bail {
62 ($($tokens:tt)*) => { return Err(format_err!($($tokens)*)) }
63}
64pub use _bail as bail;
65
66impl Definition {
67 pub fn rename(&self, sema: &Semantics<RootDatabase>, new_name: &str) -> Result<SourceChange> {
68 match *self {
69 Definition::ModuleDef(hir::ModuleDef::Module(module)) => {
70 rename_mod(sema, module, new_name)
71 }
72 Definition::ModuleDef(hir::ModuleDef::BuiltinType(_)) => {
73 bail!("Cannot rename builtin type")
74 }
75 Definition::SelfType(_) => bail!("Cannot rename `Self`"),
76 def => rename_reference(sema, def, new_name),
77 }
78 }
79
80 /// Textual range of the identifier which will change when renaming this
81 /// `Definition`. Note that some definitions, like buitin types, can't be
82 /// renamed.
83 pub fn range_for_rename(self, sema: &Semantics<RootDatabase>) -> Option<FileRange> {
84 // FIXME: the `original_file_range` calls here are wrong -- they never fail,
85 // and _fall back_ to the entirety of the macro call. Such fall back is
86 // incorrect for renames. The safe behavior would be to return an error for
87 // such cases. The correct behavior would be to return an auxiliary list of
88 // "can't rename these occurrences in macros" items, and then show some kind
89 // of a dialog to the user. See:
90 cov_mark::hit!(macros_are_broken_lol);
91
92 let res = match self {
93 Definition::Macro(mac) => {
94 let src = mac.source(sema.db)?;
95 let name = match &src.value {
96 Either::Left(it) => it.name()?,
97 Either::Right(it) => it.name()?,
98 };
99 src.with_value(name.syntax()).original_file_range(sema.db)
100 }
101 Definition::Field(field) => {
102 let src = field.source(sema.db)?;
103
104 match &src.value {
105 FieldSource::Named(record_field) => {
106 let name = record_field.name()?;
107 src.with_value(name.syntax()).original_file_range(sema.db)
108 }
109 FieldSource::Pos(_) => {
110 return None;
111 }
112 }
113 }
114 Definition::ModuleDef(module_def) => match module_def {
115 hir::ModuleDef::Module(module) => {
116 let src = module.declaration_source(sema.db)?;
117 let name = src.value.name()?;
118 src.with_value(name.syntax()).original_file_range(sema.db)
119 }
120 hir::ModuleDef::Function(it) => name_range(it, sema)?,
121 hir::ModuleDef::Adt(adt) => match adt {
122 hir::Adt::Struct(it) => name_range(it, sema)?,
123 hir::Adt::Union(it) => name_range(it, sema)?,
124 hir::Adt::Enum(it) => name_range(it, sema)?,
125 },
126 hir::ModuleDef::Variant(it) => name_range(it, sema)?,
127 hir::ModuleDef::Const(it) => name_range(it, sema)?,
128 hir::ModuleDef::Static(it) => name_range(it, sema)?,
129 hir::ModuleDef::Trait(it) => name_range(it, sema)?,
130 hir::ModuleDef::TypeAlias(it) => name_range(it, sema)?,
131 hir::ModuleDef::BuiltinType(_) => return None,
132 },
133 Definition::SelfType(_) => return None,
134 Definition::Local(local) => {
135 let src = local.source(sema.db);
136 let name = match &src.value {
137 Either::Left(bind_pat) => bind_pat.name()?,
138 Either::Right(_) => return None,
139 };
140 src.with_value(name.syntax()).original_file_range(sema.db)
141 }
142 Definition::GenericParam(generic_param) => match generic_param {
143 hir::GenericParam::TypeParam(type_param) => {
144 let src = type_param.source(sema.db)?;
145 let name = match &src.value {
146 Either::Left(_) => return None,
147 Either::Right(type_param) => type_param.name()?,
148 };
149 src.with_value(name.syntax()).original_file_range(sema.db)
150 }
151 hir::GenericParam::LifetimeParam(lifetime_param) => {
152 let src = lifetime_param.source(sema.db)?;
153 let lifetime = src.value.lifetime()?;
154 src.with_value(lifetime.syntax()).original_file_range(sema.db)
155 }
156 hir::GenericParam::ConstParam(it) => name_range(it, sema)?,
157 },
158 Definition::Label(label) => {
159 let src = label.source(sema.db);
160 let lifetime = src.value.lifetime()?;
161 src.with_value(lifetime.syntax()).original_file_range(sema.db)
162 }
163 };
164 return Some(res);
165
166 fn name_range<D>(def: D, sema: &Semantics<RootDatabase>) -> Option<FileRange>
167 where
168 D: HasSource,
169 D::Ast: ast::NameOwner,
170 {
171 let src = def.source(sema.db)?;
172 let name = src.value.name()?;
173 let res = src.with_value(name.syntax()).original_file_range(sema.db);
174 Some(res)
175 }
176 }
177}
178
179fn rename_mod(
180 sema: &Semantics<RootDatabase>,
181 module: hir::Module,
182 new_name: &str,
183) -> Result<SourceChange> {
184 if IdentifierKind::classify(new_name)? != IdentifierKind::Ident {
185 bail!("Invalid name `{0}`: cannot rename module to {0}", new_name);
186 }
187
188 let mut source_change = SourceChange::default();
189
190 let InFile { file_id, value: def_source } = module.definition_source(sema.db);
191 let file_id = file_id.original_file(sema.db);
192 if let ModuleSource::SourceFile(..) = def_source {
193 // mod is defined in path/to/dir/mod.rs
194 let path = if module.is_mod_rs(sema.db) {
195 format!("../{}/mod.rs", new_name)
196 } else {
197 format!("{}.rs", new_name)
198 };
199 let dst = AnchoredPathBuf { anchor: file_id, path };
200 let move_file = FileSystemEdit::MoveFile { src: file_id, dst };
201 source_change.push_file_system_edit(move_file);
202 }
203
204 if let Some(InFile { file_id, value: decl_source }) = module.declaration_source(sema.db) {
205 let file_id = file_id.original_file(sema.db);
206 match decl_source.name() {
207 Some(name) => source_change.insert_source_edit(
208 file_id,
209 TextEdit::replace(name.syntax().text_range(), new_name.to_string()),
210 ),
211 _ => never!("Module source node is missing a name"),
212 }
213 }
214 let def = Definition::ModuleDef(hir::ModuleDef::Module(module));
215 let usages = def.usages(sema).all();
216 let ref_edits = usages.iter().map(|(&file_id, references)| {
217 (file_id, source_edit_from_references(references, def, new_name))
218 });
219 source_change.extend(ref_edits);
220
221 Ok(source_change)
222}
223
224fn rename_reference(
225 sema: &Semantics<RootDatabase>,
226 mut def: Definition,
227 new_name: &str,
228) -> Result<SourceChange> {
229 let ident_kind = IdentifierKind::classify(new_name)?;
230
231 if matches!(
232 def, // is target a lifetime?
233 Definition::GenericParam(hir::GenericParam::LifetimeParam(_)) | Definition::Label(_)
234 ) {
235 match ident_kind {
236 IdentifierKind::Ident | IdentifierKind::Underscore => {
237 cov_mark::hit!(rename_not_a_lifetime_ident_ref);
238 bail!("Invalid name `{}`: not a lifetime identifier", new_name);
239 }
240 IdentifierKind::Lifetime => cov_mark::hit!(rename_lifetime),
241 }
242 } else {
243 match (ident_kind, def) {
244 (IdentifierKind::Lifetime, _) => {
245 cov_mark::hit!(rename_not_an_ident_ref);
246 bail!("Invalid name `{}`: not an identifier", new_name);
247 }
248 (IdentifierKind::Ident, _) => cov_mark::hit!(rename_non_local),
249 (IdentifierKind::Underscore, _) => (),
250 }
251 }
252
253 def = match def {
254 // HACK: resolve trait impl items to the item def of the trait definition
255 // so that we properly resolve all trait item references
256 Definition::ModuleDef(mod_def) => mod_def
257 .as_assoc_item(sema.db)
258 .and_then(|it| it.containing_trait_impl(sema.db))
259 .and_then(|it| {
260 it.items(sema.db).into_iter().find_map(|it| match (it, mod_def) {
261 (hir::AssocItem::Function(trait_func), hir::ModuleDef::Function(func))
262 if trait_func.name(sema.db) == func.name(sema.db) =>
263 {
264 Some(Definition::ModuleDef(hir::ModuleDef::Function(trait_func)))
265 }
266 (hir::AssocItem::Const(trait_konst), hir::ModuleDef::Const(konst))
267 if trait_konst.name(sema.db) == konst.name(sema.db) =>
268 {
269 Some(Definition::ModuleDef(hir::ModuleDef::Const(trait_konst)))
270 }
271 (
272 hir::AssocItem::TypeAlias(trait_type_alias),
273 hir::ModuleDef::TypeAlias(type_alias),
274 ) if trait_type_alias.name(sema.db) == type_alias.name(sema.db) => {
275 Some(Definition::ModuleDef(hir::ModuleDef::TypeAlias(trait_type_alias)))
276 }
277 _ => None,
278 })
279 })
280 .unwrap_or(def),
281 _ => def,
282 };
283 let usages = def.usages(sema).all();
284
285 if !usages.is_empty() && ident_kind == IdentifierKind::Underscore {
286 cov_mark::hit!(rename_underscore_multiple);
287 bail!("Cannot rename reference to `_` as it is being referenced multiple times");
288 }
289 let mut source_change = SourceChange::default();
290 source_change.extend(usages.iter().map(|(&file_id, references)| {
291 (file_id, source_edit_from_references(references, def, new_name))
292 }));
293
294 let (file_id, edit) = source_edit_from_def(sema, def, new_name)?;
295 source_change.insert_source_edit(file_id, edit);
296 Ok(source_change)
297}
298
299pub fn source_edit_from_references(
300 references: &[FileReference],
301 def: Definition,
302 new_name: &str,
303) -> TextEdit {
304 let mut edit = TextEdit::builder();
305 for reference in references {
306 let (range, replacement) = match &reference.name {
307 // if the ranges differ then the node is inside a macro call, we can't really attempt
308 // to make special rewrites like shorthand syntax and such, so just rename the node in
309 // the macro input
310 ast::NameLike::NameRef(name_ref)
311 if name_ref.syntax().text_range() == reference.range =>
312 {
313 source_edit_from_name_ref(name_ref, new_name, def)
314 }
315 ast::NameLike::Name(name) if name.syntax().text_range() == reference.range => {
316 source_edit_from_name(name, new_name)
317 }
318 _ => None,
319 }
320 .unwrap_or_else(|| (reference.range, new_name.to_string()));
321 edit.replace(range, replacement);
322 }
323 edit.finish()
324}
325
326fn source_edit_from_name(name: &ast::Name, new_name: &str) -> Option<(TextRange, String)> {
327 if let Some(_) = ast::RecordPatField::for_field_name(name) {
328 if let Some(ident_pat) = name.syntax().parent().and_then(ast::IdentPat::cast) {
329 return Some((
330 TextRange::empty(ident_pat.syntax().text_range().start()),
331 [new_name, ": "].concat(),
332 ));
333 }
334 }
335 None
336}
337
338fn source_edit_from_name_ref(
339 name_ref: &ast::NameRef,
340 new_name: &str,
341 def: Definition,
342) -> Option<(TextRange, String)> {
343 if let Some(record_field) = ast::RecordExprField::for_name_ref(name_ref) {
344 let rcf_name_ref = record_field.name_ref();
345 let rcf_expr = record_field.expr();
346 match (rcf_name_ref, rcf_expr.and_then(|it| it.name_ref())) {
347 // field: init-expr, check if we can use a field init shorthand
348 (Some(field_name), Some(init)) => {
349 if field_name == *name_ref {
350 if init.text() == new_name {
351 cov_mark::hit!(test_rename_field_put_init_shorthand);
352 // same names, we can use a shorthand here instead.
353 // we do not want to erase attributes hence this range start
354 let s = field_name.syntax().text_range().start();
355 let e = record_field.syntax().text_range().end();
356 return Some((TextRange::new(s, e), new_name.to_owned()));
357 }
358 } else if init == *name_ref {
359 if field_name.text() == new_name {
360 cov_mark::hit!(test_rename_local_put_init_shorthand);
361 // same names, we can use a shorthand here instead.
362 // we do not want to erase attributes hence this range start
363 let s = field_name.syntax().text_range().start();
364 let e = record_field.syntax().text_range().end();
365 return Some((TextRange::new(s, e), new_name.to_owned()));
366 }
367 }
368 None
369 }
370 // init shorthand
371 // FIXME: instead of splitting the shorthand, recursively trigger a rename of the
372 // other name https://github.com/rust-analyzer/rust-analyzer/issues/6547
373 (None, Some(_)) if matches!(def, Definition::Field(_)) => {
374 cov_mark::hit!(test_rename_field_in_field_shorthand);
375 let s = name_ref.syntax().text_range().start();
376 Some((TextRange::empty(s), format!("{}: ", new_name)))
377 }
378 (None, Some(_)) if matches!(def, Definition::Local(_)) => {
379 cov_mark::hit!(test_rename_local_in_field_shorthand);
380 let s = name_ref.syntax().text_range().end();
381 Some((TextRange::empty(s), format!(": {}", new_name)))
382 }
383 _ => None,
384 }
385 } else if let Some(record_field) = ast::RecordPatField::for_field_name_ref(name_ref) {
386 let rcf_name_ref = record_field.name_ref();
387 let rcf_pat = record_field.pat();
388 match (rcf_name_ref, rcf_pat) {
389 // field: rename
390 (Some(field_name), Some(ast::Pat::IdentPat(pat))) if field_name == *name_ref => {
391 // field name is being renamed
392 if pat.name().map_or(false, |it| it.text() == new_name) {
393 cov_mark::hit!(test_rename_field_put_init_shorthand_pat);
394 // same names, we can use a shorthand here instead/
395 // we do not want to erase attributes hence this range start
396 let s = field_name.syntax().text_range().start();
397 let e = record_field.syntax().text_range().end();
398 Some((TextRange::new(s, e), pat.to_string()))
399 } else {
400 None
401 }
402 }
403 _ => None,
404 }
405 } else {
406 None
407 }
408}
409
410fn source_edit_from_def(
411 sema: &Semantics<RootDatabase>,
412 def: Definition,
413 new_name: &str,
414) -> Result<(FileId, TextEdit)> {
415 let frange = def
416 .range_for_rename(sema)
417 .ok_or_else(|| format_err!("No identifier available to rename"))?;
418
419 let mut replacement_text = String::new();
420 let mut repl_range = frange.range;
421 if let Definition::Local(local) = def {
422 if let Either::Left(pat) = local.source(sema.db).value {
423 if matches!(
424 pat.syntax().parent().and_then(ast::RecordPatField::cast),
425 Some(pat_field) if pat_field.name_ref().is_none()
426 ) {
427 replacement_text.push_str(": ");
428 replacement_text.push_str(new_name);
429 repl_range = TextRange::new(
430 pat.syntax().text_range().end(),
431 pat.syntax().text_range().end(),
432 );
433 }
434 }
435 }
436 if replacement_text.is_empty() {
437 replacement_text.push_str(new_name);
438 }
439 let edit = TextEdit::replace(repl_range, replacement_text);
440 Ok((frange.file_id, edit))
441}
442
443#[derive(Copy, Clone, Debug, PartialEq)]
444pub enum IdentifierKind {
445 Ident,
446 Lifetime,
447 Underscore,
448}
449
450impl IdentifierKind {
451 pub fn classify(new_name: &str) -> Result<IdentifierKind> {
452 match lex_single_syntax_kind(new_name) {
453 Some(res) => match res {
454 (SyntaxKind::IDENT, _) => Ok(IdentifierKind::Ident),
455 (T![_], _) => Ok(IdentifierKind::Underscore),
456 (SyntaxKind::LIFETIME_IDENT, _) if new_name != "'static" && new_name != "'_" => {
457 Ok(IdentifierKind::Lifetime)
458 }
459 (SyntaxKind::LIFETIME_IDENT, _) => {
460 bail!("Invalid name `{}`: not a lifetime identifier", new_name)
461 }
462 (_, Some(syntax_error)) => bail!("Invalid name `{}`: {}", new_name, syntax_error),
463 (_, None) => bail!("Invalid name `{}`: not an identifier", new_name),
464 },
465 None => bail!("Invalid name `{}`: not an identifier", new_name),
466 }
467 }
468}
diff --git a/crates/ide_diagnostics/Cargo.toml b/crates/ide_diagnostics/Cargo.toml
new file mode 100644
index 000000000..fa2adf212
--- /dev/null
+++ b/crates/ide_diagnostics/Cargo.toml
@@ -0,0 +1,29 @@
1[package]
2name = "ide_diagnostics"
3version = "0.0.0"
4description = "TBD"
5license = "MIT OR Apache-2.0"
6authors = ["rust-analyzer developers"]
7edition = "2018"
8
9[lib]
10doctest = false
11
12[dependencies]
13cov-mark = "2.0.0-pre.1"
14itertools = "0.10.0"
15rustc-hash = "1.1.0"
16either = "1.5.3"
17
18profile = { path = "../profile", version = "0.0.0" }
19stdx = { path = "../stdx", version = "0.0.0" }
20syntax = { path = "../syntax", version = "0.0.0" }
21text_edit = { path = "../text_edit", version = "0.0.0" }
22cfg = { path = "../cfg", version = "0.0.0" }
23hir = { path = "../hir", version = "0.0.0" }
24ide_db = { path = "../ide_db", version = "0.0.0" }
25
26[dev-dependencies]
27expect-test = "1.1"
28
29test_utils = { path = "../test_utils" }
diff --git a/crates/ide/src/diagnostics/field_shorthand.rs b/crates/ide_diagnostics/src/field_shorthand.rs
index c7f4dab8e..0b6af9965 100644
--- a/crates/ide/src/diagnostics/field_shorthand.rs
+++ b/crates/ide_diagnostics/src/field_shorthand.rs
@@ -5,7 +5,7 @@ use ide_db::{base_db::FileId, source_change::SourceChange};
5use syntax::{ast, match_ast, AstNode, SyntaxNode}; 5use syntax::{ast, match_ast, AstNode, SyntaxNode};
6use text_edit::TextEdit; 6use text_edit::TextEdit;
7 7
8use crate::{diagnostics::fix, Diagnostic, Severity}; 8use crate::{fix, Diagnostic, Severity};
9 9
10pub(super) fn check(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) { 10pub(super) fn check(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) {
11 match_ast! { 11 match_ast! {
@@ -101,7 +101,7 @@ fn check_pat_field_shorthand(
101 101
102#[cfg(test)] 102#[cfg(test)]
103mod tests { 103mod tests {
104 use crate::diagnostics::tests::{check_diagnostics, check_fix}; 104 use crate::tests::{check_diagnostics, check_fix};
105 105
106 #[test] 106 #[test]
107 fn test_check_expr_field_shorthand() { 107 fn test_check_expr_field_shorthand() {
diff --git a/crates/ide/src/diagnostics/break_outside_of_loop.rs b/crates/ide_diagnostics/src/handlers/break_outside_of_loop.rs
index 80e68f3cc..5ad0fbd1b 100644
--- a/crates/ide/src/diagnostics/break_outside_of_loop.rs
+++ b/crates/ide_diagnostics/src/handlers/break_outside_of_loop.rs
@@ -1,9 +1,9 @@
1use crate::diagnostics::{Diagnostic, DiagnosticsContext}; 1use crate::{Diagnostic, DiagnosticsContext};
2 2
3// Diagnostic: break-outside-of-loop 3// Diagnostic: break-outside-of-loop
4// 4//
5// This diagnostic is triggered if the `break` keyword is used outside of a loop. 5// This diagnostic is triggered if the `break` keyword is used outside of a loop.
6pub(super) fn break_outside_of_loop( 6pub(crate) fn break_outside_of_loop(
7 ctx: &DiagnosticsContext<'_>, 7 ctx: &DiagnosticsContext<'_>,
8 d: &hir::BreakOutsideOfLoop, 8 d: &hir::BreakOutsideOfLoop,
9) -> Diagnostic { 9) -> Diagnostic {
@@ -16,7 +16,7 @@ pub(super) fn break_outside_of_loop(
16 16
17#[cfg(test)] 17#[cfg(test)]
18mod tests { 18mod tests {
19 use crate::diagnostics::tests::check_diagnostics; 19 use crate::tests::check_diagnostics;
20 20
21 #[test] 21 #[test]
22 fn break_outside_of_loop() { 22 fn break_outside_of_loop() {
diff --git a/crates/ide/src/diagnostics/inactive_code.rs b/crates/ide_diagnostics/src/handlers/inactive_code.rs
index d9d3e88c1..4b722fd64 100644
--- a/crates/ide/src/diagnostics/inactive_code.rs
+++ b/crates/ide_diagnostics/src/handlers/inactive_code.rs
@@ -1,15 +1,12 @@
1use cfg::DnfExpr; 1use cfg::DnfExpr;
2use stdx::format_to; 2use stdx::format_to;
3 3
4use crate::{ 4use crate::{Diagnostic, DiagnosticsContext, Severity};
5 diagnostics::{Diagnostic, DiagnosticsContext},
6 Severity,
7};
8 5
9// Diagnostic: inactive-code 6// Diagnostic: inactive-code
10// 7//
11// This diagnostic is shown for code with inactive `#[cfg]` attributes. 8// This diagnostic is shown for code with inactive `#[cfg]` attributes.
12pub(super) fn inactive_code( 9pub(crate) fn inactive_code(
13 ctx: &DiagnosticsContext<'_>, 10 ctx: &DiagnosticsContext<'_>,
14 d: &hir::InactiveCode, 11 d: &hir::InactiveCode,
15) -> Option<Diagnostic> { 12) -> Option<Diagnostic> {
@@ -37,7 +34,7 @@ pub(super) fn inactive_code(
37 34
38#[cfg(test)] 35#[cfg(test)]
39mod tests { 36mod tests {
40 use crate::{diagnostics::tests::check_diagnostics_with_config, DiagnosticsConfig}; 37 use crate::{tests::check_diagnostics_with_config, DiagnosticsConfig};
41 38
42 pub(crate) fn check(ra_fixture: &str) { 39 pub(crate) fn check(ra_fixture: &str) {
43 let config = DiagnosticsConfig::default(); 40 let config = DiagnosticsConfig::default();
diff --git a/crates/ide/src/diagnostics/incorrect_case.rs b/crates/ide_diagnostics/src/handlers/incorrect_case.rs
index 832394400..3a33029cf 100644
--- a/crates/ide/src/diagnostics/incorrect_case.rs
+++ b/crates/ide_diagnostics/src/handlers/incorrect_case.rs
@@ -1,18 +1,19 @@
1use hir::{db::AstDatabase, InFile}; 1use hir::{db::AstDatabase, InFile};
2use ide_assists::Assist; 2use ide_db::{assists::Assist, defs::NameClass};
3use ide_db::base_db::FilePosition;
4use syntax::AstNode; 3use syntax::AstNode;
5 4
6use crate::{ 5use crate::{
7 diagnostics::{unresolved_fix, Diagnostic, DiagnosticsContext}, 6 // references::rename::rename_with_semantics,
8 references::rename::rename_with_semantics, 7 unresolved_fix,
8 Diagnostic,
9 DiagnosticsContext,
9 Severity, 10 Severity,
10}; 11};
11 12
12// Diagnostic: incorrect-ident-case 13// Diagnostic: incorrect-ident-case
13// 14//
14// This diagnostic is triggered if an item name doesn't follow https://doc.rust-lang.org/1.0.0/style/style/naming/README.html[Rust naming convention]. 15// This diagnostic is triggered if an item name doesn't follow https://doc.rust-lang.org/1.0.0/style/style/naming/README.html[Rust naming convention].
15pub(super) fn incorrect_case(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Diagnostic { 16pub(crate) fn incorrect_case(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Diagnostic {
16 Diagnostic::new( 17 Diagnostic::new(
17 "incorrect-ident-case", 18 "incorrect-ident-case",
18 format!( 19 format!(
@@ -28,15 +29,15 @@ pub(super) fn incorrect_case(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCas
28fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Option<Vec<Assist>> { 29fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Option<Vec<Assist>> {
29 let root = ctx.sema.db.parse_or_expand(d.file)?; 30 let root = ctx.sema.db.parse_or_expand(d.file)?;
30 let name_node = d.ident.to_node(&root); 31 let name_node = d.ident.to_node(&root);
32 let def = NameClass::classify(&ctx.sema, &name_node)?.defined(ctx.sema.db)?;
31 33
32 let name_node = InFile::new(d.file, name_node.syntax()); 34 let name_node = InFile::new(d.file, name_node.syntax());
33 let frange = name_node.original_file_range(ctx.sema.db); 35 let frange = name_node.original_file_range(ctx.sema.db);
34 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
35 36
36 let label = format!("Rename to {}", d.suggested_text); 37 let label = format!("Rename to {}", d.suggested_text);
37 let mut res = unresolved_fix("change_case", &label, frange.range); 38 let mut res = unresolved_fix("change_case", &label, frange.range);
38 if ctx.resolve.should_resolve(&res.id) { 39 if ctx.resolve.should_resolve(&res.id) {
39 let source_change = rename_with_semantics(&ctx.sema, file_position, &d.suggested_text); 40 let source_change = def.rename(&ctx.sema, &d.suggested_text);
40 res.source_change = Some(source_change.ok().unwrap_or_default()); 41 res.source_change = Some(source_change.ok().unwrap_or_default());
41 } 42 }
42 43
@@ -45,10 +46,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Option<Vec<Ass
45 46
46#[cfg(test)] 47#[cfg(test)]
47mod change_case { 48mod change_case {
48 use crate::{ 49 use crate::tests::{check_diagnostics, check_fix};
49 diagnostics::tests::{check_diagnostics, check_fix},
50 fixture, AssistResolveStrategy, DiagnosticsConfig,
51 };
52 50
53 #[test] 51 #[test]
54 fn test_rename_incorrect_case() { 52 fn test_rename_incorrect_case() {
@@ -116,7 +114,7 @@ fn some_fn() {
116 check_diagnostics( 114 check_diagnostics(
117 r#" 115 r#"
118fn foo() { 116fn foo() {
119 const ANOTHER_ITEM$0: &str = "some_item"; 117 const ANOTHER_ITEM: &str = "some_item";
120} 118}
121"#, 119"#,
122 ); 120 );
@@ -148,20 +146,13 @@ impl TestStruct {
148 146
149 #[test] 147 #[test]
150 fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() { 148 fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() {
151 let input = r#"fn FOO$0() {}"#; 149 check_diagnostics(
152 let expected = r#"fn foo() {}"#; 150 r#"
153 151fn FOO() {}
154 let (analysis, file_position) = fixture::position(input); 152// ^^^ Function `FOO` should have snake_case name, e.g. `foo`
155 let diagnostics = analysis 153"#,
156 .diagnostics( 154 );
157 &DiagnosticsConfig::default(), 155 check_fix(r#"fn FOO$0() {}"#, r#"fn foo() {}"#);
158 AssistResolveStrategy::All,
159 file_position.file_id,
160 )
161 .unwrap();
162 assert_eq!(diagnostics.len(), 1);
163
164 check_fix(input, expected);
165 } 156 }
166 157
167 #[test] 158 #[test]
diff --git a/crates/ide/src/diagnostics/macro_error.rs b/crates/ide_diagnostics/src/handlers/macro_error.rs
index 5f97f190d..d4d928ad1 100644
--- a/crates/ide/src/diagnostics/macro_error.rs
+++ b/crates/ide_diagnostics/src/handlers/macro_error.rs
@@ -1,9 +1,9 @@
1use crate::diagnostics::{Diagnostic, DiagnosticsContext}; 1use crate::{Diagnostic, DiagnosticsContext};
2 2
3// Diagnostic: macro-error 3// Diagnostic: macro-error
4// 4//
5// This diagnostic is shown for macro expansion errors. 5// This diagnostic is shown for macro expansion errors.
6pub(super) fn macro_error(ctx: &DiagnosticsContext<'_>, d: &hir::MacroError) -> Diagnostic { 6pub(crate) fn macro_error(ctx: &DiagnosticsContext<'_>, d: &hir::MacroError) -> Diagnostic {
7 Diagnostic::new( 7 Diagnostic::new(
8 "macro-error", 8 "macro-error",
9 d.message.clone(), 9 d.message.clone(),
@@ -15,7 +15,7 @@ pub(super) fn macro_error(ctx: &DiagnosticsContext<'_>, d: &hir::MacroError) ->
15#[cfg(test)] 15#[cfg(test)]
16mod tests { 16mod tests {
17 use crate::{ 17 use crate::{
18 diagnostics::tests::{check_diagnostics, check_diagnostics_with_config}, 18 tests::{check_diagnostics, check_diagnostics_with_config},
19 DiagnosticsConfig, 19 DiagnosticsConfig,
20 }; 20 };
21 21
diff --git a/crates/ide/src/diagnostics/mismatched_arg_count.rs b/crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs
index 08e1cfa5f..ce313b2cc 100644
--- a/crates/ide/src/diagnostics/mismatched_arg_count.rs
+++ b/crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs
@@ -1,9 +1,9 @@
1use crate::diagnostics::{Diagnostic, DiagnosticsContext}; 1use crate::{Diagnostic, DiagnosticsContext};
2 2
3// Diagnostic: mismatched-arg-count 3// Diagnostic: mismatched-arg-count
4// 4//
5// This diagnostic is triggered if a function is invoked with an incorrect amount of arguments. 5// This diagnostic is triggered if a function is invoked with an incorrect amount of arguments.
6pub(super) fn mismatched_arg_count( 6pub(crate) fn mismatched_arg_count(
7 ctx: &DiagnosticsContext<'_>, 7 ctx: &DiagnosticsContext<'_>,
8 d: &hir::MismatchedArgCount, 8 d: &hir::MismatchedArgCount,
9) -> Diagnostic { 9) -> Diagnostic {
@@ -18,7 +18,7 @@ pub(super) fn mismatched_arg_count(
18 18
19#[cfg(test)] 19#[cfg(test)]
20mod tests { 20mod tests {
21 use crate::diagnostics::tests::check_diagnostics; 21 use crate::tests::check_diagnostics;
22 22
23 #[test] 23 #[test]
24 fn simple_free_fn_zero() { 24 fn simple_free_fn_zero() {
diff --git a/crates/ide/src/diagnostics/missing_fields.rs b/crates/ide_diagnostics/src/handlers/missing_fields.rs
index d01f05041..bc82c0e4a 100644
--- a/crates/ide/src/diagnostics/missing_fields.rs
+++ b/crates/ide_diagnostics/src/handlers/missing_fields.rs
@@ -1,12 +1,11 @@
1use either::Either; 1use either::Either;
2use hir::{db::AstDatabase, InFile}; 2use hir::{db::AstDatabase, InFile};
3use ide_assists::Assist; 3use ide_db::{assists::Assist, source_change::SourceChange};
4use ide_db::source_change::SourceChange;
5use stdx::format_to; 4use stdx::format_to;
6use syntax::{algo, ast::make, AstNode, SyntaxNodePtr}; 5use syntax::{algo, ast::make, AstNode, SyntaxNodePtr};
7use text_edit::TextEdit; 6use text_edit::TextEdit;
8 7
9use crate::diagnostics::{fix, Diagnostic, DiagnosticsContext}; 8use crate::{fix, Diagnostic, DiagnosticsContext};
10 9
11// Diagnostic: missing-fields 10// Diagnostic: missing-fields
12// 11//
@@ -19,7 +18,7 @@ use crate::diagnostics::{fix, Diagnostic, DiagnosticsContext};
19// 18//
20// let a = A { a: 10 }; 19// let a = A { a: 10 };
21// ``` 20// ```
22pub(super) fn missing_fields(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Diagnostic { 21pub(crate) fn missing_fields(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Diagnostic {
23 let mut message = String::from("Missing structure fields:\n"); 22 let mut message = String::from("Missing structure fields:\n");
24 for field in &d.missed_fields { 23 for field in &d.missed_fields {
25 format_to!(message, "- {}\n", field); 24 format_to!(message, "- {}\n", field);
@@ -77,7 +76,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Ass
77 76
78#[cfg(test)] 77#[cfg(test)]
79mod tests { 78mod tests {
80 use crate::diagnostics::tests::{check_diagnostics, check_fix}; 79 use crate::tests::{check_diagnostics, check_fix};
81 80
82 #[test] 81 #[test]
83 fn missing_record_pat_field_diagnostic() { 82 fn missing_record_pat_field_diagnostic() {
diff --git a/crates/ide/src/diagnostics/missing_match_arms.rs b/crates/ide_diagnostics/src/handlers/missing_match_arms.rs
index b636489b3..9ea533d74 100644
--- a/crates/ide/src/diagnostics/missing_match_arms.rs
+++ b/crates/ide_diagnostics/src/handlers/missing_match_arms.rs
@@ -1,11 +1,11 @@
1use hir::InFile; 1use hir::InFile;
2 2
3use crate::diagnostics::{Diagnostic, DiagnosticsContext}; 3use crate::{Diagnostic, DiagnosticsContext};
4 4
5// Diagnostic: missing-match-arm 5// Diagnostic: missing-match-arm
6// 6//
7// This diagnostic is triggered if `match` block is missing one or more match arms. 7// This diagnostic is triggered if `match` block is missing one or more match arms.
8pub(super) fn missing_match_arms( 8pub(crate) fn missing_match_arms(
9 ctx: &DiagnosticsContext<'_>, 9 ctx: &DiagnosticsContext<'_>,
10 d: &hir::MissingMatchArms, 10 d: &hir::MissingMatchArms,
11) -> Diagnostic { 11) -> Diagnostic {
@@ -17,12 +17,12 @@ pub(super) fn missing_match_arms(
17} 17}
18 18
19#[cfg(test)] 19#[cfg(test)]
20pub(super) mod tests { 20mod tests {
21 use crate::diagnostics::tests::check_diagnostics; 21 use crate::tests::check_diagnostics;
22 22
23 fn check_diagnostics_no_bails(ra_fixture: &str) { 23 fn check_diagnostics_no_bails(ra_fixture: &str) {
24 cov_mark::check_count!(validate_match_bailed_out, 0); 24 cov_mark::check_count!(validate_match_bailed_out, 0);
25 crate::diagnostics::tests::check_diagnostics(ra_fixture) 25 crate::tests::check_diagnostics(ra_fixture)
26 } 26 }
27 27
28 #[test] 28 #[test]
diff --git a/crates/ide/src/diagnostics/missing_ok_or_some_in_tail_expr.rs b/crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs
index 06005d156..63de54570 100644
--- a/crates/ide/src/diagnostics/missing_ok_or_some_in_tail_expr.rs
+++ b/crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs
@@ -1,10 +1,9 @@
1use hir::db::AstDatabase; 1use hir::db::AstDatabase;
2use ide_assists::Assist; 2use ide_db::{assists::Assist, source_change::SourceChange};
3use ide_db::source_change::SourceChange;
4use syntax::AstNode; 3use syntax::AstNode;
5use text_edit::TextEdit; 4use text_edit::TextEdit;
6 5
7use crate::diagnostics::{fix, Diagnostic, DiagnosticsContext}; 6use crate::{fix, Diagnostic, DiagnosticsContext};
8 7
9// Diagnostic: missing-ok-or-some-in-tail-expr 8// Diagnostic: missing-ok-or-some-in-tail-expr
10// 9//
@@ -18,7 +17,7 @@ use crate::diagnostics::{fix, Diagnostic, DiagnosticsContext};
18// 10 17// 10
19// } 18// }
20// ``` 19// ```
21pub(super) fn missing_ok_or_some_in_tail_expr( 20pub(crate) fn missing_ok_or_some_in_tail_expr(
22 ctx: &DiagnosticsContext<'_>, 21 ctx: &DiagnosticsContext<'_>,
23 d: &hir::MissingOkOrSomeInTailExpr, 22 d: &hir::MissingOkOrSomeInTailExpr,
24) -> Diagnostic { 23) -> Diagnostic {
@@ -44,7 +43,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingOkOrSomeInTailExpr) -> Op
44 43
45#[cfg(test)] 44#[cfg(test)]
46mod tests { 45mod tests {
47 use crate::diagnostics::tests::{check_diagnostics, check_fix}; 46 use crate::tests::{check_diagnostics, check_fix};
48 47
49 #[test] 48 #[test]
50 fn test_wrap_return_type_option() { 49 fn test_wrap_return_type_option() {
diff --git a/crates/ide/src/diagnostics/missing_unsafe.rs b/crates/ide_diagnostics/src/handlers/missing_unsafe.rs
index 5c47e8d0a..62d8687ba 100644
--- a/crates/ide/src/diagnostics/missing_unsafe.rs
+++ b/crates/ide_diagnostics/src/handlers/missing_unsafe.rs
@@ -1,9 +1,9 @@
1use crate::diagnostics::{Diagnostic, DiagnosticsContext}; 1use crate::{Diagnostic, DiagnosticsContext};
2 2
3// Diagnostic: missing-unsafe 3// Diagnostic: missing-unsafe
4// 4//
5// This diagnostic is triggered if an operation marked as `unsafe` is used outside of an `unsafe` function or block. 5// This diagnostic is triggered if an operation marked as `unsafe` is used outside of an `unsafe` function or block.
6pub(super) fn missing_unsafe(ctx: &DiagnosticsContext<'_>, d: &hir::MissingUnsafe) -> Diagnostic { 6pub(crate) fn missing_unsafe(ctx: &DiagnosticsContext<'_>, d: &hir::MissingUnsafe) -> Diagnostic {
7 Diagnostic::new( 7 Diagnostic::new(
8 "missing-unsafe", 8 "missing-unsafe",
9 "this operation is unsafe and requires an unsafe function or block", 9 "this operation is unsafe and requires an unsafe function or block",
@@ -13,7 +13,7 @@ pub(super) fn missing_unsafe(ctx: &DiagnosticsContext<'_>, d: &hir::MissingUnsaf
13 13
14#[cfg(test)] 14#[cfg(test)]
15mod tests { 15mod tests {
16 use crate::diagnostics::tests::check_diagnostics; 16 use crate::tests::check_diagnostics;
17 17
18 #[test] 18 #[test]
19 fn missing_unsafe_diagnostic_with_raw_ptr() { 19 fn missing_unsafe_diagnostic_with_raw_ptr() {
diff --git a/crates/ide/src/diagnostics/no_such_field.rs b/crates/ide_diagnostics/src/handlers/no_such_field.rs
index edc63c246..e4cc8a840 100644
--- a/crates/ide/src/diagnostics/no_such_field.rs
+++ b/crates/ide_diagnostics/src/handlers/no_such_field.rs
@@ -6,15 +6,12 @@ use syntax::{
6}; 6};
7use text_edit::TextEdit; 7use text_edit::TextEdit;
8 8
9use crate::{ 9use crate::{fix, Assist, Diagnostic, DiagnosticsContext};
10 diagnostics::{fix, Diagnostic, DiagnosticsContext},
11 Assist,
12};
13 10
14// Diagnostic: no-such-field 11// Diagnostic: no-such-field
15// 12//
16// This diagnostic is triggered if created structure does not have field provided in record. 13// This diagnostic is triggered if created structure does not have field provided in record.
17pub(super) fn no_such_field(ctx: &DiagnosticsContext<'_>, d: &hir::NoSuchField) -> Diagnostic { 14pub(crate) fn no_such_field(ctx: &DiagnosticsContext<'_>, d: &hir::NoSuchField) -> Diagnostic {
18 Diagnostic::new( 15 Diagnostic::new(
19 "no-such-field", 16 "no-such-field",
20 "no such field", 17 "no such field",
@@ -112,7 +109,7 @@ fn missing_record_expr_field_fixes(
112 109
113#[cfg(test)] 110#[cfg(test)]
114mod tests { 111mod tests {
115 use crate::diagnostics::tests::{check_diagnostics, check_fix}; 112 use crate::tests::{check_diagnostics, check_fix};
116 113
117 #[test] 114 #[test]
118 fn no_such_field_diagnostics() { 115 fn no_such_field_diagnostics() {
diff --git a/crates/ide/src/diagnostics/remove_this_semicolon.rs b/crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs
index 814cb0f8c..b52e4dc84 100644
--- a/crates/ide/src/diagnostics/remove_this_semicolon.rs
+++ b/crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs
@@ -3,15 +3,12 @@ use ide_db::source_change::SourceChange;
3use syntax::{ast, AstNode}; 3use syntax::{ast, AstNode};
4use text_edit::TextEdit; 4use text_edit::TextEdit;
5 5
6use crate::{ 6use crate::{fix, Assist, Diagnostic, DiagnosticsContext};
7 diagnostics::{fix, Diagnostic, DiagnosticsContext},
8 Assist,
9};
10 7
11// Diagnostic: remove-this-semicolon 8// Diagnostic: remove-this-semicolon
12// 9//
13// This diagnostic is triggered when there's an erroneous `;` at the end of the block. 10// This diagnostic is triggered when there's an erroneous `;` at the end of the block.
14pub(super) fn remove_this_semicolon( 11pub(crate) fn remove_this_semicolon(
15 ctx: &DiagnosticsContext<'_>, 12 ctx: &DiagnosticsContext<'_>,
16 d: &hir::RemoveThisSemicolon, 13 d: &hir::RemoveThisSemicolon,
17) -> Diagnostic { 14) -> Diagnostic {
@@ -45,7 +42,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::RemoveThisSemicolon) -> Option<V
45 42
46#[cfg(test)] 43#[cfg(test)]
47mod tests { 44mod tests {
48 use crate::diagnostics::tests::{check_diagnostics, check_fix}; 45 use crate::tests::{check_diagnostics, check_fix};
49 46
50 #[test] 47 #[test]
51 fn missing_semicolon() { 48 fn missing_semicolon() {
diff --git a/crates/ide/src/diagnostics/replace_filter_map_next_with_find_map.rs b/crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs
index f3b011495..10d5da15d 100644
--- a/crates/ide/src/diagnostics/replace_filter_map_next_with_find_map.rs
+++ b/crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs
@@ -6,15 +6,12 @@ use syntax::{
6}; 6};
7use text_edit::TextEdit; 7use text_edit::TextEdit;
8 8
9use crate::{ 9use crate::{fix, Assist, Diagnostic, DiagnosticsContext, Severity};
10 diagnostics::{fix, Diagnostic, DiagnosticsContext},
11 Assist, Severity,
12};
13 10
14// Diagnostic: replace-filter-map-next-with-find-map 11// Diagnostic: replace-filter-map-next-with-find-map
15// 12//
16// This diagnostic is triggered when `.filter_map(..).next()` is used, rather than the more concise `.find_map(..)`. 13// This diagnostic is triggered when `.filter_map(..).next()` is used, rather than the more concise `.find_map(..)`.
17pub(super) fn replace_filter_map_next_with_find_map( 14pub(crate) fn replace_filter_map_next_with_find_map(
18 ctx: &DiagnosticsContext<'_>, 15 ctx: &DiagnosticsContext<'_>,
19 d: &hir::ReplaceFilterMapNextWithFindMap, 16 d: &hir::ReplaceFilterMapNextWithFindMap,
20) -> Diagnostic { 17) -> Diagnostic {
@@ -58,7 +55,7 @@ fn fixes(
58 55
59#[cfg(test)] 56#[cfg(test)]
60mod tests { 57mod tests {
61 use crate::diagnostics::tests::check_fix; 58 use crate::tests::check_fix;
62 59
63 // Register the required standard library types to make the tests work 60 // Register the required standard library types to make the tests work
64 #[track_caller] 61 #[track_caller]
@@ -86,7 +83,7 @@ pub mod iter {
86 } 83 }
87} 84}
88"#; 85"#;
89 crate::diagnostics::tests::check_diagnostics(&format!("{}{}{}", prefix, ra_fixture, suffix)) 86 crate::tests::check_diagnostics(&format!("{}{}{}", prefix, ra_fixture, suffix))
90 } 87 }
91 88
92 #[test] 89 #[test]
diff --git a/crates/ide/src/diagnostics/unimplemented_builtin_macro.rs b/crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs
index 09faa3bbc..e879de75c 100644
--- a/crates/ide/src/diagnostics/unimplemented_builtin_macro.rs
+++ b/crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs
@@ -1,12 +1,9 @@
1use crate::{ 1use crate::{Diagnostic, DiagnosticsContext, Severity};
2 diagnostics::{Diagnostic, DiagnosticsContext},
3 Severity,
4};
5 2
6// Diagnostic: unimplemented-builtin-macro 3// Diagnostic: unimplemented-builtin-macro
7// 4//
8// This diagnostic is shown for builtin macros which are not yet implemented by rust-analyzer 5// This diagnostic is shown for builtin macros which are not yet implemented by rust-analyzer
9pub(super) fn unimplemented_builtin_macro( 6pub(crate) fn unimplemented_builtin_macro(
10 ctx: &DiagnosticsContext<'_>, 7 ctx: &DiagnosticsContext<'_>,
11 d: &hir::UnimplementedBuiltinMacro, 8 d: &hir::UnimplementedBuiltinMacro,
12) -> Diagnostic { 9) -> Diagnostic {
diff --git a/crates/ide/src/diagnostics/unlinked_file.rs b/crates/ide_diagnostics/src/handlers/unlinked_file.rs
index a5b2e3399..8921ddde2 100644
--- a/crates/ide/src/diagnostics/unlinked_file.rs
+++ b/crates/ide_diagnostics/src/handlers/unlinked_file.rs
@@ -12,10 +12,7 @@ use syntax::{
12}; 12};
13use text_edit::TextEdit; 13use text_edit::TextEdit;
14 14
15use crate::{ 15use crate::{fix, Assist, Diagnostic, DiagnosticsContext};
16 diagnostics::{fix, DiagnosticsContext},
17 Assist, Diagnostic,
18};
19 16
20#[derive(Debug)] 17#[derive(Debug)]
21pub(crate) struct UnlinkedFile { 18pub(crate) struct UnlinkedFile {
@@ -26,7 +23,7 @@ pub(crate) struct UnlinkedFile {
26// 23//
27// This diagnostic is shown for files that are not included in any crate, or files that are part of 24// This diagnostic is shown for files that are not included in any crate, or files that are part of
28// crates rust-analyzer failed to discover. The file will not have IDE features available. 25// crates rust-analyzer failed to discover. The file will not have IDE features available.
29pub(super) fn unlinked_file(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Diagnostic { 26pub(crate) fn unlinked_file(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Diagnostic {
30 // Limit diagnostic to the first few characters in the file. This matches how VS Code 27 // Limit diagnostic to the first few characters in the file. This matches how VS Code
31 // renders it with the full span, but on other editors, and is less invasive. 28 // renders it with the full span, but on other editors, and is less invasive.
32 let range = ctx.sema.db.parse(d.file).syntax_node().text_range(); 29 let range = ctx.sema.db.parse(d.file).syntax_node().text_range();
@@ -164,7 +161,7 @@ fn make_fixes(
164 161
165#[cfg(test)] 162#[cfg(test)]
166mod tests { 163mod tests {
167 use crate::diagnostics::tests::{check_diagnostics, check_fix, check_fixes, check_no_fix}; 164 use crate::tests::{check_diagnostics, check_fix, check_fixes, check_no_fix};
168 165
169 #[test] 166 #[test]
170 fn unlinked_file_prepend_first_item() { 167 fn unlinked_file_prepend_first_item() {
diff --git a/crates/ide/src/diagnostics/unresolved_extern_crate.rs b/crates/ide_diagnostics/src/handlers/unresolved_extern_crate.rs
index 2ea79c2ee..f5313cc0c 100644
--- a/crates/ide/src/diagnostics/unresolved_extern_crate.rs
+++ b/crates/ide_diagnostics/src/handlers/unresolved_extern_crate.rs
@@ -1,9 +1,9 @@
1use crate::diagnostics::{Diagnostic, DiagnosticsContext}; 1use crate::{Diagnostic, DiagnosticsContext};
2 2
3// Diagnostic: unresolved-extern-crate 3// Diagnostic: unresolved-extern-crate
4// 4//
5// This diagnostic is triggered if rust-analyzer is unable to discover referred extern crate. 5// This diagnostic is triggered if rust-analyzer is unable to discover referred extern crate.
6pub(super) fn unresolved_extern_crate( 6pub(crate) fn unresolved_extern_crate(
7 ctx: &DiagnosticsContext<'_>, 7 ctx: &DiagnosticsContext<'_>,
8 d: &hir::UnresolvedExternCrate, 8 d: &hir::UnresolvedExternCrate,
9) -> Diagnostic { 9) -> Diagnostic {
@@ -16,7 +16,7 @@ pub(super) fn unresolved_extern_crate(
16 16
17#[cfg(test)] 17#[cfg(test)]
18mod tests { 18mod tests {
19 use crate::diagnostics::tests::check_diagnostics; 19 use crate::tests::check_diagnostics;
20 20
21 #[test] 21 #[test]
22 fn unresolved_extern_crate() { 22 fn unresolved_extern_crate() {
diff --git a/crates/ide/src/diagnostics/unresolved_import.rs b/crates/ide_diagnostics/src/handlers/unresolved_import.rs
index 1cbf96ba1..f30051c12 100644
--- a/crates/ide/src/diagnostics/unresolved_import.rs
+++ b/crates/ide_diagnostics/src/handlers/unresolved_import.rs
@@ -1,10 +1,10 @@
1use crate::diagnostics::{Diagnostic, DiagnosticsContext}; 1use crate::{Diagnostic, DiagnosticsContext};
2 2
3// Diagnostic: unresolved-import 3// Diagnostic: unresolved-import
4// 4//
5// This diagnostic is triggered if rust-analyzer is unable to resolve a path in 5// This diagnostic is triggered if rust-analyzer is unable to resolve a path in
6// a `use` declaration. 6// a `use` declaration.
7pub(super) fn unresolved_import( 7pub(crate) fn unresolved_import(
8 ctx: &DiagnosticsContext<'_>, 8 ctx: &DiagnosticsContext<'_>,
9 d: &hir::UnresolvedImport, 9 d: &hir::UnresolvedImport,
10) -> Diagnostic { 10) -> Diagnostic {
@@ -22,7 +22,7 @@ pub(super) fn unresolved_import(
22 22
23#[cfg(test)] 23#[cfg(test)]
24mod tests { 24mod tests {
25 use crate::diagnostics::tests::check_diagnostics; 25 use crate::tests::check_diagnostics;
26 26
27 #[test] 27 #[test]
28 fn unresolved_import() { 28 fn unresolved_import() {
diff --git a/crates/ide/src/diagnostics/unresolved_macro_call.rs b/crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs
index 15b6a2730..4c3c1c19a 100644
--- a/crates/ide/src/diagnostics/unresolved_macro_call.rs
+++ b/crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs
@@ -1,13 +1,13 @@
1use hir::{db::AstDatabase, InFile}; 1use hir::{db::AstDatabase, InFile};
2use syntax::{AstNode, SyntaxNodePtr}; 2use syntax::{AstNode, SyntaxNodePtr};
3 3
4use crate::diagnostics::{Diagnostic, DiagnosticsContext}; 4use crate::{Diagnostic, DiagnosticsContext};
5 5
6// Diagnostic: unresolved-macro-call 6// Diagnostic: unresolved-macro-call
7// 7//
8// This diagnostic is triggered if rust-analyzer is unable to resolve the path 8// This diagnostic is triggered if rust-analyzer is unable to resolve the path
9// to a macro in a macro invocation. 9// to a macro in a macro invocation.
10pub(super) fn unresolved_macro_call( 10pub(crate) fn unresolved_macro_call(
11 ctx: &DiagnosticsContext<'_>, 11 ctx: &DiagnosticsContext<'_>,
12 d: &hir::UnresolvedMacroCall, 12 d: &hir::UnresolvedMacroCall,
13) -> Diagnostic { 13) -> Diagnostic {
@@ -32,7 +32,7 @@ pub(super) fn unresolved_macro_call(
32 32
33#[cfg(test)] 33#[cfg(test)]
34mod tests { 34mod tests {
35 use crate::diagnostics::tests::check_diagnostics; 35 use crate::tests::check_diagnostics;
36 36
37 #[test] 37 #[test]
38 fn unresolved_macro_diag() { 38 fn unresolved_macro_diag() {
diff --git a/crates/ide/src/diagnostics/unresolved_module.rs b/crates/ide_diagnostics/src/handlers/unresolved_module.rs
index 977b46414..17166a0c6 100644
--- a/crates/ide/src/diagnostics/unresolved_module.rs
+++ b/crates/ide_diagnostics/src/handlers/unresolved_module.rs
@@ -1,14 +1,13 @@
1use hir::db::AstDatabase; 1use hir::db::AstDatabase;
2use ide_assists::Assist; 2use ide_db::{assists::Assist, base_db::AnchoredPathBuf, source_change::FileSystemEdit};
3use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit};
4use syntax::AstNode; 3use syntax::AstNode;
5 4
6use crate::diagnostics::{fix, Diagnostic, DiagnosticsContext}; 5use crate::{fix, Diagnostic, DiagnosticsContext};
7 6
8// Diagnostic: unresolved-module 7// Diagnostic: unresolved-module
9// 8//
10// This diagnostic is triggered if rust-analyzer is unable to discover referred module. 9// This diagnostic is triggered if rust-analyzer is unable to discover referred module.
11pub(super) fn unresolved_module( 10pub(crate) fn unresolved_module(
12 ctx: &DiagnosticsContext<'_>, 11 ctx: &DiagnosticsContext<'_>,
13 d: &hir::UnresolvedModule, 12 d: &hir::UnresolvedModule,
14) -> Diagnostic { 13) -> Diagnostic {
@@ -42,7 +41,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedModule) -> Option<Vec<
42mod tests { 41mod tests {
43 use expect_test::expect; 42 use expect_test::expect;
44 43
45 use crate::diagnostics::tests::{check_diagnostics, check_expect}; 44 use crate::tests::{check_diagnostics, check_expect};
46 45
47 #[test] 46 #[test]
48 fn unresolved_module() { 47 fn unresolved_module() {
diff --git a/crates/ide/src/diagnostics/unresolved_proc_macro.rs b/crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs
index 3dc6ab451..fde1d1323 100644
--- a/crates/ide/src/diagnostics/unresolved_proc_macro.rs
+++ b/crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs
@@ -1,7 +1,4 @@
1use crate::{ 1use crate::{Diagnostic, DiagnosticsContext, Severity};
2 diagnostics::{Diagnostic, DiagnosticsContext},
3 Severity,
4};
5 2
6// Diagnostic: unresolved-proc-macro 3// Diagnostic: unresolved-proc-macro
7// 4//
@@ -12,7 +9,7 @@ use crate::{
12// If you are seeing a lot of "proc macro not expanded" warnings, you can add this option to the 9// If you are seeing a lot of "proc macro not expanded" warnings, you can add this option to the
13// `rust-analyzer.diagnostics.disabled` list to prevent them from showing. Alternatively you can 10// `rust-analyzer.diagnostics.disabled` list to prevent them from showing. Alternatively you can
14// enable support for procedural macros (see `rust-analyzer.procMacro.enable`). 11// enable support for procedural macros (see `rust-analyzer.procMacro.enable`).
15pub(super) fn unresolved_proc_macro( 12pub(crate) fn unresolved_proc_macro(
16 ctx: &DiagnosticsContext<'_>, 13 ctx: &DiagnosticsContext<'_>,
17 d: &hir::UnresolvedProcMacro, 14 d: &hir::UnresolvedProcMacro,
18) -> Diagnostic { 15) -> Diagnostic {
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide_diagnostics/src/lib.rs
index 815a633e5..88037be5a 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide_diagnostics/src/lib.rs
@@ -1,34 +1,60 @@
1//! Collects diagnostics & fixits for a single file. 1//! Diagnostics rendering and fixits.
2//! 2//!
3//! The tricky bit here is that diagnostics are produced by hir in terms of 3//! Most of the diagnostics originate from the dark depth of the compiler, and
4//! macro-expanded files, but we need to present them to the users in terms of 4//! are originally expressed in term of IR. When we emit the diagnostic, we are
5//! original files. So we need to map the ranges. 5//! usually not in the position to decide how to best "render" it in terms of
6 6//! user-authored source code. We are especially not in the position to offer
7mod break_outside_of_loop; 7//! fixits, as the compiler completely lacks the infrastructure to edit the
8mod inactive_code; 8//! source code.
9mod incorrect_case; 9//!
10mod macro_error; 10//! Instead, we "bubble up" raw, structured diagnostics until the `hir` crate,
11mod mismatched_arg_count; 11//! where we "cook" them so that each diagnostic is formulated in terms of `hir`
12mod missing_fields; 12//! types. Well, at least that's the aspiration, the "cooking" is somewhat
13mod missing_match_arms; 13//! ad-hoc at the moment. Anyways, we get a bunch of ide-friendly diagnostic
14mod missing_ok_or_some_in_tail_expr; 14//! structs from hir, and we want to render them to unified serializable
15mod missing_unsafe; 15//! representation (span, level, message) here. If we can, we also provide
16mod no_such_field; 16//! fixits. By the way, that's why we want to keep diagnostics structured
17mod remove_this_semicolon; 17//! internally -- so that we have all the info to make fixes.
18mod replace_filter_map_next_with_find_map; 18//!
19mod unimplemented_builtin_macro; 19//! We have one "handler" module per diagnostic code. Such a module contains
20mod unlinked_file; 20//! rendering, optional fixes and tests. It's OK if some low-level compiler
21mod unresolved_extern_crate; 21//! functionality ends up being tested via a diagnostic.
22mod unresolved_import; 22//!
23mod unresolved_macro_call; 23//! There are also a couple of ad-hoc diagnostics implemented directly here, we
24mod unresolved_module; 24//! don't yet have a great pattern for how to do them properly.
25mod unresolved_proc_macro; 25
26mod handlers {
27 pub(crate) mod break_outside_of_loop;
28 pub(crate) mod inactive_code;
29 pub(crate) mod incorrect_case;
30 pub(crate) mod macro_error;
31 pub(crate) mod mismatched_arg_count;
32 pub(crate) mod missing_fields;
33 pub(crate) mod missing_match_arms;
34 pub(crate) mod missing_ok_or_some_in_tail_expr;
35 pub(crate) mod missing_unsafe;
36 pub(crate) mod no_such_field;
37 pub(crate) mod remove_this_semicolon;
38 pub(crate) mod replace_filter_map_next_with_find_map;
39 pub(crate) mod unimplemented_builtin_macro;
40 pub(crate) mod unlinked_file;
41 pub(crate) mod unresolved_extern_crate;
42 pub(crate) mod unresolved_import;
43 pub(crate) mod unresolved_macro_call;
44 pub(crate) mod unresolved_module;
45 pub(crate) mod unresolved_proc_macro;
46}
26 47
27mod field_shorthand; 48mod field_shorthand;
28 49
29use hir::{diagnostics::AnyDiagnostic, Semantics}; 50use hir::{diagnostics::AnyDiagnostic, Semantics};
30use ide_assists::AssistResolveStrategy; 51use ide_db::{
31use ide_db::{base_db::SourceDatabase, RootDatabase}; 52 assists::{Assist, AssistId, AssistKind, AssistResolveStrategy},
53 base_db::{FileId, SourceDatabase},
54 label::Label,
55 source_change::SourceChange,
56 RootDatabase,
57};
32use itertools::Itertools; 58use itertools::Itertools;
33use rustc_hash::FxHashSet; 59use rustc_hash::FxHashSet;
34use syntax::{ 60use syntax::{
@@ -36,9 +62,8 @@ use syntax::{
36 SyntaxNode, TextRange, 62 SyntaxNode, TextRange,
37}; 63};
38use text_edit::TextEdit; 64use text_edit::TextEdit;
39use unlinked_file::UnlinkedFile;
40 65
41use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange}; 66use crate::handlers::unlinked_file::UnlinkedFile;
42 67
43#[derive(Copy, Clone, Debug, PartialEq)] 68#[derive(Copy, Clone, Debug, PartialEq)]
44pub struct DiagnosticCode(pub &'static str); 69pub struct DiagnosticCode(pub &'static str);
@@ -113,7 +138,7 @@ struct DiagnosticsContext<'a> {
113 resolve: &'a AssistResolveStrategy, 138 resolve: &'a AssistResolveStrategy,
114} 139}
115 140
116pub(crate) fn diagnostics( 141pub fn diagnostics(
117 db: &RootDatabase, 142 db: &RootDatabase,
118 config: &DiagnosticsConfig, 143 config: &DiagnosticsConfig,
119 resolve: &AssistResolveStrategy, 144 resolve: &AssistResolveStrategy,
@@ -145,32 +170,32 @@ pub(crate) fn diagnostics(
145 let ctx = DiagnosticsContext { config, sema, resolve }; 170 let ctx = DiagnosticsContext { config, sema, resolve };
146 if module.is_none() { 171 if module.is_none() {
147 let d = UnlinkedFile { file: file_id }; 172 let d = UnlinkedFile { file: file_id };
148 let d = unlinked_file::unlinked_file(&ctx, &d); 173 let d = handlers::unlinked_file::unlinked_file(&ctx, &d);
149 res.push(d) 174 res.push(d)
150 } 175 }
151 176
152 for diag in diags { 177 for diag in diags {
153 #[rustfmt::skip] 178 #[rustfmt::skip]
154 let d = match diag { 179 let d = match diag {
155 AnyDiagnostic::BreakOutsideOfLoop(d) => break_outside_of_loop::break_outside_of_loop(&ctx, &d), 180 AnyDiagnostic::BreakOutsideOfLoop(d) => handlers::break_outside_of_loop::break_outside_of_loop(&ctx, &d),
156 AnyDiagnostic::IncorrectCase(d) => incorrect_case::incorrect_case(&ctx, &d), 181 AnyDiagnostic::IncorrectCase(d) => handlers::incorrect_case::incorrect_case(&ctx, &d),
157 AnyDiagnostic::MacroError(d) => macro_error::macro_error(&ctx, &d), 182 AnyDiagnostic::MacroError(d) => handlers::macro_error::macro_error(&ctx, &d),
158 AnyDiagnostic::MismatchedArgCount(d) => mismatched_arg_count::mismatched_arg_count(&ctx, &d), 183 AnyDiagnostic::MismatchedArgCount(d) => handlers::mismatched_arg_count::mismatched_arg_count(&ctx, &d),
159 AnyDiagnostic::MissingFields(d) => missing_fields::missing_fields(&ctx, &d), 184 AnyDiagnostic::MissingFields(d) => handlers::missing_fields::missing_fields(&ctx, &d),
160 AnyDiagnostic::MissingMatchArms(d) => missing_match_arms::missing_match_arms(&ctx, &d), 185 AnyDiagnostic::MissingMatchArms(d) => handlers::missing_match_arms::missing_match_arms(&ctx, &d),
161 AnyDiagnostic::MissingOkOrSomeInTailExpr(d) => missing_ok_or_some_in_tail_expr::missing_ok_or_some_in_tail_expr(&ctx, &d), 186 AnyDiagnostic::MissingOkOrSomeInTailExpr(d) => handlers::missing_ok_or_some_in_tail_expr::missing_ok_or_some_in_tail_expr(&ctx, &d),
162 AnyDiagnostic::MissingUnsafe(d) => missing_unsafe::missing_unsafe(&ctx, &d), 187 AnyDiagnostic::MissingUnsafe(d) => handlers::missing_unsafe::missing_unsafe(&ctx, &d),
163 AnyDiagnostic::NoSuchField(d) => no_such_field::no_such_field(&ctx, &d), 188 AnyDiagnostic::NoSuchField(d) => handlers::no_such_field::no_such_field(&ctx, &d),
164 AnyDiagnostic::RemoveThisSemicolon(d) => remove_this_semicolon::remove_this_semicolon(&ctx, &d), 189 AnyDiagnostic::RemoveThisSemicolon(d) => handlers::remove_this_semicolon::remove_this_semicolon(&ctx, &d),
165 AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d), 190 AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => handlers::replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d),
166 AnyDiagnostic::UnimplementedBuiltinMacro(d) => unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d), 191 AnyDiagnostic::UnimplementedBuiltinMacro(d) => handlers::unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d),
167 AnyDiagnostic::UnresolvedExternCrate(d) => unresolved_extern_crate::unresolved_extern_crate(&ctx, &d), 192 AnyDiagnostic::UnresolvedExternCrate(d) => handlers::unresolved_extern_crate::unresolved_extern_crate(&ctx, &d),
168 AnyDiagnostic::UnresolvedImport(d) => unresolved_import::unresolved_import(&ctx, &d), 193 AnyDiagnostic::UnresolvedImport(d) => handlers::unresolved_import::unresolved_import(&ctx, &d),
169 AnyDiagnostic::UnresolvedMacroCall(d) => unresolved_macro_call::unresolved_macro_call(&ctx, &d), 194 AnyDiagnostic::UnresolvedMacroCall(d) => handlers::unresolved_macro_call::unresolved_macro_call(&ctx, &d),
170 AnyDiagnostic::UnresolvedModule(d) => unresolved_module::unresolved_module(&ctx, &d), 195 AnyDiagnostic::UnresolvedModule(d) => handlers::unresolved_module::unresolved_module(&ctx, &d),
171 AnyDiagnostic::UnresolvedProcMacro(d) => unresolved_proc_macro::unresolved_proc_macro(&ctx, &d), 196 AnyDiagnostic::UnresolvedProcMacro(d) => handlers::unresolved_proc_macro::unresolved_proc_macro(&ctx, &d),
172 197
173 AnyDiagnostic::InactiveCode(d) => match inactive_code::inactive_code(&ctx, &d) { 198 AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) {
174 Some(it) => it, 199 Some(it) => it,
175 None => continue, 200 None => continue,
176 } 201 }
@@ -261,11 +286,15 @@ fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
261#[cfg(test)] 286#[cfg(test)]
262mod tests { 287mod tests {
263 use expect_test::Expect; 288 use expect_test::Expect;
264 use ide_assists::AssistResolveStrategy; 289 use ide_db::{
290 assists::AssistResolveStrategy,
291 base_db::{fixture::WithFixture, SourceDatabaseExt},
292 RootDatabase,
293 };
265 use stdx::trim_indent; 294 use stdx::trim_indent;
266 use test_utils::{assert_eq_text, extract_annotations}; 295 use test_utils::{assert_eq_text, extract_annotations};
267 296
268 use crate::{fixture, DiagnosticsConfig}; 297 use crate::DiagnosticsConfig;
269 298
270 /// Takes a multi-file input fixture with annotated cursor positions, 299 /// Takes a multi-file input fixture with annotated cursor positions,
271 /// and checks that: 300 /// and checks that:
@@ -291,21 +320,20 @@ mod tests {
291 fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) { 320 fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
292 let after = trim_indent(ra_fixture_after); 321 let after = trim_indent(ra_fixture_after);
293 322
294 let (analysis, file_position) = fixture::position(ra_fixture_before); 323 let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
295 let diagnostic = analysis 324 let diagnostic = super::diagnostics(
296 .diagnostics( 325 &db,
297 &DiagnosticsConfig::default(), 326 &DiagnosticsConfig::default(),
298 AssistResolveStrategy::All, 327 &AssistResolveStrategy::All,
299 file_position.file_id, 328 file_position.file_id,
300 ) 329 )
301 .unwrap() 330 .pop()
302 .pop() 331 .expect("no diagnostics");
303 .expect("no diagnostics");
304 let fix = &diagnostic.fixes.expect("diagnostic misses fixes")[nth]; 332 let fix = &diagnostic.fixes.expect("diagnostic misses fixes")[nth];
305 let actual = { 333 let actual = {
306 let source_change = fix.source_change.as_ref().unwrap(); 334 let source_change = fix.source_change.as_ref().unwrap();
307 let file_id = *source_change.source_file_edits.keys().next().unwrap(); 335 let file_id = *source_change.source_file_edits.keys().next().unwrap();
308 let mut actual = analysis.file_text(file_id).unwrap().to_string(); 336 let mut actual = db.file_text(file_id).to_string();
309 337
310 for edit in source_change.source_file_edits.values() { 338 for edit in source_change.source_file_edits.values() {
311 edit.apply(&mut actual); 339 edit.apply(&mut actual);
@@ -324,24 +352,26 @@ mod tests {
324 352
325 /// Checks that there's a diagnostic *without* fix at `$0`. 353 /// Checks that there's a diagnostic *without* fix at `$0`.
326 pub(crate) fn check_no_fix(ra_fixture: &str) { 354 pub(crate) fn check_no_fix(ra_fixture: &str) {
327 let (analysis, file_position) = fixture::position(ra_fixture); 355 let (db, file_position) = RootDatabase::with_position(ra_fixture);
328 let diagnostic = analysis 356 let diagnostic = super::diagnostics(
329 .diagnostics( 357 &db,
330 &DiagnosticsConfig::default(), 358 &DiagnosticsConfig::default(),
331 AssistResolveStrategy::All, 359 &AssistResolveStrategy::All,
332 file_position.file_id, 360 file_position.file_id,
333 ) 361 )
334 .unwrap() 362 .pop()
335 .pop() 363 .unwrap();
336 .unwrap();
337 assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic); 364 assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic);
338 } 365 }
339 366
340 pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) { 367 pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) {
341 let (analysis, file_id) = fixture::file(ra_fixture); 368 let (db, file_id) = RootDatabase::with_single_file(ra_fixture);
342 let diagnostics = analysis 369 let diagnostics = super::diagnostics(
343 .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id) 370 &db,
344 .unwrap(); 371 &DiagnosticsConfig::default(),
372 &AssistResolveStrategy::All,
373 file_id,
374 );
345 expect.assert_debug_eq(&diagnostics) 375 expect.assert_debug_eq(&diagnostics)
346 } 376 }
347 377
@@ -354,12 +384,12 @@ mod tests {
354 384
355 #[track_caller] 385 #[track_caller]
356 pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, ra_fixture: &str) { 386 pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, ra_fixture: &str) {
357 let (analysis, files) = fixture::files(ra_fixture); 387 let (db, files) = RootDatabase::with_many_files(ra_fixture);
358 for file_id in files { 388 for file_id in files {
359 let diagnostics = 389 let diagnostics =
360 analysis.diagnostics(&config, AssistResolveStrategy::All, file_id).unwrap(); 390 super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
361 391
362 let expected = extract_annotations(&*analysis.file_text(file_id).unwrap()); 392 let expected = extract_annotations(&*db.file_text(file_id));
363 let mut actual = 393 let mut actual =
364 diagnostics.into_iter().map(|d| (d.range, d.message)).collect::<Vec<_>>(); 394 diagnostics.into_iter().map(|d| (d.range, d.message)).collect::<Vec<_>>();
365 actual.sort_by_key(|(range, _)| range.start()); 395 actual.sort_by_key(|(range, _)| range.start());
@@ -455,15 +485,17 @@ mod a {
455 let mut config = DiagnosticsConfig::default(); 485 let mut config = DiagnosticsConfig::default();
456 config.disabled.insert("unresolved-module".into()); 486 config.disabled.insert("unresolved-module".into());
457 487
458 let (analysis, file_id) = fixture::file(r#"mod foo;"#); 488 let (db, file_id) = RootDatabase::with_single_file(r#"mod foo;"#);
459 489
460 let diagnostics = 490 let diagnostics = super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
461 analysis.diagnostics(&config, AssistResolveStrategy::All, file_id).unwrap();
462 assert!(diagnostics.is_empty()); 491 assert!(diagnostics.is_empty());
463 492
464 let diagnostics = analysis 493 let diagnostics = super::diagnostics(
465 .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id) 494 &db,
466 .unwrap(); 495 &DiagnosticsConfig::default(),
496 &AssistResolveStrategy::All,
497 file_id,
498 );
467 assert!(!diagnostics.is_empty()); 499 assert!(!diagnostics.is_empty());
468 } 500 }
469 501