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.rs304
1 files changed, 0 insertions, 304 deletions
diff --git a/crates/ide/src/diagnostics/unlinked_file.rs b/crates/ide/src/diagnostics/unlinked_file.rs
deleted file mode 100644
index a5b2e3399..000000000
--- a/crates/ide/src/diagnostics/unlinked_file.rs
+++ /dev/null
@@ -1,304 +0,0 @@
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::{
16 diagnostics::{fix, DiagnosticsContext},
17 Assist, Diagnostic,
18};
19
20#[derive(Debug)]
21pub(crate) struct UnlinkedFile {
22 pub(crate) file: FileId,
23}
24
25// Diagnostic: unlinked-file
26//
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.
29pub(super) fn unlinked_file(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Diagnostic {
30 // 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.
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))
38}
39
40fn fixes(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Option<Vec<Assist>> {
41 // If there's an existing module that could add `mod` or `pub mod` items to include the unlinked file,
42 // suggest that as a fix.
43
44 let source_root = ctx.sema.db.source_root(ctx.sema.db.file_source_root(d.file));
45 let our_path = source_root.path_for_file(&d.file)?;
46 let module_name = our_path.name_and_extension()?.0;
47
48 // Candidates to look for:
49 // - `mod.rs` in the same folder
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")?];
54
55 // `submod/bla.rs` -> `submod.rs`
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);
61 }
62
63 for path in &paths {
64 if let Some(parent_id) = source_root.file_for_path(path) {
65 for krate in ctx.sema.db.relevant_crates(*parent_id).iter() {
66 let crate_def_map = ctx.sema.db.crate_def_map(*krate);
67 for (_, module) in crate_def_map.modules() {
68 if module.origin.is_inline() {
69 // We don't handle inline `mod parent {}`s, they use different paths.
70 continue;
71 }
72
73 if module.origin.file_id() == Some(*parent_id) {
74 return make_fixes(ctx.sema.db, *parent_id, module_name, d.file);
75 }
76 }
77 }
78 }
79 }
80
81 None
82}
83
84fn make_fixes(
85 db: &RootDatabase,
86 parent_file_id: FileId,
87 new_mod_name: &str,
88 added_file_id: FileId,
89) -> Option<Vec<Assist>> {
90 fn is_outline_mod(item: &ast::Item) -> bool {
91 matches!(item, ast::Item::Module(m) if m.item_list().is_none())
92 }
93
94 let mod_decl = format!("mod {};", new_mod_name);
95 let pub_mod_decl = format!("pub mod {};", new_mod_name);
96
97 let ast: ast::SourceFile = db.parse(parent_file_id).tree();
98
99 let mut mod_decl_builder = TextEdit::builder();
100 let mut pub_mod_decl_builder = TextEdit::builder();
101
102 // If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's
103 // probably `#[cfg]`d out).
104 for item in ast.items() {
105 if let ast::Item::Module(m) = item {
106 if let Some(name) = m.name() {
107 if m.item_list().is_none() && name.to_string() == new_mod_name {
108 cov_mark::hit!(unlinked_file_skip_fix_when_mod_already_exists);
109 return None;
110 }
111 }
112 }
113 }
114
115 // If there are existing `mod m;` items, append after them (after the first group of them, rather).
116 match ast
117 .items()
118 .skip_while(|item| !is_outline_mod(item))
119 .take_while(|item| is_outline_mod(item))
120 .last()
121 {
122 Some(last) => {
123 cov_mark::hit!(unlinked_file_append_to_existing_mods);
124 let offset = last.syntax().text_range().end();
125 mod_decl_builder.insert(offset, format!("\n{}", mod_decl));
126 pub_mod_decl_builder.insert(offset, format!("\n{}", pub_mod_decl));
127 }
128 None => {
129 // Prepend before the first item in the file.
130 match ast.items().next() {
131 Some(item) => {
132 cov_mark::hit!(unlinked_file_prepend_before_first_item);
133 let offset = item.syntax().text_range().start();
134 mod_decl_builder.insert(offset, format!("{}\n\n", mod_decl));
135 pub_mod_decl_builder.insert(offset, format!("{}\n\n", pub_mod_decl));
136 }
137 None => {
138 // No items in the file, so just append at the end.
139 cov_mark::hit!(unlinked_file_empty_file);
140 let offset = ast.syntax().text_range().end();
141 mod_decl_builder.insert(offset, format!("{}\n", mod_decl));
142 pub_mod_decl_builder.insert(offset, format!("{}\n", pub_mod_decl));
143 }
144 }
145 }
146 }
147
148 let trigger_range = db.parse(added_file_id).tree().syntax().text_range();
149 Some(vec![
150 fix(
151 "add_mod_declaration",
152 &format!("Insert `{}`", mod_decl),
153 SourceChange::from_text_edit(parent_file_id, mod_decl_builder.finish()),
154 trigger_range,
155 ),
156 fix(
157 "add_pub_mod_declaration",
158 &format!("Insert `{}`", pub_mod_decl),
159 SourceChange::from_text_edit(parent_file_id, pub_mod_decl_builder.finish()),
160 trigger_range,
161 ),
162 ])
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}