diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-03-04 11:49:24 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2020-03-04 11:49:24 +0000 |
commit | 02b02061b6cb121204a9bd931e7cd541dba2898f (patch) | |
tree | a8e5e91aeef1d910a77ed2b664cd28e6d033de7a /crates/ra_ide_db/src/search.rs | |
parent | 66ec6bdfb0fbb8e99b58f3c184ef5012354e6d92 (diff) | |
parent | 4f50a3718762c9272b1929162ce62415a75eec8f (diff) |
Merge #3440
3440: Move search to ra_ide_db r=matklad a=matklad
bors r+
🤖
Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates/ra_ide_db/src/search.rs')
-rw-r--r-- | crates/ra_ide_db/src/search.rs | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/crates/ra_ide_db/src/search.rs b/crates/ra_ide_db/src/search.rs new file mode 100644 index 000000000..6f198df04 --- /dev/null +++ b/crates/ra_ide_db/src/search.rs | |||
@@ -0,0 +1,319 @@ | |||
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::mem; | ||
8 | |||
9 | use hir::{DefWithBody, HasSource, ModuleSource, Semantics}; | ||
10 | use once_cell::unsync::Lazy; | ||
11 | use ra_db::{FileId, FileRange, SourceDatabaseExt}; | ||
12 | use ra_prof::profile; | ||
13 | use ra_syntax::{ | ||
14 | algo::find_node_at_offset, ast, match_ast, AstNode, TextRange, TextUnit, TokenAtOffset, | ||
15 | }; | ||
16 | use rustc_hash::FxHashMap; | ||
17 | use test_utils::tested_by; | ||
18 | |||
19 | use crate::{ | ||
20 | defs::{classify_name_ref, Definition}, | ||
21 | RootDatabase, | ||
22 | }; | ||
23 | |||
24 | #[derive(Debug, Clone)] | ||
25 | pub struct Reference { | ||
26 | pub file_range: FileRange, | ||
27 | pub kind: ReferenceKind, | ||
28 | pub access: Option<ReferenceAccess>, | ||
29 | } | ||
30 | |||
31 | #[derive(Debug, Clone, PartialEq)] | ||
32 | pub enum ReferenceKind { | ||
33 | StructLiteral, | ||
34 | Other, | ||
35 | } | ||
36 | |||
37 | #[derive(Debug, Copy, Clone, PartialEq)] | ||
38 | pub enum ReferenceAccess { | ||
39 | Read, | ||
40 | Write, | ||
41 | } | ||
42 | |||
43 | /// Generally, `search_scope` returns files that might contain references for the element. | ||
44 | /// For `pub(crate)` things it's a crate, for `pub` things it's a crate and dependant crates. | ||
45 | /// In some cases, the location of the references is known to within a `TextRange`, | ||
46 | /// e.g. for things like local variables. | ||
47 | pub struct SearchScope { | ||
48 | entries: FxHashMap<FileId, Option<TextRange>>, | ||
49 | } | ||
50 | |||
51 | impl SearchScope { | ||
52 | fn new(entries: FxHashMap<FileId, Option<TextRange>>) -> SearchScope { | ||
53 | SearchScope { entries } | ||
54 | } | ||
55 | |||
56 | pub fn empty() -> SearchScope { | ||
57 | SearchScope::new(FxHashMap::default()) | ||
58 | } | ||
59 | |||
60 | pub fn single_file(file: FileId) -> SearchScope { | ||
61 | SearchScope::new(std::iter::once((file, None)).collect()) | ||
62 | } | ||
63 | |||
64 | pub fn intersection(&self, other: &SearchScope) -> SearchScope { | ||
65 | let (mut small, mut large) = (&self.entries, &other.entries); | ||
66 | if small.len() > large.len() { | ||
67 | mem::swap(&mut small, &mut large) | ||
68 | } | ||
69 | |||
70 | let res = small | ||
71 | .iter() | ||
72 | .filter_map(|(file_id, r1)| { | ||
73 | let r2 = large.get(file_id)?; | ||
74 | let r = intersect_ranges(*r1, *r2)?; | ||
75 | Some((*file_id, r)) | ||
76 | }) | ||
77 | .collect(); | ||
78 | |||
79 | return SearchScope::new(res); | ||
80 | |||
81 | fn intersect_ranges( | ||
82 | r1: Option<TextRange>, | ||
83 | r2: Option<TextRange>, | ||
84 | ) -> Option<Option<TextRange>> { | ||
85 | match (r1, r2) { | ||
86 | (None, r) | (r, None) => Some(r), | ||
87 | (Some(r1), Some(r2)) => { | ||
88 | let r = r1.intersection(&r2)?; | ||
89 | Some(Some(r)) | ||
90 | } | ||
91 | } | ||
92 | } | ||
93 | } | ||
94 | } | ||
95 | |||
96 | impl IntoIterator for SearchScope { | ||
97 | type Item = (FileId, Option<TextRange>); | ||
98 | type IntoIter = std::collections::hash_map::IntoIter<FileId, Option<TextRange>>; | ||
99 | |||
100 | fn into_iter(self) -> Self::IntoIter { | ||
101 | self.entries.into_iter() | ||
102 | } | ||
103 | } | ||
104 | |||
105 | impl Definition { | ||
106 | fn search_scope(&self, db: &RootDatabase) -> SearchScope { | ||
107 | let _p = profile("search_scope"); | ||
108 | let module = match self.module(db) { | ||
109 | Some(it) => it, | ||
110 | None => return SearchScope::empty(), | ||
111 | }; | ||
112 | let module_src = module.definition_source(db); | ||
113 | let file_id = module_src.file_id.original_file(db); | ||
114 | |||
115 | if let Definition::Local(var) = self { | ||
116 | let range = match var.parent(db) { | ||
117 | DefWithBody::Function(f) => f.source(db).value.syntax().text_range(), | ||
118 | DefWithBody::Const(c) => c.source(db).value.syntax().text_range(), | ||
119 | DefWithBody::Static(s) => s.source(db).value.syntax().text_range(), | ||
120 | }; | ||
121 | let mut res = FxHashMap::default(); | ||
122 | res.insert(file_id, Some(range)); | ||
123 | return SearchScope::new(res); | ||
124 | } | ||
125 | |||
126 | let vis = self.visibility(db).as_ref().map(|v| v.syntax().to_string()).unwrap_or_default(); | ||
127 | |||
128 | if vis.as_str() == "pub(super)" { | ||
129 | if let Some(parent_module) = module.parent(db) { | ||
130 | let mut res = FxHashMap::default(); | ||
131 | let parent_src = parent_module.definition_source(db); | ||
132 | let file_id = parent_src.file_id.original_file(db); | ||
133 | |||
134 | match parent_src.value { | ||
135 | ModuleSource::Module(m) => { | ||
136 | let range = Some(m.syntax().text_range()); | ||
137 | res.insert(file_id, range); | ||
138 | } | ||
139 | ModuleSource::SourceFile(_) => { | ||
140 | res.insert(file_id, None); | ||
141 | res.extend(parent_module.children(db).map(|m| { | ||
142 | let src = m.definition_source(db); | ||
143 | (src.file_id.original_file(db), None) | ||
144 | })); | ||
145 | } | ||
146 | } | ||
147 | return SearchScope::new(res); | ||
148 | } | ||
149 | } | ||
150 | |||
151 | if vis.as_str() != "" { | ||
152 | let source_root_id = db.file_source_root(file_id); | ||
153 | let source_root = db.source_root(source_root_id); | ||
154 | let mut res = source_root.walk().map(|id| (id, None)).collect::<FxHashMap<_, _>>(); | ||
155 | |||
156 | // FIXME: add "pub(in path)" | ||
157 | |||
158 | if vis.as_str() == "pub(crate)" { | ||
159 | return SearchScope::new(res); | ||
160 | } | ||
161 | if vis.as_str() == "pub" { | ||
162 | let krate = module.krate(); | ||
163 | for rev_dep in krate.reverse_dependencies(db) { | ||
164 | let root_file = rev_dep.root_file(db); | ||
165 | let source_root_id = db.file_source_root(root_file); | ||
166 | let source_root = db.source_root(source_root_id); | ||
167 | res.extend(source_root.walk().map(|id| (id, None))); | ||
168 | } | ||
169 | return SearchScope::new(res); | ||
170 | } | ||
171 | } | ||
172 | |||
173 | let mut res = FxHashMap::default(); | ||
174 | let range = match module_src.value { | ||
175 | ModuleSource::Module(m) => Some(m.syntax().text_range()), | ||
176 | ModuleSource::SourceFile(_) => None, | ||
177 | }; | ||
178 | res.insert(file_id, range); | ||
179 | SearchScope::new(res) | ||
180 | } | ||
181 | |||
182 | pub fn find_usages( | ||
183 | &self, | ||
184 | db: &RootDatabase, | ||
185 | search_scope: Option<SearchScope>, | ||
186 | ) -> Vec<Reference> { | ||
187 | let _p = profile("Definition::find_usages"); | ||
188 | |||
189 | let search_scope = { | ||
190 | let base = self.search_scope(db); | ||
191 | match search_scope { | ||
192 | None => base, | ||
193 | Some(scope) => base.intersection(&scope), | ||
194 | } | ||
195 | }; | ||
196 | |||
197 | let name = match self.name(db) { | ||
198 | None => return Vec::new(), | ||
199 | Some(it) => it.to_string(), | ||
200 | }; | ||
201 | |||
202 | let pat = name.as_str(); | ||
203 | let mut refs = vec![]; | ||
204 | |||
205 | for (file_id, search_range) in search_scope { | ||
206 | let text = db.file_text(file_id); | ||
207 | let search_range = | ||
208 | search_range.unwrap_or(TextRange::offset_len(0.into(), TextUnit::of_str(&text))); | ||
209 | |||
210 | let sema = Semantics::new(db); | ||
211 | let tree = Lazy::new(|| sema.parse(file_id).syntax().clone()); | ||
212 | |||
213 | for (idx, _) in text.match_indices(pat) { | ||
214 | let offset = TextUnit::from_usize(idx); | ||
215 | if !search_range.contains_inclusive(offset) { | ||
216 | tested_by!(search_filters_by_range; force); | ||
217 | continue; | ||
218 | } | ||
219 | |||
220 | let name_ref = | ||
221 | if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(&tree, offset) { | ||
222 | name_ref | ||
223 | } else { | ||
224 | // Handle macro token cases | ||
225 | let token = match tree.token_at_offset(offset) { | ||
226 | TokenAtOffset::None => continue, | ||
227 | TokenAtOffset::Single(t) => t, | ||
228 | TokenAtOffset::Between(_, t) => t, | ||
229 | }; | ||
230 | let expanded = sema.descend_into_macros(token); | ||
231 | match ast::NameRef::cast(expanded.parent()) { | ||
232 | Some(name_ref) => name_ref, | ||
233 | _ => continue, | ||
234 | } | ||
235 | }; | ||
236 | |||
237 | // FIXME: reuse sb | ||
238 | // See https://github.com/rust-lang/rust/pull/68198#issuecomment-574269098 | ||
239 | |||
240 | if let Some(d) = classify_name_ref(&sema, &name_ref) { | ||
241 | let d = d.definition(); | ||
242 | if &d == self { | ||
243 | let kind = if is_record_lit_name_ref(&name_ref) | ||
244 | || is_call_expr_name_ref(&name_ref) | ||
245 | { | ||
246 | ReferenceKind::StructLiteral | ||
247 | } else { | ||
248 | ReferenceKind::Other | ||
249 | }; | ||
250 | |||
251 | let file_range = sema.original_range(name_ref.syntax()); | ||
252 | refs.push(Reference { | ||
253 | file_range, | ||
254 | kind, | ||
255 | access: reference_access(&d, &name_ref), | ||
256 | }); | ||
257 | } | ||
258 | } | ||
259 | } | ||
260 | } | ||
261 | refs | ||
262 | } | ||
263 | } | ||
264 | |||
265 | fn reference_access(def: &Definition, name_ref: &ast::NameRef) -> Option<ReferenceAccess> { | ||
266 | // Only Locals and Fields have accesses for now. | ||
267 | match def { | ||
268 | Definition::Local(_) | Definition::StructField(_) => {} | ||
269 | _ => return None, | ||
270 | }; | ||
271 | |||
272 | let mode = name_ref.syntax().ancestors().find_map(|node| { | ||
273 | match_ast! { | ||
274 | match (node) { | ||
275 | ast::BinExpr(expr) => { | ||
276 | if expr.op_kind()?.is_assignment() { | ||
277 | // If the variable or field ends on the LHS's end then it's a Write (covers fields and locals). | ||
278 | // FIXME: This is not terribly accurate. | ||
279 | if let Some(lhs) = expr.lhs() { | ||
280 | if lhs.syntax().text_range().end() == name_ref.syntax().text_range().end() { | ||
281 | return Some(ReferenceAccess::Write); | ||
282 | } | ||
283 | } | ||
284 | } | ||
285 | Some(ReferenceAccess::Read) | ||
286 | }, | ||
287 | _ => {None} | ||
288 | } | ||
289 | } | ||
290 | }); | ||
291 | |||
292 | // Default Locals and Fields to read | ||
293 | mode.or(Some(ReferenceAccess::Read)) | ||
294 | } | ||
295 | |||
296 | fn is_call_expr_name_ref(name_ref: &ast::NameRef) -> bool { | ||
297 | name_ref | ||
298 | .syntax() | ||
299 | .ancestors() | ||
300 | .find_map(ast::CallExpr::cast) | ||
301 | .and_then(|c| match c.expr()? { | ||
302 | ast::Expr::PathExpr(p) => { | ||
303 | Some(p.path()?.segment()?.name_ref().as_ref() == Some(name_ref)) | ||
304 | } | ||
305 | _ => None, | ||
306 | }) | ||
307 | .unwrap_or(false) | ||
308 | } | ||
309 | |||
310 | fn is_record_lit_name_ref(name_ref: &ast::NameRef) -> bool { | ||
311 | name_ref | ||
312 | .syntax() | ||
313 | .ancestors() | ||
314 | .find_map(ast::RecordLit::cast) | ||
315 | .and_then(|l| l.path()) | ||
316 | .and_then(|p| p.segment()) | ||
317 | .map(|p| p.name_ref().as_ref() == Some(name_ref)) | ||
318 | .unwrap_or(false) | ||
319 | } | ||