diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-09-17 14:00:25 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2020-09-17 14:00:25 +0100 |
commit | 933fc1eb1837a902def314d400c0f3a6dd2c1283 (patch) | |
tree | 95325c5d387fbbf13ac1caca9717e7ab0f3cdede /crates/hir_def/src/nameres | |
parent | af92bdb8270f238fbb8af8c749bf6e9fc6a06710 (diff) | |
parent | 0dca7acf0fb65545f0c46f0c604bb15400aa6d91 (diff) |
Merge #6016
6016: Emit diagnostics for unresolved imports and extern crates r=jonas-schievink a=jonas-schievink
AFAIK, we don't have any major bugs in name resolution that would cause a lot of false positives here (except procedural attribute macro support and some rare issues around `#[path]` on module files), so these are *not* marked as experimental diagnostics right now.
I noticed that diagnostics in a file sometimes don't get displayed after opening, but require some edit to be performed. This seems like a preexisting issue though.
Co-authored-by: Jonas Schievink <[email protected]>
Diffstat (limited to 'crates/hir_def/src/nameres')
-rw-r--r-- | crates/hir_def/src/nameres/collector.rs | 102 | ||||
-rw-r--r-- | crates/hir_def/src/nameres/tests.rs | 1 | ||||
-rw-r--r-- | crates/hir_def/src/nameres/tests/diagnostics.rs | 131 | ||||
-rw-r--r-- | crates/hir_def/src/nameres/tests/mod_resolution.rs | 36 |
4 files changed, 214 insertions, 56 deletions
diff --git a/crates/hir_def/src/nameres/collector.rs b/crates/hir_def/src/nameres/collector.rs index 3e99c8773..818008169 100644 --- a/crates/hir_def/src/nameres/collector.rs +++ b/crates/hir_def/src/nameres/collector.rs | |||
@@ -5,6 +5,7 @@ | |||
5 | 5 | ||
6 | use base_db::{CrateId, FileId, ProcMacroId}; | 6 | use base_db::{CrateId, FileId, ProcMacroId}; |
7 | use cfg::CfgOptions; | 7 | use cfg::CfgOptions; |
8 | use hir_expand::InFile; | ||
8 | use hir_expand::{ | 9 | use hir_expand::{ |
9 | ast_id_map::FileAstId, | 10 | ast_id_map::FileAstId, |
10 | builtin_derive::find_builtin_derive, | 11 | builtin_derive::find_builtin_derive, |
@@ -14,6 +15,7 @@ use hir_expand::{ | |||
14 | HirFileId, MacroCallId, MacroDefId, MacroDefKind, | 15 | HirFileId, MacroCallId, MacroDefId, MacroDefKind, |
15 | }; | 16 | }; |
16 | use rustc_hash::FxHashMap; | 17 | use rustc_hash::FxHashMap; |
18 | use rustc_hash::FxHashSet; | ||
17 | use syntax::ast; | 19 | use syntax::ast; |
18 | use test_utils::mark; | 20 | use test_utils::mark; |
19 | 21 | ||
@@ -21,9 +23,7 @@ use crate::{ | |||
21 | attr::Attrs, | 23 | attr::Attrs, |
22 | db::DefDatabase, | 24 | db::DefDatabase, |
23 | item_scope::{ImportType, PerNsGlobImports}, | 25 | item_scope::{ImportType, PerNsGlobImports}, |
24 | item_tree::{ | 26 | item_tree::{self, ItemTree, ItemTreeId, MacroCall, Mod, ModItem, ModKind, StructDefKind}, |
25 | self, FileItemTreeId, ItemTree, ItemTreeId, MacroCall, Mod, ModItem, ModKind, StructDefKind, | ||
26 | }, | ||
27 | nameres::{ | 27 | nameres::{ |
28 | diagnostics::DefDiagnostic, mod_resolution::ModDir, path_resolution::ReachedFixedPoint, | 28 | diagnostics::DefDiagnostic, mod_resolution::ModDir, path_resolution::ReachedFixedPoint, |
29 | BuiltinShadowMode, CrateDefMap, ModuleData, ModuleOrigin, ResolveMode, | 29 | BuiltinShadowMode, CrateDefMap, ModuleData, ModuleOrigin, ResolveMode, |
@@ -112,6 +112,12 @@ impl PartialResolvedImport { | |||
112 | } | 112 | } |
113 | 113 | ||
114 | #[derive(Clone, Debug, Eq, PartialEq)] | 114 | #[derive(Clone, Debug, Eq, PartialEq)] |
115 | enum ImportSource { | ||
116 | Import(ItemTreeId<item_tree::Import>), | ||
117 | ExternCrate(ItemTreeId<item_tree::ExternCrate>), | ||
118 | } | ||
119 | |||
120 | #[derive(Clone, Debug, Eq, PartialEq)] | ||
115 | struct Import { | 121 | struct Import { |
116 | pub path: ModPath, | 122 | pub path: ModPath, |
117 | pub alias: Option<ImportAlias>, | 123 | pub alias: Option<ImportAlias>, |
@@ -120,11 +126,12 @@ struct Import { | |||
120 | pub is_prelude: bool, | 126 | pub is_prelude: bool, |
121 | pub is_extern_crate: bool, | 127 | pub is_extern_crate: bool, |
122 | pub is_macro_use: bool, | 128 | pub is_macro_use: bool, |
129 | source: ImportSource, | ||
123 | } | 130 | } |
124 | 131 | ||
125 | impl Import { | 132 | impl Import { |
126 | fn from_use(tree: &ItemTree, id: FileItemTreeId<item_tree::Import>) -> Self { | 133 | fn from_use(tree: &ItemTree, id: ItemTreeId<item_tree::Import>) -> Self { |
127 | let it = &tree[id]; | 134 | let it = &tree[id.value]; |
128 | let visibility = &tree[it.visibility]; | 135 | let visibility = &tree[it.visibility]; |
129 | Self { | 136 | Self { |
130 | path: it.path.clone(), | 137 | path: it.path.clone(), |
@@ -134,11 +141,12 @@ impl Import { | |||
134 | is_prelude: it.is_prelude, | 141 | is_prelude: it.is_prelude, |
135 | is_extern_crate: false, | 142 | is_extern_crate: false, |
136 | is_macro_use: false, | 143 | is_macro_use: false, |
144 | source: ImportSource::Import(id), | ||
137 | } | 145 | } |
138 | } | 146 | } |
139 | 147 | ||
140 | fn from_extern_crate(tree: &ItemTree, id: FileItemTreeId<item_tree::ExternCrate>) -> Self { | 148 | fn from_extern_crate(tree: &ItemTree, id: ItemTreeId<item_tree::ExternCrate>) -> Self { |
141 | let it = &tree[id]; | 149 | let it = &tree[id.value]; |
142 | let visibility = &tree[it.visibility]; | 150 | let visibility = &tree[it.visibility]; |
143 | Self { | 151 | Self { |
144 | path: it.path.clone(), | 152 | path: it.path.clone(), |
@@ -148,6 +156,7 @@ impl Import { | |||
148 | is_prelude: false, | 156 | is_prelude: false, |
149 | is_extern_crate: true, | 157 | is_extern_crate: true, |
150 | is_macro_use: it.is_macro_use, | 158 | is_macro_use: it.is_macro_use, |
159 | source: ImportSource::ExternCrate(id), | ||
151 | } | 160 | } |
152 | } | 161 | } |
153 | } | 162 | } |
@@ -245,9 +254,10 @@ impl DefCollector<'_> { | |||
245 | 254 | ||
246 | let unresolved_imports = std::mem::replace(&mut self.unresolved_imports, Vec::new()); | 255 | let unresolved_imports = std::mem::replace(&mut self.unresolved_imports, Vec::new()); |
247 | // show unresolved imports in completion, etc | 256 | // show unresolved imports in completion, etc |
248 | for directive in unresolved_imports { | 257 | for directive in &unresolved_imports { |
249 | self.record_resolved_import(&directive) | 258 | self.record_resolved_import(directive) |
250 | } | 259 | } |
260 | self.unresolved_imports = unresolved_imports; | ||
251 | 261 | ||
252 | // Record proc-macros | 262 | // Record proc-macros |
253 | self.collect_proc_macro(); | 263 | self.collect_proc_macro(); |
@@ -420,7 +430,11 @@ impl DefCollector<'_> { | |||
420 | .as_ident() | 430 | .as_ident() |
421 | .expect("extern crate should have been desugared to one-element path"), | 431 | .expect("extern crate should have been desugared to one-element path"), |
422 | ); | 432 | ); |
423 | PartialResolvedImport::Resolved(res) | 433 | if res.is_none() { |
434 | PartialResolvedImport::Unresolved | ||
435 | } else { | ||
436 | PartialResolvedImport::Resolved(res) | ||
437 | } | ||
424 | } else { | 438 | } else { |
425 | let res = self.def_map.resolve_path_fp_with_macro( | 439 | let res = self.def_map.resolve_path_fp_with_macro( |
426 | self.db, | 440 | self.db, |
@@ -774,7 +788,51 @@ impl DefCollector<'_> { | |||
774 | .collect(item_tree.top_level_items()); | 788 | .collect(item_tree.top_level_items()); |
775 | } | 789 | } |
776 | 790 | ||
777 | fn finish(self) -> CrateDefMap { | 791 | fn finish(mut self) -> CrateDefMap { |
792 | // Emit diagnostics for all remaining unresolved imports. | ||
793 | |||
794 | // We'd like to avoid emitting a diagnostics avalanche when some `extern crate` doesn't | ||
795 | // resolve. We first emit diagnostics for unresolved extern crates and collect the missing | ||
796 | // crate names. Then we emit diagnostics for unresolved imports, but only if the import | ||
797 | // doesn't start with an unresolved crate's name. Due to renaming and reexports, this is a | ||
798 | // heuristic, but it works in practice. | ||
799 | let mut diagnosed_extern_crates = FxHashSet::default(); | ||
800 | for directive in &self.unresolved_imports { | ||
801 | if let ImportSource::ExternCrate(krate) = directive.import.source { | ||
802 | let item_tree = self.db.item_tree(krate.file_id); | ||
803 | let extern_crate = &item_tree[krate.value]; | ||
804 | |||
805 | diagnosed_extern_crates.insert(extern_crate.path.segments[0].clone()); | ||
806 | |||
807 | self.def_map.diagnostics.push(DefDiagnostic::unresolved_extern_crate( | ||
808 | directive.module_id, | ||
809 | InFile::new(krate.file_id, extern_crate.ast_id), | ||
810 | )); | ||
811 | } | ||
812 | } | ||
813 | |||
814 | for directive in &self.unresolved_imports { | ||
815 | if let ImportSource::Import(import) = &directive.import.source { | ||
816 | let item_tree = self.db.item_tree(import.file_id); | ||
817 | let import_data = &item_tree[import.value]; | ||
818 | |||
819 | match (import_data.path.segments.first(), &import_data.path.kind) { | ||
820 | (Some(krate), PathKind::Plain) | (Some(krate), PathKind::Abs) => { | ||
821 | if diagnosed_extern_crates.contains(krate) { | ||
822 | continue; | ||
823 | } | ||
824 | } | ||
825 | _ => {} | ||
826 | } | ||
827 | |||
828 | self.def_map.diagnostics.push(DefDiagnostic::unresolved_import( | ||
829 | directive.module_id, | ||
830 | InFile::new(import.file_id, import_data.ast_id), | ||
831 | import_data.index, | ||
832 | )); | ||
833 | } | ||
834 | } | ||
835 | |||
778 | self.def_map | 836 | self.def_map |
779 | } | 837 | } |
780 | } | 838 | } |
@@ -830,14 +888,20 @@ impl ModCollector<'_, '_> { | |||
830 | ModItem::Import(import_id) => { | 888 | ModItem::Import(import_id) => { |
831 | self.def_collector.unresolved_imports.push(ImportDirective { | 889 | self.def_collector.unresolved_imports.push(ImportDirective { |
832 | module_id: self.module_id, | 890 | module_id: self.module_id, |
833 | import: Import::from_use(&self.item_tree, import_id), | 891 | import: Import::from_use( |
892 | &self.item_tree, | ||
893 | InFile::new(self.file_id, import_id), | ||
894 | ), | ||
834 | status: PartialResolvedImport::Unresolved, | 895 | status: PartialResolvedImport::Unresolved, |
835 | }) | 896 | }) |
836 | } | 897 | } |
837 | ModItem::ExternCrate(import_id) => { | 898 | ModItem::ExternCrate(import_id) => { |
838 | self.def_collector.unresolved_imports.push(ImportDirective { | 899 | self.def_collector.unresolved_imports.push(ImportDirective { |
839 | module_id: self.module_id, | 900 | module_id: self.module_id, |
840 | import: Import::from_extern_crate(&self.item_tree, import_id), | 901 | import: Import::from_extern_crate( |
902 | &self.item_tree, | ||
903 | InFile::new(self.file_id, import_id), | ||
904 | ), | ||
841 | status: PartialResolvedImport::Unresolved, | 905 | status: PartialResolvedImport::Unresolved, |
842 | }) | 906 | }) |
843 | } | 907 | } |
@@ -1051,13 +1115,11 @@ impl ModCollector<'_, '_> { | |||
1051 | self.import_all_legacy_macros(module_id); | 1115 | self.import_all_legacy_macros(module_id); |
1052 | } | 1116 | } |
1053 | } | 1117 | } |
1054 | Err(candidate) => self.def_collector.def_map.diagnostics.push( | 1118 | Err(candidate) => { |
1055 | DefDiagnostic::UnresolvedModule { | 1119 | self.def_collector.def_map.diagnostics.push( |
1056 | module: self.module_id, | 1120 | DefDiagnostic::unresolved_module(self.module_id, ast_id, candidate), |
1057 | declaration: ast_id, | 1121 | ); |
1058 | candidate, | 1122 | } |
1059 | }, | ||
1060 | ), | ||
1061 | }; | 1123 | }; |
1062 | } | 1124 | } |
1063 | } | 1125 | } |
diff --git a/crates/hir_def/src/nameres/tests.rs b/crates/hir_def/src/nameres/tests.rs index 5ca30dac9..11d84f808 100644 --- a/crates/hir_def/src/nameres/tests.rs +++ b/crates/hir_def/src/nameres/tests.rs | |||
@@ -2,6 +2,7 @@ mod globs; | |||
2 | mod incremental; | 2 | mod incremental; |
3 | mod macros; | 3 | mod macros; |
4 | mod mod_resolution; | 4 | mod mod_resolution; |
5 | mod diagnostics; | ||
5 | mod primitives; | 6 | mod primitives; |
6 | 7 | ||
7 | use std::sync::Arc; | 8 | use std::sync::Arc; |
diff --git a/crates/hir_def/src/nameres/tests/diagnostics.rs b/crates/hir_def/src/nameres/tests/diagnostics.rs new file mode 100644 index 000000000..576b813d2 --- /dev/null +++ b/crates/hir_def/src/nameres/tests/diagnostics.rs | |||
@@ -0,0 +1,131 @@ | |||
1 | use base_db::fixture::WithFixture; | ||
2 | use base_db::FileId; | ||
3 | use base_db::SourceDatabaseExt; | ||
4 | use hir_expand::db::AstDatabase; | ||
5 | use rustc_hash::FxHashMap; | ||
6 | use syntax::TextRange; | ||
7 | use syntax::TextSize; | ||
8 | |||
9 | use crate::test_db::TestDB; | ||
10 | |||
11 | fn check_diagnostics(ra_fixture: &str) { | ||
12 | let db: TestDB = TestDB::with_files(ra_fixture); | ||
13 | let annotations = db.extract_annotations(); | ||
14 | assert!(!annotations.is_empty()); | ||
15 | |||
16 | let mut actual: FxHashMap<FileId, Vec<(TextRange, String)>> = FxHashMap::default(); | ||
17 | db.diagnostics(|d| { | ||
18 | let src = d.display_source(); | ||
19 | let root = db.parse_or_expand(src.file_id).unwrap(); | ||
20 | // FIXME: macros... | ||
21 | let file_id = src.file_id.original_file(&db); | ||
22 | let range = src.value.to_node(&root).text_range(); | ||
23 | let message = d.message().to_owned(); | ||
24 | actual.entry(file_id).or_default().push((range, message)); | ||
25 | }); | ||
26 | |||
27 | for (file_id, diags) in actual.iter_mut() { | ||
28 | diags.sort_by_key(|it| it.0.start()); | ||
29 | let text = db.file_text(*file_id); | ||
30 | // For multiline spans, place them on line start | ||
31 | for (range, content) in diags { | ||
32 | if text[*range].contains('\n') { | ||
33 | *range = TextRange::new(range.start(), range.start() + TextSize::from(1)); | ||
34 | *content = format!("... {}", content); | ||
35 | } | ||
36 | } | ||
37 | } | ||
38 | |||
39 | assert_eq!(annotations, actual); | ||
40 | } | ||
41 | |||
42 | #[test] | ||
43 | fn unresolved_import() { | ||
44 | check_diagnostics( | ||
45 | r" | ||
46 | use does_exist; | ||
47 | use does_not_exist; | ||
48 | //^^^^^^^^^^^^^^ unresolved import | ||
49 | |||
50 | mod does_exist {} | ||
51 | ", | ||
52 | ); | ||
53 | } | ||
54 | |||
55 | #[test] | ||
56 | fn unresolved_import_in_use_tree() { | ||
57 | // Only the relevant part of a nested `use` item should be highlighted. | ||
58 | check_diagnostics( | ||
59 | r" | ||
60 | use does_exist::{Exists, DoesntExist}; | ||
61 | //^^^^^^^^^^^ unresolved import | ||
62 | |||
63 | use {does_not_exist::*, does_exist}; | ||
64 | //^^^^^^^^^^^^^^^^^ unresolved import | ||
65 | |||
66 | use does_not_exist::{ | ||
67 | a, | ||
68 | //^ unresolved import | ||
69 | b, | ||
70 | //^ unresolved import | ||
71 | c, | ||
72 | //^ unresolved import | ||
73 | }; | ||
74 | |||
75 | mod does_exist { | ||
76 | pub struct Exists; | ||
77 | } | ||
78 | ", | ||
79 | ); | ||
80 | } | ||
81 | |||
82 | #[test] | ||
83 | fn unresolved_extern_crate() { | ||
84 | check_diagnostics( | ||
85 | r" | ||
86 | //- /main.rs crate:main deps:core | ||
87 | extern crate core; | ||
88 | extern crate doesnotexist; | ||
89 | //^^^^^^^^^^^^^^^^^^^^^^^^^^ unresolved extern crate | ||
90 | //- /lib.rs crate:core | ||
91 | ", | ||
92 | ); | ||
93 | } | ||
94 | |||
95 | #[test] | ||
96 | fn dedup_unresolved_import_from_unresolved_crate() { | ||
97 | check_diagnostics( | ||
98 | r" | ||
99 | //- /main.rs crate:main | ||
100 | mod a { | ||
101 | extern crate doesnotexist; | ||
102 | //^^^^^^^^^^^^^^^^^^^^^^^^^^ unresolved extern crate | ||
103 | |||
104 | // Should not error, since we already errored for the missing crate. | ||
105 | use doesnotexist::{self, bla, *}; | ||
106 | |||
107 | use crate::doesnotexist; | ||
108 | //^^^^^^^^^^^^^^^^^^^ unresolved import | ||
109 | } | ||
110 | |||
111 | mod m { | ||
112 | use super::doesnotexist; | ||
113 | //^^^^^^^^^^^^^^^^^^^ unresolved import | ||
114 | } | ||
115 | ", | ||
116 | ); | ||
117 | } | ||
118 | |||
119 | #[test] | ||
120 | fn unresolved_module() { | ||
121 | check_diagnostics( | ||
122 | r" | ||
123 | //- /lib.rs | ||
124 | mod foo; | ||
125 | mod bar; | ||
126 | //^^^^^^^^ unresolved module | ||
127 | mod baz {} | ||
128 | //- /foo.rs | ||
129 | ", | ||
130 | ); | ||
131 | } | ||
diff --git a/crates/hir_def/src/nameres/tests/mod_resolution.rs b/crates/hir_def/src/nameres/tests/mod_resolution.rs index 1f619787e..f93337a6e 100644 --- a/crates/hir_def/src/nameres/tests/mod_resolution.rs +++ b/crates/hir_def/src/nameres/tests/mod_resolution.rs | |||
@@ -672,42 +672,6 @@ pub struct Baz; | |||
672 | } | 672 | } |
673 | 673 | ||
674 | #[test] | 674 | #[test] |
675 | fn unresolved_module_diagnostics() { | ||
676 | let db = TestDB::with_files( | ||
677 | r" | ||
678 | //- /lib.rs | ||
679 | mod foo; | ||
680 | mod bar; | ||
681 | mod baz {} | ||
682 | //- /foo.rs | ||
683 | ", | ||
684 | ); | ||
685 | let krate = db.test_crate(); | ||
686 | |||
687 | let crate_def_map = db.crate_def_map(krate); | ||
688 | |||
689 | expect![[r#" | ||
690 | [ | ||
691 | UnresolvedModule { | ||
692 | module: Idx::<ModuleData>(0), | ||
693 | declaration: InFile { | ||
694 | file_id: HirFileId( | ||
695 | FileId( | ||
696 | FileId( | ||
697 | 0, | ||
698 | ), | ||
699 | ), | ||
700 | ), | ||
701 | value: FileAstId::<syntax::ast::generated::nodes::Module>(1), | ||
702 | }, | ||
703 | candidate: "bar.rs", | ||
704 | }, | ||
705 | ] | ||
706 | "#]] | ||
707 | .assert_debug_eq(&crate_def_map.diagnostics); | ||
708 | } | ||
709 | |||
710 | #[test] | ||
711 | fn module_resolution_decl_inside_module_in_non_crate_root_2() { | 675 | fn module_resolution_decl_inside_module_in_non_crate_root_2() { |
712 | check( | 676 | check( |
713 | r#" | 677 | r#" |