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