ide_ssr/
search.rs

1//! Searching for matches.
2
3use crate::{
4    Match, MatchFinder, matching,
5    resolving::{ResolvedPath, ResolvedPattern, ResolvedRule},
6};
7use hir::FileRange;
8use ide_db::{
9    FileId, FxHashSet,
10    defs::Definition,
11    search::{SearchScope, UsageSearchResult},
12    symbol_index::LocalRoots,
13};
14use syntax::{AstNode, SyntaxKind, SyntaxNode, ast};
15
16/// A cache for the results of find_usages. This is for when we have multiple patterns that have the
17/// same path. e.g. if the pattern was `foo::Bar` that can parse as a path, an expression, a type
18/// and as a pattern. In each, the usages of `foo::Bar` are the same and we'd like to avoid finding
19/// them more than once.
20#[derive(Default)]
21pub(crate) struct UsageCache {
22    usages: Vec<(Definition, UsageSearchResult)>,
23}
24
25impl<'db> MatchFinder<'db> {
26    /// Adds all matches for `rule` to `matches_out`. Matches may overlap in ways that make
27    /// replacement impossible, so further processing is required in order to properly nest matches
28    /// and remove overlapping matches. This is done in the `nesting` module.
29    pub(crate) fn find_matches_for_rule(
30        &self,
31        rule: &ResolvedRule<'db>,
32        usage_cache: &mut UsageCache,
33        matches_out: &mut Vec<Match>,
34    ) {
35        if rule.pattern.contains_self {
36            // If the pattern contains `self` we restrict the scope of the search to just the
37            // current method. No other method can reference the same `self`. This makes the
38            // behavior of `self` consistent with other variables.
39            if let Some(current_function) = self.resolution_scope.current_function() {
40                self.slow_scan_node(&current_function, rule, &None, matches_out);
41            }
42            return;
43        }
44        if pick_path_for_usages(&rule.pattern).is_none() {
45            self.slow_scan(rule, matches_out);
46            return;
47        }
48        self.find_matches_for_pattern_tree(rule, &rule.pattern, usage_cache, matches_out);
49    }
50
51    fn find_matches_for_pattern_tree(
52        &self,
53        rule: &ResolvedRule<'db>,
54        pattern: &ResolvedPattern<'db>,
55        usage_cache: &mut UsageCache,
56        matches_out: &mut Vec<Match>,
57    ) {
58        if let Some(resolved_path) = pick_path_for_usages(pattern) {
59            let definition: Definition = resolved_path.resolution.into();
60            for file_range in self.find_usages(usage_cache, definition).file_ranges() {
61                for node_to_match in self.find_nodes_to_match(resolved_path, file_range) {
62                    if !is_search_permitted_ancestors(&node_to_match) {
63                        cov_mark::hit!(use_declaration_with_braces);
64                        continue;
65                    }
66                    self.try_add_match(rule, &node_to_match, &None, matches_out);
67                }
68            }
69        }
70    }
71
72    fn find_nodes_to_match(
73        &self,
74        resolved_path: &ResolvedPath,
75        file_range: FileRange,
76    ) -> Vec<SyntaxNode> {
77        let file = self.sema.parse(file_range.file_id);
78        let depth = resolved_path.depth as usize;
79        let offset = file_range.range.start();
80
81        let mut paths = self
82            .sema
83            .find_nodes_at_offset_with_descend::<ast::Path>(file.syntax(), offset)
84            .peekable();
85
86        if paths.peek().is_some() {
87            paths
88                .filter_map(|path| {
89                    self.sema.ancestors_with_macros(path.syntax().clone()).nth(depth)
90                })
91                .collect::<Vec<_>>()
92        } else {
93            self.sema
94                .find_nodes_at_offset_with_descend::<ast::MethodCallExpr>(file.syntax(), offset)
95                .filter_map(|path| {
96                    // If the pattern contained a path and we found a reference to that path that wasn't
97                    // itself a path, but was a method call, then we need to adjust how far up to try
98                    // matching by how deep the path was within a CallExpr. The structure would have been
99                    // CallExpr, PathExpr, Path - i.e. a depth offset of 2. We don't need to check if the
100                    // path was part of a CallExpr because if it wasn't then all that will happen is we'll
101                    // fail to match, which is the desired behavior.
102                    const PATH_DEPTH_IN_CALL_EXPR: usize = 2;
103                    if depth < PATH_DEPTH_IN_CALL_EXPR {
104                        return None;
105                    }
106                    self.sema
107                        .ancestors_with_macros(path.syntax().clone())
108                        .nth(depth - PATH_DEPTH_IN_CALL_EXPR)
109                })
110                .collect::<Vec<_>>()
111        }
112    }
113
114    fn find_usages<'a>(
115        &self,
116        usage_cache: &'a mut UsageCache,
117        definition: Definition,
118    ) -> &'a UsageSearchResult {
119        // Logically if a lookup succeeds we should just return it. Unfortunately returning it would
120        // extend the lifetime of the borrow, then we wouldn't be able to do the insertion on a
121        // cache miss. This is a limitation of NLL and is fixed with Polonius. For now we do two
122        // lookups in the case of a cache hit.
123        if usage_cache.find(&definition).is_none() {
124            let usages = definition.usages(&self.sema).in_scope(&self.search_scope()).all();
125            usage_cache.usages.push((definition, usages));
126            return &usage_cache.usages.last().unwrap().1;
127        }
128        usage_cache.find(&definition).unwrap()
129    }
130
131    /// Returns the scope within which we want to search. We don't want un unrestricted search
132    /// scope, since we don't want to find references in external dependencies.
133    fn search_scope(&self) -> SearchScope {
134        // FIXME: We should ideally have a test that checks that we edit local roots and not library
135        // roots. This probably would require some changes to fixtures, since currently everything
136        // seems to get put into a single source root.
137        let mut files = Vec::new();
138        self.search_files_do(|file_id| {
139            files.push(self.sema.attach_first_edition(file_id));
140        });
141        SearchScope::files(&files)
142    }
143
144    fn slow_scan(&self, rule: &ResolvedRule<'db>, matches_out: &mut Vec<Match>) {
145        self.search_files_do(|file_id| {
146            let file = self.sema.parse_guess_edition(file_id);
147            let code = file.syntax();
148            self.slow_scan_node(code, rule, &None, matches_out);
149        })
150    }
151
152    fn search_files_do(&self, mut callback: impl FnMut(FileId)) {
153        if self.restrict_ranges.is_empty() {
154            // Unrestricted search.
155            use ide_db::base_db::SourceDatabase;
156            for &root in LocalRoots::get(self.sema.db).roots(self.sema.db).iter() {
157                let sr = self.sema.db.source_root(root).source_root(self.sema.db);
158                for file_id in sr.iter() {
159                    callback(file_id);
160                }
161            }
162        } else {
163            // Search is restricted, deduplicate file IDs (generally only one).
164            let mut files = FxHashSet::default();
165            for range in &self.restrict_ranges {
166                if files.insert(range.file_id) {
167                    callback(range.file_id);
168                }
169            }
170        }
171    }
172
173    fn slow_scan_node(
174        &self,
175        code: &SyntaxNode,
176        rule: &ResolvedRule<'db>,
177        restrict_range: &Option<FileRange>,
178        matches_out: &mut Vec<Match>,
179    ) {
180        if !is_search_permitted(code) {
181            return;
182        }
183        self.try_add_match(rule, code, restrict_range, matches_out);
184        // If we've got a macro call, we already tried matching it pre-expansion, which is the only
185        // way to match the whole macro, now try expanding it and matching the expansion.
186        if let Some(macro_call) = ast::MacroCall::cast(code.clone())
187            && let Some(expanded) = self.sema.expand_macro_call(&macro_call)
188            && let Some(tt) = macro_call.token_tree()
189        {
190            // When matching within a macro expansion, we only want to allow matches of
191            // nodes that originated entirely from within the token tree of the macro call.
192            // i.e. we don't want to match something that came from the macro itself.
193            if let Some(range) = self.sema.original_range_opt(tt.syntax()) {
194                self.slow_scan_node(&expanded.value, rule, &Some(range), matches_out);
195            }
196        }
197        for child in code.children() {
198            self.slow_scan_node(&child, rule, restrict_range, matches_out);
199        }
200    }
201
202    fn try_add_match(
203        &self,
204        rule: &ResolvedRule<'db>,
205        code: &SyntaxNode,
206        restrict_range: &Option<FileRange>,
207        matches_out: &mut Vec<Match>,
208    ) {
209        if !self.within_range_restrictions(code) {
210            cov_mark::hit!(replace_nonpath_within_selection);
211            return;
212        }
213        if let Ok(m) = matching::get_match(false, rule, code, restrict_range, &self.sema) {
214            matches_out.push(m);
215        }
216    }
217
218    /// Returns whether `code` is within one of our range restrictions if we have any. No range
219    /// restrictions is considered unrestricted and always returns true.
220    fn within_range_restrictions(&self, code: &SyntaxNode) -> bool {
221        if self.restrict_ranges.is_empty() {
222            // There is no range restriction.
223            return true;
224        }
225        let Some(node_range) = self.sema.original_range_opt(code) else { return false };
226        for range in &self.restrict_ranges {
227            if range.file_id == node_range.file_id.file_id(self.sema.db)
228                && range.range.contains_range(node_range.range)
229            {
230                return true;
231            }
232        }
233        false
234    }
235}
236
237/// Returns whether we support matching within `node` and all of its ancestors.
238fn is_search_permitted_ancestors(node: &SyntaxNode) -> bool {
239    if let Some(parent) = node.parent()
240        && !is_search_permitted_ancestors(&parent)
241    {
242        return false;
243    }
244    is_search_permitted(node)
245}
246
247/// Returns whether we support matching within this kind of node.
248fn is_search_permitted(node: &SyntaxNode) -> bool {
249    // FIXME: Properly handle use declarations. At the moment, if our search pattern is `foo::bar`
250    // and the code is `use foo::{baz, bar}`, we'll match `bar`, since it resolves to `foo::bar`.
251    // However we'll then replace just the part we matched `bar`. We probably need to instead remove
252    // `bar` and insert a new use declaration.
253    node.kind() != SyntaxKind::USE
254}
255
256impl UsageCache {
257    fn find(&mut self, definition: &Definition) -> Option<&UsageSearchResult> {
258        // We expect a very small number of cache entries (generally 1), so a linear scan should be
259        // fast enough and avoids the need to implement Hash for Definition.
260        for (d, refs) in &self.usages {
261            if d == definition {
262                return Some(refs);
263            }
264        }
265        None
266    }
267}
268
269/// Returns a path that's suitable for path resolution. We exclude builtin types, since they aren't
270/// something that we can find references to. We then somewhat arbitrarily pick the path that is the
271/// longest as this is hopefully more likely to be less common, making it faster to find.
272fn pick_path_for_usages<'a>(pattern: &'a ResolvedPattern<'_>) -> Option<&'a ResolvedPath> {
273    // FIXME: Take the scope of the resolved path into account. e.g. if there are any paths that are
274    // private to the current module, then we definitely would want to pick them over say a path
275    // from std. Possibly we should go further than this and intersect the search scopes for all
276    // resolved paths then search only in that scope.
277    pattern
278        .resolved_paths
279        .iter()
280        .filter(|(_, p)| {
281            !matches!(p.resolution, hir::PathResolution::Def(hir::ModuleDef::BuiltinType(_)))
282        })
283        .map(|(node, resolved)| (node.text().len(), resolved))
284        .max_by(|(a, _), (b, _)| a.cmp(b))
285        .map(|(_, resolved)| resolved)
286}