ide_diagnostics/handlers/
unlinked_file.rs1use std::iter;
4
5use hir::crate_def_map;
6use hir::{InFile, ModuleSource};
7use ide_db::base_db::RootQueryDb;
8use ide_db::text_edit::TextEdit;
9use ide_db::{
10 FileId, FileRange, LineIndexDatabase, base_db::SourceDatabase, source_change::SourceChange,
11};
12use paths::Utf8Component;
13use syntax::{
14 AstNode, TextRange,
15 ast::{self, HasModuleItem, HasName, edit::IndentLevel},
16};
17
18use crate::{Assist, Diagnostic, DiagnosticCode, DiagnosticsContext, Severity, fix};
19
20pub(crate) fn unlinked_file(
25 ctx: &DiagnosticsContext<'_>,
26 acc: &mut Vec<Diagnostic>,
27 file_id: FileId,
28) {
29 let mut range = TextRange::up_to(ctx.sema.db.line_index(file_id).len());
30 let fixes = fixes(ctx, file_id, range);
31 let message = if fixes.is_none() {
34 "This file is not included in any crates, so rust-analyzer can't offer IDE services."
35 } else {
36 "This file is not included anywhere in the module tree, so rust-analyzer can't offer IDE services."
37 };
38
39 let message = format!(
40 "{message}\n\nIf you're intentionally working on unowned files, you can silence this warning by adding \"unlinked-file\" to rust-analyzer.diagnostics.disabled in your settings."
41 );
42
43 let mut unused = true;
44
45 if fixes.is_none() {
46 range = SourceDatabase::file_text(ctx.sema.db, file_id)
53 .text(ctx.sema.db)
54 .char_indices()
55 .take(3)
56 .last()
57 .map(|(i, _)| i)
58 .map(|i| TextRange::up_to(i.try_into().unwrap()))
59 .unwrap_or(range);
60 unused = false;
63 }
64
65 acc.push(
66 Diagnostic::new(
67 DiagnosticCode::Ra("unlinked-file", Severity::WeakWarning),
68 message,
69 FileRange { file_id, range },
70 )
71 .with_unused(unused)
72 .stable()
73 .with_fixes(fixes),
74 );
75}
76
77fn fixes(
78 ctx: &DiagnosticsContext<'_>,
79 file_id: FileId,
80 trigger_range: TextRange,
81) -> Option<Vec<Assist>> {
82 let db = ctx.sema.db;
86
87 let source_root = ctx.sema.db.file_source_root(file_id).source_root_id(db);
88 let source_root = ctx.sema.db.source_root(source_root).source_root(db);
89
90 let our_path = source_root.path_for_file(&file_id)?;
91 let parent = our_path.parent()?;
92 let (module_name, _) = our_path.name_and_extension()?;
93 let (parent, module_name) = match module_name {
94 "mod" => {
97 let (name, _) = parent.name_and_extension()?;
98 (parent.parent()?, name.to_owned())
99 }
100 _ => (parent, module_name.to_owned()),
101 };
102
103 let relevant_crates = db.relevant_crates(file_id);
105 'crates: for &krate in &*relevant_crates {
106 let crate_def_map = crate_def_map(ctx.sema.db, krate);
108
109 let root_module = &crate_def_map[crate_def_map.root_module_id()];
110 let Some(root_file_id) = root_module.origin.file_id() else { continue };
111 let Some(crate_root_path) = source_root.path_for_file(&root_file_id.file_id(ctx.sema.db))
112 else {
113 continue;
114 };
115 let Some(rel) = parent.strip_prefix(&crate_root_path.parent()?) else { continue };
116
117 let mut current = root_module;
119 for ele in rel.as_utf8_path().components() {
120 let seg = match ele {
121 Utf8Component::Normal(seg) => seg,
122 Utf8Component::RootDir => continue,
123 _ => continue 'crates,
125 };
126 match current.children.iter().find(|(name, _)| name.as_str() == seg) {
127 Some((_, &child)) => current = &crate_def_map[child],
128 None => continue 'crates,
129 }
130 if !current.origin.is_inline() {
131 continue 'crates;
132 }
133 }
134
135 let InFile { file_id: parent_file_id, value: source } =
136 current.definition_source(ctx.sema.db);
137 let parent_file_id = parent_file_id.file_id()?;
138 return make_fixes(
139 parent_file_id.file_id(ctx.sema.db),
140 source,
141 &module_name,
142 trigger_range,
143 );
144 }
145
146 let paths = iter::successors(Some(parent), |prev| prev.parent()).filter_map(|path| {
150 let parent = path.parent()?;
151 let (name, _) = path.name_and_extension()?;
152 Some(([parent.join(&format!("{name}.rs"))?, path.join("mod.rs")?], name.to_owned()))
153 });
154 let mut stack = vec![];
155 let &parent_id =
156 paths.inspect(|(_, name)| stack.push(name.clone())).find_map(|(paths, _)| {
157 paths.into_iter().find_map(|path| source_root.file_for_path(&path))
158 })?;
159 stack.pop();
160 let relevant_crates = db.relevant_crates(parent_id);
161 'crates: for &krate in relevant_crates.iter() {
162 let crate_def_map = crate_def_map(ctx.sema.db, krate);
163 let Some((_, module)) = crate_def_map.modules().find(|(_, module)| {
164 module.origin.file_id().map(|file_id| file_id.file_id(ctx.sema.db)) == Some(parent_id)
165 && !module.origin.is_inline()
166 }) else {
167 continue;
168 };
169
170 if stack.is_empty() {
171 return make_fixes(
172 parent_id,
173 module.definition_source(ctx.sema.db).value,
174 &module_name,
175 trigger_range,
176 );
177 } else {
178 let mut current = module;
181 for s in stack.iter().rev() {
182 match module.children.iter().find(|(name, _)| name.as_str() == s) {
183 Some((_, child)) => {
184 current = &crate_def_map[*child];
185 }
186 None => continue 'crates,
187 }
188 if !current.origin.is_inline() {
189 continue 'crates;
190 }
191 }
192 let InFile { file_id: parent_file_id, value: source } =
193 current.definition_source(ctx.sema.db);
194 let parent_file_id = parent_file_id.file_id()?;
195 return make_fixes(
196 parent_file_id.file_id(ctx.sema.db),
197 source,
198 &module_name,
199 trigger_range,
200 );
201 }
202 }
203
204 None
205}
206
207fn make_fixes(
208 parent_file_id: FileId,
209 source: ModuleSource,
210 new_mod_name: &str,
211 trigger_range: TextRange,
212) -> Option<Vec<Assist>> {
213 fn is_outline_mod(item: &ast::Item) -> bool {
214 matches!(item, ast::Item::Module(m) if m.item_list().is_none())
215 }
216
217 let mod_decl = format!("mod {new_mod_name};");
218 let pub_mod_decl = format!("pub mod {new_mod_name};");
219 let pub_crate_mod_decl = format!("pub(crate) mod {new_mod_name};");
220
221 let mut mod_decl_builder = TextEdit::builder();
222 let mut pub_mod_decl_builder = TextEdit::builder();
223 let mut pub_crate_mod_decl_builder = TextEdit::builder();
224
225 let mut items = match &source {
226 ModuleSource::SourceFile(it) => it.items(),
227 ModuleSource::Module(it) => it.item_list()?.items(),
228 ModuleSource::BlockExpr(_) => return None,
229 };
230
231 for item in items.clone() {
234 if let ast::Item::Module(m) = item
235 && let Some(name) = m.name()
236 && m.item_list().is_none()
237 && name.to_string() == new_mod_name
238 {
239 cov_mark::hit!(unlinked_file_skip_fix_when_mod_already_exists);
240 return None;
241 }
242 }
243
244 match items.clone().skip_while(|item| !is_outline_mod(item)).take_while(is_outline_mod).last() {
246 Some(last) => {
247 cov_mark::hit!(unlinked_file_append_to_existing_mods);
248 let offset = last.syntax().text_range().end();
249 let indent = IndentLevel::from_node(last.syntax());
250 mod_decl_builder.insert(offset, format!("\n{indent}{mod_decl}"));
251 pub_mod_decl_builder.insert(offset, format!("\n{indent}{pub_mod_decl}"));
252 pub_crate_mod_decl_builder.insert(offset, format!("\n{indent}{pub_crate_mod_decl}"));
253 }
254 None => {
255 match items.next() {
257 Some(first) => {
258 cov_mark::hit!(unlinked_file_prepend_before_first_item);
259 let offset = first.syntax().text_range().start();
260 let indent = IndentLevel::from_node(first.syntax());
261 mod_decl_builder.insert(offset, format!("{mod_decl}\n\n{indent}"));
262 pub_mod_decl_builder.insert(offset, format!("{pub_mod_decl}\n\n{indent}"));
263 pub_crate_mod_decl_builder
264 .insert(offset, format!("{pub_crate_mod_decl}\n\n{indent}"));
265 }
266 None => {
267 cov_mark::hit!(unlinked_file_empty_file);
269 let mut indent = IndentLevel::from(0);
270 let offset = match &source {
271 ModuleSource::SourceFile(it) => it.syntax().text_range().end(),
272 ModuleSource::Module(it) => {
273 indent = IndentLevel::from_node(it.syntax()) + 1;
274 it.item_list()?.r_curly_token()?.text_range().start()
275 }
276 ModuleSource::BlockExpr(it) => {
277 it.stmt_list()?.r_curly_token()?.text_range().start()
278 }
279 };
280 mod_decl_builder.insert(offset, format!("{indent}{mod_decl}\n"));
281 pub_mod_decl_builder.insert(offset, format!("{indent}{pub_mod_decl}\n"));
282 pub_crate_mod_decl_builder
283 .insert(offset, format!("{indent}{pub_crate_mod_decl}\n"));
284 }
285 }
286 }
287 }
288
289 Some(vec![
290 fix(
291 "add_mod_declaration",
292 &format!("Insert `{mod_decl}`"),
293 SourceChange::from_text_edit(parent_file_id, mod_decl_builder.finish()),
294 trigger_range,
295 ),
296 fix(
297 "add_pub_mod_declaration",
298 &format!("Insert `{pub_mod_decl}`"),
299 SourceChange::from_text_edit(parent_file_id, pub_mod_decl_builder.finish()),
300 trigger_range,
301 ),
302 fix(
303 "add_pub_crate_mod_declaration",
304 &format!("Insert `{pub_crate_mod_decl}`"),
305 SourceChange::from_text_edit(parent_file_id, pub_crate_mod_decl_builder.finish()),
306 trigger_range,
307 ),
308 ])
309}
310
311#[cfg(test)]
312mod tests {
313 use crate::tests::{check_diagnostics, check_fix, check_fixes, check_no_fix};
314
315 #[test]
316 fn unlinked_file_prepend_first_item() {
317 cov_mark::check!(unlinked_file_prepend_before_first_item);
318 check_fixes(
320 r#"
321//- /main.rs
322fn f() {}
323//- /foo.rs
324$0
325"#,
326 vec![
327 r#"
328mod foo;
329
330fn f() {}
331"#,
332 r#"
333pub mod foo;
334
335fn f() {}
336"#,
337 r#"
338pub(crate) mod foo;
339
340fn f() {}
341"#,
342 ],
343 );
344 }
345
346 #[test]
347 fn unlinked_file_append_mod() {
348 cov_mark::check!(unlinked_file_append_to_existing_mods);
349 check_fix(
350 r#"
351//- /main.rs
352//! Comment on top
353
354mod preexisting;
355
356mod preexisting2;
357
358struct S;
359
360mod preexisting_bottom;)
361//- /foo.rs
362$0
363"#,
364 r#"
365//! Comment on top
366
367mod preexisting;
368
369mod preexisting2;
370mod foo;
371
372struct S;
373
374mod preexisting_bottom;)
375"#,
376 );
377 }
378
379 #[test]
380 fn unlinked_file_insert_in_empty_file() {
381 cov_mark::check!(unlinked_file_empty_file);
382 check_fix(
383 r#"
384//- /main.rs
385//- /foo.rs
386$0
387"#,
388 r#"
389mod foo;
390"#,
391 );
392 }
393
394 #[test]
395 fn unlinked_file_insert_in_empty_file_mod_file() {
396 check_fix(
397 r#"
398//- /main.rs
399//- /foo/mod.rs
400$0
401"#,
402 r#"
403mod foo;
404"#,
405 );
406 check_fix(
407 r#"
408//- /main.rs
409mod bar;
410//- /bar.rs
411// bar module
412//- /bar/foo/mod.rs
413$0
414"#,
415 r#"
416// bar module
417mod foo;
418"#,
419 );
420 }
421
422 #[test]
423 fn unlinked_file_old_style_modrs() {
424 check_fix(
425 r#"
426//- /main.rs
427mod submod;
428//- /submod/mod.rs
429// in mod.rs
430//- /submod/foo.rs
431$0
432"#,
433 r#"
434// in mod.rs
435mod foo;
436"#,
437 );
438 }
439
440 #[test]
441 fn unlinked_file_new_style_mod() {
442 check_fix(
443 r#"
444//- /main.rs
445mod submod;
446//- /submod.rs
447//- /submod/foo.rs
448$0
449"#,
450 r#"
451mod foo;
452"#,
453 );
454 }
455
456 #[test]
457 fn unlinked_file_with_cfg_off() {
458 cov_mark::check!(unlinked_file_skip_fix_when_mod_already_exists);
459 check_no_fix(
460 r#"
461//- /main.rs
462#[cfg(never)]
463mod foo;
464
465//- /foo.rs
466$0
467"#,
468 );
469 }
470
471 #[test]
472 fn unlinked_file_with_cfg_on() {
473 check_diagnostics(
474 r#"
475//- /main.rs
476#[cfg(not(never))]
477mod foo;
478
479//- /foo.rs
480"#,
481 );
482 }
483
484 #[test]
485 fn unlinked_file_insert_into_inline_simple() {
486 check_fix(
487 r#"
488//- /main.rs
489mod bar;
490//- /bar.rs
491mod foo {
492}
493//- /bar/foo/baz.rs
494$0
495"#,
496 r#"
497mod foo {
498 mod baz;
499}
500"#,
501 );
502 }
503
504 #[test]
505 fn unlinked_file_insert_into_inline_simple_modrs() {
506 check_fix(
507 r#"
508//- /main.rs
509mod bar;
510//- /bar.rs
511mod baz {
512}
513//- /bar/baz/foo/mod.rs
514$0
515"#,
516 r#"
517mod baz {
518 mod foo;
519}
520"#,
521 );
522 }
523
524 #[test]
525 fn unlinked_file_insert_into_inline_simple_modrs_main() {
526 check_fix(
527 r#"
528//- /main.rs
529mod bar {
530}
531//- /bar/foo/mod.rs
532$0
533"#,
534 r#"
535mod bar {
536 mod foo;
537}
538"#,
539 );
540 }
541
542 #[test]
543 fn include_macro_works() {
544 check_diagnostics(
545 r#"
546//- minicore: include
547//- /main.rs
548include!("bar/foo/mod.rs");
549//- /bar/foo/mod.rs
550"#,
551 );
552 }
553}