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.rs291
1 files changed, 0 insertions, 291 deletions
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs
deleted file mode 100644
index 73abfecb2..000000000
--- a/crates/ra_ssr/src/lib.rs
+++ /dev/null
@@ -1,291 +0,0 @@
1//! Structural Search Replace
2//!
3//! Allows searching the AST for code that matches one or more patterns and then replacing that code
4//! based on a template.
5
6mod matching;
7mod nester;
8mod parsing;
9mod replacing;
10mod resolving;
11mod search;
12#[macro_use]
13mod errors;
14#[cfg(test)]
15mod tests;
16
17use crate::errors::bail;
18pub use crate::errors::SsrError;
19pub use crate::matching::Match;
20use crate::matching::MatchFailureReason;
21use hir::Semantics;
22use ra_db::{FileId, FilePosition, FileRange};
23use ra_ide_db::source_change::SourceFileEdit;
24use ra_syntax::{ast, AstNode, SyntaxNode, TextRange};
25use resolving::ResolvedRule;
26use rustc_hash::FxHashMap;
27
28// A structured search replace rule. Create by calling `parse` on a str.
29#[derive(Debug)]
30pub struct SsrRule {
31 /// A structured pattern that we're searching for.
32 pattern: parsing::RawPattern,
33 /// What we'll replace it with.
34 template: parsing::RawPattern,
35 parsed_rules: Vec<parsing::ParsedRule>,
36}
37
38#[derive(Debug)]
39pub struct SsrPattern {
40 raw: parsing::RawPattern,
41 parsed_rules: Vec<parsing::ParsedRule>,
42}
43
44#[derive(Debug, Default)]
45pub struct SsrMatches {
46 pub matches: Vec<Match>,
47}
48
49/// Searches a crate for pattern matches and possibly replaces them with something else.
50pub struct MatchFinder<'db> {
51 /// Our source of information about the user's code.
52 sema: Semantics<'db, ra_ide_db::RootDatabase>,
53 rules: Vec<ResolvedRule>,
54 resolution_scope: resolving::ResolutionScope<'db>,
55 restrict_ranges: Vec<FileRange>,
56}
57
58impl<'db> MatchFinder<'db> {
59 /// Constructs a new instance where names will be looked up as if they appeared at
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 }
75 }
76
77 /// Constructs an instance using the start of the first file in `db` as the lookup context.
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 }
95 }
96
97 /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
98 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
99 /// match to it.
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(())
109 }
110
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;
159 let file = self.sema.parse(file_id);
160 let mut res = Vec::new();
161 let file_text = self.sema.db.file_text(file_id);
162 let mut remaining_text = file_text.as_str();
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
178 }
179
180 fn output_debug_for_nodes_at_range(
181 &self,
182 node: &SyntaxNode,
183 range: FileRange,
184 restrict_range: &Option<FileRange>,
185 out: &mut Vec<MatchDebugInfo>,
186 ) {
187 for node in node.children() {
188 let node_range = self.sema.original_range(&node);
189 if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range)
190 {
191 continue;
192 }
193 if node_range.range == range.range {
194 for rule in &self.rules {
195 // For now we ignore rules that have a different kind than our node, otherwise
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 }
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 });
216 }
217 } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) {
218 if let Some(expanded) = self.sema.expand(&macro_call) {
219 if let Some(tt) = macro_call.token_tree() {
220 self.output_debug_for_nodes_at_range(
221 &expanded,
222 range,
223 &Some(self.sema.original_range(tt.syntax())),
224 out,
225 );
226 }
227 }
228 }
229 self.output_debug_for_nodes_at_range(&node, range, restrict_range, out);
230 }
231 }
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)?,
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(())
257 }
258}
259
260impl SsrMatches {
261 /// Returns `self` with any nested matches removed and made into top-level matches.
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()
281 }
282}
283
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}