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.rs308
1 files changed, 230 insertions, 78 deletions
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs
index e148f4564..73abfecb2 100644
--- a/crates/ra_ssr/src/lib.rs
+++ b/crates/ra_ssr/src/lib.rs
@@ -4,136 +4,288 @@
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;
12#[macro_use]
13mod errors;
9#[cfg(test)] 14#[cfg(test)]
10mod tests; 15mod tests;
11 16
12use crate::matching::Match; 17use crate::errors::bail;
18pub use crate::errors::SsrError;
19pub use crate::matching::Match;
20use crate::matching::MatchFailureReason;
13use hir::Semantics; 21use hir::Semantics;
14use ra_db::{FileId, FileRange}; 22use ra_db::{FileId, FilePosition, FileRange};
15use ra_syntax::{ast, AstNode, SmolStr, SyntaxNode}; 23use ra_ide_db::source_change::SourceFileEdit;
16use ra_text_edit::TextEdit; 24use ra_syntax::{ast, AstNode, SyntaxNode, TextRange};
25use resolving::ResolvedRule;
17use rustc_hash::FxHashMap; 26use rustc_hash::FxHashMap;
18 27
19// 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.
20#[derive(Debug)] 29#[derive(Debug)]
21pub struct SsrRule { 30pub struct SsrRule {
22 /// A structured pattern that we're searching for. 31 /// A structured pattern that we're searching for.
23 pattern: SsrPattern, 32 pattern: parsing::RawPattern,
24 /// What we'll replace it with. 33 /// What we'll replace it with.
25 template: parsing::SsrTemplate, 34 template: parsing::RawPattern,
35 parsed_rules: Vec<parsing::ParsedRule>,
26} 36}
27 37
28#[derive(Debug)] 38#[derive(Debug)]
29struct SsrPattern { 39pub struct SsrPattern {
30 raw: parsing::RawSearchPattern, 40 raw: parsing::RawPattern,
31 /// Placeholders keyed by the stand-in ident that we use in Rust source code. 41 parsed_rules: Vec<parsing::ParsedRule>,
32 placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>,
33 // We store our search pattern, parsed as each different kind of thing we can look for. As we
34 // traverse the AST, we get the appropriate one of these for the type of node we're on. For many
35 // search patterns, only some of these will be present.
36 expr: Option<SyntaxNode>,
37 type_ref: Option<SyntaxNode>,
38 item: Option<SyntaxNode>,
39 path: Option<SyntaxNode>,
40 pattern: Option<SyntaxNode>,
41} 42}
42 43
43#[derive(Debug, PartialEq)]
44pub struct SsrError(String);
45
46#[derive(Debug, Default)] 44#[derive(Debug, Default)]
47pub struct SsrMatches { 45pub struct SsrMatches {
48 matches: Vec<Match>, 46 pub matches: Vec<Match>,
49} 47}
50 48
51/// Searches a crate for pattern matches and possibly replaces them with something else. 49/// Searches a crate for pattern matches and possibly replaces them with something else.
52pub struct MatchFinder<'db> { 50pub struct MatchFinder<'db> {
53 /// Our source of information about the user's code. 51 /// Our source of information about the user's code.
54 sema: Semantics<'db, ra_ide_db::RootDatabase>, 52 sema: Semantics<'db, ra_ide_db::RootDatabase>,
55 rules: Vec<SsrRule>, 53 rules: Vec<ResolvedRule>,
54 resolution_scope: resolving::ResolutionScope<'db>,
55 restrict_ranges: Vec<FileRange>,
56} 56}
57 57
58impl<'db> MatchFinder<'db> { 58impl<'db> MatchFinder<'db> {
59 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
60 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 }
61 } 75 }
62 76
63 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.
64 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 }
65 } 95 }
66 96
67 pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { 97 /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
68 let matches = self.find_matches_in_file(file_id); 98 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
69 if matches.matches.is_empty() { 99 /// match to it.
70 None 100 pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
71 } else { 101 for parsed_rule in rule.parsed_rules {
72 use ra_db::SourceDatabaseExt; 102 self.rules.push(ResolvedRule::new(
73 Some(replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id))) 103 parsed_rule,
104 &self.resolution_scope,
105 self.rules.len(),
106 )?);
74 } 107 }
108 Ok(())
75 } 109 }
76 110
77 fn find_matches_in_file(&self, file_id: FileId) -> SsrMatches { 111 /// Finds matches for all added rules and returns edits for all found matches.
112 pub fn edits(&self) -> Vec<SourceFileEdit> {
113 use ra_db::SourceDatabaseExt;
114 let mut matches_by_file = FxHashMap::default();
115 for m in self.matches().matches {
116 matches_by_file
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 });
127 }
128 edits
129 }
130
131 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
132 /// intend to do replacement, use `add_rule` instead.
133 pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> {
134 for parsed_rule in pattern.parsed_rules {
135 self.rules.push(ResolvedRule::new(
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)
152 }
153
154 /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match
155 /// them, while recording reasons why they don't match. This API is useful for command
156 /// line-based debugging where providing a range is difficult.
157 pub fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec<MatchDebugInfo> {
158 use ra_db::SourceDatabaseExt;
78 let file = self.sema.parse(file_id); 159 let file = self.sema.parse(file_id);
79 let code = file.syntax(); 160 let mut res = Vec::new();
80 let mut matches = SsrMatches::default(); 161 let file_text = self.sema.db.file_text(file_id);
81 self.find_matches(code, &None, &mut matches); 162 let mut remaining_text = file_text.as_str();
82 matches 163 let mut base = 0;
164 let len = snippet.len() as u32;
165 while let Some(offset) = remaining_text.find(snippet) {
166 let start = base + offset as u32;
167 let end = start + len;
168 self.output_debug_for_nodes_at_range(
169 file.syntax(),
170 FileRange { file_id, range: TextRange::new(start.into(), end.into()) },
171 &None,
172 &mut res,
173 );
174 remaining_text = &remaining_text[offset + snippet.len()..];
175 base = end;
176 }
177 res
83 } 178 }
84 179
85 fn find_matches( 180 fn output_debug_for_nodes_at_range(
86 &self, 181 &self,
87 code: &SyntaxNode, 182 node: &SyntaxNode,
183 range: FileRange,
88 restrict_range: &Option<FileRange>, 184 restrict_range: &Option<FileRange>,
89 matches_out: &mut SsrMatches, 185 out: &mut Vec<MatchDebugInfo>,
90 ) { 186 ) {
91 for rule in &self.rules { 187 for node in node.children() {
92 if let Ok(mut m) = matching::get_match(false, rule, &code, restrict_range, &self.sema) { 188 let node_range = self.sema.original_range(&node);
93 // Continue searching in each of our placeholders. 189 if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range)
94 for placeholder_value in m.placeholder_values.values_mut() { 190 {
95 if let Some(placeholder_node) = &placeholder_value.node { 191 continue;
96 // Don't search our placeholder if it's the entire matched node, otherwise we'd 192 }
97 // find the same match over and over until we got a stack overflow. 193 if node_range.range == range.range {
98 if placeholder_node != code { 194 for rule in &self.rules {
99 self.find_matches( 195 // For now we ignore rules that have a different kind than our node, otherwise
100 placeholder_node, 196 // we get lots of noise. If at some point we add support for restricting rules
101 restrict_range, 197 // to a particular kind of thing (e.g. only match type references), then we can
102 &mut placeholder_value.inner_matches, 198 // relax this. We special-case expressions, since function calls can match
103 ); 199 // method calls.
104 } 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;
105 } 205 }
206 out.push(MatchDebugInfo {
207 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
208 .map_err(|e| MatchFailureReason {
209 reason: e.reason.unwrap_or_else(|| {
210 "Match failed, but no reason was given".to_owned()
211 }),
212 }),
213 pattern: rule.pattern.node.clone(),
214 node: node.clone(),
215 });
106 } 216 }
107 matches_out.matches.push(m); 217 } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) {
108 return; 218 if let Some(expanded) = self.sema.expand(&macro_call) {
109 } 219 if let Some(tt) = macro_call.token_tree() {
110 } 220 self.output_debug_for_nodes_at_range(
111 // If we've got a macro call, we already tried matching it pre-expansion, which is the only 221 &expanded,
112 // way to match the whole macro, now try expanding it and matching the expansion. 222 range,
113 if let Some(macro_call) = ast::MacroCall::cast(code.clone()) { 223 &Some(self.sema.original_range(tt.syntax())),
114 if let Some(expanded) = self.sema.expand(&macro_call) { 224 out,
115 if let Some(tt) = macro_call.token_tree() { 225 );
116 // When matching within a macro expansion, we only want to allow matches of 226 }
117 // nodes that originated entirely from within the token tree of the macro call.
118 // i.e. we don't want to match something that came from the macro itself.
119 self.find_matches(
120 &expanded,
121 &Some(self.sema.original_range(tt.syntax())),
122 matches_out,
123 );
124 } 227 }
125 } 228 }
229 self.output_debug_for_nodes_at_range(&node, range, restrict_range, out);
126 } 230 }
127 for child in code.children() { 231 }
128 self.find_matches(&child, restrict_range, matches_out); 232}
233
234pub struct MatchDebugInfo {
235 node: SyntaxNode,
236 /// Our search pattern parsed as an expression or item, etc
237 pattern: SyntaxNode,
238 matched: Result<Match, MatchFailureReason>,
239}
240
241impl std::fmt::Debug for MatchDebugInfo {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 match &self.matched {
244 Ok(_) => writeln!(f, "Node matched")?,
245 Err(reason) => writeln!(f, "Node failed to match because: {}", reason.reason)?,
129 } 246 }
247 writeln!(
248 f,
249 "============ AST ===========\n\
250 {:#?}",
251 self.node
252 )?;
253 writeln!(f, "========= PATTERN ==========")?;
254 writeln!(f, "{:#?}", self.pattern)?;
255 writeln!(f, "============================")?;
256 Ok(())
130 } 257 }
131} 258}
132 259
133impl std::fmt::Display for SsrError { 260impl SsrMatches {
134 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 261 /// Returns `self` with any nested matches removed and made into top-level matches.
135 write!(f, "Parse error: {}", self.0) 262 pub fn flattened(self) -> SsrMatches {
263 let mut out = SsrMatches::default();
264 self.flatten_into(&mut out);
265 out
266 }
267
268 fn flatten_into(self, out: &mut SsrMatches) {
269 for mut m in self.matches {
270 for p in m.placeholder_values.values_mut() {
271 std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out);
272 }
273 out.matches.push(m);
274 }
275 }
276}
277
278impl Match {
279 pub fn matched_text(&self) -> String {
280 self.matched_node.text().to_string()
136 } 281 }
137} 282}
138 283
139impl std::error::Error for SsrError {} 284impl std::error::Error for SsrError {}
285
286#[cfg(test)]
287impl MatchDebugInfo {
288 pub(crate) fn match_failure_reason(&self) -> Option<&str> {
289 self.matched.as_ref().err().map(|r| r.reason.as_str())
290 }
291}