]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/diagnostics/unlinked_file.rs
Merge #8220
[rust.git] / crates / ide / src / diagnostics / unlinked_file.rs
1 //! Diagnostic emitted for files that aren't part of any crate.
2
3 use hir::{
4     db::DefDatabase,
5     diagnostics::{Diagnostic, DiagnosticCode},
6     InFile,
7 };
8 use ide_db::{
9     base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt},
10     source_change::SourceChange,
11     RootDatabase,
12 };
13 use syntax::{
14     ast::{self, ModuleItemOwner, NameOwner},
15     AstNode, SyntaxNodePtr,
16 };
17 use text_edit::TextEdit;
18
19 use crate::Fix;
20
21 use super::fixes::DiagnosticWithFix;
22
23 // Diagnostic: unlinked-file
24 //
25 // This diagnostic is shown for files that are not included in any crate, or files that are part of
26 // crates rust-analyzer failed to discover. The file will not have IDE features available.
27 #[derive(Debug)]
28 pub(crate) struct UnlinkedFile {
29     pub(crate) file_id: FileId,
30     pub(crate) node: SyntaxNodePtr,
31 }
32
33 impl Diagnostic for UnlinkedFile {
34     fn code(&self) -> DiagnosticCode {
35         DiagnosticCode("unlinked-file")
36     }
37
38     fn message(&self) -> String {
39         "file not included in module tree".to_string()
40     }
41
42     fn display_source(&self) -> InFile<SyntaxNodePtr> {
43         InFile::new(self.file_id.into(), self.node.clone())
44     }
45
46     fn as_any(&self) -> &(dyn std::any::Any + Send + 'static) {
47         self
48     }
49 }
50
51 impl DiagnosticWithFix for UnlinkedFile {
52     fn fix(&self, sema: &hir::Semantics<RootDatabase>) -> Option<Fix> {
53         // If there's an existing module that could add a `mod` item to include the unlinked file,
54         // suggest that as a fix.
55
56         let source_root = sema.db.source_root(sema.db.file_source_root(self.file_id));
57         let our_path = source_root.path_for_file(&self.file_id)?;
58         let module_name = our_path.name_and_extension()?.0;
59
60         // Candidates to look for:
61         // - `mod.rs` in the same folder
62         //   - we also check `main.rs` and `lib.rs`
63         // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
64         let parent = our_path.parent()?;
65         let mut paths =
66             vec![parent.join("mod.rs")?, parent.join("main.rs")?, parent.join("lib.rs")?];
67
68         // `submod/bla.rs` -> `submod.rs`
69         if let Some(newmod) = (|| {
70             let name = parent.name_and_extension()?.0;
71             parent.parent()?.join(&format!("{}.rs", name))
72         })() {
73             paths.push(newmod);
74         }
75
76         for path in &paths {
77             if let Some(parent_id) = source_root.file_for_path(path) {
78                 for krate in sema.db.relevant_crates(*parent_id).iter() {
79                     let crate_def_map = sema.db.crate_def_map(*krate);
80                     for (_, module) in crate_def_map.modules() {
81                         if module.origin.is_inline() {
82                             // We don't handle inline `mod parent {}`s, they use different paths.
83                             continue;
84                         }
85
86                         if module.origin.file_id() == Some(*parent_id) {
87                             return make_fix(sema.db, *parent_id, module_name, self.file_id);
88                         }
89                     }
90                 }
91             }
92         }
93
94         None
95     }
96 }
97
98 fn make_fix(
99     db: &RootDatabase,
100     parent_file_id: FileId,
101     new_mod_name: &str,
102     added_file_id: FileId,
103 ) -> Option<Fix> {
104     fn is_outline_mod(item: &ast::Item) -> bool {
105         matches!(item, ast::Item::Module(m) if m.item_list().is_none())
106     }
107
108     let mod_decl = format!("mod {};", new_mod_name);
109     let ast: ast::SourceFile = db.parse(parent_file_id).tree();
110
111     let mut builder = TextEdit::builder();
112
113     // If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's
114     // probably `#[cfg]`d out).
115     for item in ast.items() {
116         if let ast::Item::Module(m) = item {
117             if let Some(name) = m.name() {
118                 if m.item_list().is_none() && name.to_string() == new_mod_name {
119                     cov_mark::hit!(unlinked_file_skip_fix_when_mod_already_exists);
120                     return None;
121                 }
122             }
123         }
124     }
125
126     // If there are existing `mod m;` items, append after them (after the first group of them, rather).
127     match ast
128         .items()
129         .skip_while(|item| !is_outline_mod(item))
130         .take_while(|item| is_outline_mod(item))
131         .last()
132     {
133         Some(last) => {
134             cov_mark::hit!(unlinked_file_append_to_existing_mods);
135             builder.insert(last.syntax().text_range().end(), format!("\n{}", mod_decl));
136         }
137         None => {
138             // Prepend before the first item in the file.
139             match ast.items().next() {
140                 Some(item) => {
141                     cov_mark::hit!(unlinked_file_prepend_before_first_item);
142                     builder.insert(item.syntax().text_range().start(), format!("{}\n\n", mod_decl));
143                 }
144                 None => {
145                     // No items in the file, so just append at the end.
146                     cov_mark::hit!(unlinked_file_empty_file);
147                     builder.insert(ast.syntax().text_range().end(), format!("{}\n", mod_decl));
148                 }
149             }
150         }
151     }
152
153     let edit = builder.finish();
154     let trigger_range = db.parse(added_file_id).tree().syntax().text_range();
155     Some(Fix::new(
156         &format!("Insert `{}`", mod_decl),
157         SourceChange::from_text_edit(parent_file_id, edit),
158         trigger_range,
159     ))
160 }