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