diff options
Diffstat (limited to 'crates/ide/src/diagnostics/unlinked_file.rs')
-rw-r--r-- | crates/ide/src/diagnostics/unlinked_file.rs | 259 |
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 | ||
3 | use hir::{ | 3 | use hir::db::DefDatabase; |
4 | db::DefDatabase, | ||
5 | diagnostics::{Diagnostic, DiagnosticCode}, | ||
6 | InFile, | ||
7 | }; | ||
8 | use ide_assists::AssistResolveStrategy; | ||
9 | use ide_db::{ | 4 | use 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 | }; |
14 | use syntax::{ | 9 | use syntax::{ |
15 | ast::{self, ModuleItemOwner, NameOwner}, | 10 | ast::{self, ModuleItemOwner, NameOwner}, |
16 | AstNode, SyntaxNodePtr, | 11 | AstNode, TextRange, TextSize, |
17 | }; | 12 | }; |
18 | use text_edit::TextEdit; | 13 | use text_edit::TextEdit; |
19 | 14 | ||
20 | use crate::{ | 15 | use crate::{ |
21 | diagnostics::{fix, fixes::DiagnosticWithFixes}, | 16 | diagnostics::{fix, DiagnosticsContext}, |
22 | Assist, | 17 | Assist, Diagnostic, |
23 | }; | 18 | }; |
24 | 19 | ||
20 | #[derive(Debug)] | ||
21 | pub(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)] | 29 | pub(super) fn unlinked_file(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Diagnostic { |
30 | pub(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 | ||
35 | impl Diagnostic for UnlinkedFile { | 40 | fn 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 | ||
53 | impl 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 | ||
104 | fn make_fixes( | 84 | fn make_fixes( |
@@ -181,3 +161,144 @@ fn make_fixes( | |||
181 | ), | 161 | ), |
182 | ]) | 162 | ]) |
183 | } | 163 | } |
164 | |||
165 | #[cfg(test)] | ||
166 | mod 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 | ||
176 | fn f() {} | ||
177 | //- /foo.rs | ||
178 | $0 | ||
179 | "#, | ||
180 | vec![ | ||
181 | r#" | ||
182 | mod foo; | ||
183 | |||
184 | fn f() {} | ||
185 | "#, | ||
186 | r#" | ||
187 | pub mod foo; | ||
188 | |||
189 | fn 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 | |||
203 | mod preexisting; | ||
204 | |||
205 | mod preexisting2; | ||
206 | |||
207 | struct S; | ||
208 | |||
209 | mod preexisting_bottom;) | ||
210 | //- /foo.rs | ||
211 | $0 | ||
212 | "#, | ||
213 | r#" | ||
214 | //! Comment on top | ||
215 | |||
216 | mod preexisting; | ||
217 | |||
218 | mod preexisting2; | ||
219 | mod foo; | ||
220 | |||
221 | struct S; | ||
222 | |||
223 | mod 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#" | ||
238 | mod foo; | ||
239 | "#, | ||
240 | ); | ||
241 | } | ||
242 | |||
243 | #[test] | ||
244 | fn unlinked_file_old_style_modrs() { | ||
245 | check_fix( | ||
246 | r#" | ||
247 | //- /main.rs | ||
248 | mod submod; | ||
249 | //- /submod/mod.rs | ||
250 | // in mod.rs | ||
251 | //- /submod/foo.rs | ||
252 | $0 | ||
253 | "#, | ||
254 | r#" | ||
255 | // in mod.rs | ||
256 | mod foo; | ||
257 | "#, | ||
258 | ); | ||
259 | } | ||
260 | |||
261 | #[test] | ||
262 | fn unlinked_file_new_style_mod() { | ||
263 | check_fix( | ||
264 | r#" | ||
265 | //- /main.rs | ||
266 | mod submod; | ||
267 | //- /submod.rs | ||
268 | //- /submod/foo.rs | ||
269 | $0 | ||
270 | "#, | ||
271 | r#" | ||
272 | mod 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)] | ||
284 | mod 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))] | ||
298 | mod foo; | ||
299 | |||
300 | //- /foo.rs | ||
301 | "#, | ||
302 | ); | ||
303 | } | ||
304 | } | ||