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.rs233
1 files changed, 123 insertions, 110 deletions
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs
index cca4576ce..2fb326b45 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,112 @@ 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 scope: hir::SemanticsScope<'db>,
55 hygiene: hir::Hygiene,
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 ) -> MatchFinder<'db> {
65 let sema = Semantics::new(db);
66 let file = sema.parse(lookup_context.file_id);
67 // Find a node at the requested position, falling back to the whole file.
68 let node = file
69 .syntax()
70 .token_at_offset(lookup_context.offset)
71 .left_biased()
72 .map(|token| token.parent())
73 .unwrap_or_else(|| file.syntax().clone());
74 let scope = sema.scope(&node);
75 MatchFinder {
76 sema: Semantics::new(db),
77 rules: Vec::new(),
78 scope,
79 hygiene: hir::Hygiene::new(db, lookup_context.file_id.into()),
80 }
62 } 81 }
63 82
64 pub fn add_rule(&mut self, rule: SsrRule) { 83 /// Constructs an instance using the start of the first file in `db` as the lookup context.
65 self.rules.push(rule); 84 pub fn at_first_file(db: &'db ra_ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> {
85 use ra_db::SourceDatabaseExt;
86 use ra_ide_db::symbol_index::SymbolsDatabase;
87 if let Some(first_file_id) = db
88 .local_roots()
89 .iter()
90 .next()
91 .and_then(|root| db.source_root(root.clone()).iter().next())
92 {
93 Ok(MatchFinder::in_context(
94 db,
95 FilePosition { file_id: first_file_id, offset: 0.into() },
96 ))
97 } else {
98 bail!("No files to search");
99 }
66 } 100 }
67 101
68 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you 102 /// 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. 103 /// 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) { 104 /// match to it.
71 self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() }) 105 pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
106 for parsed_rule in rule.parsed_rules {
107 self.rules.push(ResolvedRule::new(
108 parsed_rule,
109 &self.scope,
110 &self.hygiene,
111 self.rules.len(),
112 )?);
113 }
114 Ok(())
72 } 115 }
73 116
74 pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { 117 /// Finds matches for all added rules and returns edits for all found matches.
75 let matches = self.find_matches_in_file(file_id); 118 pub fn edits(&self) -> Vec<SourceFileEdit> {
76 if matches.matches.is_empty() { 119 use ra_db::SourceDatabaseExt;
77 None 120 let mut matches_by_file = FxHashMap::default();
78 } else { 121 for m in self.matches().matches {
79 use ra_db::SourceDatabaseExt; 122 matches_by_file
80 Some(replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id))) 123 .entry(m.range.file_id)
124 .or_insert_with(|| SsrMatches::default())
125 .matches
126 .push(m);
127 }
128 let mut edits = vec![];
129 for (file_id, matches) in matches_by_file {
130 let edit =
131 replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id), &self.rules);
132 edits.push(SourceFileEdit { file_id, edit });
81 } 133 }
134 edits
82 } 135 }
83 136
84 pub fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { 137 /// 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); 138 /// intend to do replacement, use `add_rule` instead.
86 let code = file.syntax(); 139 pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> {
87 let mut matches = SsrMatches::default(); 140 for parsed_rule in pattern.parsed_rules {
88 self.find_matches(code, &None, &mut matches); 141 self.rules.push(ResolvedRule::new(
89 matches 142 parsed_rule,
143 &self.scope,
144 &self.hygiene,
145 self.rules.len(),
146 )?);
147 }
148 Ok(())
149 }
150
151 /// Returns matches for all added rules.
152 pub fn matches(&self) -> SsrMatches {
153 let mut matches = Vec::new();
154 let mut usage_cache = search::UsageCache::default();
155 for rule in &self.rules {
156 self.find_matches_for_rule(rule, &mut usage_cache, &mut matches);
157 }
158 nester::nest_and_remove_collisions(matches, &self.sema)
90 } 159 }
91 160
92 /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match 161 /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match
@@ -115,53 +184,6 @@ impl<'db> MatchFinder<'db> {
115 res 184 res
116 } 185 }
117 186
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( 187 fn output_debug_for_nodes_at_range(
166 &self, 188 &self,
167 node: &SyntaxNode, 189 node: &SyntaxNode,
@@ -177,8 +199,17 @@ impl<'db> MatchFinder<'db> {
177 } 199 }
178 if node_range.range == range.range { 200 if node_range.range == range.range {
179 for rule in &self.rules { 201 for rule in &self.rules {
180 let pattern = 202 // 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()); 203 // we get lots of noise. If at some point we add support for restricting rules
204 // to a particular kind of thing (e.g. only match type references), then we can
205 // relax this. We special-case expressions, since function calls can match
206 // method calls.
207 if rule.pattern.node.kind() != node.kind()
208 && !(ast::Expr::can_cast(rule.pattern.node.kind())
209 && ast::Expr::can_cast(node.kind()))
210 {
211 continue;
212 }
182 out.push(MatchDebugInfo { 213 out.push(MatchDebugInfo {
183 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) 214 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
184 .map_err(|e| MatchFailureReason { 215 .map_err(|e| MatchFailureReason {
@@ -186,7 +217,7 @@ impl<'db> MatchFinder<'db> {
186 "Match failed, but no reason was given".to_owned() 217 "Match failed, but no reason was given".to_owned()
187 }), 218 }),
188 }), 219 }),
189 pattern, 220 pattern: rule.pattern.node.clone(),
190 node: node.clone(), 221 node: node.clone(),
191 }); 222 });
192 } 223 }
@@ -209,9 +240,8 @@ impl<'db> MatchFinder<'db> {
209 240
210pub struct MatchDebugInfo { 241pub struct MatchDebugInfo {
211 node: SyntaxNode, 242 node: SyntaxNode,
212 /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item, 243 /// 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. 244 pattern: SyntaxNode,
214 pattern: Result<SyntaxNode, MatchFailureReason>,
215 matched: Result<Match, MatchFailureReason>, 245 matched: Result<Match, MatchFailureReason>,
216} 246}
217 247
@@ -228,29 +258,12 @@ impl std::fmt::Debug for MatchDebugInfo {
228 self.node 258 self.node
229 )?; 259 )?;
230 writeln!(f, "========= PATTERN ==========")?; 260 writeln!(f, "========= PATTERN ==========")?;
231 match &self.pattern { 261 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, "============================")?; 262 writeln!(f, "============================")?;
240 Ok(()) 263 Ok(())
241 } 264 }
242} 265}
243 266
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 { 267impl SsrMatches {
255 /// Returns `self` with any nested matches removed and made into top-level matches. 268 /// Returns `self` with any nested matches removed and made into top-level matches.
256 pub fn flattened(self) -> SsrMatches { 269 pub fn flattened(self) -> SsrMatches {