diff options
Diffstat (limited to 'crates/hir_ty/src/tests.rs')
-rw-r--r-- | crates/hir_ty/src/tests.rs | 235 |
1 files changed, 142 insertions, 93 deletions
diff --git a/crates/hir_ty/src/tests.rs b/crates/hir_ty/src/tests.rs index b873585c4..0651f34ae 100644 --- a/crates/hir_ty/src/tests.rs +++ b/crates/hir_ty/src/tests.rs | |||
@@ -11,23 +11,21 @@ mod incremental; | |||
11 | 11 | ||
12 | use std::{collections::HashMap, env, sync::Arc}; | 12 | use std::{collections::HashMap, env, sync::Arc}; |
13 | 13 | ||
14 | use base_db::{fixture::WithFixture, FileRange, SourceDatabase, SourceDatabaseExt}; | 14 | use base_db::{fixture::WithFixture, FileRange, SourceDatabaseExt}; |
15 | use expect_test::Expect; | 15 | use expect_test::Expect; |
16 | use hir_def::{ | 16 | use hir_def::{ |
17 | body::{Body, BodySourceMap, SyntheticSyntax}, | 17 | body::{Body, BodySourceMap, SyntheticSyntax}, |
18 | child_by_source::ChildBySource, | ||
19 | db::DefDatabase, | 18 | db::DefDatabase, |
19 | expr::{ExprId, PatId}, | ||
20 | item_scope::ItemScope, | 20 | item_scope::ItemScope, |
21 | keys, | ||
22 | nameres::DefMap, | 21 | nameres::DefMap, |
23 | src::HasSource, | 22 | src::HasSource, |
24 | AssocItemId, DefWithBodyId, LocalModuleId, Lookup, ModuleDefId, | 23 | AssocItemId, DefWithBodyId, HasModule, LocalModuleId, Lookup, ModuleDefId, |
25 | }; | 24 | }; |
26 | use hir_expand::{db::AstDatabase, InFile}; | 25 | use hir_expand::{db::AstDatabase, InFile}; |
27 | use once_cell::race::OnceBool; | 26 | use once_cell::race::OnceBool; |
28 | use stdx::format_to; | 27 | use stdx::format_to; |
29 | use syntax::{ | 28 | use syntax::{ |
30 | algo, | ||
31 | ast::{self, AstNode, NameOwner}, | 29 | ast::{self, AstNode, NameOwner}, |
32 | SyntaxNode, | 30 | SyntaxNode, |
33 | }; | 31 | }; |
@@ -59,51 +57,55 @@ fn setup_tracing() -> Option<tracing::subscriber::DefaultGuard> { | |||
59 | } | 57 | } |
60 | 58 | ||
61 | fn check_types(ra_fixture: &str) { | 59 | fn check_types(ra_fixture: &str) { |
62 | check_types_impl(ra_fixture, false) | 60 | check_impl(ra_fixture, false, true, false) |
63 | } | 61 | } |
64 | 62 | ||
65 | fn check_types_source_code(ra_fixture: &str) { | 63 | fn check_types_source_code(ra_fixture: &str) { |
66 | check_types_impl(ra_fixture, true) | 64 | check_impl(ra_fixture, false, true, true) |
67 | } | ||
68 | |||
69 | fn check_types_impl(ra_fixture: &str, display_source: bool) { | ||
70 | let _tracing = setup_tracing(); | ||
71 | let db = TestDB::with_files(ra_fixture); | ||
72 | let mut checked_one = false; | ||
73 | for (file_id, annotations) in db.extract_annotations() { | ||
74 | for (range, expected) in annotations { | ||
75 | let ty = type_at_range(&db, FileRange { file_id, range }); | ||
76 | let actual = if display_source { | ||
77 | let module = db.module_for_file(file_id); | ||
78 | ty.display_source_code(&db, module).unwrap() | ||
79 | } else { | ||
80 | ty.display_test(&db).to_string() | ||
81 | }; | ||
82 | assert_eq!(expected, actual); | ||
83 | checked_one = true; | ||
84 | } | ||
85 | } | ||
86 | |||
87 | assert!(checked_one, "no `//^` annotations found"); | ||
88 | } | 65 | } |
89 | 66 | ||
90 | fn check_no_mismatches(ra_fixture: &str) { | 67 | fn check_no_mismatches(ra_fixture: &str) { |
91 | check_mismatches_impl(ra_fixture, true) | 68 | check_impl(ra_fixture, true, false, false) |
92 | } | 69 | } |
93 | 70 | ||
94 | #[allow(unused)] | 71 | fn check(ra_fixture: &str) { |
95 | fn check_mismatches(ra_fixture: &str) { | 72 | check_impl(ra_fixture, false, false, false) |
96 | check_mismatches_impl(ra_fixture, false) | ||
97 | } | 73 | } |
98 | 74 | ||
99 | fn check_mismatches_impl(ra_fixture: &str, allow_none: bool) { | 75 | fn check_impl(ra_fixture: &str, allow_none: bool, only_types: bool, display_source: bool) { |
100 | let _tracing = setup_tracing(); | 76 | let _tracing = setup_tracing(); |
101 | let (db, file_id) = TestDB::with_single_file(ra_fixture); | 77 | let (db, files) = TestDB::with_many_files(ra_fixture); |
102 | let module = db.module_for_file(file_id); | 78 | |
103 | let def_map = module.def_map(&db); | 79 | let mut had_annotations = false; |
80 | let mut mismatches = HashMap::new(); | ||
81 | let mut types = HashMap::new(); | ||
82 | for (file_id, annotations) in db.extract_annotations() { | ||
83 | for (range, expected) in annotations { | ||
84 | let file_range = FileRange { file_id, range }; | ||
85 | if only_types { | ||
86 | types.insert(file_range, expected); | ||
87 | } else if expected.starts_with("type: ") { | ||
88 | types.insert(file_range, expected.trim_start_matches("type: ").to_string()); | ||
89 | } else if expected.starts_with("expected") { | ||
90 | mismatches.insert(file_range, expected); | ||
91 | } else { | ||
92 | panic!("unexpected annotation: {}", expected); | ||
93 | } | ||
94 | had_annotations = true; | ||
95 | } | ||
96 | } | ||
97 | assert!(had_annotations || allow_none, "no `//^` annotations found"); | ||
104 | 98 | ||
105 | let mut defs: Vec<DefWithBodyId> = Vec::new(); | 99 | let mut defs: Vec<DefWithBodyId> = Vec::new(); |
106 | visit_module(&db, &def_map, module.local_id, &mut |it| defs.push(it)); | 100 | for file_id in files { |
101 | let module = db.module_for_file_opt(file_id); | ||
102 | let module = match module { | ||
103 | Some(m) => m, | ||
104 | None => continue, | ||
105 | }; | ||
106 | let def_map = module.def_map(&db); | ||
107 | visit_module(&db, &def_map, module.local_id, &mut |it| defs.push(it)); | ||
108 | } | ||
107 | defs.sort_by_key(|def| match def { | 109 | defs.sort_by_key(|def| match def { |
108 | DefWithBodyId::FunctionId(it) => { | 110 | DefWithBodyId::FunctionId(it) => { |
109 | let loc = it.lookup(&db); | 111 | let loc = it.lookup(&db); |
@@ -118,37 +120,59 @@ fn check_mismatches_impl(ra_fixture: &str, allow_none: bool) { | |||
118 | loc.source(&db).value.syntax().text_range().start() | 120 | loc.source(&db).value.syntax().text_range().start() |
119 | } | 121 | } |
120 | }); | 122 | }); |
121 | let mut mismatches = HashMap::new(); | 123 | let mut unexpected_type_mismatches = String::new(); |
122 | let mut push_mismatch = |src_ptr: InFile<SyntaxNode>, mismatch: TypeMismatch| { | ||
123 | let range = src_ptr.value.text_range(); | ||
124 | if src_ptr.file_id.call_node(&db).is_some() { | ||
125 | panic!("type mismatch in macro expansion"); | ||
126 | } | ||
127 | let file_range = FileRange { file_id: src_ptr.file_id.original_file(&db), range }; | ||
128 | let actual = format!( | ||
129 | "expected {}, got {}", | ||
130 | mismatch.expected.display_test(&db), | ||
131 | mismatch.actual.display_test(&db) | ||
132 | ); | ||
133 | mismatches.insert(file_range, actual); | ||
134 | }; | ||
135 | for def in defs { | 124 | for def in defs { |
136 | let (_body, body_source_map) = db.body_with_source_map(def); | 125 | let (_body, body_source_map) = db.body_with_source_map(def); |
137 | let inference_result = db.infer(def); | 126 | let inference_result = db.infer(def); |
127 | |||
128 | for (pat, ty) in inference_result.type_of_pat.iter() { | ||
129 | let node = match pat_node(&body_source_map, pat, &db) { | ||
130 | Some(value) => value, | ||
131 | None => continue, | ||
132 | }; | ||
133 | let range = node.as_ref().original_file_range(&db); | ||
134 | if let Some(expected) = types.remove(&range) { | ||
135 | let actual = if display_source { | ||
136 | ty.display_source_code(&db, def.module(&db)).unwrap() | ||
137 | } else { | ||
138 | ty.display_test(&db).to_string() | ||
139 | }; | ||
140 | assert_eq!(actual, expected); | ||
141 | } | ||
142 | } | ||
143 | |||
144 | for (expr, ty) in inference_result.type_of_expr.iter() { | ||
145 | let node = match expr_node(&body_source_map, expr, &db) { | ||
146 | Some(value) => value, | ||
147 | None => continue, | ||
148 | }; | ||
149 | let range = node.as_ref().original_file_range(&db); | ||
150 | if let Some(expected) = types.remove(&range) { | ||
151 | let actual = if display_source { | ||
152 | ty.display_source_code(&db, def.module(&db)).unwrap() | ||
153 | } else { | ||
154 | ty.display_test(&db).to_string() | ||
155 | }; | ||
156 | assert_eq!(actual, expected); | ||
157 | } | ||
158 | } | ||
159 | |||
138 | for (pat, mismatch) in inference_result.pat_type_mismatches() { | 160 | for (pat, mismatch) in inference_result.pat_type_mismatches() { |
139 | let syntax_ptr = match body_source_map.pat_syntax(pat) { | 161 | let node = match pat_node(&body_source_map, pat, &db) { |
140 | Ok(sp) => { | 162 | Some(value) => value, |
141 | let root = db.parse_or_expand(sp.file_id).unwrap(); | 163 | None => continue, |
142 | sp.map(|ptr| { | ||
143 | ptr.either( | ||
144 | |it| it.to_node(&root).syntax().clone(), | ||
145 | |it| it.to_node(&root).syntax().clone(), | ||
146 | ) | ||
147 | }) | ||
148 | } | ||
149 | Err(SyntheticSyntax) => continue, | ||
150 | }; | 164 | }; |
151 | push_mismatch(syntax_ptr, mismatch.clone()); | 165 | let range = node.as_ref().original_file_range(&db); |
166 | let actual = format!( | ||
167 | "expected {}, got {}", | ||
168 | mismatch.expected.display_test(&db), | ||
169 | mismatch.actual.display_test(&db) | ||
170 | ); | ||
171 | if let Some(annotation) = mismatches.remove(&range) { | ||
172 | assert_eq!(actual, annotation); | ||
173 | } else { | ||
174 | format_to!(unexpected_type_mismatches, "{:?}: {}\n", range.range, actual); | ||
175 | } | ||
152 | } | 176 | } |
153 | for (expr, mismatch) in inference_result.expr_type_mismatches() { | 177 | for (expr, mismatch) in inference_result.expr_type_mismatches() { |
154 | let node = match body_source_map.expr_syntax(expr) { | 178 | let node = match body_source_map.expr_syntax(expr) { |
@@ -158,45 +182,70 @@ fn check_mismatches_impl(ra_fixture: &str, allow_none: bool) { | |||
158 | } | 182 | } |
159 | Err(SyntheticSyntax) => continue, | 183 | Err(SyntheticSyntax) => continue, |
160 | }; | 184 | }; |
161 | push_mismatch(node, mismatch.clone()); | 185 | let range = node.as_ref().original_file_range(&db); |
162 | } | 186 | let actual = format!( |
163 | } | 187 | "expected {}, got {}", |
164 | let mut checked_one = false; | 188 | mismatch.expected.display_test(&db), |
165 | for (file_id, annotations) in db.extract_annotations() { | 189 | mismatch.actual.display_test(&db) |
166 | for (range, expected) in annotations { | 190 | ); |
167 | let file_range = FileRange { file_id, range }; | 191 | if let Some(annotation) = mismatches.remove(&range) { |
168 | if let Some(mismatch) = mismatches.remove(&file_range) { | 192 | assert_eq!(actual, annotation); |
169 | assert_eq!(mismatch, expected); | ||
170 | } else { | 193 | } else { |
171 | assert!(false, "Expected mismatch not encountered: {}\n", expected); | 194 | format_to!(unexpected_type_mismatches, "{:?}: {}\n", range.range, actual); |
172 | } | 195 | } |
173 | checked_one = true; | ||
174 | } | 196 | } |
175 | } | 197 | } |
198 | |||
176 | let mut buf = String::new(); | 199 | let mut buf = String::new(); |
177 | for (range, mismatch) in mismatches { | 200 | if !unexpected_type_mismatches.is_empty() { |
178 | format_to!(buf, "{:?}: {}\n", range.range, mismatch,); | 201 | format_to!(buf, "Unexpected type mismatches:\n{}", unexpected_type_mismatches); |
202 | } | ||
203 | if !mismatches.is_empty() { | ||
204 | format_to!(buf, "Unchecked mismatch annotations:\n"); | ||
205 | for m in mismatches { | ||
206 | format_to!(buf, "{:?}: {}\n", m.0.range, m.1); | ||
207 | } | ||
179 | } | 208 | } |
180 | assert!(buf.is_empty(), "Unexpected type mismatches:\n{}", buf); | 209 | if !types.is_empty() { |
210 | format_to!(buf, "Unchecked type annotations:\n"); | ||
211 | for t in types { | ||
212 | format_to!(buf, "{:?}: type {}\n", t.0.range, t.1); | ||
213 | } | ||
214 | } | ||
215 | assert!(buf.is_empty(), "{}", buf); | ||
216 | } | ||
181 | 217 | ||
182 | assert!(checked_one || allow_none, "no `//^` annotations found"); | 218 | fn expr_node( |
219 | body_source_map: &BodySourceMap, | ||
220 | expr: ExprId, | ||
221 | db: &TestDB, | ||
222 | ) -> Option<InFile<SyntaxNode>> { | ||
223 | Some(match body_source_map.expr_syntax(expr) { | ||
224 | Ok(sp) => { | ||
225 | let root = db.parse_or_expand(sp.file_id).unwrap(); | ||
226 | sp.map(|ptr| ptr.to_node(&root).syntax().clone()) | ||
227 | } | ||
228 | Err(SyntheticSyntax) => return None, | ||
229 | }) | ||
183 | } | 230 | } |
184 | 231 | ||
185 | fn type_at_range(db: &TestDB, pos: FileRange) -> Ty { | 232 | fn pat_node( |
186 | let file = db.parse(pos.file_id).ok().unwrap(); | 233 | body_source_map: &BodySourceMap, |
187 | let expr = algo::find_node_at_range::<ast::Expr>(file.syntax(), pos.range).unwrap(); | 234 | pat: PatId, |
188 | let fn_def = expr.syntax().ancestors().find_map(ast::Fn::cast).unwrap(); | 235 | db: &TestDB, |
189 | let module = db.module_for_file(pos.file_id); | 236 | ) -> Option<InFile<SyntaxNode>> { |
190 | let func = *module.child_by_source(db)[keys::FUNCTION] | 237 | Some(match body_source_map.pat_syntax(pat) { |
191 | .get(&InFile::new(pos.file_id.into(), fn_def)) | 238 | Ok(sp) => { |
192 | .unwrap(); | 239 | let root = db.parse_or_expand(sp.file_id).unwrap(); |
193 | 240 | sp.map(|ptr| { | |
194 | let (_body, source_map) = db.body_with_source_map(func.into()); | 241 | ptr.either( |
195 | if let Some(expr_id) = source_map.node_expr(InFile::new(pos.file_id.into(), &expr)) { | 242 | |it| it.to_node(&root).syntax().clone(), |
196 | let infer = db.infer(func.into()); | 243 | |it| it.to_node(&root).syntax().clone(), |
197 | return infer[expr_id].clone(); | 244 | ) |
198 | } | 245 | }) |
199 | panic!("Can't find expression") | 246 | } |
247 | Err(SyntheticSyntax) => return None, | ||
248 | }) | ||
200 | } | 249 | } |
201 | 250 | ||
202 | fn infer(ra_fixture: &str) -> String { | 251 | fn infer(ra_fixture: &str) -> String { |