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