diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-01-10 21:40:57 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2020-01-10 21:40:57 +0000 |
commit | a9ba32e2e659dbbe78f7921f5b0abe7288c83aa9 (patch) | |
tree | fc742b1b41c72f8016329a243f0f5302d57bad85 /crates/ra_ide/src/references.rs | |
parent | 19eb7fa1db7da8417314ddfafe7addbbd9c3b46a (diff) | |
parent | a633a6275ab823396f57b1e93d45e58d98f8d32f (diff) |
Merge #2749
2749: Basic DocumentHighlightKind support for assignments r=matklad a=kjeremy
Wraps references per #2738 and adds limited support for DocumentHighlightKind Read/Write for simple binops assignments.
I think I need some help with determining reads/writes.
Towards #2560
Co-authored-by: Jeremy Kolb <[email protected]>
Co-authored-by: kjeremy <[email protected]>
Diffstat (limited to 'crates/ra_ide/src/references.rs')
-rw-r--r-- | crates/ra_ide/src/references.rs | 253 |
1 files changed, 198 insertions, 55 deletions
diff --git a/crates/ra_ide/src/references.rs b/crates/ra_ide/src/references.rs index 5a3ec4eb9..4e52e0e7b 100644 --- a/crates/ra_ide/src/references.rs +++ b/crates/ra_ide/src/references.rs | |||
@@ -19,8 +19,9 @@ use once_cell::unsync::Lazy; | |||
19 | use ra_db::{SourceDatabase, SourceDatabaseExt}; | 19 | use ra_db::{SourceDatabase, SourceDatabaseExt}; |
20 | use ra_prof::profile; | 20 | use ra_prof::profile; |
21 | use ra_syntax::{ | 21 | use ra_syntax::{ |
22 | algo::find_node_at_offset, ast, AstNode, SourceFile, SyntaxKind, SyntaxNode, TextUnit, | 22 | algo::find_node_at_offset, |
23 | TokenAtOffset, | 23 | ast::{self, NameOwner}, |
24 | match_ast, AstNode, SourceFile, SyntaxKind, SyntaxNode, TextRange, TextUnit, TokenAtOffset, | ||
24 | }; | 25 | }; |
25 | 26 | ||
26 | use crate::{ | 27 | use crate::{ |
@@ -37,15 +38,22 @@ pub use self::search_scope::SearchScope; | |||
37 | 38 | ||
38 | #[derive(Debug, Clone)] | 39 | #[derive(Debug, Clone)] |
39 | pub struct ReferenceSearchResult { | 40 | pub struct ReferenceSearchResult { |
40 | declaration: NavigationTarget, | 41 | declaration: Declaration, |
41 | declaration_kind: ReferenceKind, | ||
42 | references: Vec<Reference>, | 42 | references: Vec<Reference>, |
43 | } | 43 | } |
44 | 44 | ||
45 | #[derive(Debug, Clone)] | 45 | #[derive(Debug, Clone)] |
46 | pub struct Declaration { | ||
47 | pub nav: NavigationTarget, | ||
48 | pub kind: ReferenceKind, | ||
49 | pub access: Option<ReferenceAccess>, | ||
50 | } | ||
51 | |||
52 | #[derive(Debug, Clone)] | ||
46 | pub struct Reference { | 53 | pub struct Reference { |
47 | pub file_range: FileRange, | 54 | pub file_range: FileRange, |
48 | pub kind: ReferenceKind, | 55 | pub kind: ReferenceKind, |
56 | pub access: Option<ReferenceAccess>, | ||
49 | } | 57 | } |
50 | 58 | ||
51 | #[derive(Debug, Clone, PartialEq)] | 59 | #[derive(Debug, Clone, PartialEq)] |
@@ -54,11 +62,21 @@ pub enum ReferenceKind { | |||
54 | Other, | 62 | Other, |
55 | } | 63 | } |
56 | 64 | ||
65 | #[derive(Debug, Copy, Clone, PartialEq)] | ||
66 | pub enum ReferenceAccess { | ||
67 | Read, | ||
68 | Write, | ||
69 | } | ||
70 | |||
57 | impl ReferenceSearchResult { | 71 | impl ReferenceSearchResult { |
58 | pub fn declaration(&self) -> &NavigationTarget { | 72 | pub fn declaration(&self) -> &Declaration { |
59 | &self.declaration | 73 | &self.declaration |
60 | } | 74 | } |
61 | 75 | ||
76 | pub fn decl_target(&self) -> &NavigationTarget { | ||
77 | &self.declaration.nav | ||
78 | } | ||
79 | |||
62 | pub fn references(&self) -> &[Reference] { | 80 | pub fn references(&self) -> &[Reference] { |
63 | &self.references | 81 | &self.references |
64 | } | 82 | } |
@@ -72,7 +90,7 @@ impl ReferenceSearchResult { | |||
72 | } | 90 | } |
73 | 91 | ||
74 | // allow turning ReferenceSearchResult into an iterator | 92 | // allow turning ReferenceSearchResult into an iterator |
75 | // over FileRanges | 93 | // over References |
76 | impl IntoIterator for ReferenceSearchResult { | 94 | impl IntoIterator for ReferenceSearchResult { |
77 | type Item = Reference; | 95 | type Item = Reference; |
78 | type IntoIter = std::vec::IntoIter<Reference>; | 96 | type IntoIter = std::vec::IntoIter<Reference>; |
@@ -81,10 +99,11 @@ impl IntoIterator for ReferenceSearchResult { | |||
81 | let mut v = Vec::with_capacity(self.len()); | 99 | let mut v = Vec::with_capacity(self.len()); |
82 | v.push(Reference { | 100 | v.push(Reference { |
83 | file_range: FileRange { | 101 | file_range: FileRange { |
84 | file_id: self.declaration.file_id(), | 102 | file_id: self.declaration.nav.file_id(), |
85 | range: self.declaration.range(), | 103 | range: self.declaration.nav.range(), |
86 | }, | 104 | }, |
87 | kind: self.declaration_kind, | 105 | kind: self.declaration.kind, |
106 | access: self.declaration.access, | ||
88 | }); | 107 | }); |
89 | v.append(&mut self.references); | 108 | v.append(&mut self.references); |
90 | v.into_iter() | 109 | v.into_iter() |
@@ -131,15 +150,20 @@ pub(crate) fn find_all_refs( | |||
131 | } | 150 | } |
132 | }; | 151 | }; |
133 | 152 | ||
153 | let decl_range = declaration.range(); | ||
154 | |||
155 | let declaration = Declaration { | ||
156 | nav: declaration, | ||
157 | kind: ReferenceKind::Other, | ||
158 | access: decl_access(&def.kind, &name, &syntax, decl_range), | ||
159 | }; | ||
160 | |||
134 | let references = process_definition(db, def, name, search_scope) | 161 | let references = process_definition(db, def, name, search_scope) |
135 | .into_iter() | 162 | .into_iter() |
136 | .filter(|r| search_kind == ReferenceKind::Other || search_kind == r.kind) | 163 | .filter(|r| search_kind == ReferenceKind::Other || search_kind == r.kind) |
137 | .collect(); | 164 | .collect(); |
138 | 165 | ||
139 | Some(RangeInfo::new( | 166 | Some(RangeInfo::new(range, ReferenceSearchResult { declaration, references })) |
140 | range, | ||
141 | ReferenceSearchResult { declaration, references, declaration_kind: ReferenceKind::Other }, | ||
142 | )) | ||
143 | } | 167 | } |
144 | 168 | ||
145 | fn find_name<'a>( | 169 | fn find_name<'a>( |
@@ -201,7 +225,12 @@ fn process_definition( | |||
201 | } else { | 225 | } else { |
202 | ReferenceKind::Other | 226 | ReferenceKind::Other |
203 | }; | 227 | }; |
204 | refs.push(Reference { file_range: FileRange { file_id, range }, kind }); | 228 | |
229 | refs.push(Reference { | ||
230 | file_range: FileRange { file_id, range }, | ||
231 | kind, | ||
232 | access: reference_access(&d.kind, &name_ref), | ||
233 | }); | ||
205 | } | 234 | } |
206 | } | 235 | } |
207 | } | 236 | } |
@@ -210,11 +239,69 @@ fn process_definition( | |||
210 | refs | 239 | refs |
211 | } | 240 | } |
212 | 241 | ||
242 | fn decl_access( | ||
243 | kind: &NameKind, | ||
244 | name: &str, | ||
245 | syntax: &SyntaxNode, | ||
246 | range: TextRange, | ||
247 | ) -> Option<ReferenceAccess> { | ||
248 | match kind { | ||
249 | NameKind::Local(_) | NameKind::Field(_) => {} | ||
250 | _ => return None, | ||
251 | }; | ||
252 | |||
253 | let stmt = find_node_at_offset::<ast::LetStmt>(syntax, range.start())?; | ||
254 | if let Some(_) = stmt.initializer() { | ||
255 | let pat = stmt.pat()?; | ||
256 | match pat { | ||
257 | ast::Pat::BindPat(it) => { | ||
258 | if it.name()?.text().as_str() == name { | ||
259 | return Some(ReferenceAccess::Write); | ||
260 | } | ||
261 | } | ||
262 | _ => {} | ||
263 | } | ||
264 | } | ||
265 | |||
266 | None | ||
267 | } | ||
268 | |||
269 | fn reference_access(kind: &NameKind, name_ref: &ast::NameRef) -> Option<ReferenceAccess> { | ||
270 | // Only Locals and Fields have accesses for now. | ||
271 | match kind { | ||
272 | NameKind::Local(_) | NameKind::Field(_) => {} | ||
273 | _ => return None, | ||
274 | }; | ||
275 | |||
276 | let mode = name_ref.syntax().ancestors().find_map(|node| { | ||
277 | match_ast! { | ||
278 | match (node) { | ||
279 | ast::BinExpr(expr) => { | ||
280 | if expr.op_kind()?.is_assignment() { | ||
281 | // If the variable or field ends on the LHS's end then it's a Write (covers fields and locals). | ||
282 | // FIXME: This is not terribly accurate. | ||
283 | if let Some(lhs) = expr.lhs() { | ||
284 | if lhs.syntax().text_range().end() == name_ref.syntax().text_range().end() { | ||
285 | return Some(ReferenceAccess::Write); | ||
286 | } | ||
287 | } | ||
288 | } | ||
289 | return Some(ReferenceAccess::Read); | ||
290 | }, | ||
291 | _ => {None} | ||
292 | } | ||
293 | } | ||
294 | }); | ||
295 | |||
296 | // Default Locals and Fields to read | ||
297 | mode.or(Some(ReferenceAccess::Read)) | ||
298 | } | ||
299 | |||
213 | #[cfg(test)] | 300 | #[cfg(test)] |
214 | mod tests { | 301 | mod tests { |
215 | use crate::{ | 302 | use crate::{ |
216 | mock_analysis::{analysis_and_position, single_file_with_position, MockAnalysis}, | 303 | mock_analysis::{analysis_and_position, single_file_with_position, MockAnalysis}, |
217 | Reference, ReferenceKind, ReferenceSearchResult, SearchScope, | 304 | Declaration, Reference, ReferenceSearchResult, SearchScope, |
218 | }; | 305 | }; |
219 | 306 | ||
220 | #[test] | 307 | #[test] |
@@ -234,8 +321,7 @@ mod tests { | |||
234 | let refs = get_all_refs(code); | 321 | let refs = get_all_refs(code); |
235 | check_result( | 322 | check_result( |
236 | refs, | 323 | refs, |
237 | "Foo STRUCT_DEF FileId(1) [5; 39) [12; 15)", | 324 | "Foo STRUCT_DEF FileId(1) [5; 39) [12; 15) Other", |
238 | ReferenceKind::Other, | ||
239 | &["FileId(1) [142; 145) StructLiteral"], | 325 | &["FileId(1) [142; 145) StructLiteral"], |
240 | ); | 326 | ); |
241 | } | 327 | } |
@@ -258,13 +344,12 @@ mod tests { | |||
258 | let refs = get_all_refs(code); | 344 | let refs = get_all_refs(code); |
259 | check_result( | 345 | check_result( |
260 | refs, | 346 | refs, |
261 | "i BIND_PAT FileId(1) [33; 34)", | 347 | "i BIND_PAT FileId(1) [33; 34) Other Write", |
262 | ReferenceKind::Other, | ||
263 | &[ | 348 | &[ |
264 | "FileId(1) [67; 68) Other", | 349 | "FileId(1) [67; 68) Other Write", |
265 | "FileId(1) [71; 72) Other", | 350 | "FileId(1) [71; 72) Other Read", |
266 | "FileId(1) [101; 102) Other", | 351 | "FileId(1) [101; 102) Other Write", |
267 | "FileId(1) [127; 128) Other", | 352 | "FileId(1) [127; 128) Other Write", |
268 | ], | 353 | ], |
269 | ); | 354 | ); |
270 | } | 355 | } |
@@ -279,9 +364,8 @@ mod tests { | |||
279 | let refs = get_all_refs(code); | 364 | let refs = get_all_refs(code); |
280 | check_result( | 365 | check_result( |
281 | refs, | 366 | refs, |
282 | "i BIND_PAT FileId(1) [12; 13)", | 367 | "i BIND_PAT FileId(1) [12; 13) Other", |
283 | ReferenceKind::Other, | 368 | &["FileId(1) [38; 39) Other Read"], |
284 | &["FileId(1) [38; 39) Other"], | ||
285 | ); | 369 | ); |
286 | } | 370 | } |
287 | 371 | ||
@@ -295,9 +379,8 @@ mod tests { | |||
295 | let refs = get_all_refs(code); | 379 | let refs = get_all_refs(code); |
296 | check_result( | 380 | check_result( |
297 | refs, | 381 | refs, |
298 | "i BIND_PAT FileId(1) [12; 13)", | 382 | "i BIND_PAT FileId(1) [12; 13) Other", |
299 | ReferenceKind::Other, | 383 | &["FileId(1) [38; 39) Other Read"], |
300 | &["FileId(1) [38; 39) Other"], | ||
301 | ); | 384 | ); |
302 | } | 385 | } |
303 | 386 | ||
@@ -317,9 +400,8 @@ mod tests { | |||
317 | let refs = get_all_refs(code); | 400 | let refs = get_all_refs(code); |
318 | check_result( | 401 | check_result( |
319 | refs, | 402 | refs, |
320 | "spam RECORD_FIELD_DEF FileId(1) [66; 79) [70; 74)", | 403 | "spam RECORD_FIELD_DEF FileId(1) [66; 79) [70; 74) Other", |
321 | ReferenceKind::Other, | 404 | &["FileId(1) [152; 156) Other Read"], |
322 | &["FileId(1) [152; 156) Other"], | ||
323 | ); | 405 | ); |
324 | } | 406 | } |
325 | 407 | ||
@@ -334,7 +416,7 @@ mod tests { | |||
334 | "#; | 416 | "#; |
335 | 417 | ||
336 | let refs = get_all_refs(code); | 418 | let refs = get_all_refs(code); |
337 | check_result(refs, "f FN_DEF FileId(1) [88; 104) [91; 92)", ReferenceKind::Other, &[]); | 419 | check_result(refs, "f FN_DEF FileId(1) [88; 104) [91; 92) Other", &[]); |
338 | } | 420 | } |
339 | 421 | ||
340 | #[test] | 422 | #[test] |
@@ -349,7 +431,7 @@ mod tests { | |||
349 | "#; | 431 | "#; |
350 | 432 | ||
351 | let refs = get_all_refs(code); | 433 | let refs = get_all_refs(code); |
352 | check_result(refs, "B ENUM_VARIANT FileId(1) [83; 84) [83; 84)", ReferenceKind::Other, &[]); | 434 | check_result(refs, "B ENUM_VARIANT FileId(1) [83; 84) [83; 84) Other", &[]); |
353 | } | 435 | } |
354 | 436 | ||
355 | #[test] | 437 | #[test] |
@@ -390,8 +472,7 @@ mod tests { | |||
390 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | 472 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); |
391 | check_result( | 473 | check_result( |
392 | refs, | 474 | refs, |
393 | "Foo STRUCT_DEF FileId(2) [16; 50) [27; 30)", | 475 | "Foo STRUCT_DEF FileId(2) [16; 50) [27; 30) Other", |
394 | ReferenceKind::Other, | ||
395 | &["FileId(1) [52; 55) StructLiteral", "FileId(3) [77; 80) StructLiteral"], | 476 | &["FileId(1) [52; 55) StructLiteral", "FileId(3) [77; 80) StructLiteral"], |
396 | ); | 477 | ); |
397 | } | 478 | } |
@@ -421,8 +502,7 @@ mod tests { | |||
421 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | 502 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); |
422 | check_result( | 503 | check_result( |
423 | refs, | 504 | refs, |
424 | "foo SOURCE_FILE FileId(2) [0; 35)", | 505 | "foo SOURCE_FILE FileId(2) [0; 35) Other", |
425 | ReferenceKind::Other, | ||
426 | &["FileId(1) [13; 16) Other"], | 506 | &["FileId(1) [13; 16) Other"], |
427 | ); | 507 | ); |
428 | } | 508 | } |
@@ -451,8 +531,7 @@ mod tests { | |||
451 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | 531 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); |
452 | check_result( | 532 | check_result( |
453 | refs, | 533 | refs, |
454 | "Foo STRUCT_DEF FileId(3) [0; 41) [18; 21)", | 534 | "Foo STRUCT_DEF FileId(3) [0; 41) [18; 21) Other", |
455 | ReferenceKind::Other, | ||
456 | &["FileId(2) [20; 23) Other", "FileId(2) [46; 49) StructLiteral"], | 535 | &["FileId(2) [20; 23) Other", "FileId(2) [46; 49) StructLiteral"], |
457 | ); | 536 | ); |
458 | } | 537 | } |
@@ -480,8 +559,7 @@ mod tests { | |||
480 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | 559 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); |
481 | check_result( | 560 | check_result( |
482 | refs, | 561 | refs, |
483 | "quux FN_DEF FileId(1) [18; 34) [25; 29)", | 562 | "quux FN_DEF FileId(1) [18; 34) [25; 29) Other", |
484 | ReferenceKind::Other, | ||
485 | &["FileId(2) [16; 20) Other", "FileId(3) [16; 20) Other"], | 563 | &["FileId(2) [16; 20) Other", "FileId(3) [16; 20) Other"], |
486 | ); | 564 | ); |
487 | 565 | ||
@@ -489,8 +567,7 @@ mod tests { | |||
489 | analysis.find_all_refs(pos, Some(SearchScope::single_file(bar))).unwrap().unwrap(); | 567 | analysis.find_all_refs(pos, Some(SearchScope::single_file(bar))).unwrap().unwrap(); |
490 | check_result( | 568 | check_result( |
491 | refs, | 569 | refs, |
492 | "quux FN_DEF FileId(1) [18; 34) [25; 29)", | 570 | "quux FN_DEF FileId(1) [18; 34) [25; 29) Other", |
493 | ReferenceKind::Other, | ||
494 | &["FileId(3) [16; 20) Other"], | 571 | &["FileId(3) [16; 20) Other"], |
495 | ); | 572 | ); |
496 | } | 573 | } |
@@ -509,33 +586,99 @@ mod tests { | |||
509 | let refs = get_all_refs(code); | 586 | let refs = get_all_refs(code); |
510 | check_result( | 587 | check_result( |
511 | refs, | 588 | refs, |
512 | "m1 MACRO_CALL FileId(1) [9; 63) [46; 48)", | 589 | "m1 MACRO_CALL FileId(1) [9; 63) [46; 48) Other", |
513 | ReferenceKind::Other, | ||
514 | &["FileId(1) [96; 98) Other", "FileId(1) [114; 116) Other"], | 590 | &["FileId(1) [96; 98) Other", "FileId(1) [114; 116) Other"], |
515 | ); | 591 | ); |
516 | } | 592 | } |
517 | 593 | ||
594 | #[test] | ||
595 | fn test_basic_highlight_read_write() { | ||
596 | let code = r#" | ||
597 | fn foo() { | ||
598 | let i<|> = 0; | ||
599 | i = i + 1; | ||
600 | }"#; | ||
601 | |||
602 | let refs = get_all_refs(code); | ||
603 | check_result( | ||
604 | refs, | ||
605 | "i BIND_PAT FileId(1) [36; 37) Other Write", | ||
606 | &["FileId(1) [55; 56) Other Write", "FileId(1) [59; 60) Other Read"], | ||
607 | ); | ||
608 | } | ||
609 | |||
610 | #[test] | ||
611 | fn test_basic_highlight_field_read_write() { | ||
612 | let code = r#" | ||
613 | struct S { | ||
614 | f: u32, | ||
615 | } | ||
616 | |||
617 | fn foo() { | ||
618 | let mut s = S{f: 0}; | ||
619 | s.f<|> = 0; | ||
620 | }"#; | ||
621 | |||
622 | let refs = get_all_refs(code); | ||
623 | check_result( | ||
624 | refs, | ||
625 | "f RECORD_FIELD_DEF FileId(1) [32; 38) [32; 33) Other", | ||
626 | &["FileId(1) [96; 97) Other Read", "FileId(1) [117; 118) Other Write"], | ||
627 | ); | ||
628 | } | ||
629 | |||
630 | #[test] | ||
631 | fn test_basic_highlight_decl_no_write() { | ||
632 | let code = r#" | ||
633 | fn foo() { | ||
634 | let i<|>; | ||
635 | i = 1; | ||
636 | }"#; | ||
637 | |||
638 | let refs = get_all_refs(code); | ||
639 | check_result( | ||
640 | refs, | ||
641 | "i BIND_PAT FileId(1) [36; 37) Other", | ||
642 | &["FileId(1) [51; 52) Other Write"], | ||
643 | ); | ||
644 | } | ||
645 | |||
518 | fn get_all_refs(text: &str) -> ReferenceSearchResult { | 646 | fn get_all_refs(text: &str) -> ReferenceSearchResult { |
519 | let (analysis, position) = single_file_with_position(text); | 647 | let (analysis, position) = single_file_with_position(text); |
520 | analysis.find_all_refs(position, None).unwrap().unwrap() | 648 | analysis.find_all_refs(position, None).unwrap().unwrap() |
521 | } | 649 | } |
522 | 650 | ||
523 | fn check_result( | 651 | fn check_result(res: ReferenceSearchResult, expected_decl: &str, expected_refs: &[&str]) { |
524 | res: ReferenceSearchResult, | ||
525 | expected_decl: &str, | ||
526 | decl_kind: ReferenceKind, | ||
527 | expected_refs: &[&str], | ||
528 | ) { | ||
529 | res.declaration().assert_match(expected_decl); | 652 | res.declaration().assert_match(expected_decl); |
530 | assert_eq!(res.declaration_kind, decl_kind); | ||
531 | |||
532 | assert_eq!(res.references.len(), expected_refs.len()); | 653 | assert_eq!(res.references.len(), expected_refs.len()); |
533 | res.references().iter().enumerate().for_each(|(i, r)| r.assert_match(expected_refs[i])); | 654 | res.references().iter().enumerate().for_each(|(i, r)| r.assert_match(expected_refs[i])); |
534 | } | 655 | } |
535 | 656 | ||
657 | impl Declaration { | ||
658 | fn debug_render(&self) -> String { | ||
659 | let mut s = format!("{} {:?}", self.nav.debug_render(), self.kind); | ||
660 | if let Some(access) = self.access { | ||
661 | s.push_str(&format!(" {:?}", access)); | ||
662 | } | ||
663 | s | ||
664 | } | ||
665 | |||
666 | fn assert_match(&self, expected: &str) { | ||
667 | let actual = self.debug_render(); | ||
668 | test_utils::assert_eq_text!(expected.trim(), actual.trim(),); | ||
669 | } | ||
670 | } | ||
671 | |||
536 | impl Reference { | 672 | impl Reference { |
537 | fn debug_render(&self) -> String { | 673 | fn debug_render(&self) -> String { |
538 | format!("{:?} {:?} {:?}", self.file_range.file_id, self.file_range.range, self.kind) | 674 | let mut s = format!( |
675 | "{:?} {:?} {:?}", | ||
676 | self.file_range.file_id, self.file_range.range, self.kind | ||
677 | ); | ||
678 | if let Some(access) = self.access { | ||
679 | s.push_str(&format!(" {:?}", access)); | ||
680 | } | ||
681 | s | ||
539 | } | 682 | } |
540 | 683 | ||
541 | fn assert_match(&self, expected: &str) { | 684 | fn assert_match(&self, expected: &str) { |