1mod error_code;
4mod extension_fn;
5
6use std::collections::HashSet;
7
8use ink_analyzer_ir::ast::{AstNode, HasName};
9use ink_analyzer_ir::meta::MetaValue;
10use ink_analyzer_ir::{
11 ast, ChainExtension, Extension, Function, InkArg, InkArgKind, InkAttributeKind, InkEntity,
12 InkFile, InkMacroKind, IsChainExtensionFn, IsInkTrait, IsIntId,
13};
14
15use super::common;
16use crate::analysis::{actions::entity as entity_actions, text_edit::TextEdit, utils};
17use crate::{Action, ActionKind, Diagnostic, Severity, TextRange, Version};
18
19const SCOPE_NAME: &str = "chain extension";
20
21pub fn diagnostics(
29 results: &mut Vec<Diagnostic>,
30 chain_extension: &ChainExtension,
31 version: Version,
32) {
33 if version.is_gte_v6() {
36 return;
37 }
38
39 common::run_generic_diagnostics(results, chain_extension, version);
41
42 if version.is_v5() {
43 if let Some(diagnostic) = ensure_extension_arg(chain_extension) {
46 results.push(diagnostic);
47 }
48 }
49
50 if let Some(diagnostic) = common::ensure_trait(chain_extension, SCOPE_NAME) {
53 results.push(diagnostic);
54 }
55
56 if let Some(trait_item) = chain_extension.trait_item() {
57 common::ensure_trait_invariants(results, trait_item, SCOPE_NAME);
61 }
62
63 ensure_trait_item_invariants(results, chain_extension, version);
66
67 for item in chain_extension.extensions() {
69 extension_fn::diagnostics(results, item, version);
70 }
71
72 ensure_error_code_type_quantity(results, chain_extension);
74
75 ensure_no_overlapping_ids(results, chain_extension, version);
77
78 ensure_valid_quasi_direct_ink_descendants(results, chain_extension, version);
81
82 error_code::diagnostics(results, chain_extension, version);
84}
85
86fn ensure_extension_arg(chain_extension: &ChainExtension) -> Option<Diagnostic> {
95 chain_extension.extension_arg().is_none().then(|| {
96 let range = chain_extension
98 .ink_attr()
99 .map(|attr| attr.syntax().text_range())
100 .unwrap_or_else(|| utils::ink_trait_declaration_range(chain_extension));
101 Diagnostic {
102 message: "An ink! chain extension must have a ink! extension argument.".to_owned(),
103 range,
104 severity: Severity::Error,
105 quickfixes: chain_extension
106 .ink_attr()
107 .and_then(|attr| utils::ink_arg_insert_offset_and_affixes(attr, None))
108 .map(|(insert_offset, insert_prefix, insert_suffix)| {
109 let range = TextRange::new(insert_offset, insert_offset);
111 let suggested_id = chain_extension
113 .syntax()
114 .ancestors()
115 .last()
116 .and_then(InkFile::cast)
117 .and_then(|file| {
118 let unavailable_ids = file
119 .chain_extensions()
120 .iter()
121 .filter_map(ChainExtension::id)
122 .collect();
123 utils::suggest_unique_id(None, &unavailable_ids)
124 })
125 .unwrap_or(1);
126
127 vec![Action {
128 label: "Add ink! extension argument.".to_owned(),
129 kind: ActionKind::QuickFix,
130 range,
131 edits: vec![TextEdit::insert_with_snippet(
132 format!(
133 "{}extension = {suggested_id}{}",
134 insert_prefix.unwrap_or_default(),
135 insert_suffix.unwrap_or_default()
136 ),
137 insert_offset,
138 Some(format!(
139 "{}extension = ${{1:{suggested_id}}}{}",
140 insert_prefix.unwrap_or_default(),
141 insert_suffix.unwrap_or_default()
142 )),
143 )],
144 }]
145 }),
146 }
147 })
148}
149
150fn ensure_trait_item_invariants(
159 results: &mut Vec<Diagnostic>,
160 chain_extension: &ChainExtension,
161 version: Version,
162) {
163 let assoc_type_validator = |results: &mut Vec<Diagnostic>, type_alias: &ast::TypeAlias| {
164 let (is_named_error_code, name_marker) = match type_alias.name() {
167 Some(name) => (name.to_string() == "ErrorCode", Some(name)),
168 None => (false, None),
169 };
170
171 if !is_named_error_code {
172 results.push(Diagnostic {
173 message: "The associated type of a ink! chain extension must be named `ErrorCode`."
174 .to_owned(),
175 range: name_marker
176 .as_ref()
177 .map(|it| it.syntax().text_range())
179 .unwrap_or_else(|| utils::ink_trait_declaration_range(chain_extension)),
180 severity: Severity::Error,
181 quickfixes: name_marker.as_ref().map(|name| {
182 vec![Action {
183 label: "Rename associated type to `ErrorCode`.".to_owned(),
184 kind: ActionKind::QuickFix,
185 range: name.syntax().text_range(),
186 edits: vec![TextEdit::replace(
187 "ErrorCode".to_owned(),
188 name.syntax().text_range(),
189 )],
190 }]
191 }),
192 });
193 }
194
195 if let Some(diagnostic) =
196 common::ensure_no_generics(type_alias, "chain extension `ErrorCode` type")
197 {
198 results.push(diagnostic);
199 }
200
201 if let Some(diagnostic) = common::ensure_no_trait_bounds(
202 type_alias,
203 "Trait bounds on ink! chain extension `ErrorCode` types are not supported.",
204 ) {
205 results.push(diagnostic);
206 }
207
208 if type_alias.ty().is_none() {
209 let insert_offset = type_alias
211 .eq_token()
212 .map(|it| it.text_range().end())
213 .or_else(|| {
214 type_alias
215 .semicolon_token()
216 .map(|it| it.text_range().start())
217 })
218 .unwrap_or(type_alias.syntax().text_range().start());
219 let insert_prefix = if type_alias.eq_token().is_none() {
220 " = "
221 } else {
222 ""
223 };
224 let insert_suffix = if type_alias.semicolon_token().is_none() {
225 ";"
226 } else {
227 ""
228 };
229 results.push(Diagnostic {
230 message: "ink! chain extension `ErrorCode` types must have a default type."
231 .to_owned(),
232 range: type_alias.syntax().text_range(),
233 severity: Severity::Error,
234 quickfixes: Some(vec![Action {
235 label: "Add `ErrorCode` default type.".to_owned(),
236 kind: ActionKind::QuickFix,
237 range: type_alias.syntax().text_range(),
238 edits: vec![TextEdit::insert_with_snippet(
239 format!("{insert_prefix}(){insert_suffix}"),
240 insert_offset,
241 Some(format!("{insert_prefix}${{1:()}}{insert_suffix}")),
242 )],
243 }]),
244 });
245 }
246 };
247 macro_rules! trait_item_validator {
248 ($trait_item: ident, $entity: ty, $id_arg_kind: ident, $id_type: ty) => {
249 let mut unavailable_ids: HashSet<$id_type> = init_unavailable_ids(chain_extension, version);
251 common::ensure_trait_item_invariants(
252 results,
253 $trait_item,
254 "chain extension",
255 |results, fn_item| {
256 let extension_fn = ink_analyzer_ir::ink_attrs(fn_item.syntax())
260 .find_map(ink_analyzer_ir::ink_attr_to_entity::<$entity>);
261 if let Some(extension_fn) = extension_fn {
262 extension_fn::diagnostics(results, &extension_fn, version);
264 } else {
265 assoc_fn_validator(results, fn_item, &mut unavailable_ids, InkArgKind::$id_arg_kind)
267 }
268 },
269 assoc_type_validator,
270 );
271 };
272 }
273
274 if let Some(trait_item) = chain_extension.trait_item() {
275 if version.is_legacy() {
276 trait_item_validator!(trait_item, Extension, Extension, u32);
277 } else {
278 trait_item_validator!(trait_item, Function, Function, u16);
279 }
280 }
281
282 fn assoc_fn_validator<T>(
283 results: &mut Vec<Diagnostic>,
284 fn_item: &ast::Fn,
285 unavailable_ids: &mut HashSet<T>,
286 id_arg_kind: InkArgKind,
287 ) where
288 T: IsIntId,
289 {
290 let insert_offset = utils::first_ink_attribute_insert_offset(fn_item.syntax());
292 let suggested_id = utils::suggest_unique_id_mut(None, unavailable_ids).unwrap_or(1.into());
294 let range = utils::ast_item_declaration_range(&ast::Item::Fn(fn_item.clone()))
296 .unwrap_or(fn_item.syntax().text_range());
297
298 let is_extension_arg =
302 || ink_analyzer_ir::ink_arg_by_kind(fn_item.syntax(), InkArgKind::Extension).is_some();
303 if id_arg_kind == InkArgKind::Function && is_extension_arg() {
304 return;
305 }
306
307 results.push(Diagnostic {
308 message: format!("All ink! chain extension functions must be ink! {id_arg_kind}s."),
309 range,
310 severity: Severity::Error,
311 quickfixes: Some(vec![Action {
312 label: format!("Add ink! {id_arg_kind} attribute."),
313 kind: ActionKind::QuickFix,
314 range,
315 edits: [TextEdit::insert_with_snippet(
316 format!("#[ink({id_arg_kind} = {suggested_id})]"),
317 insert_offset,
318 Some(format!("#[ink({id_arg_kind} = ${{1:{suggested_id}}})]")),
319 )]
320 .into_iter()
321 .chain(
322 ink_analyzer_ir::ink_attrs(fn_item.syntax()).filter_map(|attr| {
323 (*attr.kind() != InkAttributeKind::Arg(id_arg_kind)
324 && *attr.kind() != InkAttributeKind::Arg(InkArgKind::HandleStatus))
325 .then(|| TextEdit::delete(attr.syntax().text_range()))
326 }),
327 )
328 .collect(),
329 }]),
330 });
331 }
332}
333
334fn ensure_error_code_type_quantity(
340 results: &mut Vec<Diagnostic>,
341 chain_extension: &ChainExtension,
342) {
343 if let Some(trait_item) = chain_extension.trait_item() {
344 if let Some(assoc_item_list) = trait_item.assoc_item_list() {
345 let error_codes: Vec<ast::TypeAlias> = assoc_item_list
346 .assoc_items()
347 .filter_map(|assoc_item| match assoc_item {
348 ast::AssocItem::TypeAlias(type_alias) => {
349 let name = type_alias.name()?;
350 (name.to_string() == "ErrorCode").then_some(type_alias)
351 }
352 _ => None,
353 })
354 .collect();
355
356 if error_codes.is_empty() {
357 results.push(Diagnostic {
359 message: "Missing `ErrorCode` associated type for ink! chain extension."
360 .to_owned(),
361 range: utils::ink_trait_declaration_range(chain_extension),
362 severity: Severity::Error,
363 quickfixes: entity_actions::add_error_code(
364 chain_extension,
365 ActionKind::QuickFix,
366 None,
367 )
368 .map(|action| vec![action]),
369 });
370 } else if error_codes.len() > 1 {
371 for item in &error_codes[1..] {
372 results.push(Diagnostic {
373 message: "Duplicate `ErrorCode` associated type for ink! chain extension."
374 .to_owned(),
375 range: item.syntax().text_range(),
376 severity: Severity::Error,
377 quickfixes: Some(vec![Action {
378 label: "Remove duplicate `ErrorCode` type for ink! chain extension."
379 .to_owned(),
380 kind: ActionKind::QuickFix,
381 range: item.syntax().text_range(),
382 edits: vec![TextEdit::delete(item.syntax().text_range())],
383 }]),
384 });
385 }
386 };
387 }
388 }
389}
390
391fn ensure_no_overlapping_ids(
395 results: &mut Vec<Diagnostic>,
396 chain_extension: &ChainExtension,
397 version: Version,
398) {
399 if version.is_legacy() {
400 let mut unavailable_ids = init_unavailable_ids(chain_extension, version);
401 ensure_no_overlapping_ids_inner::<_, u32>(
402 results,
403 chain_extension.extensions(),
404 &mut unavailable_ids,
405 version,
406 );
407 } else {
408 let mut unavailable_ids = init_unavailable_ids(chain_extension, version);
409 ensure_no_overlapping_ids_inner::<_, u16>(
410 results,
411 chain_extension.functions(),
412 &mut unavailable_ids,
413 version,
414 );
415 }
416
417 fn ensure_no_overlapping_ids_inner<E, I>(
418 results: &mut Vec<Diagnostic>,
419 extension_fns: &[E],
420 unavailable_ids: &mut HashSet<I>,
421 version: Version,
422 ) where
423 E: IsChainExtensionFn,
424 I: IsIntId,
425 {
426 let mut seen_ids: HashSet<I> = HashSet::new();
427 for extension_fn in extension_fns {
428 if let Some(id) = extension_fn.id() {
429 if seen_ids.contains(&id) {
430 let value_range_option = extension_fn
432 .id_arg()
433 .as_ref()
434 .and_then(InkArg::value)
435 .map(MetaValue::text_range);
436 results.push(Diagnostic {
437 message: format!(
438 "{} ids must be unique across all associated functions \
439 in an ink! chain extension.",
440 if version.is_legacy() {
441 "Extension"
442 } else {
443 "Function"
444 }
445 ),
446 range: value_range_option
447 .or_else(|| {
448 extension_fn
449 .ink_attr()
450 .map(|attr| attr.syntax().text_range())
451 })
452 .unwrap_or(extension_fn.syntax().text_range()),
453 severity: Severity::Error,
454 quickfixes: value_range_option
455 .zip(utils::suggest_unique_id_mut(None, unavailable_ids))
456 .map(|(range, suggested_id)| {
457 vec![Action {
458 label: format!(
459 "Replace with a unique {} id.",
460 if version.is_legacy() {
461 "extension"
462 } else {
463 "function"
464 }
465 ),
466 kind: ActionKind::QuickFix,
467 range,
468 edits: vec![TextEdit::replace_with_snippet(
469 format!("{suggested_id}"),
470 range,
471 Some(format!("${{1:{suggested_id}}}")),
472 )],
473 }]
474 }),
475 });
476 }
477
478 seen_ids.insert(id);
479 }
480 }
481 }
482}
483
484fn ensure_valid_quasi_direct_ink_descendants(
489 results: &mut Vec<Diagnostic>,
490 chain_extension: &ChainExtension,
491 version: Version,
492) {
493 common::ensure_valid_quasi_direct_ink_descendants_by_kind(
494 results,
495 chain_extension,
496 InkAttributeKind::Macro(InkMacroKind::ChainExtension),
497 version,
498 SCOPE_NAME,
499 );
500}
501
502fn init_unavailable_ids<T>(chain_extension: &ChainExtension, version: Version) -> HashSet<T>
504where
505 T: IsIntId,
506{
507 fn init_unavailable_ids_inner<E, I>(assoc_fns: &[E]) -> HashSet<I>
508 where
509 E: IsChainExtensionFn,
510 I: IsIntId,
511 {
512 assoc_fns
513 .iter()
514 .filter_map(IsChainExtensionFn::id)
515 .collect()
516 }
517
518 if version.is_legacy() {
519 init_unavailable_ids_inner(chain_extension.extensions())
520 } else {
521 init_unavailable_ids_inner(chain_extension.functions())
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use crate::test_utils::*;
529 use ink_analyzer_ir::{
530 syntax::{TextRange, TextSize},
531 MinorVersion,
532 };
533 use quote::quote;
534 use test_utils::{
535 parse_offset_at, quote_as_pretty_string, quote_as_str, TestResultAction,
536 TestResultTextRange,
537 };
538
539 fn parse_first_chain_extension(code: &str) -> ChainExtension {
540 parse_first_ink_entity_of_type(code)
541 }
542
543 macro_rules! valid_chain_extensions {
547 () => {
548 valid_chain_extensions!(v5)
549 };
550 (v4) => {
551 valid_chain_extensions!(extension, derive(scale::Encode, scale::Decode, scale_info::TypeInfo))
552 };
553 (v5) => {
554 valid_chain_extensions!(function, ink::scale_derive(Encode, Decode, TypeInfo), extension=1)
555 };
556 ($id_arg_kind: expr, $scale_derive_attr: meta $(, $macro_args: meta)?) => {
557 [
558 quote! {
560 },
562 quote! {
564 #[ink($id_arg_kind=1)]
565 fn my_extension();
566
567 #[ink($id_arg_kind=2)]
568 fn my_extension2();
569 },
570 quote! {
572 #[ink($id_arg_kind=1)]
573 fn my_extension();
574
575 #[ink($id_arg_kind=2)]
576 fn my_extension2(a: i32);
577
578 #[ink($id_arg_kind=3)]
579 fn my_extension3() -> bool;
580
581 #[ink($id_arg_kind=4)]
582 fn my_extension4(a: i32) -> bool;
583
584 #[ink($id_arg_kind=5)]
585 fn my_extension5(a: i32) -> (i32, u64, bool);
586
587 #[ink($id_arg_kind=6)]
588 fn my_extension6(a: i32, b: u64, c: [u8; 32]) -> bool;
589
590 #[ink($id_arg_kind=7)]
591 fn my_extension7(a: i32, b: u64, c: [u8; 32]) -> (i32, u64, bool);
592 },
593 quote! {
595 #[ink($id_arg_kind=1, handle_status=true)]
596 fn my_extension();
597
598 #[ink($id_arg_kind=2, handle_status=false)]
599 fn my_extension2(a: i32);
600
601 #[ink($id_arg_kind=3, handle_status=true)]
602 fn my_extension3() -> bool;
603
604 #[ink($id_arg_kind=4, handle_status=false)]
605 fn my_extension4(a: i32) -> bool;
606
607 #[ink($id_arg_kind=5, handle_status=true)]
608 fn my_extension5(a: i32) -> (i32, u64, bool);
609
610 #[ink($id_arg_kind=6, handle_status=false)]
611 fn my_extension6(a: i32, b: u64, c: [u8; 32]) -> bool;
612
613 #[ink($id_arg_kind=7, handle_status=true)]
614 fn my_extension7(a: i32, b: u64, c: [u8; 32]) -> (i32, u64, bool);
615 },
616 ]
617 .iter()
618 .flat_map(|extensions| {
619 [
620 quote! {
622 #[ink::chain_extension$(($macro_args))?]
623 pub trait MyChainExtension {
624 type ErrorCode = MyErrorCode;
625
626 #extensions
627 }
628
629 #[$scale_derive_attr]
630 pub enum MyErrorCode {
631 InvalidKey,
632 CannotWriteToKey,
633 CannotReadFromKey,
634 }
635
636 impl ink::env::chain_extension::FromStatusCode for MyErrorCode {
637 fn from_status_code(status_code: u32) -> Result<(), Self> {
638 match status_code {
639 0 => Ok(()),
640 1 => Err(Self::InvalidKey),
641 2 => Err(Self::CannotWriteToKey),
642 3 => Err(Self::CannotReadFromKey),
643 _ => panic!("encountered unknown status code"),
644 }
645 }
646 }
647 },
648 ]
649 })
650 };
651 }
652
653 #[test]
654 fn v5_extension_arg_works() {
655 for code in valid_chain_extensions!(v5) {
656 let chain_extension = parse_first_chain_extension(quote_as_str! {
657 #code
658 });
659
660 let result = ensure_extension_arg(&chain_extension);
661 assert!(result.is_none(), "chain extension: {code}");
662 }
663 }
664
665 #[test]
666 fn v5_missing_extension_arg_fails() {
667 let code = quote_as_pretty_string! {
668 #[ink::chain_extension]
669 pub trait MyChainExtension {}
670 };
671 let chain_extension = parse_first_chain_extension(&code);
672
673 let result = ensure_extension_arg(&chain_extension);
674
675 assert!(result.is_some());
677 assert_eq!(result.as_ref().unwrap().severity, Severity::Error);
678 let expected_quickfixes = [TestResultAction {
680 label: "Add ink! extension",
681 edits: vec![TestResultTextRange {
682 text: "(extension = 1)",
683 start_pat: Some("#[ink::chain_extension"),
684 end_pat: Some("#[ink::chain_extension"),
685 }],
686 }];
687 let quickfixes = result.as_ref().unwrap().quickfixes.as_ref().unwrap();
688 verify_actions(&code, quickfixes, &expected_quickfixes);
689 }
690
691 #[test]
692 fn valid_trait_properties_works() {
693 for code in valid_chain_extensions!() {
694 let chain_extension = parse_first_chain_extension(quote_as_str! {
695 #code
696 });
697
698 let mut results = Vec::new();
699 common::ensure_trait_invariants(
700 &mut results,
701 chain_extension.trait_item().unwrap(),
702 "chain extension",
703 );
704 assert!(results.is_empty(), "chain extension: {code}");
705 }
706 }
707
708 #[test]
709 fn invalid_trait_properties_fails() {
710 for (code, expected_quickfixes) in [
711 (
714 quote! {
715 trait MyChainExtension {}
716 },
717 vec![TestResultAction {
718 label: "`pub`",
719 edits: vec![TestResultTextRange {
720 text: "pub",
721 start_pat: Some("<-trait"),
722 end_pat: Some("<-trait"),
723 }],
724 }],
725 ),
726 (
727 quote! {
728 pub(crate) trait MyChainExtension {}
729 },
730 vec![TestResultAction {
731 label: "`pub`",
732 edits: vec![TestResultTextRange {
733 text: "pub",
734 start_pat: Some("<-pub(crate)"),
735 end_pat: Some("pub(crate)"),
736 }],
737 }],
738 ),
739 (
740 quote! {
741 pub(self) trait MyChainExtension {}
742 },
743 vec![TestResultAction {
744 label: "`pub`",
745 edits: vec![TestResultTextRange {
746 text: "pub",
747 start_pat: Some("<-pub(self)"),
748 end_pat: Some("pub(self)"),
749 }],
750 }],
751 ),
752 (
753 quote! {
754 pub(super) trait MyChainExtension {}
755 },
756 vec![TestResultAction {
757 label: "`pub`",
758 edits: vec![TestResultTextRange {
759 text: "pub",
760 start_pat: Some("<-pub(super)"),
761 end_pat: Some("pub(super)"),
762 }],
763 }],
764 ),
765 (
766 quote! {
767 pub(in my::path) trait MyChainExtension {}
768 },
769 vec![TestResultAction {
770 label: "`pub`",
771 edits: vec![TestResultTextRange {
772 text: "pub",
773 start_pat: Some("<-pub(in my::path)"),
774 end_pat: Some("pub(in my::path)"),
775 }],
776 }],
777 ),
778 (
781 quote! {
782 pub unsafe trait MyChainExtension {}
783 },
784 vec![TestResultAction {
785 label: "Remove `unsafe`",
786 edits: vec![TestResultTextRange {
787 text: "",
788 start_pat: Some("<-unsafe"),
789 end_pat: Some("unsafe "),
790 }],
791 }],
792 ),
793 (
796 quote! {
797 pub auto trait MyChainExtension {}
798 },
799 vec![TestResultAction {
800 label: "Remove `auto`",
801 edits: vec![TestResultTextRange {
802 text: "",
803 start_pat: Some("<-auto"),
804 end_pat: Some("auto "),
805 }],
806 }],
807 ),
808 (
811 quote! {
812 pub trait MyChainExtension<T> {}
813 },
814 vec![TestResultAction {
815 label: "Remove generic",
816 edits: vec![TestResultTextRange {
817 text: "",
818 start_pat: Some("<-<T>"),
819 end_pat: Some("<T>"),
820 }],
821 }],
822 ),
823 (
826 quote! {
827 pub trait MyChainExtension: SuperChainExtension {}
828 },
829 vec![TestResultAction {
830 label: "Remove type",
831 edits: vec![TestResultTextRange {
832 text: "",
833 start_pat: Some("<-: SuperChainExtension"),
834 end_pat: Some(": SuperChainExtension"),
835 }],
836 }],
837 ),
838 ] {
839 let code = quote_as_pretty_string! {
840 #[ink::chain_extension]
841 #code
842 };
843 let chain_extension = parse_first_chain_extension(&code);
844
845 let mut results = Vec::new();
846 common::ensure_trait_invariants(
847 &mut results,
848 chain_extension.trait_item().unwrap(),
849 "chain extension",
850 );
851
852 assert_eq!(results.len(), 1, "chain extension: {code}");
854 assert_eq!(
855 results[0].severity,
856 Severity::Error,
857 "chain extension: {code}"
858 );
859 verify_actions(
861 &code,
862 results[0].quickfixes.as_ref().unwrap(),
863 &expected_quickfixes,
864 );
865 }
866 }
867
868 #[test]
869 fn valid_trait_items_works() {
870 for (version, chain_extensions) in versioned_fixtures!(@legacy valid_chain_extensions) {
871 for code in chain_extensions {
872 let chain_extension = parse_first_chain_extension(quote_as_str! {
873 #code
874 });
875
876 let mut results = Vec::new();
877 ensure_trait_item_invariants(&mut results, &chain_extension, version);
878 assert!(
879 results.is_empty(),
880 "chain extension: {code}, version: {:?}",
881 version
882 );
883 }
884 }
885 }
886
887 #[test]
888 fn invalid_trait_items_fails() {
889 for (version, id_arg_name, id_arg_kind, macro_args) in [
890 (
891 Version::Legacy,
892 "extension",
893 quote! { extension },
894 quote! {},
895 ),
896 (
897 Version::V5(MinorVersion::Base),
898 "function",
899 quote! { function },
900 quote! { (extension=1) },
901 ),
902 ] {
903 for (items, expected_quickfixes) in [
904 (
907 quote! {
908 const T: i32;
909 },
910 vec![TestResultAction {
911 label: "Remove",
912 edits: vec![TestResultTextRange {
913 text: "",
914 start_pat: Some("<-const"),
915 end_pat: Some("i32;"),
916 }],
917 }],
918 ),
919 (
924 quote! {
925 type Type = ();
926 },
927 vec![TestResultAction {
928 label: "`ErrorCode`",
929 edits: vec![TestResultTextRange {
930 text: "ErrorCode",
931 start_pat: Some("<-Type"),
932 end_pat: Some("Type"),
933 }],
934 }],
935 ),
936 (
937 quote! {
938 type IncorrectName = ();
939 },
940 vec![TestResultAction {
941 label: "`ErrorCode`",
942 edits: vec![TestResultTextRange {
943 text: "ErrorCode",
944 start_pat: Some("<-IncorrectName"),
945 end_pat: Some("IncorrectName"),
946 }],
947 }],
948 ),
949 (
952 quote! {
953 type ErrorCode<T> = (); },
955 vec![TestResultAction {
956 label: "Remove generic",
957 edits: vec![TestResultTextRange {
958 text: "",
959 start_pat: Some("<-<T>"),
960 end_pat: Some("<T>"),
961 }],
962 }],
963 ),
964 (
965 quote! {
966 type ErrorCode: Copy = (); },
968 vec![TestResultAction {
969 label: "Remove type",
970 edits: vec![TestResultTextRange {
971 text: "",
972 start_pat: Some("<-: Copy"),
973 end_pat: Some(": Copy"),
974 }],
975 }],
976 ),
977 (
978 quote! {
979 type ErrorCode; },
981 vec![TestResultAction {
982 label: "Add",
983 edits: vec![TestResultTextRange {
984 text: "=",
985 start_pat: Some("ErrorCode"),
986 end_pat: Some("ErrorCode"),
987 }],
988 }],
989 ),
990 (
993 quote! {
994 my_macro_call!();
995 },
996 vec![TestResultAction {
997 label: "Remove macro",
998 edits: vec![TestResultTextRange {
999 text: "",
1000 start_pat: Some("<-my_macro_call"),
1001 end_pat: Some("my_macro_call!();"),
1002 }],
1003 }],
1004 ),
1005 (
1008 quote! {
1009 fn non_flagged();
1010 },
1011 vec![TestResultAction {
1012 label: "Add",
1013 edits: vec![TestResultTextRange {
1014 text: id_arg_name,
1015 start_pat: Some("<-fn"),
1016 end_pat: Some("<-fn"),
1017 }],
1018 }],
1019 ),
1020 (
1023 quote! {
1024 #[ink(#id_arg_kind=1)]
1025 fn default_implemented() {}
1026 },
1027 vec![TestResultAction {
1028 label: "Remove",
1029 edits: vec![TestResultTextRange {
1030 text: "",
1031 start_pat: Some("<-{}"),
1032 end_pat: Some("{}"),
1033 }],
1034 }],
1035 ),
1036 (
1039 quote! {
1040 #[ink(#id_arg_kind=1)]
1041 const fn const_extension();
1042 },
1043 vec![TestResultAction {
1044 label: "Remove `const`",
1045 edits: vec![TestResultTextRange {
1046 text: "",
1047 start_pat: Some("<-const"),
1048 end_pat: Some("const "),
1049 }],
1050 }],
1051 ),
1052 (
1055 quote! {
1056 #[ink(#id_arg_kind=1)]
1057 async fn async_extension();
1058 },
1059 vec![TestResultAction {
1060 label: "Remove `async`",
1061 edits: vec![TestResultTextRange {
1062 text: "",
1063 start_pat: Some("<-async"),
1064 end_pat: Some("async "),
1065 }],
1066 }],
1067 ),
1068 (
1071 quote! {
1072 #[ink(#id_arg_kind=1)]
1073 unsafe fn unsafe_extension();
1074 },
1075 vec![TestResultAction {
1076 label: "Remove `unsafe`",
1077 edits: vec![TestResultTextRange {
1078 text: "",
1079 start_pat: Some("<-unsafe"),
1080 end_pat: Some("unsafe "),
1081 }],
1082 }],
1083 ),
1084 (
1087 quote! {
1088 #[ink(#id_arg_kind=1)]
1089 extern fn extern_extension();
1090 },
1091 vec![TestResultAction {
1092 label: "Remove explicit ABI",
1093 edits: vec![TestResultTextRange {
1094 text: "",
1095 start_pat: Some("<-extern"),
1096 end_pat: Some("extern "),
1097 }],
1098 }],
1099 ),
1100 (
1103 quote! {
1104 #[ink(#id_arg_kind=1)]
1105 fn variadic_extension(...);
1106 },
1107 vec![TestResultAction {
1108 label: "un-variadic",
1109 edits: vec![TestResultTextRange {
1110 text: "",
1111 start_pat: Some("<-..."),
1112 end_pat: Some("..."),
1113 }],
1114 }],
1115 ),
1116 (
1119 quote! {
1120 #[ink(#id_arg_kind=1)]
1121 fn generic_message<T>();
1122 },
1123 vec![TestResultAction {
1124 label: "Remove generic",
1125 edits: vec![TestResultTextRange {
1126 text: "",
1127 start_pat: Some("<-<T>"),
1128 end_pat: Some("<T>"),
1129 }],
1130 }],
1131 ),
1132 (
1135 quote! {
1136 #[ink(constructor)]
1137 fn my_constructor() -> Self;
1138 },
1139 vec![TestResultAction {
1140 label: "Add",
1141 edits: vec![
1142 TestResultTextRange {
1144 text: id_arg_name,
1145 start_pat: Some("<-#[ink(constructor)]"),
1146 end_pat: Some("<-#[ink(constructor)]"),
1147 },
1148 TestResultTextRange {
1150 text: "",
1151 start_pat: Some("<-#[ink(constructor)]"),
1152 end_pat: Some("#[ink(constructor)]"),
1153 },
1154 ],
1155 }],
1156 ),
1157 (
1158 quote! {
1159 #[ink(message)]
1160 fn my_message();
1161 },
1162 vec![TestResultAction {
1163 label: "Add",
1164 edits: vec![
1165 TestResultTextRange {
1167 text: id_arg_name,
1168 start_pat: Some("<-#[ink(message)]"),
1169 end_pat: Some("<-#[ink(message)]"),
1170 },
1171 TestResultTextRange {
1173 text: "",
1174 start_pat: Some("<-#[ink(message)]"),
1175 end_pat: Some("#[ink(message)]"),
1176 },
1177 ],
1178 }],
1179 ),
1180 (
1181 quote! {
1182 #[ink(unknown)]
1183 fn unknown_fn();
1184 },
1185 vec![TestResultAction {
1186 label: "Add",
1187 edits: vec![
1188 TestResultTextRange {
1190 text: id_arg_name,
1191 start_pat: Some("<-#[ink(unknown)]"),
1192 end_pat: Some("<-#[ink(unknown)]"),
1193 },
1194 TestResultTextRange {
1196 text: "",
1197 start_pat: Some("<-#[ink(unknown)]"),
1198 end_pat: Some("#[ink(unknown)]"),
1199 },
1200 ],
1201 }],
1202 ),
1203 (
1206 quote! {
1207 #[ink(#id_arg_kind=1)]
1208 fn has_self_receiver(self);
1209 },
1210 vec![TestResultAction {
1211 label: "Remove self",
1212 edits: vec![TestResultTextRange {
1213 text: "",
1214 start_pat: Some("<-self)"),
1215 end_pat: Some("(self"),
1216 }],
1217 }],
1218 ),
1219 (
1220 quote! {
1221 #[ink(#id_arg_kind=1)]
1222 fn has_self_receiver(mut self);
1223 },
1224 vec![TestResultAction {
1225 label: "Remove self",
1226 edits: vec![TestResultTextRange {
1227 text: "",
1228 start_pat: Some("<-mut self"),
1229 end_pat: Some("mut self"),
1230 }],
1231 }],
1232 ),
1233 (
1234 quote! {
1235 #[ink(#id_arg_kind=1)]
1236 fn has_self_receiver(&self);
1237 },
1238 vec![TestResultAction {
1239 label: "Remove self",
1240 edits: vec![TestResultTextRange {
1241 text: "",
1242 start_pat: Some("<-&self"),
1243 end_pat: Some("&self"),
1244 }],
1245 }],
1246 ),
1247 (
1248 quote! {
1249 #[ink(#id_arg_kind=1)]
1250 fn has_self_receiver(&mut self);
1251 },
1252 vec![TestResultAction {
1253 label: "Remove self",
1254 edits: vec![TestResultTextRange {
1255 text: "",
1256 start_pat: Some("<-&mut self"),
1257 end_pat: Some("&mut self"),
1258 }],
1259 }],
1260 ),
1261 ] {
1262 let code = quote_as_pretty_string! {
1263 #[ink::chain_extension #macro_args]
1264 pub trait MyChainExtension {
1265 #items
1266 }
1267 };
1268 let chain_extension = parse_first_chain_extension(&code);
1269
1270 let mut results = Vec::new();
1271 ensure_trait_item_invariants(&mut results, &chain_extension, version);
1272
1273 assert_eq!(results.len(), 1, "chain extension: {items}");
1275 assert_eq!(
1276 results[0].severity,
1277 Severity::Error,
1278 "chain extension: {code}, version: {:?}",
1279 version
1280 );
1281 verify_actions(
1283 &code,
1284 results[0].quickfixes.as_ref().unwrap(),
1285 &expected_quickfixes,
1286 );
1287 }
1288 }
1289 }
1290
1291 #[test]
1292 fn one_error_code_type_works() {
1293 for code in valid_chain_extensions!() {
1294 let chain_extension = parse_first_chain_extension(quote_as_str! {
1295 #code
1296 });
1297
1298 let mut results = Vec::new();
1299 ensure_error_code_type_quantity(&mut results, &chain_extension);
1300 assert!(results.is_empty(), "chain extension: {code}");
1301 }
1302 }
1303
1304 #[test]
1305 fn multiple_error_code_types_fails() {
1306 for idx in 2..=5 {
1308 let error_code_types = (1..=idx).map(|_| {
1310 quote! {
1311 type ErrorCode = ();
1312 }
1313 });
1314
1315 let chain_extension = parse_first_chain_extension(quote_as_str! {
1317 #[ink::chain_extension]
1318 pub trait MyChainExtension {
1319 #( #error_code_types )*
1320 }
1321 });
1322
1323 let mut results = Vec::new();
1324 ensure_error_code_type_quantity(&mut results, &chain_extension);
1325 assert_eq!(results.len(), idx - 1);
1327 assert_eq!(
1329 results
1330 .iter()
1331 .filter(|item| item.severity == Severity::Error)
1332 .count(),
1333 idx - 1
1334 );
1335 for item in results {
1337 let fix = &item.quickfixes.as_ref().unwrap()[0];
1338 assert!(fix.label.contains("Remove duplicate `ErrorCode`"));
1339 assert_eq!(&fix.edits[0].text, "");
1340 }
1341 }
1342 }
1343
1344 #[test]
1345 fn missing_error_code_type_fails() {
1346 let code = quote_as_pretty_string! {
1347 #[ink::chain_extension]
1348 pub trait MyChainExtension {
1349 }
1350 };
1351 let chain_extension = parse_first_chain_extension(&code);
1352
1353 let mut results = Vec::new();
1354 ensure_error_code_type_quantity(&mut results, &chain_extension);
1355
1356 assert_eq!(results.len(), 1);
1358 assert_eq!(results[0].severity, Severity::Error);
1359 assert!(results[0].quickfixes.as_ref().unwrap()[0]
1361 .label
1362 .contains("Add `ErrorCode`"));
1363 let offset = TextSize::from(parse_offset_at(&code, Some("{")).unwrap() as u32);
1364 assert_eq!(
1365 results[0].quickfixes.as_ref().unwrap()[0].edits[0].range,
1366 TextRange::new(offset, offset)
1367 );
1368 }
1369
1370 #[test]
1371 fn non_overlapping_ids_works() {
1372 for (version, chain_extensions) in versioned_fixtures!(@legacy valid_chain_extensions) {
1373 for code in chain_extensions {
1374 let chain_extension = parse_first_chain_extension(quote_as_str! {
1375 #code
1376 });
1377
1378 let mut results = Vec::new();
1379 ensure_no_overlapping_ids(&mut results, &chain_extension, version);
1380 assert!(
1381 results.is_empty(),
1382 "chain extension: {code}, version: {:?}",
1383 version
1384 );
1385 }
1386 }
1387 }
1388
1389 #[test]
1390 fn overlapping_ids_fails() {
1392 for (version, id_arg_kind, macro_args) in [
1393 (Version::Legacy, quote! { extension }, quote! {}),
1394 (
1395 Version::V5(MinorVersion::Base),
1396 quote! { function },
1397 quote! { (extension=1) },
1398 ),
1399 ] {
1400 for code in [
1401 quote! {
1403 #[ink(#id_arg_kind=1)]
1404 fn my_extension();
1405
1406 #[ink(#id_arg_kind=1)]
1407 fn my_extension2();
1408 },
1409 quote! {
1411 #[ink(#id_arg_kind=0x1)]
1412 fn my_extension();
1413
1414 #[ink(#id_arg_kind=0x1)]
1415 fn my_extension2();
1416 },
1417 quote! {
1419 #[ink(#id_arg_kind=1)]
1420 fn my_extension();
1421
1422 #[ink(#id_arg_kind=0x1)]
1423 fn my_extension2();
1424 },
1425 ] {
1426 let chain_extension = parse_first_chain_extension(quote_as_str! {
1427 #[ink::chain_extension #macro_args]
1428 pub trait MyChainExtension {
1429 #code
1430 }
1431 });
1432
1433 let mut results = Vec::new();
1434 ensure_no_overlapping_ids(&mut results, &chain_extension, version);
1435 assert_eq!(
1437 results.len(),
1438 1,
1439 "chain extension: {code}, version: {:?}",
1440 version
1441 );
1442 assert_eq!(
1444 results[0].severity,
1445 Severity::Error,
1446 "chain extension: {code}, version: {:?}",
1447 version
1448 );
1449 let quick_fix_label = &results[0].quickfixes.as_ref().unwrap()[0].label;
1451 assert!(
1452 quick_fix_label.contains("Replace") && quick_fix_label.contains("unique"),
1453 "chain extension: {code}, version: {:?}",
1454 version
1455 );
1456 }
1457 }
1458 }
1459
1460 #[test]
1461 fn valid_quasi_direct_descendant_works() {
1462 for (version, chain_extensions) in versioned_fixtures!(@legacy valid_chain_extensions) {
1463 for code in chain_extensions {
1464 let chain_extension = parse_first_chain_extension(quote_as_str! {
1465 #code
1466 });
1467
1468 let mut results = Vec::new();
1469 ensure_valid_quasi_direct_ink_descendants(&mut results, &chain_extension, version);
1470 assert!(
1471 results.is_empty(),
1472 "chain extension: {code}, version: {:?}",
1473 version
1474 );
1475 }
1476 }
1477 }
1478
1479 #[test]
1480 fn invalid_quasi_direct_descendant_fails() {
1482 let code = quote_as_pretty_string! {
1483 #[ink::chain_extension]
1484 pub trait MyChainExtension {
1485 #[ink(constructor)]
1486 fn my_constructor() -> Self;
1487
1488 #[ink(message)]
1489 fn my_message(&self);
1490 }
1491 };
1492 let chain_extension = parse_first_chain_extension(&code);
1493
1494 for version in [Version::Legacy, Version::V5(MinorVersion::Base)] {
1495 let mut results = Vec::new();
1496 ensure_valid_quasi_direct_ink_descendants(&mut results, &chain_extension, version);
1497
1498 assert_eq!(
1500 results.len(),
1501 2,
1502 "chain extension: {code}, version: {:?}",
1503 version
1504 );
1505 assert_eq!(
1507 results
1508 .iter()
1509 .filter(|item| item.severity == Severity::Error)
1510 .count(),
1511 2,
1512 "chain extension: {code}, version: {:?}",
1513 version
1514 );
1515 let expected_quickfixes = [
1517 vec![
1518 TestResultAction {
1519 label: "Remove `#[ink(constructor)]`",
1520 edits: vec![TestResultTextRange {
1521 text: "",
1522 start_pat: Some("<-#[ink(constructor)]"),
1523 end_pat: Some("#[ink(constructor)]"),
1524 }],
1525 },
1526 TestResultAction {
1527 label: "Remove item",
1528 edits: vec![TestResultTextRange {
1529 text: "",
1530 start_pat: Some("<-#[ink(constructor)]"),
1531 end_pat: Some("fn my_constructor() -> Self;"),
1532 }],
1533 },
1534 ],
1535 vec![
1536 TestResultAction {
1537 label: "Remove `#[ink(message)]`",
1538 edits: vec![TestResultTextRange {
1539 text: "",
1540 start_pat: Some("<-#[ink(message)]"),
1541 end_pat: Some("#[ink(message)]"),
1542 }],
1543 },
1544 TestResultAction {
1545 label: "Remove item",
1546 edits: vec![TestResultTextRange {
1547 text: "",
1548 start_pat: Some("<-#[ink(message)]"),
1549 end_pat: Some("fn my_message(&self);"),
1550 }],
1551 },
1552 ],
1553 ];
1554 for (idx, item) in results.iter().enumerate() {
1555 let quickfixes = item.quickfixes.as_ref().unwrap();
1556 verify_actions(&code, quickfixes, &expected_quickfixes[idx]);
1557 }
1558 }
1559 }
1560
1561 #[test]
1562 fn compound_diagnostic_works() {
1563 for (version, chain_extensions) in versioned_fixtures!(@legacy valid_chain_extensions) {
1564 for code in chain_extensions {
1565 let chain_extension = parse_first_chain_extension(quote_as_str! {
1566 #code
1567 });
1568
1569 let mut results = Vec::new();
1570 diagnostics(&mut results, &chain_extension, version);
1571 assert!(
1572 results.is_empty(),
1573 "chain extension: {code}, version: {:?}",
1574 version
1575 );
1576 }
1577 }
1578 }
1579}