aboutsummaryrefslogtreecommitdiff
path: root/crates/ide_diagnostics/src/handlers/unlinked_file.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide_diagnostics/src/handlers/unlinked_file.rs')
-rw-r--r--crates/ide_diagnostics/src/handlers/unlinked_file.rs298
1 files changed, 298 insertions, 0 deletions
diff --git a/crates/ide_diagnostics/src/handlers/unlinked_file.rs b/crates/ide_diagnostics/src/handlers/unlinked_file.rs
new file mode 100644
index 000000000..8e601fa48
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/unlinked_file.rs
@@ -0,0 +1,298 @@
1//! Diagnostic emitted for files that aren't part of any crate.
2
3use hir::db::DefDatabase;
4use ide_db::{
5 base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt},
6 source_change::SourceChange,
7 RootDatabase,
8};
9use syntax::{
10 ast::{self, ModuleItemOwner, NameOwner},
11 AstNode, TextRange, TextSize,
12};
13use text_edit::TextEdit;
14
15use crate::{fix, Assist, Diagnostic, DiagnosticsContext};
16
17// Diagnostic: unlinked-file
18//
19// This diagnostic is shown for files that are not included in any crate, or files that are part of
20// crates rust-analyzer failed to discover. The file will not have IDE features available.
21pub(crate) fn unlinked_file(ctx: &DiagnosticsContext, acc: &mut Vec<Diagnostic>, file_id: FileId) {
22 // Limit diagnostic to the first few characters in the file. This matches how VS Code
23 // renders it with the full span, but on other editors, and is less invasive.
24 let range = ctx.sema.db.parse(file_id).syntax_node().text_range();
25 // FIXME: This is wrong if one of the first three characters is not ascii: `//Ы`.
26 let range = range.intersect(TextRange::up_to(TextSize::of("..."))).unwrap_or(range);
27
28 acc.push(
29 Diagnostic::new("unlinked-file", "file not included in module tree", range)
30 .with_fixes(fixes(ctx, file_id)),
31 );
32}
33
34fn fixes(ctx: &DiagnosticsContext, file_id: FileId) -> Option<Vec<Assist>> {
35 // If there's an existing module that could add `mod` or `pub mod` items to include the unlinked file,
36 // suggest that as a fix.
37
38 let source_root = ctx.sema.db.source_root(ctx.sema.db.file_source_root(file_id));
39 let our_path = source_root.path_for_file(&file_id)?;
40 let module_name = our_path.name_and_extension()?.0;
41
42 // Candidates to look for:
43 // - `mod.rs` in the same folder
44 // - we also check `main.rs` and `lib.rs`
45 // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
46 let parent = our_path.parent()?;
47 let mut paths = vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?];
48
49 // `submod/bla.rs` -> `submod.rs`
50 if let Some(newmod) = (|| {
51 let name = parent.name_and_extension()?.0;
52 parent.parent()?.join(&format!("{}.rs", name))
53 })() {
54 paths.push(newmod);
55 }
56
57 for path in &paths {
58 if let Some(parent_id) = source_root.file_for_path(path) {
59 for krate in ctx.sema.db.relevant_crates(*parent_id).iter() {
60 let crate_def_map = ctx.sema.db.crate_def_map(*krate);
61 for (_, module) in crate_def_map.modules() {
62 if module.origin.is_inline() {
63 // We don't handle inline `mod parent {}`s, they use different paths.
64 continue;
65 }
66
67 if module.origin.file_id() == Some(*parent_id) {
68 return make_fixes(ctx.sema.db, *parent_id, module_name, file_id);
69 }
70 }
71 }
72 }
73 }
74
75 None
76}
77
78fn make_fixes(
79 db: &RootDatabase,
80 parent_file_id: FileId,
81 new_mod_name: &str,
82 added_file_id: FileId,
83) -> Option<Vec<Assist>> {
84 fn is_outline_mod(item: &ast::Item) -> bool {
85 matches!(item, ast::Item::Module(m) if m.item_list().is_none())
86 }
87
88 let mod_decl = format!("mod {};", new_mod_name);
89 let pub_mod_decl = format!("pub mod {};", new_mod_name);
90
91 let ast: ast::SourceFile = db.parse(parent_file_id).tree();
92
93 let mut mod_decl_builder = TextEdit::builder();
94 let mut pub_mod_decl_builder = TextEdit::builder();
95
96 // If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's
97 // probably `#[cfg]`d out).
98 for item in ast.items() {
99 if let ast::Item::Module(m) = item {
100 if let Some(name) = m.name() {
101 if m.item_list().is_none() && name.to_string() == new_mod_name {
102 cov_mark::hit!(unlinked_file_skip_fix_when_mod_already_exists);
103 return None;
104 }
105 }
106 }
107 }
108
109 // If there are existing `mod m;` items, append after them (after the first group of them, rather).
110 match ast
111 .items()
112 .skip_while(|item| !is_outline_mod(item))
113 .take_while(|item| is_outline_mod(item))
114 .last()
115 {
116 Some(last) => {
117 cov_mark::hit!(unlinked_file_append_to_existing_mods);
118 let offset = last.syntax().text_range().end();
119 mod_decl_builder.insert(offset, format!("\n{}", mod_decl));
120 pub_mod_decl_builder.insert(offset, format!("\n{}", pub_mod_decl));
121 }
122 None => {
123 // Prepend before the first item in the file.
124 match ast.items().next() {
125 Some(item) => {
126 cov_mark::hit!(unlinked_file_prepend_before_first_item);
127 let offset = item.syntax().text_range().start();
128 mod_decl_builder.insert(offset, format!("{}\n\n", mod_decl));
129 pub_mod_decl_builder.insert(offset, format!("{}\n\n", pub_mod_decl));
130 }
131 None => {
132 // No items in the file, so just append at the end.
133 cov_mark::hit!(unlinked_file_empty_file);
134 let offset = ast.syntax().text_range().end();
135 mod_decl_builder.insert(offset, format!("{}\n", mod_decl));
136 pub_mod_decl_builder.insert(offset, format!("{}\n", pub_mod_decl));
137 }
138 }
139 }
140 }
141
142 let trigger_range = db.parse(added_file_id).tree().syntax().text_range();
143 Some(vec![
144 fix(
145 "add_mod_declaration",
146 &format!("Insert `{}`", mod_decl),
147 SourceChange::from_text_edit(parent_file_id, mod_decl_builder.finish()),
148 trigger_range,
149 ),
150 fix(
151 "add_pub_mod_declaration",
152 &format!("Insert `{}`", pub_mod_decl),
153 SourceChange::from_text_edit(parent_file_id, pub_mod_decl_builder.finish()),
154 trigger_range,
155 ),
156 ])
157}
158
159#[cfg(test)]
160mod tests {
161 use crate::tests::{check_diagnostics, check_fix, check_fixes, check_no_fix};
162
163 #[test]
164 fn unlinked_file_prepend_first_item() {
165 cov_mark::check!(unlinked_file_prepend_before_first_item);
166 // Only tests the first one for `pub mod` since the rest are the same
167 check_fixes(
168 r#"
169//- /main.rs
170fn f() {}
171//- /foo.rs
172$0
173"#,
174 vec![
175 r#"
176mod foo;
177
178fn f() {}
179"#,
180 r#"
181pub mod foo;
182
183fn f() {}
184"#,
185 ],
186 );
187 }
188
189 #[test]
190 fn unlinked_file_append_mod() {
191 cov_mark::check!(unlinked_file_append_to_existing_mods);
192 check_fix(
193 r#"
194//- /main.rs
195//! Comment on top
196
197mod preexisting;
198
199mod preexisting2;
200
201struct S;
202
203mod preexisting_bottom;)
204//- /foo.rs
205$0
206"#,
207 r#"
208//! Comment on top
209
210mod preexisting;
211
212mod preexisting2;
213mod foo;
214
215struct S;
216
217mod preexisting_bottom;)
218"#,
219 );
220 }
221
222 #[test]
223 fn unlinked_file_insert_in_empty_file() {
224 cov_mark::check!(unlinked_file_empty_file);
225 check_fix(
226 r#"
227//- /main.rs
228//- /foo.rs
229$0
230"#,
231 r#"
232mod foo;
233"#,
234 );
235 }
236
237 #[test]
238 fn unlinked_file_old_style_modrs() {
239 check_fix(
240 r#"
241//- /main.rs
242mod submod;
243//- /submod/mod.rs
244// in mod.rs
245//- /submod/foo.rs
246$0
247"#,
248 r#"
249// in mod.rs
250mod foo;
251"#,
252 );
253 }
254
255 #[test]
256 fn unlinked_file_new_style_mod() {
257 check_fix(
258 r#"
259//- /main.rs
260mod submod;
261//- /submod.rs
262//- /submod/foo.rs
263$0
264"#,
265 r#"
266mod foo;
267"#,
268 );
269 }
270
271 #[test]
272 fn unlinked_file_with_cfg_off() {
273 cov_mark::check!(unlinked_file_skip_fix_when_mod_already_exists);
274 check_no_fix(
275 r#"
276//- /main.rs
277#[cfg(never)]
278mod foo;
279
280//- /foo.rs
281$0
282"#,
283 );
284 }
285
286 #[test]
287 fn unlinked_file_with_cfg_on() {
288 check_diagnostics(
289 r#"
290//- /main.rs
291#[cfg(not(never))]
292mod foo;
293
294//- /foo.rs
295"#,
296 );
297 }
298}