ink_analyzer/analysis/hover.rs
1//! ink! attribute hover content.
2
3mod args;
4mod macros;
5
6use ink_analyzer_ir::syntax::{AstNode, AstToken, TextRange};
7use ink_analyzer_ir::{InkArgKind, InkAttributeKind, InkFile, InkMacroKind};
8
9use crate::analysis::utils;
10use crate::Version;
11
12/// An ink! attribute hover result.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Hover {
15 /// Range the hover content applies to.
16 pub range: TextRange,
17 /// Hover text.
18 pub content: String,
19}
20
21/// Returns descriptive/informational text for the ink! attribute at the given position (if any).
22pub fn hover(file: &InkFile, range: TextRange, version: Version) -> Option<Hover> {
23 // Finds the covering ink! attribute for the text range (if any).
24 let covering_ink_attr = utils::covering_ink_attribute(file, range);
25
26 // Returns hover content only if the text range is covered by an ink! attribute.
27 covering_ink_attr.and_then(|ink_attr| {
28 // Finds the covered ink! attribute argument (if any).
29 let ink_arg = ink_attr
30 .args()
31 .iter()
32 .find(|arg| arg.text_range().contains_range(range));
33 match ink_arg {
34 // Returns hover content for the covered ink! attribute argument if it's valid.
35 Some(ink_arg) => {
36 let arg_attr_kind = InkAttributeKind::Arg(*ink_arg.kind());
37 let primary_attr_kind =
38 (*ink_attr.kind() != arg_attr_kind).then_some(ink_attr.kind());
39 let doc = content(&arg_attr_kind, version, primary_attr_kind);
40 (!doc.is_empty()).then(|| Hover {
41 range: ink_arg
42 .name()
43 .map(|ink_arg_name| ink_arg_name.syntax().text_range())
44 .unwrap_or_else(|| ink_arg.text_range()),
45 content: doc.to_owned(),
46 })
47 }
48 // Returns hover content based on the ink! attribute macro, ink! e2e attribute macro
49 // or "primary" ink! attribute argument for the ink! attribute.
50 None => {
51 let doc = content(ink_attr.kind(), version, None);
52 (!doc.is_empty()).then(|| Hover {
53 range: match ink_attr.kind() {
54 InkAttributeKind::Arg(_) => ink_attr
55 .ink_arg_name()
56 .map(|ink_arg_name| ink_arg_name.syntax().text_range())
57 .unwrap_or_else(|| ink_attr.syntax().text_range()),
58 InkAttributeKind::Macro(_) => ink_attr
59 .ink_macro()
60 .map(|path_segment| path_segment.syntax().text_range())
61 .unwrap_or_else(|| ink_attr.syntax().text_range()),
62 },
63 content: doc.to_owned(),
64 })
65 }
66 }
67 })
68}
69
70/// Returns documentation for the ink! attribute kind.
71pub fn content(
72 attr_kind: &InkAttributeKind,
73 version: Version,
74 primary_attr_kind: Option<&InkAttributeKind>,
75) -> &'static str {
76 match attr_kind {
77 InkAttributeKind::Arg(arg_kind) => match arg_kind {
78 InkArgKind::Abi if version.is_gte_v6() => args::ABI_DOC,
79 InkArgKind::AdditionalContracts if version.is_legacy() => {
80 args::ADDITIONAL_CONTRACTS_DOC_V4
81 }
82 InkArgKind::AdditionalContracts => args::ADDITIONAL_CONTRACTS_DOC_DEPRECATED,
83 InkArgKind::Anonymous if version.is_legacy() => args::ANONYMOUS_DOC_V4,
84 InkArgKind::Anonymous => args::ANONYMOUS_DOC,
85 // We enumerate the nested args for `backend` arg here, but, at the moment,
86 // only the "top-level" `backend` arg is ever sent even when focused on a nested arg.
87 InkArgKind::Backend
88 | InkArgKind::Node
89 | InkArgKind::Url
90 | InkArgKind::RuntimeOnly
91 | InkArgKind::Sandbox
92 if version.is_v5_0() =>
93 {
94 args::BACKEND_DOC_V5_0
95 }
96 InkArgKind::Backend
97 | InkArgKind::Node
98 | InkArgKind::Url
99 | InkArgKind::RuntimeOnly
100 | InkArgKind::Sandbox
101 if version.is_gte_v5_1() =>
102 {
103 args::BACKEND_DOC
104 }
105 InkArgKind::Constructor => args::CONSTRUCTOR_DOC,
106 InkArgKind::Decode if version.is_gte_v5() => args::DECODE_DOC,
107 InkArgKind::Default => args::DEFAULT_DOC,
108 InkArgKind::Derive => args::DERIVE_DOC,
109 InkArgKind::Encode if version.is_gte_v5() => args::ENCODE_DOC,
110 InkArgKind::Env | InkArgKind::Environment if version.is_lte_v5() => {
111 args::ENV_DOC_LTE_V5
112 }
113 InkArgKind::Env | InkArgKind::Environment => args::ENV_DOC,
114 InkArgKind::Event => args::EVENT_DOC,
115 InkArgKind::Extension if version.is_legacy() => args::EXTENSION_DOC_V4,
116 InkArgKind::Extension if version.is_v5() => args::EXTENSION_DOC,
117 InkArgKind::Extension => macros::CHAIN_EXTENSION_DOC_DEPRECATED,
118 InkArgKind::Function if version.is_v5() => args::FUNCTION_DOC,
119 InkArgKind::Function if version.is_gte_v6() => macros::CHAIN_EXTENSION_DOC_DEPRECATED,
120 InkArgKind::HandleStatus if version.is_lte_v5() => args::HANDLE_STATUS_DOC,
121 InkArgKind::HandleStatus => macros::CHAIN_EXTENSION_DOC_DEPRECATED,
122 InkArgKind::Impl => args::IMPL_DOC,
123 InkArgKind::KeepAttr if version.is_legacy() => args::KEEP_ATTR_DOC_V4,
124 InkArgKind::KeepAttr
125 if matches!(
126 primary_attr_kind,
127 Some(InkAttributeKind::Macro(InkMacroKind::E2ETest))
128 ) =>
129 {
130 args::KEEP_ATTR_E2E_DOC_DEPRECATED
131 }
132 InkArgKind::KeepAttr => args::KEEP_ATTR_DOC,
133 InkArgKind::Message => args::MESSAGE_DOC,
134 InkArgKind::Name if version.is_gte_v6() => args::NAME_DOC,
135 InkArgKind::Namespace => args::NAMESPACE_DOC,
136 InkArgKind::Packed if version.is_gte_v6() => args::PACKED_DOC,
137 InkArgKind::Payable => args::PAYABLE_DOC,
138 InkArgKind::Selector if version.is_legacy() => args::SELECTOR_DOC_V4,
139 InkArgKind::Selector => args::SELECTOR_DOC,
140 InkArgKind::SignatureTopic if version.is_gte_v5() => args::SIGNATURE_TOPIC,
141 InkArgKind::Storage => args::STORAGE_DOC,
142 InkArgKind::Topic if version.is_legacy() => args::TOPIC_DOC_V4,
143 InkArgKind::Topic => args::TOPIC_DOC,
144 InkArgKind::TypeInfo if version.is_gte_v5() => args::TYPE_INFO_DOC,
145 _ => "",
146 },
147 InkAttributeKind::Macro(macro_kind) => match macro_kind {
148 InkMacroKind::ChainExtension if version.is_legacy() => macros::CHAIN_EXTENSION_DOC_V4,
149 InkMacroKind::ChainExtension if version.is_v5() => macros::CHAIN_EXTENSION_DOC,
150 InkMacroKind::ChainExtension => macros::CHAIN_EXTENSION_DOC_DEPRECATED,
151 InkMacroKind::Contract => macros::CONTRACT_DOC,
152 InkMacroKind::ContractRef if version.is_gte_v6() => macros::CONTRACT_REF_DOC,
153 InkMacroKind::Error if version.is_gte_v6() => macros::ERROR_DOC,
154 InkMacroKind::Event if version.is_gte_v5() => macros::EVENT_DOC,
155 InkMacroKind::ScaleDerive if version.is_gte_v5() => macros::SCALE_DERIVE_DOC,
156 InkMacroKind::StorageItem => macros::STORAGE_ITEM_DOC,
157 InkMacroKind::Test => macros::TEST_DOC,
158 InkMacroKind::TraitDefinition => macros::TRAIT_DEFINITION_DOC,
159 InkMacroKind::E2ETest => macros::E2E_TEST_DOC,
160 _ => "",
161 },
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use ink_analyzer_ir::{syntax::TextSize, InkArgKind, InkMacroKind, MinorVersion};
169 use test_utils::parse_offset_at;
170
171 #[test]
172 fn hover_works() {
173 for version in [
174 Version::Legacy,
175 Version::V5(MinorVersion::Base),
176 Version::V5(MinorVersion::Latest),
177 Version::V6,
178 ] {
179 for (code, test_cases) in [
180 // (code, pat, [(text, pat_start, pat_end)]) where:
181 // code = source code,
182 // pat = substring used to find the cursor offset (see `test_utils::parse_offset_at` doc),
183 // text = text expected to be present in the hover content (represented without whitespace for simplicity),
184 // pat_start = substring used to find the start of the hover offset (see `test_utils::parse_offset_at` doc),
185 // pat_end = substring used to find the end of the hover offset (see `test_utils::parse_offset_at` doc).
186
187 // ink! attribute macros.
188 (
189 "#[ink::contract]",
190 vec![
191 (
192 Some("<-#"),
193 Some("<-#"),
194 Some((
195 InkAttributeKind::Macro(InkMacroKind::Contract),
196 Some("<-contract"),
197 Some("contract"),
198 )),
199 ),
200 (
201 Some("<-#"),
202 Some("ink"),
203 Some((
204 InkAttributeKind::Macro(InkMacroKind::Contract),
205 Some("<-contract"),
206 Some("contract"),
207 )),
208 ),
209 (
210 Some("<-contract"),
211 Some("contract"),
212 Some((
213 InkAttributeKind::Macro(InkMacroKind::Contract),
214 Some("<-contract"),
215 Some("contract"),
216 )),
217 ),
218 (
219 Some("<-#"),
220 Some("]"),
221 Some((
222 InkAttributeKind::Macro(InkMacroKind::Contract),
223 Some("<-contract"),
224 Some("contract"),
225 )),
226 ),
227 ],
228 ),
229 (
230 r#"
231 #[ink::contract(env=my::env::Types, keep_attr="foo,bar")]
232 "#,
233 vec![
234 (
235 Some("<-#"),
236 Some("<-#"),
237 Some((
238 InkAttributeKind::Macro(InkMacroKind::Contract),
239 Some("<-contract"),
240 Some("contract"),
241 )),
242 ),
243 (
244 Some("<-#"),
245 Some("ink"),
246 Some((
247 InkAttributeKind::Macro(InkMacroKind::Contract),
248 Some("<-contract"),
249 Some("contract"),
250 )),
251 ),
252 (
253 Some("<-contract"),
254 Some("contract"),
255 Some((
256 InkAttributeKind::Macro(InkMacroKind::Contract),
257 Some("<-contract"),
258 Some("contract"),
259 )),
260 ),
261 (
262 Some("<-#"),
263 Some("]"),
264 Some((
265 InkAttributeKind::Macro(InkMacroKind::Contract),
266 Some("<-contract"),
267 Some("contract"),
268 )),
269 ),
270 (
271 Some("<-env="),
272 Some("(env"),
273 Some((
274 InkAttributeKind::Arg(InkArgKind::Env),
275 Some("<-env="),
276 Some("(env"),
277 )),
278 ),
279 (
280 Some("<-my::env::Types"),
281 Some("my::env::Types"),
282 Some((
283 InkAttributeKind::Arg(InkArgKind::Env),
284 Some("<-env="),
285 Some("(env"),
286 )),
287 ),
288 (
289 Some("<-,"),
290 Some(","),
291 Some((
292 InkAttributeKind::Macro(InkMacroKind::Contract),
293 Some("<-contract"),
294 Some("contract"),
295 )),
296 ),
297 (
298 Some("<-keep_attr"),
299 Some("keep_attr"),
300 Some((
301 InkAttributeKind::Arg(InkArgKind::KeepAttr),
302 Some("<-keep_attr"),
303 Some("keep_attr"),
304 )),
305 ),
306 (
307 Some(r#"<-"foo,bar""#),
308 Some(r#""foo,bar""#),
309 Some((
310 InkAttributeKind::Arg(InkArgKind::KeepAttr),
311 Some("<-keep_attr"),
312 Some("keep_attr"),
313 )),
314 ),
315 ],
316 ),
317 (
318 "#[ink::contract_ref]",
319 vec![
320 (
321 Some("<-#"),
322 Some("<-#"),
323 if version.is_lte_v5() {
324 None
325 } else {
326 Some((
327 InkAttributeKind::Macro(InkMacroKind::ContractRef),
328 Some("<-contract_ref"),
329 Some("contract_ref"),
330 ))
331 },
332 ),
333 (
334 Some("<-#"),
335 Some("ink"),
336 if version.is_lte_v5() {
337 None
338 } else {
339 Some((
340 InkAttributeKind::Macro(InkMacroKind::ContractRef),
341 Some("<-contract_ref"),
342 Some("contract_ref"),
343 ))
344 },
345 ),
346 (
347 Some("<-contract_ref"),
348 Some("contract_ref"),
349 if version.is_lte_v5() {
350 None
351 } else {
352 Some((
353 InkAttributeKind::Macro(InkMacroKind::ContractRef),
354 Some("<-contract_ref"),
355 Some("contract_ref"),
356 ))
357 },
358 ),
359 (
360 Some("<-#"),
361 Some("]"),
362 if version.is_lte_v5() {
363 None
364 } else {
365 Some((
366 InkAttributeKind::Macro(InkMacroKind::ContractRef),
367 Some("<-contract_ref"),
368 Some("contract_ref"),
369 ))
370 },
371 ),
372 ],
373 ),
374 (
375 r#"
376 #[ink::contract_ref(abi="sol", env=my::env::Types)]
377 "#,
378 vec![
379 (
380 Some("<-#"),
381 Some("<-#"),
382 if version.is_lte_v5() {
383 None
384 } else {
385 Some((
386 InkAttributeKind::Macro(InkMacroKind::ContractRef),
387 Some("<-contract_ref"),
388 Some("contract_ref"),
389 ))
390 },
391 ),
392 (
393 Some("<-#"),
394 Some("ink"),
395 if version.is_lte_v5() {
396 None
397 } else {
398 Some((
399 InkAttributeKind::Macro(InkMacroKind::ContractRef),
400 Some("<-contract_ref"),
401 Some("contract_ref"),
402 ))
403 },
404 ),
405 (
406 Some("<-contract_ref"),
407 Some("contract_ref"),
408 if version.is_lte_v5() {
409 None
410 } else {
411 Some((
412 InkAttributeKind::Macro(InkMacroKind::ContractRef),
413 Some("<-contract_ref"),
414 Some("contract_ref"),
415 ))
416 },
417 ),
418 (
419 Some("<-#"),
420 Some("]"),
421 if version.is_lte_v5() {
422 None
423 } else {
424 Some((
425 InkAttributeKind::Macro(InkMacroKind::ContractRef),
426 Some("<-contract_ref"),
427 Some("contract_ref"),
428 ))
429 },
430 ),
431 (
432 Some("<-abi"),
433 Some("abi"),
434 if version.is_lte_v5() {
435 None
436 } else {
437 Some((
438 InkAttributeKind::Arg(InkArgKind::Abi),
439 Some("<-abi"),
440 Some("abi"),
441 ))
442 },
443 ),
444 (
445 Some(r#"<-"sol""#),
446 Some(r#""sol""#),
447 if version.is_lte_v5() {
448 None
449 } else {
450 Some((
451 InkAttributeKind::Arg(InkArgKind::Abi),
452 Some("<-abi"),
453 Some("abi"),
454 ))
455 },
456 ),
457 (
458 Some("<-,"),
459 Some(","),
460 if version.is_lte_v5() {
461 None
462 } else {
463 Some((
464 InkAttributeKind::Macro(InkMacroKind::ContractRef),
465 Some("<-contract_ref"),
466 Some("contract_ref"),
467 ))
468 },
469 ),
470 (
471 Some("<-env="),
472 Some(", env"),
473 Some((
474 InkAttributeKind::Arg(InkArgKind::Env),
475 Some("<-env="),
476 Some(", env"),
477 )),
478 ),
479 (
480 Some("<-my::env::Types"),
481 Some("my::env::Types"),
482 Some((
483 InkAttributeKind::Arg(InkArgKind::Env),
484 Some("<-env="),
485 Some(", env"),
486 )),
487 ),
488 ],
489 ),
490 (
491 "#[ink::error]",
492 vec![
493 (
494 Some("<-#"),
495 Some("<-#"),
496 if version.is_lte_v5() {
497 None
498 } else {
499 Some((
500 InkAttributeKind::Macro(InkMacroKind::Error),
501 Some("<-error"),
502 Some("error"),
503 ))
504 },
505 ),
506 (
507 Some("<-#"),
508 Some("ink"),
509 if version.is_lte_v5() {
510 None
511 } else {
512 Some((
513 InkAttributeKind::Macro(InkMacroKind::Error),
514 Some("<-error"),
515 Some("error"),
516 ))
517 },
518 ),
519 (
520 Some("<-error"),
521 Some("error"),
522 if version.is_lte_v5() {
523 None
524 } else {
525 Some((
526 InkAttributeKind::Macro(InkMacroKind::Error),
527 Some("<-error"),
528 Some("error"),
529 ))
530 },
531 ),
532 (
533 Some("<-#"),
534 Some("]"),
535 if version.is_lte_v5() {
536 None
537 } else {
538 Some((
539 InkAttributeKind::Macro(InkMacroKind::Error),
540 Some("<-error"),
541 Some("error"),
542 ))
543 },
544 ),
545 ],
546 ),
547 (
548 "#[ink_e2e::test]",
549 vec![
550 (
551 Some("<-#"),
552 Some("<-#"),
553 Some((
554 InkAttributeKind::Macro(InkMacroKind::E2ETest),
555 Some("<-test"),
556 Some("test"),
557 )),
558 ),
559 (
560 Some("<-#"),
561 Some("ink"),
562 Some((
563 InkAttributeKind::Macro(InkMacroKind::E2ETest),
564 Some("<-test"),
565 Some("test"),
566 )),
567 ),
568 (
569 Some("<-test"),
570 Some("test"),
571 Some((
572 InkAttributeKind::Macro(InkMacroKind::E2ETest),
573 Some("<-test"),
574 Some("test"),
575 )),
576 ),
577 (
578 Some("<-#"),
579 Some("]"),
580 Some((
581 InkAttributeKind::Macro(InkMacroKind::E2ETest),
582 Some("<-test"),
583 Some("test"),
584 )),
585 ),
586 ],
587 ),
588 // ink! attribute arguments.
589 (
590 "#[ink(storage)]",
591 vec![
592 (
593 Some("<-#"),
594 Some("<-#"),
595 Some((
596 InkAttributeKind::Arg(InkArgKind::Storage),
597 Some("<-storage"),
598 Some("storage"),
599 )),
600 ),
601 (
602 Some("<-#"),
603 Some("ink"),
604 Some((
605 InkAttributeKind::Arg(InkArgKind::Storage),
606 Some("<-storage"),
607 Some("storage"),
608 )),
609 ),
610 (
611 Some("<-storage"),
612 Some("storage"),
613 Some((
614 InkAttributeKind::Arg(InkArgKind::Storage),
615 Some("<-storage"),
616 Some("storage"),
617 )),
618 ),
619 (
620 Some("<-#"),
621 Some("]"),
622 Some((
623 InkAttributeKind::Arg(InkArgKind::Storage),
624 Some("<-storage"),
625 Some("storage"),
626 )),
627 ),
628 ],
629 ),
630 (
631 r#"#[ink(message, default, payable, selector=_, name = "name")]"#,
632 vec![
633 (
634 Some("<-#"),
635 Some("<-#"),
636 Some((
637 InkAttributeKind::Arg(InkArgKind::Message),
638 Some("<-message"),
639 Some("message"),
640 )),
641 ),
642 (
643 Some("<-#"),
644 Some("ink"),
645 Some((
646 InkAttributeKind::Arg(InkArgKind::Message),
647 Some("<-message"),
648 Some("message"),
649 )),
650 ),
651 (
652 Some("<-message"),
653 Some("message"),
654 Some((
655 InkAttributeKind::Arg(InkArgKind::Message),
656 Some("<-message"),
657 Some("message"),
658 )),
659 ),
660 (
661 Some("<-#"),
662 Some("]"),
663 Some((
664 InkAttributeKind::Arg(InkArgKind::Message),
665 Some("<-message"),
666 Some("message"),
667 )),
668 ),
669 (
670 Some("<-payable"),
671 Some("payable"),
672 Some((
673 InkAttributeKind::Arg(InkArgKind::Payable),
674 Some("<-payable"),
675 Some("payable"),
676 )),
677 ),
678 (
679 Some("<-selector"),
680 Some("selector"),
681 Some((
682 InkAttributeKind::Arg(InkArgKind::Selector),
683 Some("<-selector"),
684 Some("selector"),
685 )),
686 ),
687 (
688 Some("<-_"),
689 Some("_"),
690 Some((
691 InkAttributeKind::Arg(InkArgKind::Selector),
692 Some("<-selector"),
693 Some("selector"),
694 )),
695 ),
696 (
697 Some("<-name"),
698 Some("name"),
699 if version.is_lte_v5() {
700 None
701 } else {
702 Some((
703 InkAttributeKind::Arg(InkArgKind::Name),
704 Some("<-name"),
705 Some("name"),
706 ))
707 },
708 ),
709 (
710 Some(r#"<-"name""#),
711 Some(r#""name""#),
712 if version.is_lte_v5() {
713 None
714 } else {
715 Some((
716 InkAttributeKind::Arg(InkArgKind::Name),
717 Some("<-name"),
718 Some("name"),
719 ))
720 },
721 ),
722 ],
723 ),
724 (
725 "#[ink(extension=1, handle_status=true)]",
726 vec![
727 (
728 Some("<-#"),
729 Some("<-#"),
730 Some((
731 InkAttributeKind::Arg(InkArgKind::Extension),
732 Some("<-extension"),
733 Some("extension"),
734 )),
735 ),
736 (
737 Some("<-#"),
738 Some("ink"),
739 Some((
740 InkAttributeKind::Arg(InkArgKind::Extension),
741 Some("<-extension"),
742 Some("extension"),
743 )),
744 ),
745 (
746 Some("<-extension"),
747 Some("extension"),
748 Some((
749 InkAttributeKind::Arg(InkArgKind::Extension),
750 Some("<-extension"),
751 Some("extension"),
752 )),
753 ),
754 (
755 Some("<-#"),
756 Some("]"),
757 Some((
758 InkAttributeKind::Arg(InkArgKind::Extension),
759 Some("<-extension"),
760 Some("extension"),
761 )),
762 ),
763 (
764 Some("<-1"),
765 Some("1"),
766 Some((
767 InkAttributeKind::Arg(InkArgKind::Extension),
768 Some("<-extension"),
769 Some("extension"),
770 )),
771 ),
772 (
773 Some("<-handle_status"),
774 Some("handle_status"),
775 Some((
776 InkAttributeKind::Arg(InkArgKind::HandleStatus),
777 Some("<-handle_status"),
778 Some("handle_status"),
779 )),
780 ),
781 (
782 Some("<-true"),
783 Some("true"),
784 Some((
785 InkAttributeKind::Arg(InkArgKind::HandleStatus),
786 Some("<-handle_status"),
787 Some("handle_status"),
788 )),
789 ),
790 ],
791 ),
792 ] {
793 for (pat_start, pat_end, expect_result) in test_cases {
794 let range = TextRange::new(
795 TextSize::from(parse_offset_at(code, pat_start).unwrap() as u32),
796 TextSize::from(parse_offset_at(code, pat_end).unwrap() as u32),
797 );
798
799 let result = hover(&InkFile::parse(code), range, version);
800
801 assert_eq!(
802 result.as_ref().map(|hover_result| (
803 hover_result.content.as_str(),
804 hover_result.range
805 )),
806 expect_result.map(|(attr_kind, pat_start, pat_end)| (
807 content(&attr_kind, version, None),
808 TextRange::new(
809 TextSize::from(parse_offset_at(code, pat_start).unwrap() as u32),
810 TextSize::from(parse_offset_at(code, pat_end).unwrap() as u32)
811 )
812 )),
813 "code: {code}, start: {:?}, end: {:?}, version: {:?}",
814 pat_start,
815 pat_end,
816 version,
817 );
818 }
819 }
820 }
821 }
822}