ink_analyzer/analysis/diagnostics/
chain_extension.rs

1//! ink! chain extension diagnostics.
2
3mod 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
21/// Runs all ink! chain extension diagnostics.
22///
23/// The entry point for finding ink! chain extension semantic rules is the `chain_extension` module of the `ink_ir` crate.
24///
25/// Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L201-L211>.
26///
27/// Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L188-L197>.
28pub fn diagnostics(
29    results: &mut Vec<Diagnostic>,
30    chain_extension: &ChainExtension,
31    version: Version,
32) {
33    // Chain extensions are deprecated in ink! >= 6.x
34    // Note: deprecation warnings are handled in `common::validate_entity_attributes`.
35    if version.is_gte_v6() {
36        return;
37    }
38
39    // Runs generic diagnostics, see `utils::run_generic_diagnostics` doc.
40    common::run_generic_diagnostics(results, chain_extension, version);
41
42    if version.is_v5() {
43        // For ink! v5, ensures that ink! chain extension has an ink! extension attribute argument,
44        // see `ensure_extension_arg` doc.
45        if let Some(diagnostic) = ensure_extension_arg(chain_extension) {
46            results.push(diagnostic);
47        }
48    }
49
50    // Ensures that ink! chain extension is a `trait` item, see `utils::ensure_trait` doc.
51    // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L222>.
52    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        // Ensures that ink! chain extension `trait` item satisfies all common invariants of trait-based ink! entities,
58        // see `utils::ensure_trait_invariants` doc.
59        // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L213-L254>.
60        common::ensure_trait_invariants(results, trait_item, SCOPE_NAME);
61    }
62
63    // Ensures that ink! chain extension `trait` item's associated items satisfy all invariants,
64    // see `ensure_trait_item_invariants` doc.
65    ensure_trait_item_invariants(results, chain_extension, version);
66
67    // Runs ink! extension diagnostics, see `extension_fn::diagnostics` doc.
68    for item in chain_extension.extensions() {
69        extension_fn::diagnostics(results, item, version);
70    }
71
72    // Ensures that exactly one `ErrorCode` associated type is defined, see `ensure_error_code_quantity` doc.
73    ensure_error_code_type_quantity(results, chain_extension);
74
75    // Ensures that no ink! extension ids are overlapping, see `ensure_no_overlapping_ids` doc.
76    ensure_no_overlapping_ids(results, chain_extension, version);
77
78    // Ensures that only valid quasi-direct ink! attribute descendants (i.e ink! descendants without any ink! ancestors),
79    // see `ensure_valid_quasi_direct_ink_descendants` doc.
80    ensure_valid_quasi_direct_ink_descendants(results, chain_extension, version);
81
82    // Runs ink! chain extension `ErrorCode` type diagnostics, see `error_code::diagnostics` doc.
83    error_code::diagnostics(results, chain_extension, version);
84}
85
86/// Ensures that ink! chain extension has an ink! extension attribute argument,
87/// see `ensure_extension_arg` doc.
88///
89/// Ref: <https://paritytech.github.io/ink/ink/attr.chain_extension.html#macro-attributes>
90///
91/// Ref: <https://github.com/paritytech/ink/blob/v5.0.0-rc.1/crates/ink/ir/src/ir/chain_extension.rs#L117-L121>
92///
93/// NOTE: Should only be used for ink! v5.
94fn ensure_extension_arg(chain_extension: &ChainExtension) -> Option<Diagnostic> {
95    chain_extension.extension_arg().is_none().then(|| {
96        // Text range for the diagnostic.
97        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                    // Range for the quickfix.
110                    let range = TextRange::new(insert_offset, insert_offset);
111                    // Suggested id for the extension.
112                    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
150/// Ensures that ink! chain extension is a `trait` item whose associated items satisfy all invariants.
151///
152/// See reference below for details about checked invariants.
153///
154/// Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L213-L254>.
155///
156/// See `utils::ensure_trait_item_invariants` doc for common invariants for all trait-based ink! entities that are handled by that utility.
157/// This utility also runs `extension_fn::diagnostics` on trait functions with an ink! extension attribute.
158fn 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        // Associated type invariants.
165        // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L256-L307>.
166        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                    // Defaults to the declaration range for the chain extension.
178                    .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            // Get the insert offset and affixes for the quickfix.
210            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            // Tracks already used and suggested ids for quickfixes.
250            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                    // All trait functions should be ink! extensions.
257                    // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L447-L464>.
258                    // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L467-L501>.
259                    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                        // Runs ink! function/extension diagnostics, see `extension_fn::diagnostics` doc.
263                        extension_fn::diagnostics(results, &extension_fn, version);
264                    } else {
265                        // Add diagnostic if function isn't an ink! function/extension.
266                        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        // Determines quickfix insertion offset and affixes.
291        let insert_offset = utils::first_ink_attribute_insert_offset(fn_item.syntax());
292        // Computes a unique id for the chain extension function.
293        let suggested_id = utils::suggest_unique_id_mut(None, unavailable_ids).unwrap_or(1.into());
294        // Gets the declaration range for the item.
295        let range = utils::ast_item_declaration_range(&ast::Item::Fn(fn_item.clone()))
296            .unwrap_or(fn_item.syntax().text_range());
297
298        // Suppress missing `function` arg warnings if the deprecated `extension` arg is present
299        // to reduce noise, because there will be a deprecation warning (and quickfix) added by
300        // `utils::validate_entity_attributes`.
301        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
334/// Ensures that exactly one `ErrorCode` associated type is defined.
335///
336/// Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L292-L305>.
337///
338/// Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L383-L391>.
339fn 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                // Creates diagnostic and quickfix for missing `ErrorCode` type.
358                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
391/// Ensures that no ink! extension ids are overlapping.
392///
393/// Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L292-L306>.
394fn 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                    // Determines text range for the argument value.
431                    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
484/// Ensures that only valid quasi-direct ink! attribute descendants
485/// (i.e. ink! descendants without any ink! ancestors).
486///
487/// Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L476-L487>.
488fn 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
502/// Initializes unavailable extension ids.
503fn 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    // List of valid minimal ink! chain extensions used for positive(`works`) tests for ink! chain extension verifying utilities.
544    // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L875-L888>.
545    // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L923-L940>.
546    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                // No functions.
559                quote! {
560                    // Chain extension with no functions is valid.
561                },
562                // Simple.
563                quote! {
564                    #[ink($id_arg_kind=1)]
565                    fn my_extension();
566
567                    #[ink($id_arg_kind=2)]
568                    fn my_extension2();
569                },
570                // Input + output variations.
571                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                // Handle status.
594                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                    // Simple.
621                    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        // Verifies diagnostics.
676        assert!(result.is_some());
677        assert_eq!(result.as_ref().unwrap().severity, Severity::Error);
678        // Verifies quickfixes.
679        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            // Visibility.
712            // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L539-L549>.
713            (
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            // Unsafe.
779            // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L523-L529>.
780            (
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            // Auto.
794            // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L531-L537>.
795            (
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            // Generic.
809            // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L551-L557>.
810            (
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            // Supertrait.
824            // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L559-L565>.
825            (
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            // Verifies diagnostics.
853            assert_eq!(results.len(), 1, "chain extension: {code}");
854            assert_eq!(
855                results[0].severity,
856                Severity::Error,
857                "chain extension: {code}"
858            );
859            // Verifies quickfixes.
860            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                // Const.
905                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L567-L575>.
906                (
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                // Associated type name.
920                // NOTE: default type set to `()` to test only the name.
921                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L577-L585>.
922                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L587-L620>.
923                (
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                // Associated type invariants.
950                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L587-L620>.
951                (
952                    quote! {
953                        type ErrorCode<T> = (); // generics.
954                    },
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 = (); // trait bounds.
967                    },
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; // no default type.
980                    },
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                // Macro.
991                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L622-L630>.
992                (
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                // Non-flagged function.
1006                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L632-L652>.
1007                (
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                // Default implementation.
1021                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L654-L663>.
1022                (
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                // Const function.
1037                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L665-L674>.
1038                (
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                // Async function.
1053                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L676-L685>.
1054                (
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                // Unsafe function.
1069                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L687-L696>.
1070                (
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                // Explicit ABI.
1085                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L698-L707>.
1086                (
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                // Variadic function.
1101                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L709-L718>.
1102                (
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                // Generic function.
1117                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L720-L729>.
1118                (
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                // Unsupported ink! attribute.
1133                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L731-L749>.
1134                (
1135                    quote! {
1136                        #[ink(constructor)]
1137                        fn my_constructor() -> Self;
1138                    },
1139                    vec![TestResultAction {
1140                        label: "Add",
1141                        edits: vec![
1142                            // Add ink! extension attribute.
1143                            TestResultTextRange {
1144                                text: id_arg_name,
1145                                start_pat: Some("<-#[ink(constructor)]"),
1146                                end_pat: Some("<-#[ink(constructor)]"),
1147                            },
1148                            // Remove ink! constructor attribute.
1149                            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                            // Add ink! extension/function attribute.
1166                            TestResultTextRange {
1167                                text: id_arg_name,
1168                                start_pat: Some("<-#[ink(message)]"),
1169                                end_pat: Some("<-#[ink(message)]"),
1170                            },
1171                            // Remove ink! message attribute.
1172                            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                            // Add ink! extension/function attribute.
1189                            TestResultTextRange {
1190                                text: id_arg_name,
1191                                start_pat: Some("<-#[ink(unknown)]"),
1192                                end_pat: Some("<-#[ink(unknown)]"),
1193                            },
1194                            // Remove unknown ink! attribute.
1195                            TestResultTextRange {
1196                                text: "",
1197                                start_pat: Some("<-#[ink(unknown)]"),
1198                                end_pat: Some("#[ink(unknown)]"),
1199                            },
1200                        ],
1201                    }],
1202                ),
1203                // self receiver.
1204                // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L810-L857>.
1205                (
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                // Verifies diagnostics.
1274                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                // Verifies quickfixes.
1282                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        // Tests snippets with btn 2 and 5 error code types.
1307        for idx in 2..=5 {
1308            // Creates multiple error code types.
1309            let error_code_types = (1..=idx).map(|_| {
1310                quote! {
1311                    type ErrorCode = ();
1312                }
1313            });
1314
1315            // Creates contract with multiple error code types.
1316            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            // There should be `idx-1` extraneous error code types.
1326            assert_eq!(results.len(), idx - 1);
1327            // All diagnostics should be errors.
1328            assert_eq!(
1329                results
1330                    .iter()
1331                    .filter(|item| item.severity == Severity::Error)
1332                    .count(),
1333                idx - 1
1334            );
1335            // All quickfixes should be for removal.
1336            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        // Verifies diagnostics.
1357        assert_eq!(results.len(), 1);
1358        assert_eq!(results[0].severity, Severity::Error);
1359        // Verifies quickfixes.
1360        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    // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L859-L870>.
1391    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                // Overlapping decimal.
1402                quote! {
1403                    #[ink(#id_arg_kind=1)]
1404                    fn my_extension();
1405
1406                    #[ink(#id_arg_kind=1)]
1407                    fn my_extension2();
1408                },
1409                // Overlapping hexadecimal.
1410                quote! {
1411                    #[ink(#id_arg_kind=0x1)]
1412                    fn my_extension();
1413
1414                    #[ink(#id_arg_kind=0x1)]
1415                    fn my_extension2();
1416                },
1417                // Overlapping detected across decimal and hex representations.
1418                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                // 1 error the overlapping extension id.
1436                assert_eq!(
1437                    results.len(),
1438                    1,
1439                    "chain extension: {code}, version: {:?}",
1440                    version
1441                );
1442                // All diagnostics should be errors.
1443                assert_eq!(
1444                    results[0].severity,
1445                    Severity::Error,
1446                    "chain extension: {code}, version: {:?}",
1447                    version
1448                );
1449                // Verifies quickfixes.
1450                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    // Ref: <https://github.com/paritytech/ink/blob/v4.1.0/crates/ink/ir/src/ir/chain_extension.rs#L731-L749>.
1481    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            // 1 diagnostic each for `constructor` and `message`.
1499            assert_eq!(
1500                results.len(),
1501                2,
1502                "chain extension: {code}, version: {:?}",
1503                version
1504            );
1505            // All diagnostics should be errors.
1506            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            // Verifies quickfixes.
1516            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}