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