1 //! Diagnostic emitted for files that aren't part of any crate.
3 use hir
::db
::DefDatabase
;
5 base_db
::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt}
,
6 source_change
::SourceChange
,
10 ast
::{self, HasModuleItem, HasName}
,
11 AstNode
, TextRange
, TextSize
,
13 use text_edit
::TextEdit
;
15 use crate::{fix, Assist, Diagnostic, DiagnosticsContext, Severity}
;
17 // Diagnostic: unlinked-file
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
>,
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
);
33 Diagnostic
::new("unlinked-file", "file not included in module tree", range
)
34 .severity(Severity
::WeakWarning
)
35 .with_fixes(fixes(ctx
, file_id
)),
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.
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()?
;
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()?
;
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()?
;
62 vec
![parent
.join("mod.rs")?
, parent
.join("lib.rs")?
, parent
.join("main.rs")?
];
64 // `submod/bla.rs` -> `submod.rs`
65 let parent_mod
= (|| {
66 let (name
, _
) = parent
.name_and_extension()?
;
67 parent
.parent()?
.join(&format
!("{}.rs", name
))
69 paths
.extend(parent_mod
);
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.
82 if module
.origin
.file_id() == Some(parent_id
) {
83 return make_fixes(ctx
.sema
.db
, parent_id
, module_name
, file_id
);
94 parent_file_id
: FileId
,
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())
102 let mod_decl
= format
!("mod {};", new_mod_name
);
103 let pub_mod_decl
= format
!("pub mod {};", new_mod_name
);
105 let ast
: ast
::SourceFile
= db
.parse(parent_file_id
).tree();
107 let mut mod_decl_builder
= TextEdit
::builder();
108 let mut pub_mod_decl_builder
= TextEdit
::builder();
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
);
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() {
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
));
132 // Prepend before the first item in the file.
133 match ast
.items().next() {
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
));
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
));
151 let trigger_range
= db
.parse(added_file_id
).tree().syntax().text_range();
154 "add_mod_declaration",
155 &format
!("Insert `{}`", mod_decl
),
156 SourceChange
::from_text_edit(parent_file_id
, mod_decl_builder
.finish()),
160 "add_pub_mod_declaration",
161 &format
!("Insert `{}`", pub_mod_decl
),
162 SourceChange
::from_text_edit(parent_file_id
, pub_mod_decl_builder
.finish()),
171 use crate::tests
::{check_diagnostics, check_fix, check_fixes, check_no_fix}
;
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
200 fn unlinked_file_append_mod() {
201 cov_mark
::check
!(unlinked_file_append_to_existing_mods
);
213 mod preexisting_bottom;)
227 mod preexisting_bottom;)
233 fn unlinked_file_insert_in_empty_file() {
234 cov_mark
::check
!(unlinked_file_empty_file
);
248 fn unlinked_file_insert_in_empty_file_mod_file() {
276 fn unlinked_file_old_style_modrs() {
294 fn unlinked_file_new_style_mod() {
310 fn unlinked_file_with_cfg_off() {
311 cov_mark
::check
!(unlinked_file_skip_fix_when_mod_already_exists
);
325 fn unlinked_file_with_cfg_on() {