diff options
Diffstat (limited to 'crates/ide_db/src/search.rs')
-rw-r--r-- | crates/ide_db/src/search.rs | 357 |
1 files changed, 357 insertions, 0 deletions
diff --git a/crates/ide_db/src/search.rs b/crates/ide_db/src/search.rs new file mode 100644 index 000000000..edab1d644 --- /dev/null +++ b/crates/ide_db/src/search.rs | |||
@@ -0,0 +1,357 @@ | |||
1 | //! Implementation of find-usages functionality. | ||
2 | //! | ||
3 | //! It is based on the standard ide trick: first, we run a fast text search to | ||
4 | //! get a super-set of matches. Then, we we confirm each match using precise | ||
5 | //! name resolution. | ||
6 | |||
7 | use std::{convert::TryInto, mem}; | ||
8 | |||
9 | use base_db::{FileId, FileRange, SourceDatabaseExt}; | ||
10 | use hir::{DefWithBody, HasSource, Module, ModuleSource, Semantics, Visibility}; | ||
11 | use once_cell::unsync::Lazy; | ||
12 | use rustc_hash::FxHashMap; | ||
13 | use syntax::{ast, match_ast, AstNode, TextRange, TextSize}; | ||
14 | |||
15 | use crate::{ | ||
16 | defs::{classify_name_ref, Definition, NameRefClass}, | ||
17 | RootDatabase, | ||
18 | }; | ||
19 | |||
20 | #[derive(Debug, Clone)] | ||
21 | pub struct Reference { | ||
22 | pub file_range: FileRange, | ||
23 | pub kind: ReferenceKind, | ||
24 | pub access: Option<ReferenceAccess>, | ||
25 | } | ||
26 | |||
27 | #[derive(Debug, Clone, PartialEq)] | ||
28 | pub enum ReferenceKind { | ||
29 | FieldShorthandForField, | ||
30 | FieldShorthandForLocal, | ||
31 | StructLiteral, | ||
32 | Other, | ||
33 | } | ||
34 | |||
35 | #[derive(Debug, Copy, Clone, PartialEq)] | ||
36 | pub enum ReferenceAccess { | ||
37 | Read, | ||
38 | Write, | ||
39 | } | ||
40 | |||
41 | /// Generally, `search_scope` returns files that might contain references for the element. | ||
42 | /// For `pub(crate)` things it's a crate, for `pub` things it's a crate and dependant crates. | ||
43 | /// In some cases, the location of the references is known to within a `TextRange`, | ||
44 | /// e.g. for things like local variables. | ||
45 | pub struct SearchScope { | ||
46 | entries: FxHashMap<FileId, Option<TextRange>>, | ||
47 | } | ||
48 | |||
49 | impl SearchScope { | ||
50 | fn new(entries: FxHashMap<FileId, Option<TextRange>>) -> SearchScope { | ||
51 | SearchScope { entries } | ||
52 | } | ||
53 | |||
54 | pub fn empty() -> SearchScope { | ||
55 | SearchScope::new(FxHashMap::default()) | ||
56 | } | ||
57 | |||
58 | pub fn single_file(file: FileId) -> SearchScope { | ||
59 | SearchScope::new(std::iter::once((file, None)).collect()) | ||
60 | } | ||
61 | |||
62 | pub fn files(files: &[FileId]) -> SearchScope { | ||
63 | SearchScope::new(files.iter().map(|f| (*f, None)).collect()) | ||
64 | } | ||
65 | |||
66 | pub fn intersection(&self, other: &SearchScope) -> SearchScope { | ||
67 | let (mut small, mut large) = (&self.entries, &other.entries); | ||
68 | if small.len() > large.len() { | ||
69 | mem::swap(&mut small, &mut large) | ||
70 | } | ||
71 | |||
72 | let res = small | ||
73 | .iter() | ||
74 | .filter_map(|(file_id, r1)| { | ||
75 | let r2 = large.get(file_id)?; | ||
76 | let r = intersect_ranges(*r1, *r2)?; | ||
77 | Some((*file_id, r)) | ||
78 | }) | ||
79 | .collect(); | ||
80 | |||
81 | return SearchScope::new(res); | ||
82 | |||
83 | fn intersect_ranges( | ||
84 | r1: Option<TextRange>, | ||
85 | r2: Option<TextRange>, | ||
86 | ) -> Option<Option<TextRange>> { | ||
87 | match (r1, r2) { | ||
88 | (None, r) | (r, None) => Some(r), | ||
89 | (Some(r1), Some(r2)) => { | ||
90 | let r = r1.intersect(r2)?; | ||
91 | Some(Some(r)) | ||
92 | } | ||
93 | } | ||
94 | } | ||
95 | } | ||
96 | } | ||
97 | |||
98 | impl IntoIterator for SearchScope { | ||
99 | type Item = (FileId, Option<TextRange>); | ||
100 | type IntoIter = std::collections::hash_map::IntoIter<FileId, Option<TextRange>>; | ||
101 | |||
102 | fn into_iter(self) -> Self::IntoIter { | ||
103 | self.entries.into_iter() | ||
104 | } | ||
105 | } | ||
106 | |||
107 | impl Definition { | ||
108 | fn search_scope(&self, db: &RootDatabase) -> SearchScope { | ||
109 | let _p = profile::span("search_scope"); | ||
110 | let module = match self.module(db) { | ||
111 | Some(it) => it, | ||
112 | None => return SearchScope::empty(), | ||
113 | }; | ||
114 | let module_src = module.definition_source(db); | ||
115 | let file_id = module_src.file_id.original_file(db); | ||
116 | |||
117 | if let Definition::Local(var) = self { | ||
118 | let range = match var.parent(db) { | ||
119 | DefWithBody::Function(f) => f.source(db).value.syntax().text_range(), | ||
120 | DefWithBody::Const(c) => c.source(db).value.syntax().text_range(), | ||
121 | DefWithBody::Static(s) => s.source(db).value.syntax().text_range(), | ||
122 | }; | ||
123 | let mut res = FxHashMap::default(); | ||
124 | res.insert(file_id, Some(range)); | ||
125 | return SearchScope::new(res); | ||
126 | } | ||
127 | |||
128 | let vis = self.visibility(db); | ||
129 | |||
130 | if let Some(Visibility::Module(module)) = vis.and_then(|it| it.into()) { | ||
131 | let module: Module = module.into(); | ||
132 | let mut res = FxHashMap::default(); | ||
133 | |||
134 | let mut to_visit = vec![module]; | ||
135 | let mut is_first = true; | ||
136 | while let Some(module) = to_visit.pop() { | ||
137 | let src = module.definition_source(db); | ||
138 | let file_id = src.file_id.original_file(db); | ||
139 | match src.value { | ||
140 | ModuleSource::Module(m) => { | ||
141 | if is_first { | ||
142 | let range = Some(m.syntax().text_range()); | ||
143 | res.insert(file_id, range); | ||
144 | } else { | ||
145 | // We have already added the enclosing file to the search scope, | ||
146 | // so do nothing. | ||
147 | } | ||
148 | } | ||
149 | ModuleSource::SourceFile(_) => { | ||
150 | res.insert(file_id, None); | ||
151 | } | ||
152 | }; | ||
153 | is_first = false; | ||
154 | to_visit.extend(module.children(db)); | ||
155 | } | ||
156 | |||
157 | return SearchScope::new(res); | ||
158 | } | ||
159 | |||
160 | if let Some(Visibility::Public) = vis { | ||
161 | let source_root_id = db.file_source_root(file_id); | ||
162 | let source_root = db.source_root(source_root_id); | ||
163 | let mut res = source_root.iter().map(|id| (id, None)).collect::<FxHashMap<_, _>>(); | ||
164 | |||
165 | let krate = module.krate(); | ||
166 | for rev_dep in krate.reverse_dependencies(db) { | ||
167 | let root_file = rev_dep.root_file(db); | ||
168 | let source_root_id = db.file_source_root(root_file); | ||
169 | let source_root = db.source_root(source_root_id); | ||
170 | res.extend(source_root.iter().map(|id| (id, None))); | ||
171 | } | ||
172 | return SearchScope::new(res); | ||
173 | } | ||
174 | |||
175 | let mut res = FxHashMap::default(); | ||
176 | let range = match module_src.value { | ||
177 | ModuleSource::Module(m) => Some(m.syntax().text_range()), | ||
178 | ModuleSource::SourceFile(_) => None, | ||
179 | }; | ||
180 | res.insert(file_id, range); | ||
181 | SearchScope::new(res) | ||
182 | } | ||
183 | |||
184 | pub fn usages<'a>(&'a self, sema: &'a Semantics<RootDatabase>) -> FindUsages<'a> { | ||
185 | FindUsages { def: self, sema, scope: None } | ||
186 | } | ||
187 | } | ||
188 | |||
189 | pub struct FindUsages<'a> { | ||
190 | def: &'a Definition, | ||
191 | sema: &'a Semantics<'a, RootDatabase>, | ||
192 | scope: Option<SearchScope>, | ||
193 | } | ||
194 | |||
195 | impl<'a> FindUsages<'a> { | ||
196 | pub fn in_scope(self, scope: SearchScope) -> FindUsages<'a> { | ||
197 | self.set_scope(Some(scope)) | ||
198 | } | ||
199 | pub fn set_scope(mut self, scope: Option<SearchScope>) -> FindUsages<'a> { | ||
200 | assert!(self.scope.is_none()); | ||
201 | self.scope = scope; | ||
202 | self | ||
203 | } | ||
204 | |||
205 | pub fn at_least_one(self) -> bool { | ||
206 | let mut found = false; | ||
207 | self.search(&mut |_reference| { | ||
208 | found = true; | ||
209 | true | ||
210 | }); | ||
211 | found | ||
212 | } | ||
213 | |||
214 | pub fn all(self) -> Vec<Reference> { | ||
215 | let mut res = Vec::new(); | ||
216 | self.search(&mut |reference| { | ||
217 | res.push(reference); | ||
218 | false | ||
219 | }); | ||
220 | res | ||
221 | } | ||
222 | |||
223 | fn search(self, sink: &mut dyn FnMut(Reference) -> bool) { | ||
224 | let _p = profile::span("FindUsages:search"); | ||
225 | let sema = self.sema; | ||
226 | |||
227 | let search_scope = { | ||
228 | let base = self.def.search_scope(sema.db); | ||
229 | match self.scope { | ||
230 | None => base, | ||
231 | Some(scope) => base.intersection(&scope), | ||
232 | } | ||
233 | }; | ||
234 | |||
235 | let name = match self.def.name(sema.db) { | ||
236 | Some(it) => it.to_string(), | ||
237 | None => return, | ||
238 | }; | ||
239 | |||
240 | let pat = name.as_str(); | ||
241 | for (file_id, search_range) in search_scope { | ||
242 | let text = sema.db.file_text(file_id); | ||
243 | let search_range = | ||
244 | search_range.unwrap_or(TextRange::up_to(TextSize::of(text.as_str()))); | ||
245 | |||
246 | let tree = Lazy::new(|| sema.parse(file_id).syntax().clone()); | ||
247 | |||
248 | for (idx, _) in text.match_indices(pat) { | ||
249 | let offset: TextSize = idx.try_into().unwrap(); | ||
250 | if !search_range.contains_inclusive(offset) { | ||
251 | continue; | ||
252 | } | ||
253 | |||
254 | let name_ref: ast::NameRef = | ||
255 | match sema.find_node_at_offset_with_descend(&tree, offset) { | ||
256 | Some(it) => it, | ||
257 | None => continue, | ||
258 | }; | ||
259 | |||
260 | match classify_name_ref(&sema, &name_ref) { | ||
261 | Some(NameRefClass::Definition(def)) if &def == self.def => { | ||
262 | let kind = if is_record_lit_name_ref(&name_ref) | ||
263 | || is_call_expr_name_ref(&name_ref) | ||
264 | { | ||
265 | ReferenceKind::StructLiteral | ||
266 | } else { | ||
267 | ReferenceKind::Other | ||
268 | }; | ||
269 | |||
270 | let reference = Reference { | ||
271 | file_range: sema.original_range(name_ref.syntax()), | ||
272 | kind, | ||
273 | access: reference_access(&def, &name_ref), | ||
274 | }; | ||
275 | if sink(reference) { | ||
276 | return; | ||
277 | } | ||
278 | } | ||
279 | Some(NameRefClass::FieldShorthand { local, field }) => { | ||
280 | let reference = match self.def { | ||
281 | Definition::Field(_) if &field == self.def => Reference { | ||
282 | file_range: self.sema.original_range(name_ref.syntax()), | ||
283 | kind: ReferenceKind::FieldShorthandForField, | ||
284 | access: reference_access(&field, &name_ref), | ||
285 | }, | ||
286 | Definition::Local(l) if &local == l => Reference { | ||
287 | file_range: self.sema.original_range(name_ref.syntax()), | ||
288 | kind: ReferenceKind::FieldShorthandForLocal, | ||
289 | access: reference_access(&Definition::Local(local), &name_ref), | ||
290 | }, | ||
291 | _ => continue, // not a usage | ||
292 | }; | ||
293 | if sink(reference) { | ||
294 | return; | ||
295 | } | ||
296 | } | ||
297 | _ => {} // not a usage | ||
298 | } | ||
299 | } | ||
300 | } | ||
301 | } | ||
302 | } | ||
303 | |||
304 | fn reference_access(def: &Definition, name_ref: &ast::NameRef) -> Option<ReferenceAccess> { | ||
305 | // Only Locals and Fields have accesses for now. | ||
306 | if !matches!(def, Definition::Local(_) | Definition::Field(_)) { | ||
307 | return None; | ||
308 | } | ||
309 | |||
310 | let mode = name_ref.syntax().ancestors().find_map(|node| { | ||
311 | match_ast! { | ||
312 | match (node) { | ||
313 | ast::BinExpr(expr) => { | ||
314 | if expr.op_kind()?.is_assignment() { | ||
315 | // If the variable or field ends on the LHS's end then it's a Write (covers fields and locals). | ||
316 | // FIXME: This is not terribly accurate. | ||
317 | if let Some(lhs) = expr.lhs() { | ||
318 | if lhs.syntax().text_range().end() == name_ref.syntax().text_range().end() { | ||
319 | return Some(ReferenceAccess::Write); | ||
320 | } | ||
321 | } | ||
322 | } | ||
323 | Some(ReferenceAccess::Read) | ||
324 | }, | ||
325 | _ => None | ||
326 | } | ||
327 | } | ||
328 | }); | ||
329 | |||
330 | // Default Locals and Fields to read | ||
331 | mode.or(Some(ReferenceAccess::Read)) | ||
332 | } | ||
333 | |||
334 | fn is_call_expr_name_ref(name_ref: &ast::NameRef) -> bool { | ||
335 | name_ref | ||
336 | .syntax() | ||
337 | .ancestors() | ||
338 | .find_map(ast::CallExpr::cast) | ||
339 | .and_then(|c| match c.expr()? { | ||
340 | ast::Expr::PathExpr(p) => { | ||
341 | Some(p.path()?.segment()?.name_ref().as_ref() == Some(name_ref)) | ||
342 | } | ||
343 | _ => None, | ||
344 | }) | ||
345 | .unwrap_or(false) | ||
346 | } | ||
347 | |||
348 | fn is_record_lit_name_ref(name_ref: &ast::NameRef) -> bool { | ||
349 | name_ref | ||
350 | .syntax() | ||
351 | .ancestors() | ||
352 | .find_map(ast::RecordExpr::cast) | ||
353 | .and_then(|l| l.path()) | ||
354 | .and_then(|p| p.segment()) | ||
355 | .map(|p| p.name_ref().as_ref() == Some(name_ref)) | ||
356 | .unwrap_or(false) | ||
357 | } | ||