aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ssr/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ssr/src/lib.rs')
-rw-r--r--crates/ra_ssr/src/lib.rs226
1 files changed, 116 insertions, 110 deletions
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs
index cca4576ce..73abfecb2 100644
--- a/crates/ra_ssr/src/lib.rs
+++ b/crates/ra_ssr/src/lib.rs
@@ -4,44 +4,41 @@
4//! based on a template. 4//! based on a template.
5 5
6mod matching; 6mod matching;
7mod nester;
7mod parsing; 8mod parsing;
8mod replacing; 9mod replacing;
10mod resolving;
11mod search;
9#[macro_use] 12#[macro_use]
10mod errors; 13mod errors;
11#[cfg(test)] 14#[cfg(test)]
12mod tests; 15mod tests;
13 16
17use crate::errors::bail;
14pub use crate::errors::SsrError; 18pub use crate::errors::SsrError;
15pub use crate::matching::Match; 19pub use crate::matching::Match;
16use crate::matching::{record_match_fails_reasons_scope, MatchFailureReason}; 20use crate::matching::MatchFailureReason;
17use hir::Semantics; 21use hir::Semantics;
18use ra_db::{FileId, FileRange}; 22use ra_db::{FileId, FilePosition, FileRange};
19use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, TextRange}; 23use ra_ide_db::source_change::SourceFileEdit;
20use ra_text_edit::TextEdit; 24use ra_syntax::{ast, AstNode, SyntaxNode, TextRange};
25use resolving::ResolvedRule;
21use rustc_hash::FxHashMap; 26use rustc_hash::FxHashMap;
22 27
23// A structured search replace rule. Create by calling `parse` on a str. 28// A structured search replace rule. Create by calling `parse` on a str.
24#[derive(Debug)] 29#[derive(Debug)]
25pub struct SsrRule { 30pub struct SsrRule {
26 /// A structured pattern that we're searching for. 31 /// A structured pattern that we're searching for.
27 pattern: SsrPattern, 32 pattern: parsing::RawPattern,
28 /// What we'll replace it with. 33 /// What we'll replace it with.
29 template: parsing::SsrTemplate, 34 template: parsing::RawPattern,
35 parsed_rules: Vec<parsing::ParsedRule>,
30} 36}
31 37
32#[derive(Debug)] 38#[derive(Debug)]
33pub struct SsrPattern { 39pub struct SsrPattern {
34 raw: parsing::RawSearchPattern, 40 raw: parsing::RawPattern,
35 /// Placeholders keyed by the stand-in ident that we use in Rust source code. 41 parsed_rules: Vec<parsing::ParsedRule>,
36 placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>,
37 // We store our search pattern, parsed as each different kind of thing we can look for. As we
38 // traverse the AST, we get the appropriate one of these for the type of node we're on. For many
39 // search patterns, only some of these will be present.
40 expr: Option<SyntaxNode>,
41 type_ref: Option<SyntaxNode>,
42 item: Option<SyntaxNode>,
43 path: Option<SyntaxNode>,
44 pattern: Option<SyntaxNode>,
45} 42}
46 43
47#[derive(Debug, Default)] 44#[derive(Debug, Default)]
@@ -53,40 +50,105 @@ pub struct SsrMatches {
53pub struct MatchFinder<'db> { 50pub struct MatchFinder<'db> {
54 /// Our source of information about the user's code. 51 /// Our source of information about the user's code.
55 sema: Semantics<'db, ra_ide_db::RootDatabase>, 52 sema: Semantics<'db, ra_ide_db::RootDatabase>,
56 rules: Vec<SsrRule>, 53 rules: Vec<ResolvedRule>,
54 resolution_scope: resolving::ResolutionScope<'db>,
55 restrict_ranges: Vec<FileRange>,
57} 56}
58 57
59impl<'db> MatchFinder<'db> { 58impl<'db> MatchFinder<'db> {
60 pub fn new(db: &'db ra_ide_db::RootDatabase) -> MatchFinder<'db> { 59 /// Constructs a new instance where names will be looked up as if they appeared at
61 MatchFinder { sema: Semantics::new(db), rules: Vec::new() } 60 /// `lookup_context`.
61 pub fn in_context(
62 db: &'db ra_ide_db::RootDatabase,
63 lookup_context: FilePosition,
64 mut restrict_ranges: Vec<FileRange>,
65 ) -> MatchFinder<'db> {
66 restrict_ranges.retain(|range| !range.range.is_empty());
67 let sema = Semantics::new(db);
68 let resolution_scope = resolving::ResolutionScope::new(&sema, lookup_context);
69 MatchFinder {
70 sema: Semantics::new(db),
71 rules: Vec::new(),
72 resolution_scope,
73 restrict_ranges,
74 }
62 } 75 }
63 76
64 pub fn add_rule(&mut self, rule: SsrRule) { 77 /// Constructs an instance using the start of the first file in `db` as the lookup context.
65 self.rules.push(rule); 78 pub fn at_first_file(db: &'db ra_ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> {
79 use ra_db::SourceDatabaseExt;
80 use ra_ide_db::symbol_index::SymbolsDatabase;
81 if let Some(first_file_id) = db
82 .local_roots()
83 .iter()
84 .next()
85 .and_then(|root| db.source_root(root.clone()).iter().next())
86 {
87 Ok(MatchFinder::in_context(
88 db,
89 FilePosition { file_id: first_file_id, offset: 0.into() },
90 vec![],
91 ))
92 } else {
93 bail!("No files to search");
94 }
66 } 95 }
67 96
68 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you 97 /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
69 /// intend to do replacement, use `add_rule` instead. 98 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
70 pub fn add_search_pattern(&mut self, pattern: SsrPattern) { 99 /// match to it.
71 self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() }) 100 pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
101 for parsed_rule in rule.parsed_rules {
102 self.rules.push(ResolvedRule::new(
103 parsed_rule,
104 &self.resolution_scope,
105 self.rules.len(),
106 )?);
107 }
108 Ok(())
72 } 109 }
73 110
74 pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { 111 /// Finds matches for all added rules and returns edits for all found matches.
75 let matches = self.find_matches_in_file(file_id); 112 pub fn edits(&self) -> Vec<SourceFileEdit> {
76 if matches.matches.is_empty() { 113 use ra_db::SourceDatabaseExt;
77 None 114 let mut matches_by_file = FxHashMap::default();
78 } else { 115 for m in self.matches().matches {
79 use ra_db::SourceDatabaseExt; 116 matches_by_file
80 Some(replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id))) 117 .entry(m.range.file_id)
118 .or_insert_with(|| SsrMatches::default())
119 .matches
120 .push(m);
121 }
122 let mut edits = vec![];
123 for (file_id, matches) in matches_by_file {
124 let edit =
125 replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id), &self.rules);
126 edits.push(SourceFileEdit { file_id, edit });
81 } 127 }
128 edits
82 } 129 }
83 130
84 pub fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { 131 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
85 let file = self.sema.parse(file_id); 132 /// intend to do replacement, use `add_rule` instead.
86 let code = file.syntax(); 133 pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> {
87 let mut matches = SsrMatches::default(); 134 for parsed_rule in pattern.parsed_rules {
88 self.find_matches(code, &None, &mut matches); 135 self.rules.push(ResolvedRule::new(
89 matches 136 parsed_rule,
137 &self.resolution_scope,
138 self.rules.len(),
139 )?);
140 }
141 Ok(())
142 }
143
144 /// Returns matches for all added rules.
145 pub fn matches(&self) -> SsrMatches {
146 let mut matches = Vec::new();
147 let mut usage_cache = search::UsageCache::default();
148 for rule in &self.rules {
149 self.find_matches_for_rule(rule, &mut usage_cache, &mut matches);
150 }
151 nester::nest_and_remove_collisions(matches, &self.sema)
90 } 152 }
91 153
92 /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match 154 /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match
@@ -115,53 +177,6 @@ impl<'db> MatchFinder<'db> {
115 res 177 res
116 } 178 }
117 179
118 fn find_matches(
119 &self,
120 code: &SyntaxNode,
121 restrict_range: &Option<FileRange>,
122 matches_out: &mut SsrMatches,
123 ) {
124 for rule in &self.rules {
125 if let Ok(mut m) = matching::get_match(false, rule, &code, restrict_range, &self.sema) {
126 // Continue searching in each of our placeholders.
127 for placeholder_value in m.placeholder_values.values_mut() {
128 if let Some(placeholder_node) = &placeholder_value.node {
129 // Don't search our placeholder if it's the entire matched node, otherwise we'd
130 // find the same match over and over until we got a stack overflow.
131 if placeholder_node != code {
132 self.find_matches(
133 placeholder_node,
134 restrict_range,
135 &mut placeholder_value.inner_matches,
136 );
137 }
138 }
139 }
140 matches_out.matches.push(m);
141 return;
142 }
143 }
144 // If we've got a macro call, we already tried matching it pre-expansion, which is the only
145 // way to match the whole macro, now try expanding it and matching the expansion.
146 if let Some(macro_call) = ast::MacroCall::cast(code.clone()) {
147 if let Some(expanded) = self.sema.expand(&macro_call) {
148 if let Some(tt) = macro_call.token_tree() {
149 // When matching within a macro expansion, we only want to allow matches of
150 // nodes that originated entirely from within the token tree of the macro call.
151 // i.e. we don't want to match something that came from the macro itself.
152 self.find_matches(
153 &expanded,
154 &Some(self.sema.original_range(tt.syntax())),
155 matches_out,
156 );
157 }
158 }
159 }
160 for child in code.children() {
161 self.find_matches(&child, restrict_range, matches_out);
162 }
163 }
164
165 fn output_debug_for_nodes_at_range( 180 fn output_debug_for_nodes_at_range(
166 &self, 181 &self,
167 node: &SyntaxNode, 182 node: &SyntaxNode,
@@ -177,8 +192,17 @@ impl<'db> MatchFinder<'db> {
177 } 192 }
178 if node_range.range == range.range { 193 if node_range.range == range.range {
179 for rule in &self.rules { 194 for rule in &self.rules {
180 let pattern = 195 // For now we ignore rules that have a different kind than our node, otherwise
181 rule.pattern.tree_for_kind_with_reason(node.kind()).map(|p| p.clone()); 196 // we get lots of noise. If at some point we add support for restricting rules
197 // to a particular kind of thing (e.g. only match type references), then we can
198 // relax this. We special-case expressions, since function calls can match
199 // method calls.
200 if rule.pattern.node.kind() != node.kind()
201 && !(ast::Expr::can_cast(rule.pattern.node.kind())
202 && ast::Expr::can_cast(node.kind()))
203 {
204 continue;
205 }
182 out.push(MatchDebugInfo { 206 out.push(MatchDebugInfo {
183 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) 207 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
184 .map_err(|e| MatchFailureReason { 208 .map_err(|e| MatchFailureReason {
@@ -186,7 +210,7 @@ impl<'db> MatchFinder<'db> {
186 "Match failed, but no reason was given".to_owned() 210 "Match failed, but no reason was given".to_owned()
187 }), 211 }),
188 }), 212 }),
189 pattern, 213 pattern: rule.pattern.node.clone(),
190 node: node.clone(), 214 node: node.clone(),
191 }); 215 });
192 } 216 }
@@ -209,9 +233,8 @@ impl<'db> MatchFinder<'db> {
209 233
210pub struct MatchDebugInfo { 234pub struct MatchDebugInfo {
211 node: SyntaxNode, 235 node: SyntaxNode,
212 /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item, 236 /// Our search pattern parsed as an expression or item, etc
213 /// etc. Will be absent if the pattern can't be parsed as that kind. 237 pattern: SyntaxNode,
214 pattern: Result<SyntaxNode, MatchFailureReason>,
215 matched: Result<Match, MatchFailureReason>, 238 matched: Result<Match, MatchFailureReason>,
216} 239}
217 240
@@ -228,29 +251,12 @@ impl std::fmt::Debug for MatchDebugInfo {
228 self.node 251 self.node
229 )?; 252 )?;
230 writeln!(f, "========= PATTERN ==========")?; 253 writeln!(f, "========= PATTERN ==========")?;
231 match &self.pattern { 254 writeln!(f, "{:#?}", self.pattern)?;
232 Ok(pattern) => {
233 writeln!(f, "{:#?}", pattern)?;
234 }
235 Err(err) => {
236 writeln!(f, "{}", err.reason)?;
237 }
238 }
239 writeln!(f, "============================")?; 255 writeln!(f, "============================")?;
240 Ok(()) 256 Ok(())
241 } 257 }
242} 258}
243 259
244impl SsrPattern {
245 fn tree_for_kind_with_reason(
246 &self,
247 kind: SyntaxKind,
248 ) -> Result<&SyntaxNode, MatchFailureReason> {
249 record_match_fails_reasons_scope(true, || self.tree_for_kind(kind))
250 .map_err(|e| MatchFailureReason { reason: e.reason.unwrap() })
251 }
252}
253
254impl SsrMatches { 260impl SsrMatches {
255 /// Returns `self` with any nested matches removed and made into top-level matches. 261 /// Returns `self` with any nested matches removed and made into top-level matches.
256 pub fn flattened(self) -> SsrMatches { 262 pub fn flattened(self) -> SsrMatches {