aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ssr/src/tests.rs
diff options
context:
space:
mode:
authorDavid Lattimore <[email protected]>2020-06-30 06:55:20 +0100
committerDavid Lattimore <[email protected]>2020-07-01 07:50:45 +0100
commit95f8310514238fc2c3b4d1ddecbd1b79d2c963a2 (patch)
treefae3a79e0597e8d26bbb2d7e1200ecfa594e082b /crates/ra_ssr/src/tests.rs
parentb1a2d016450c96581c9cc70cc464f8aab2e5c0f3 (diff)
Structured search debugging
Diffstat (limited to 'crates/ra_ssr/src/tests.rs')
-rw-r--r--crates/ra_ssr/src/tests.rs189
1 files changed, 42 insertions, 147 deletions
diff --git a/crates/ra_ssr/src/tests.rs b/crates/ra_ssr/src/tests.rs
index 57b2f50b2..c692c97e2 100644
--- a/crates/ra_ssr/src/tests.rs
+++ b/crates/ra_ssr/src/tests.rs
@@ -1,150 +1,5 @@
1use crate::matching::MatchFailureReason; 1use crate::{MatchFinder, SsrRule};
2use crate::{matching, Match, MatchFinder, SsrMatches, SsrPattern, SsrRule}; 2use ra_db::{FileId, SourceDatabaseExt};
3use matching::record_match_fails_reasons_scope;
4use ra_db::{FileId, FileRange, SourceDatabaseExt};
5use ra_syntax::ast::AstNode;
6use ra_syntax::{ast, SyntaxKind, SyntaxNode, TextRange};
7
8struct MatchDebugInfo {
9 node: SyntaxNode,
10 /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item,
11 /// etc. Will be absent if the pattern can't be parsed as that kind.
12 pattern: Result<SyntaxNode, MatchFailureReason>,
13 matched: Result<Match, MatchFailureReason>,
14}
15
16impl SsrPattern {
17 pub(crate) fn tree_for_kind_with_reason(
18 &self,
19 kind: SyntaxKind,
20 ) -> Result<&SyntaxNode, MatchFailureReason> {
21 record_match_fails_reasons_scope(true, || self.tree_for_kind(kind))
22 .map_err(|e| MatchFailureReason { reason: e.reason.unwrap() })
23 }
24}
25
26impl std::fmt::Debug for MatchDebugInfo {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "========= PATTERN ==========\n")?;
29 match &self.pattern {
30 Ok(pattern) => {
31 write!(f, "{:#?}", pattern)?;
32 }
33 Err(err) => {
34 write!(f, "{}", err.reason)?;
35 }
36 }
37 write!(
38 f,
39 "\n============ AST ===========\n\
40 {:#?}\n============================",
41 self.node
42 )?;
43 match &self.matched {
44 Ok(_) => write!(f, "Node matched")?,
45 Err(reason) => write!(f, "Node failed to match because: {}", reason.reason)?,
46 }
47 Ok(())
48 }
49}
50
51impl SsrMatches {
52 /// Returns `self` with any nested matches removed and made into top-level matches.
53 pub(crate) fn flattened(self) -> SsrMatches {
54 let mut out = SsrMatches::default();
55 self.flatten_into(&mut out);
56 out
57 }
58
59 fn flatten_into(self, out: &mut SsrMatches) {
60 for mut m in self.matches {
61 for p in m.placeholder_values.values_mut() {
62 std::mem::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out);
63 }
64 out.matches.push(m);
65 }
66 }
67}
68
69impl Match {
70 pub(crate) fn matched_text(&self) -> String {
71 self.matched_node.text().to_string()
72 }
73}
74
75impl<'db> MatchFinder<'db> {
76 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
77 /// intend to do replacement, use `add_rule` instead.
78 fn add_search_pattern(&mut self, pattern: SsrPattern) {
79 self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() })
80 }
81
82 /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match
83 /// them, while recording reasons why they don't match. This API is useful for command
84 /// line-based debugging where providing a range is difficult.
85 fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec<MatchDebugInfo> {
86 let file = self.sema.parse(file_id);
87 let mut res = Vec::new();
88 let file_text = self.sema.db.file_text(file_id);
89 let mut remaining_text = file_text.as_str();
90 let mut base = 0;
91 let len = snippet.len() as u32;
92 while let Some(offset) = remaining_text.find(snippet) {
93 let start = base + offset as u32;
94 let end = start + len;
95 self.output_debug_for_nodes_at_range(
96 file.syntax(),
97 TextRange::new(start.into(), end.into()),
98 &None,
99 &mut res,
100 );
101 remaining_text = &remaining_text[offset + snippet.len()..];
102 base = end;
103 }
104 res
105 }
106
107 fn output_debug_for_nodes_at_range(
108 &self,
109 node: &SyntaxNode,
110 range: TextRange,
111 restrict_range: &Option<FileRange>,
112 out: &mut Vec<MatchDebugInfo>,
113 ) {
114 for node in node.children() {
115 if !node.text_range().contains_range(range) {
116 continue;
117 }
118 if node.text_range() == range {
119 for rule in &self.rules {
120 let pattern =
121 rule.pattern.tree_for_kind_with_reason(node.kind()).map(|p| p.clone());
122 out.push(MatchDebugInfo {
123 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
124 .map_err(|e| MatchFailureReason {
125 reason: e.reason.unwrap_or_else(|| {
126 "Match failed, but no reason was given".to_owned()
127 }),
128 }),
129 pattern,
130 node: node.clone(),
131 });
132 }
133 } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) {
134 if let Some(expanded) = self.sema.expand(&macro_call) {
135 if let Some(tt) = macro_call.token_tree() {
136 self.output_debug_for_nodes_at_range(
137 &expanded,
138 range,
139 &Some(self.sema.original_range(tt.syntax())),
140 out,
141 );
142 }
143 }
144 }
145 }
146 }
147}
148 3
149fn parse_error_text(query: &str) -> String { 4fn parse_error_text(query: &str) -> String {
150 format!("{}", query.parse::<SsrRule>().unwrap_err()) 5 format!("{}", query.parse::<SsrRule>().unwrap_err())
@@ -260,6 +115,19 @@ fn assert_no_match(pattern: &str, code: &str) {
260 assert_matches(pattern, code, &[]); 115 assert_matches(pattern, code, &[]);
261} 116}
262 117
118fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expected_reason: &str) {
119 let (db, file_id) = single_file(code);
120 let mut match_finder = MatchFinder::new(&db);
121 match_finder.add_search_pattern(pattern.parse().unwrap());
122 let mut reasons = Vec::new();
123 for d in match_finder.debug_where_text_equal(file_id, snippet) {
124 if let Some(reason) = d.match_failure_reason() {
125 reasons.push(reason.to_owned());
126 }
127 }
128 assert_eq!(reasons, vec![expected_reason]);
129}
130
263#[test] 131#[test]
264fn ssr_function_to_method() { 132fn ssr_function_to_method() {
265 assert_ssr_transform( 133 assert_ssr_transform(
@@ -623,3 +491,30 @@ fn preserves_whitespace_within_macro_expansion() {
623 fn f() {macro1!(4 - 3 - 1 * 2}"#, 491 fn f() {macro1!(4 - 3 - 1 * 2}"#,
624 ) 492 )
625} 493}
494
495#[test]
496fn match_failure_reasons() {
497 let code = r#"
498 macro_rules! foo {
499 ($a:expr) => {
500 1 + $a + 2
501 };
502 }
503 fn f1() {
504 bar(1, 2);
505 foo!(5 + 43.to_string() + 5);
506 }
507 "#;
508 assert_match_failure_reason(
509 "bar($a, 3)",
510 code,
511 "bar(1, 2)",
512 r#"Pattern wanted token '3' (INT_NUMBER), but code had token '2' (INT_NUMBER)"#,
513 );
514 assert_match_failure_reason(
515 "42.to_string()",
516 code,
517 "43.to_string()",
518 r#"Pattern wanted token '42' (INT_NUMBER), but code had token '43' (INT_NUMBER)"#,
519 );
520}