1#![cfg_attr(feature = "in-rust-tree", feature(rustc_private))]
67
68#[cfg(feature = "in-rust-tree")]
69extern crate rustc_driver as _;
70
71mod fragments;
72mod from_comment;
73mod matching;
74mod nester;
75mod parsing;
76mod replacing;
77mod resolving;
78mod search;
79#[macro_use]
80mod errors;
81#[cfg(test)]
82mod tests;
83
84pub use crate::{errors::SsrError, from_comment::ssr_from_comment, matching::Match};
85
86use crate::{errors::bail, matching::MatchFailureReason};
87use hir::{FileRange, Semantics};
88use ide_db::symbol_index::LocalRoots;
89use ide_db::text_edit::TextEdit;
90use ide_db::{EditionedFileId, FileId, FxHashMap, RootDatabase, base_db::SourceDatabase};
91use resolving::ResolvedRule;
92use syntax::{AstNode, SyntaxNode, TextRange, ast};
93
94#[derive(Debug)]
96pub struct SsrRule {
97 pattern: parsing::RawPattern,
99 template: parsing::RawPattern,
101 parsed_rules: Vec<parsing::ParsedRule>,
102}
103
104#[derive(Debug)]
105pub struct SsrPattern {
106 parsed_rules: Vec<parsing::ParsedRule>,
107}
108
109#[derive(Debug, Default)]
110pub struct SsrMatches {
111 pub matches: Vec<Match>,
112}
113
114pub struct MatchFinder<'db> {
116 sema: Semantics<'db, ide_db::RootDatabase>,
118 rules: Vec<ResolvedRule<'db>>,
119 resolution_scope: resolving::ResolutionScope<'db>,
120 restrict_ranges: Vec<ide_db::FileRange>,
121}
122
123impl<'db> MatchFinder<'db> {
124 pub fn in_context(
127 db: &'db RootDatabase,
128 lookup_context: ide_db::FilePosition,
129 mut restrict_ranges: Vec<ide_db::FileRange>,
130 ) -> Result<MatchFinder<'db>, SsrError> {
131 restrict_ranges.retain(|range| !range.range.is_empty());
132 let sema = Semantics::new(db);
133 let file_id = sema.attach_first_edition(lookup_context.file_id);
134 let resolution_scope = resolving::ResolutionScope::new(
135 &sema,
136 hir::FilePosition { file_id, offset: lookup_context.offset },
137 )
138 .ok_or_else(|| SsrError("no resolution scope for file".into()))?;
139 Ok(MatchFinder { sema, rules: Vec::new(), resolution_scope, restrict_ranges })
140 }
141
142 pub fn at_first_file(db: &'db ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> {
144 if let Some(first_file_id) = LocalRoots::get(db)
145 .roots(db)
146 .iter()
147 .next()
148 .and_then(|root| db.source_root(*root).source_root(db).iter().next())
149 {
150 MatchFinder::in_context(
151 db,
152 ide_db::FilePosition { file_id: first_file_id, offset: 0.into() },
153 vec![],
154 )
155 } else {
156 bail!("No files to search");
157 }
158 }
159
160 pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
164 for parsed_rule in rule.parsed_rules {
165 self.rules.push(ResolvedRule::new(
166 parsed_rule,
167 &self.resolution_scope,
168 self.rules.len(),
169 )?);
170 }
171 Ok(())
172 }
173
174 pub fn edits(&self) -> FxHashMap<FileId, TextEdit> {
176 let mut matches_by_file = FxHashMap::default();
177 for m in self.matches().matches {
178 matches_by_file
179 .entry(m.range.file_id.file_id(self.sema.db))
180 .or_insert_with(SsrMatches::default)
181 .matches
182 .push(m);
183 }
184 matches_by_file
185 .into_iter()
186 .map(|(file_id, matches)| {
187 (
188 file_id,
189 replacing::matches_to_edit(
190 self.sema.db,
191 &matches,
192 self.sema.db.file_text(file_id).text(self.sema.db),
193 &self.rules,
194 ),
195 )
196 })
197 .collect()
198 }
199
200 pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> {
203 for parsed_rule in pattern.parsed_rules {
204 self.rules.push(ResolvedRule::new(
205 parsed_rule,
206 &self.resolution_scope,
207 self.rules.len(),
208 )?);
209 }
210 Ok(())
211 }
212
213 pub fn matches(&self) -> SsrMatches {
215 let mut matches = Vec::new();
216 let mut usage_cache = search::UsageCache::default();
217 for rule in &self.rules {
218 self.find_matches_for_rule(rule, &mut usage_cache, &mut matches);
219 }
220 nester::nest_and_remove_collisions(matches, &self.sema)
221 }
222
223 pub fn debug_where_text_equal(
227 &self,
228 file_id: EditionedFileId,
229 snippet: &str,
230 ) -> Vec<MatchDebugInfo> {
231 let file = self.sema.parse(file_id);
232 let mut res = Vec::new();
233 let file_text = self.sema.db.file_text(file_id.file_id(self.sema.db)).text(self.sema.db);
234 let mut remaining_text = &**file_text;
235 let mut base = 0;
236 let len = snippet.len() as u32;
237 while let Some(offset) = remaining_text.find(snippet) {
238 let start = base + offset as u32;
239 let end = start + len;
240 self.output_debug_for_nodes_at_range(
241 file.syntax(),
242 FileRange { file_id, range: TextRange::new(start.into(), end.into()) },
243 &None,
244 &mut res,
245 );
246 remaining_text = &remaining_text[offset + snippet.len()..];
247 base = end;
248 }
249 res
250 }
251
252 fn output_debug_for_nodes_at_range(
253 &self,
254 node: &SyntaxNode,
255 range: FileRange,
256 restrict_range: &Option<FileRange>,
257 out: &mut Vec<MatchDebugInfo>,
258 ) {
259 for node in node.children() {
260 let node_range = self.sema.original_range(&node);
261 if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range)
262 {
263 continue;
264 }
265 if node_range.range == range.range {
266 for rule in &self.rules {
267 if rule.pattern.node.kind() != node.kind()
273 && !(ast::Expr::can_cast(rule.pattern.node.kind())
274 && ast::Expr::can_cast(node.kind()))
275 {
276 continue;
277 }
278 out.push(MatchDebugInfo {
279 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
280 .map_err(|e| MatchFailureReason {
281 reason: e.reason.unwrap_or_else(|| {
282 "Match failed, but no reason was given".to_owned()
283 }),
284 }),
285 pattern: rule.pattern.node.clone(),
286 node: node.clone(),
287 });
288 }
289 } else if let Some(macro_call) = ast::MacroCall::cast(node.clone())
290 && let Some(expanded) = self.sema.expand_macro_call(¯o_call)
291 && let Some(tt) = macro_call.token_tree()
292 {
293 self.output_debug_for_nodes_at_range(
294 &expanded.value,
295 range,
296 &Some(self.sema.original_range(tt.syntax())),
297 out,
298 );
299 }
300 self.output_debug_for_nodes_at_range(&node, range, restrict_range, out);
301 }
302 }
303}
304
305pub struct MatchDebugInfo {
306 node: SyntaxNode,
307 pattern: SyntaxNode,
309 matched: Result<Match, MatchFailureReason>,
310}
311
312impl std::fmt::Debug for MatchDebugInfo {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 match &self.matched {
315 Ok(_) => writeln!(f, "Node matched")?,
316 Err(reason) => writeln!(f, "Node failed to match because: {}", reason.reason)?,
317 }
318 writeln!(
319 f,
320 "============ AST ===========\n\
321 {:#?}",
322 self.node
323 )?;
324 writeln!(f, "========= PATTERN ==========")?;
325 writeln!(f, "{:#?}", self.pattern)?;
326 writeln!(f, "============================")?;
327 Ok(())
328 }
329}
330
331impl SsrMatches {
332 pub fn flattened(self) -> SsrMatches {
334 let mut out = SsrMatches::default();
335 self.flatten_into(&mut out);
336 out
337 }
338
339 fn flatten_into(self, out: &mut SsrMatches) {
340 for mut m in self.matches {
341 for p in m.placeholder_values.values_mut() {
342 std::mem::take(&mut p.inner_matches).flatten_into(out);
343 }
344 out.matches.push(m);
345 }
346 }
347}
348
349impl Match {
350 pub fn matched_text(&self) -> String {
351 self.matched_node.text().to_string()
352 }
353}
354
355impl std::error::Error for SsrError {}
356
357#[cfg(test)]
358impl MatchDebugInfo {
359 pub fn match_failure_reason(&self) -> Option<&str> {
360 self.matched.as_ref().err().map(|r| r.reason.as_str())
361 }
362}