diff options
author | Dmitry <[email protected]> | 2020-08-14 19:32:05 +0100 |
---|---|---|
committer | Dmitry <[email protected]> | 2020-08-14 19:32:05 +0100 |
commit | 178c3e135a2a249692f7784712492e7884ae0c00 (patch) | |
tree | ac6b769dbf7162150caa0c1624786a4dd79ff3be /crates/ide_db/src/search.rs | |
parent | 06ff8e6c760ff05f10e868b5d1f9d79e42fbb49c (diff) | |
parent | c2594daf2974dbd4ce3d9b7ec72481764abaceb5 (diff) |
Merge remote-tracking branch 'origin/master'
Diffstat (limited to 'crates/ide_db/src/search.rs')
-rw-r--r-- | crates/ide_db/src/search.rs | 322 |
1 files changed, 322 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..b9360bf12 --- /dev/null +++ b/crates/ide_db/src/search.rs | |||
@@ -0,0 +1,322 @@ | |||
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 find_usages( | ||
185 | &self, | ||
186 | sema: &Semantics<RootDatabase>, | ||
187 | search_scope: Option<SearchScope>, | ||
188 | ) -> Vec<Reference> { | ||
189 | let _p = profile::span("Definition::find_usages"); | ||
190 | |||
191 | let search_scope = { | ||
192 | let base = self.search_scope(sema.db); | ||
193 | match search_scope { | ||
194 | None => base, | ||
195 | Some(scope) => base.intersection(&scope), | ||
196 | } | ||
197 | }; | ||
198 | |||
199 | let name = match self.name(sema.db) { | ||
200 | None => return Vec::new(), | ||
201 | Some(it) => it.to_string(), | ||
202 | }; | ||
203 | |||
204 | let pat = name.as_str(); | ||
205 | let mut refs = vec![]; | ||
206 | |||
207 | for (file_id, search_range) in search_scope { | ||
208 | let text = sema.db.file_text(file_id); | ||
209 | let search_range = | ||
210 | search_range.unwrap_or(TextRange::up_to(TextSize::of(text.as_str()))); | ||
211 | |||
212 | let tree = Lazy::new(|| sema.parse(file_id).syntax().clone()); | ||
213 | |||
214 | for (idx, _) in text.match_indices(pat) { | ||
215 | let offset: TextSize = idx.try_into().unwrap(); | ||
216 | if !search_range.contains_inclusive(offset) { | ||
217 | continue; | ||
218 | } | ||
219 | |||
220 | let name_ref: ast::NameRef = | ||
221 | if let Some(name_ref) = sema.find_node_at_offset_with_descend(&tree, offset) { | ||
222 | name_ref | ||
223 | } else { | ||
224 | continue; | ||
225 | }; | ||
226 | |||
227 | match classify_name_ref(&sema, &name_ref) { | ||
228 | Some(NameRefClass::Definition(def)) if &def == self => { | ||
229 | let kind = if is_record_lit_name_ref(&name_ref) | ||
230 | || is_call_expr_name_ref(&name_ref) | ||
231 | { | ||
232 | ReferenceKind::StructLiteral | ||
233 | } else { | ||
234 | ReferenceKind::Other | ||
235 | }; | ||
236 | |||
237 | let file_range = sema.original_range(name_ref.syntax()); | ||
238 | refs.push(Reference { | ||
239 | file_range, | ||
240 | kind, | ||
241 | access: reference_access(&def, &name_ref), | ||
242 | }); | ||
243 | } | ||
244 | Some(NameRefClass::FieldShorthand { local, field }) => { | ||
245 | match self { | ||
246 | Definition::Field(_) if &field == self => refs.push(Reference { | ||
247 | file_range: sema.original_range(name_ref.syntax()), | ||
248 | kind: ReferenceKind::FieldShorthandForField, | ||
249 | access: reference_access(&field, &name_ref), | ||
250 | }), | ||
251 | Definition::Local(l) if &local == l => refs.push(Reference { | ||
252 | file_range: sema.original_range(name_ref.syntax()), | ||
253 | kind: ReferenceKind::FieldShorthandForLocal, | ||
254 | access: reference_access(&Definition::Local(local), &name_ref), | ||
255 | }), | ||
256 | |||
257 | _ => {} // not a usage | ||
258 | }; | ||
259 | } | ||
260 | _ => {} // not a usage | ||
261 | } | ||
262 | } | ||
263 | } | ||
264 | refs | ||
265 | } | ||
266 | } | ||
267 | |||
268 | fn reference_access(def: &Definition, name_ref: &ast::NameRef) -> Option<ReferenceAccess> { | ||
269 | // Only Locals and Fields have accesses for now. | ||
270 | match def { | ||
271 | Definition::Local(_) | Definition::Field(_) => {} | ||
272 | _ => return None, | ||
273 | }; | ||
274 | |||
275 | let mode = name_ref.syntax().ancestors().find_map(|node| { | ||
276 | match_ast! { | ||
277 | match (node) { | ||
278 | ast::BinExpr(expr) => { | ||
279 | if expr.op_kind()?.is_assignment() { | ||
280 | // If the variable or field ends on the LHS's end then it's a Write (covers fields and locals). | ||
281 | // FIXME: This is not terribly accurate. | ||
282 | if let Some(lhs) = expr.lhs() { | ||
283 | if lhs.syntax().text_range().end() == name_ref.syntax().text_range().end() { | ||
284 | return Some(ReferenceAccess::Write); | ||
285 | } | ||
286 | } | ||
287 | } | ||
288 | Some(ReferenceAccess::Read) | ||
289 | }, | ||
290 | _ => None | ||
291 | } | ||
292 | } | ||
293 | }); | ||
294 | |||
295 | // Default Locals and Fields to read | ||
296 | mode.or(Some(ReferenceAccess::Read)) | ||
297 | } | ||
298 | |||
299 | fn is_call_expr_name_ref(name_ref: &ast::NameRef) -> bool { | ||
300 | name_ref | ||
301 | .syntax() | ||
302 | .ancestors() | ||
303 | .find_map(ast::CallExpr::cast) | ||
304 | .and_then(|c| match c.expr()? { | ||
305 | ast::Expr::PathExpr(p) => { | ||
306 | Some(p.path()?.segment()?.name_ref().as_ref() == Some(name_ref)) | ||
307 | } | ||
308 | _ => None, | ||
309 | }) | ||
310 | .unwrap_or(false) | ||
311 | } | ||
312 | |||
313 | fn is_record_lit_name_ref(name_ref: &ast::NameRef) -> bool { | ||
314 | name_ref | ||
315 | .syntax() | ||
316 | .ancestors() | ||
317 | .find_map(ast::RecordExpr::cast) | ||
318 | .and_then(|l| l.path()) | ||
319 | .and_then(|p| p.segment()) | ||
320 | .map(|p| p.name_ref().as_ref() == Some(name_ref)) | ||
321 | .unwrap_or(false) | ||
322 | } | ||