aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/diagnostics/unlinked_file.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src/diagnostics/unlinked_file.rs')
-rw-r--r--crates/ide/src/diagnostics/unlinked_file.rs259
1 files changed, 190 insertions, 69 deletions
diff --git a/crates/ide/src/diagnostics/unlinked_file.rs b/crates/ide/src/diagnostics/unlinked_file.rs
index 51fe0f360..a5b2e3399 100644
--- a/crates/ide/src/diagnostics/unlinked_file.rs
+++ b/crates/ide/src/diagnostics/unlinked_file.rs
@@ -1,11 +1,6 @@
1//! Diagnostic emitted for files that aren't part of any crate. 1//! Diagnostic emitted for files that aren't part of any crate.
2 2
3use hir::{ 3use hir::db::DefDatabase;
4 db::DefDatabase,
5 diagnostics::{Diagnostic, DiagnosticCode},
6 InFile,
7};
8use ide_assists::AssistResolveStrategy;
9use ide_db::{ 4use ide_db::{
10 base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt}, 5 base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt},
11 source_change::SourceChange, 6 source_change::SourceChange,
@@ -13,92 +8,77 @@ use ide_db::{
13}; 8};
14use syntax::{ 9use syntax::{
15 ast::{self, ModuleItemOwner, NameOwner}, 10 ast::{self, ModuleItemOwner, NameOwner},
16 AstNode, SyntaxNodePtr, 11 AstNode, TextRange, TextSize,
17}; 12};
18use text_edit::TextEdit; 13use text_edit::TextEdit;
19 14
20use crate::{ 15use crate::{
21 diagnostics::{fix, fixes::DiagnosticWithFixes}, 16 diagnostics::{fix, DiagnosticsContext},
22 Assist, 17 Assist, Diagnostic,
23}; 18};
24 19
20#[derive(Debug)]
21pub(crate) struct UnlinkedFile {
22 pub(crate) file: FileId,
23}
24
25// Diagnostic: unlinked-file 25// Diagnostic: unlinked-file
26// 26//
27// This diagnostic is shown for files that are not included in any crate, or files that are part of 27// 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. 28// crates rust-analyzer failed to discover. The file will not have IDE features available.
29#[derive(Debug)] 29pub(super) fn unlinked_file(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Diagnostic {
30pub(crate) struct UnlinkedFile { 30 // Limit diagnostic to the first few characters in the file. This matches how VS Code
31 pub(crate) file_id: FileId, 31 // renders it with the full span, but on other editors, and is less invasive.
32 pub(crate) node: SyntaxNodePtr, 32 let range = ctx.sema.db.parse(d.file).syntax_node().text_range();
33 // FIXME: This is wrong if one of the first three characters is not ascii: `//Ы`.
34 let range = range.intersect(TextRange::up_to(TextSize::of("..."))).unwrap_or(range);
35
36 Diagnostic::new("unlinked-file", "file not included in module tree", range)
37 .with_fixes(fixes(ctx, d))
33} 38}
34 39
35impl Diagnostic for UnlinkedFile { 40fn fixes(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Option<Vec<Assist>> {
36 fn code(&self) -> DiagnosticCode { 41 // If there's an existing module that could add `mod` or `pub mod` items to include the unlinked file,
37 DiagnosticCode("unlinked-file") 42 // suggest that as a fix.
38 }
39 43
40 fn message(&self) -> String { 44 let source_root = ctx.sema.db.source_root(ctx.sema.db.file_source_root(d.file));
41 "file not included in module tree".to_string() 45 let our_path = source_root.path_for_file(&d.file)?;
42 } 46 let module_name = our_path.name_and_extension()?.0;
43 47
44 fn display_source(&self) -> InFile<SyntaxNodePtr> { 48 // Candidates to look for:
45 InFile::new(self.file_id.into(), self.node.clone()) 49 // - `mod.rs` in the same folder
46 } 50 // - we also check `main.rs` and `lib.rs`
51 // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
52 let parent = our_path.parent()?;
53 let mut paths = vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?];
47 54
48 fn as_any(&self) -> &(dyn std::any::Any + Send + 'static) { 55 // `submod/bla.rs` -> `submod.rs`
49 self 56 if let Some(newmod) = (|| {
57 let name = parent.name_and_extension()?.0;
58 parent.parent()?.join(&format!("{}.rs", name))
59 })() {
60 paths.push(newmod);
50 } 61 }
51}
52 62
53impl DiagnosticWithFixes for UnlinkedFile { 63 for path in &paths {
54 fn fixes( 64 if let Some(parent_id) = source_root.file_for_path(path) {
55 &self, 65 for krate in ctx.sema.db.relevant_crates(*parent_id).iter() {
56 sema: &hir::Semantics<RootDatabase>, 66 let crate_def_map = ctx.sema.db.crate_def_map(*krate);
57 _resolve: &AssistResolveStrategy, 67 for (_, module) in crate_def_map.modules() {
58 ) -> Option<Vec<Assist>> { 68 if module.origin.is_inline() {
59 // If there's an existing module that could add `mod` or `pub mod` items to include the unlinked file, 69 // We don't handle inline `mod parent {}`s, they use different paths.
60 // suggest that as a fix. 70 continue;
61 71 }
62 let source_root = sema.db.source_root(sema.db.file_source_root(self.file_id));
63 let our_path = source_root.path_for_file(&self.file_id)?;
64 let module_name = our_path.name_and_extension()?.0;
65
66 // Candidates to look for:
67 // - `mod.rs` in the same folder
68 // - we also check `main.rs` and `lib.rs`
69 // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
70 let parent = our_path.parent()?;
71 let mut paths =
72 vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?];
73
74 // `submod/bla.rs` -> `submod.rs`
75 if let Some(newmod) = (|| {
76 let name = parent.name_and_extension()?.0;
77 parent.parent()?.join(&format!("{}.rs", name))
78 })() {
79 paths.push(newmod);
80 }
81 72
82 for path in &paths { 73 if module.origin.file_id() == Some(*parent_id) {
83 if let Some(parent_id) = source_root.file_for_path(path) { 74 return make_fixes(ctx.sema.db, *parent_id, module_name, d.file);
84 for krate in sema.db.relevant_crates(*parent_id).iter() {
85 let crate_def_map = sema.db.crate_def_map(*krate);
86 for (_, module) in crate_def_map.modules() {
87 if module.origin.is_inline() {
88 // We don't handle inline `mod parent {}`s, they use different paths.
89 continue;
90 }
91
92 if module.origin.file_id() == Some(*parent_id) {
93 return make_fixes(sema.db, *parent_id, module_name, self.file_id);
94 }
95 } 75 }
96 } 76 }
97 } 77 }
98 } 78 }
99
100 None
101 } 79 }
80
81 None
102} 82}
103 83
104fn make_fixes( 84fn make_fixes(
@@ -181,3 +161,144 @@ fn make_fixes(
181 ), 161 ),
182 ]) 162 ])
183} 163}
164
165#[cfg(test)]
166mod tests {
167 use crate::diagnostics::tests::{check_diagnostics, check_fix, check_fixes, check_no_fix};
168
169 #[test]
170 fn unlinked_file_prepend_first_item() {
171 cov_mark::check!(unlinked_file_prepend_before_first_item);
172 // Only tests the first one for `pub mod` since the rest are the same
173 check_fixes(
174 r#"
175//- /main.rs
176fn f() {}
177//- /foo.rs
178$0
179"#,
180 vec![
181 r#"
182mod foo;
183
184fn f() {}
185"#,
186 r#"
187pub mod foo;
188
189fn f() {}
190"#,
191 ],
192 );
193 }
194
195 #[test]
196 fn unlinked_file_append_mod() {
197 cov_mark::check!(unlinked_file_append_to_existing_mods);
198 check_fix(
199 r#"
200//- /main.rs
201//! Comment on top
202
203mod preexisting;
204
205mod preexisting2;
206
207struct S;
208
209mod preexisting_bottom;)
210//- /foo.rs
211$0
212"#,
213 r#"
214//! Comment on top
215
216mod preexisting;
217
218mod preexisting2;
219mod foo;
220
221struct S;
222
223mod preexisting_bottom;)
224"#,
225 );
226 }
227
228 #[test]
229 fn unlinked_file_insert_in_empty_file() {
230 cov_mark::check!(unlinked_file_empty_file);
231 check_fix(
232 r#"
233//- /main.rs
234//- /foo.rs
235$0
236"#,
237 r#"
238mod foo;
239"#,
240 );
241 }
242
243 #[test]
244 fn unlinked_file_old_style_modrs() {
245 check_fix(
246 r#"
247//- /main.rs
248mod submod;
249//- /submod/mod.rs
250// in mod.rs
251//- /submod/foo.rs
252$0
253"#,
254 r#"
255// in mod.rs
256mod foo;
257"#,
258 );
259 }
260
261 #[test]
262 fn unlinked_file_new_style_mod() {
263 check_fix(
264 r#"
265//- /main.rs
266mod submod;
267//- /submod.rs
268//- /submod/foo.rs
269$0
270"#,
271 r#"
272mod foo;
273"#,
274 );
275 }
276
277 #[test]
278 fn unlinked_file_with_cfg_off() {
279 cov_mark::check!(unlinked_file_skip_fix_when_mod_already_exists);
280 check_no_fix(
281 r#"
282//- /main.rs
283#[cfg(never)]
284mod foo;
285
286//- /foo.rs
287$0
288"#,
289 );
290 }
291
292 #[test]
293 fn unlinked_file_with_cfg_on() {
294 check_diagnostics(
295 r#"
296//- /main.rs
297#[cfg(not(never))]
298mod foo;
299
300//- /foo.rs
301"#,
302 );
303 }
304}