mod constructor;
mod ink_impl;
mod storage;
use std::collections::HashSet;
use ink_analyzer_ir::ast::HasName;
use ink_analyzer_ir::meta::MetaValue;
use ink_analyzer_ir::syntax::{AstNode, SyntaxKind, SyntaxNode, SyntaxToken};
use ink_analyzer_ir::{
ast, Contract, InkArg, InkArgKind, InkAttributeKind, InkEntity, InkMacroKind, IsInkCallable,
IsInkFn, Message, Selector, SelectorArg, Storage,
};
use super::{common, environment, error, event, ink_e2e_test, ink_test, message};
use crate::analysis::{actions::entity as entity_actions, text_edit::TextEdit, utils};
use crate::{Action, ActionKind, Diagnostic, Severity, Version};
pub fn diagnostics(results: &mut Vec<Diagnostic>, contract: &Contract, version: Version) {
common::run_generic_diagnostics(results, contract, version);
if let Some(diagnostic) = ensure_inline_module(contract) {
results.push(diagnostic);
}
ensure_storage_quantity(results, contract);
for item in ink_analyzer_ir::ink_closest_descendants::<Storage>(contract.syntax()) {
storage::diagnostics(results, &item, version);
}
for item in contract.events() {
event::diagnostics(results, item, version);
}
for item in contract.errors() {
error::diagnostics(results, item, version);
}
for item in contract.impls() {
ink_impl::diagnostics(results, item, version, true);
}
if let Some(diagnostic) = ensure_contains_constructor(contract) {
results.push(diagnostic);
}
for item in contract.constructors() {
constructor::diagnostics(results, item, version);
}
if let Some(diagnostic) = ensure_contains_message(contract) {
results.push(diagnostic);
}
for item in contract.messages() {
message::diagnostics(results, item, version);
}
ensure_no_overlapping_selectors(results, contract);
ensure_at_most_one_wildcard_selector(results, contract);
if version.is_gte_v5() {
validate_wildcard_complement_selector(results, contract);
}
ensure_root_items(results, contract);
ensure_impl_parent_for_callables(results, contract);
for item in contract.tests() {
ink_test::diagnostics(results, item, version);
}
for item in contract.e2e_tests() {
ink_e2e_test::diagnostics(results, item, version);
}
ensure_valid_quasi_direct_ink_descendants(results, contract, version);
environment::diagnostics(results, contract, version);
}
fn ensure_inline_module(contract: &Contract) -> Option<Diagnostic> {
let declaration_range = utils::contract_declaration_range(contract);
match contract.module() {
Some(module) => module.item_list().is_none().then(|| {
let semicolon_token = contract.module().and_then(ast::Module::semicolon_token);
let quickfix_range = semicolon_token
.as_ref()
.map(SyntaxToken::text_range)
.unwrap_or_else(|| contract.syntax().text_range());
Diagnostic {
message: "The content of an ink! contract's `mod` item must be defined inline."
.to_owned(),
range: declaration_range,
severity: Severity::Error,
quickfixes: Some(vec![Action {
label: "Add inline body to ink! contract `mod`.".to_owned(),
kind: ActionKind::QuickFix,
range: declaration_range,
edits: vec![TextEdit::replace(
format!("{}{{}}", if semicolon_token.is_some() { " " } else { "" }),
quickfix_range,
)],
}]),
}
}),
None => Some(Diagnostic {
message: "ink! contracts must be inline `mod` items".to_owned(),
range: declaration_range,
severity: Severity::Error,
quickfixes: if contract.syntax().kind() == SyntaxKind::ITEM_LIST {
contract
.ink_attr()
.map(|attr| vec![Action::remove_attribute(attr)])
} else {
contract
.ink_attr()
.map(|attr| {
vec![
Action::remove_attribute(attr),
Action::remove_item(contract.syntax()),
]
})
.or_else(|| Some(vec![Action::remove_item(contract.syntax())]))
},
}),
}
}
fn ensure_storage_quantity(results: &mut Vec<Diagnostic>, contract: &Contract) {
common::ensure_exactly_one_item(
results,
&ink_analyzer_ir::ink_closest_descendants::<Storage>(contract.syntax())
.collect::<Vec<Storage>>(),
Diagnostic {
message: "Missing ink! storage definition.".to_owned(),
range: utils::contract_declaration_range(contract),
severity: Severity::Error,
quickfixes: entity_actions::add_storage(contract, ActionKind::QuickFix, None)
.map(|action| vec![action]),
},
"Only one ink! storage definition can be defined for an ink! contract.",
Severity::Error,
);
}
fn ensure_contains_constructor(contract: &Contract) -> Option<Diagnostic> {
let range = utils::contract_declaration_range(contract);
common::ensure_at_least_one_item(
contract.constructors(),
Diagnostic {
message: "At least one ink! constructor must be defined for an ink! contract."
.to_owned(),
range,
severity: Severity::Error,
quickfixes: entity_actions::add_constructor_to_contract(
contract,
ActionKind::QuickFix,
None,
)
.map(|action| vec![action]),
},
)
}
fn ensure_contains_message(contract: &Contract) -> Option<Diagnostic> {
let range = utils::contract_declaration_range(contract);
common::ensure_at_least_one_item(
contract.messages(),
Diagnostic {
message: "At least one ink! message must be defined for an ink! contract.".to_owned(),
range,
severity: Severity::Error,
quickfixes: entity_actions::add_message_to_contract(
contract,
ActionKind::QuickFix,
None,
)
.map(|action| vec![action]),
},
)
}
fn get_composed_selectors<T>(items: &[T]) -> Vec<(Selector, SyntaxNode, Option<SelectorArg>)>
where
T: IsInkCallable,
{
items
.iter()
.filter_map(|item| {
item.composed_selector()
.map(|selector| (selector, item.syntax().clone(), item.selector_arg()))
})
.collect()
}
fn ensure_no_overlapping_selectors(results: &mut Vec<Diagnostic>, contract: &Contract) {
for (selectors, name, mut unavailable_ids) in [
(
get_composed_selectors(contract.constructors()),
"constructor",
contract
.constructors()
.iter()
.filter_map(|it| it.composed_selector().map(Selector::into_be_u32))
.collect::<HashSet<u32>>(),
),
(
get_composed_selectors(contract.messages()),
"message",
contract
.messages()
.iter()
.filter_map(|it| it.composed_selector().map(Selector::into_be_u32))
.collect::<HashSet<u32>>(),
),
] {
let mut seen_selectors: HashSet<u32> = HashSet::new();
for (selector, node, selector_arg) in selectors {
let selector_value = selector.into_be_u32();
if seen_selectors.contains(&selector_value) {
let value_range_option = selector_arg
.as_ref()
.map(SelectorArg::arg)
.and_then(InkArg::value)
.map(MetaValue::text_range);
let fn_item_option = || ast::Fn::cast(node.clone());
let fn_name_option = || {
fn_item_option()
.as_ref()
.and_then(HasName::name)
};
let fn_declaration_range = || {
fn_item_option().and_then(|fn_item| {
utils::ast_item_declaration_range(&ast::Item::Fn(fn_item))
})
};
results.push(Diagnostic {
message: format!(
"Selector{} must be unique across all ink! {name}s in an ink! contract.",
match value_range_option {
Some(_) => " values",
None => "s",
}
),
range: value_range_option
.or_else(|| fn_name_option().map(|name| name.syntax().text_range()))
.or_else(fn_declaration_range)
.unwrap_or(node.text_range()),
severity: Severity::Error,
quickfixes: value_range_option
.zip(utils::suggest_unique_id_mut(None, &mut unavailable_ids))
.map(|(range, suggested_id)| {
vec![Action {
label: "Replace with a unique selector.".to_owned(),
kind: ActionKind::QuickFix,
range,
edits: vec![TextEdit::replace_with_snippet(
format!("{suggested_id}"),
range,
Some(format!("${{1:{suggested_id}}}")),
)],
}]
})
.or_else(|| {
fn_name_option().map(|name| {
vec![Action {
label: "Replace with a unique name.".to_owned(),
kind: ActionKind::QuickFix,
range: name.syntax().text_range(),
edits: vec![TextEdit::replace_with_snippet(
format!("{name}2"),
name.syntax().text_range(),
Some(format!("${{1:{name}2}}")),
)],
}]
})
}),
});
}
seen_selectors.insert(selector_value);
}
}
}
fn get_selector_args<T>(items: &[T]) -> Vec<SelectorArg>
where
T: InkEntity,
{
items
.iter()
.flat_map(|item| {
item.tree()
.ink_args_by_kind(InkArgKind::Selector)
.filter_map(SelectorArg::cast)
})
.collect()
}
fn ensure_at_most_one_wildcard_selector(results: &mut Vec<Diagnostic>, contract: &Contract) {
for (selectors, name) in [
(get_selector_args(contract.constructors()), "constructor"),
(get_selector_args(contract.messages()), "message"),
] {
let mut has_seen_wildcard = false;
for selector in selectors {
if selector.is_wildcard() {
if has_seen_wildcard {
let range = utils::ink_arg_and_delimiter_removal_range(selector.arg(), None);
results.push(Diagnostic {
message: format!("At most one wildcard (`_`) selector can be defined across all ink! {name}s in an ink! contract."),
range: selector.text_range(),
severity: Severity::Error,
quickfixes: Some(vec![Action {
label: "Remove wildcard selector.".to_owned(),
kind: ActionKind::QuickFix,
range,
edits: vec![TextEdit::delete(range)],
}]),
});
} else {
has_seen_wildcard = true;
}
}
}
}
}
fn validate_wildcard_complement_selector(results: &mut Vec<Diagnostic>, contract: &Contract) {
let has_wildcard_selector = |message: &Message| {
message
.tree()
.ink_args_by_kind(InkArgKind::Selector)
.any(|arg| arg.value().is_some_and(MetaValue::is_wildcard))
};
let has_wildcard_complement_selector = |message: &Message| {
message
.tree()
.ink_args_by_kind(InkArgKind::Selector)
.any(|arg| {
SelectorArg::cast(arg)
.as_ref()
.is_some_and(SelectorArg::is_wildcard_complement)
})
};
let wildcard_exists = contract.messages().iter().any(has_wildcard_selector);
let wildcard_complement_exists = contract
.messages()
.iter()
.any(has_wildcard_complement_selector);
if !wildcard_exists && !wildcard_complement_exists {
return;
}
let mut other_messages = contract
.messages()
.iter()
.filter(|message| !has_wildcard_selector(message))
.peekable();
if wildcard_exists && other_messages.peek().is_none() {
let range = utils::contract_declaration_range(contract);
results.push(Diagnostic {
message: "An ink! contract that contains a wildcard (`_`) selector must also define \
exactly one other message annotated with a wildcard complement (`@`) selector."
.to_owned(),
range,
severity: Severity::Error,
quickfixes: entity_actions::add_message_selector_to_contract(
contract,
ActionKind::QuickFix,
"@",
"handler",
None,
Some("Add wildcard complement selector.".to_owned()),
)
.map(|action| vec![action]),
});
return;
}
let remove_selector_quickfix = |selector: &SelectorArg| {
let range = utils::ink_arg_and_delimiter_removal_range(selector.arg(), None);
Action {
label: "Remove wildcard complement selector.".to_owned(),
kind: ActionKind::QuickFix,
range,
edits: vec![TextEdit::delete(range)],
}
};
let mut has_seen_wildcard_complement = false;
for message in other_messages {
let wildcard_complement_selector = message
.tree()
.ink_args_by_kind(InkArgKind::Selector)
.filter_map(SelectorArg::cast)
.find(SelectorArg::is_wildcard_complement);
if wildcard_exists {
if let Some(selector) = &wildcard_complement_selector {
if has_seen_wildcard_complement {
results.push(Diagnostic {
message: "At most one wildcard complement (`@`) selector can be defined \
in an ink! contract that contains a wildcard (`_`) selector."
.to_owned(),
range: selector.text_range(),
severity: Severity::Error,
quickfixes: Some(vec![Action::remove_item(message.syntax())]),
});
}
} else {
let decl_range = || {
message
.fn_item()
.and_then(|fn_item| {
utils::ast_item_declaration_range(&ast::Item::Fn(fn_item.clone()))
})
.unwrap_or_else(|| message.syntax().text_range())
};
let quickfix = if wildcard_complement_exists {
Some(Action::remove_item(message.syntax()))
} else if let Some(selector) = message.selector_arg().as_ref().map(SelectorArg::arg)
{
Some(Action {
label: "Replace with wildcard complement selector.".to_owned(),
kind: ActionKind::QuickFix,
range: selector.text_range(),
edits: vec![TextEdit::replace(
"selector = @".to_owned(),
selector.text_range(),
)],
})
} else {
message.ink_attr().and_then(|ink_attr| {
utils::ink_arg_insert_offset_and_affixes(ink_attr, None).map(
|(insert_offset, insert_prefix, insert_suffix)| Action {
label: "Add wildcard complement selector.".to_owned(),
kind: ActionKind::QuickFix,
range: decl_range(),
edits: vec![TextEdit::insert(
format!(
"{}selector = @{}",
insert_prefix.unwrap_or_default(),
insert_suffix.unwrap_or_default()
),
insert_offset,
)],
},
)
})
};
results.push(Diagnostic {
message: "Exactly one other message that must be annotated with \
a wildcard complement (`@`) selector can be defined in an ink! contract \
that contains a wildcard (`_`) selector."
.to_owned(),
range: message
.selector_arg()
.as_ref()
.map(SelectorArg::arg)
.map(InkArg::text_range)
.unwrap_or_else(decl_range),
severity: Severity::Error,
quickfixes: quickfix.map(|action| vec![action]),
});
}
} else if let Some(selector) = &wildcard_complement_selector {
results.push(Diagnostic {
message: "The wildcard complement (`@`) selector can only used \
if another message with a wildcard (`_`) is defined."
.to_owned(),
range: selector.text_range(),
severity: Severity::Error,
quickfixes: Some(vec![remove_selector_quickfix(selector)]),
});
}
has_seen_wildcard_complement |= wildcard_complement_selector.is_some();
}
}
fn ensure_parent_contract<T>(
contract: &Contract,
item: &T,
ink_scope_name: &str,
) -> Option<Diagnostic>
where
T: InkEntity,
{
let is_parent = match ink_analyzer_ir::ink_parent::<Contract>(item.syntax()) {
Some(parent_contract) => parent_contract.syntax() == contract.syntax(),
None => false,
};
(!is_parent).then(|| Diagnostic {
message: format!(
"ink! {ink_scope_name}s must be defined in the root of the ink! contract's `mod` item."
),
range: item.syntax().text_range(),
severity: Severity::Error,
quickfixes: contract
.module()
.and_then(ast::Module::item_list)
.map(|item_list| {
vec![Action::move_item(
item.syntax(),
utils::item_insert_offset_by_scope_name(&item_list, ink_scope_name),
"Move item to the root of the ink! contract's `mod` item.".to_owned(),
Some(utils::item_children_indenting(contract.syntax()).as_str()),
)]
}),
})
}
fn ensure_root_items(results: &mut Vec<Diagnostic>, contract: &Contract) {
results.extend(
ink_analyzer_ir::ink_closest_descendants::<Storage>(contract.syntax())
.filter_map(|item| ensure_parent_contract(contract, &item, "storage"))
.chain(
contract
.events()
.iter()
.filter_map(|item| ensure_parent_contract(contract, item, "event")),
)
.chain(
contract
.impls()
.iter()
.filter_map(|item| ensure_parent_contract(contract, item, "impl")),
),
);
}
fn ensure_impl_parent_for_callables(results: &mut Vec<Diagnostic>, contract: &Contract) {
for diagnostic in contract
.constructors()
.iter()
.filter_map(|item| common::ensure_impl_parent(item, "constructor"))
.chain(
contract
.messages()
.iter()
.filter_map(|item| common::ensure_impl_parent(item, "message")),
)
{
results.push(diagnostic);
}
}
fn ensure_valid_quasi_direct_ink_descendants(
results: &mut Vec<Diagnostic>,
contract: &Contract,
version: Version,
) {
common::ensure_valid_quasi_direct_ink_descendants_by_kind(
results,
contract,
InkAttributeKind::Macro(InkMacroKind::Contract),
version,
"contract",
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use ink_analyzer_ir::MinorVersion;
use quote::{format_ident, quote};
use test_utils::{quote_as_pretty_string, quote_as_str, TestResultAction, TestResultTextRange};
fn parse_first_contract(code: &str) -> Contract {
parse_first_ink_entity_of_type(code)
}
macro_rules! valid_contracts_versioned {
(v4) => {
[
quote! {
mod my_contract {
#[ink(storage)]
pub struct MyContract {}
impl MyContract {
#[ink(constructor, selector=_)]
pub fn new() -> Self {}
#[ink(message, selector=_)]
pub fn message(&self) {}
}
}
},
quote! {
mod my_contract {
#[ink(storage)]
pub struct MyContract {}
impl MyContract {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(constructor, selector=_)]
pub fn new2() -> Self {}
#[ink(constructor, selector=3)]
pub fn new3() -> Self {}
#[ink(constructor, selector=0x4)]
pub fn new4() -> Self {}
#[ink(message)]
pub fn message(&self) {}
#[ink(message, selector=_)]
pub fn message2(&self) {}
#[ink(message, selector=3)]
pub fn message3(&self) {}
#[ink(message, selector=0x4)]
pub fn message4(&self) {}
}
}
},
]
.into_iter()
.chain(valid_contracts_versioned!(@lte v5))
};
(v5) => {
valid_contracts_versioned!(@lte v5)
.into_iter()
.chain(valid_contracts_versioned!(@gte v5))
};
(v6) => {
[
quote! {
mod my_contract {
#[ink(storage)]
pub struct MyContract {}
#[ink::error]
pub struct Error;
impl MyContract {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message)]
pub fn message(&self) {}
}
}
},
quote! {
mod my_contract {
#[ink(storage)]
pub struct MyContract {}
#[ink::error]
pub enum Error {
Minor,
Criticial
}
impl MyContract {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message)]
pub fn message(&self) {}
}
}
}
]
.into_iter()
.chain(valid_contracts_versioned!(@gte v5))
};
(@lte v5) => {
[
quote! {
mod my_contract {
#[ink(storage)]
pub struct MyContract {}
impl MyContract {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message, payable)]
pub fn message(&self) {}
}
}
},
]
};
(@gte v5) => {
[
quote! {
mod my_contract {
#[ink(storage)]
pub struct MyContract {}
impl MyContract {
#[ink(constructor, selector=_)]
pub fn new() -> Self {}
#[ink(message, selector=_)]
pub fn fallback(&self) {}
#[ink(message, selector=@)]
pub fn handler(&self) {}
}
}
},
quote! {
mod my_contract {
#[ink(storage)]
pub struct MyContract {}
impl MyContract {
#[ink(constructor, selector=_)]
pub fn new() -> Self {}
#[ink(message, selector=_)]
pub fn fallback(&self) {}
#[ink(message, selector=0x9BAE9D5E)]
pub fn handler(&self) {}
}
}
},
quote! {
mod my_contract {
#[ink(storage)]
pub struct MyContract {}
impl MyContract {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(constructor, selector=_)]
pub fn new2() -> Self {}
#[ink(constructor, selector=3)]
pub fn new3() -> Self {}
#[ink(constructor, selector=0x4)]
pub fn new4() -> Self {}
#[ink(message, selector=_)]
pub fn fallback(&self) {}
#[ink(message, selector=@)]
pub fn handler(&self) {}
}
}
},
]
};
}
macro_rules! valid_contracts {
() => {
valid_contracts!(v6)
};
($version: tt) => {
[
quote! {
mod minimal {
#[ink(storage)]
pub struct Minimal {}
impl Minimal {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message)]
pub fn minimal_message(&self) {}
}
}
},
quote! {
mod minimal {
#[ink(storage)]
pub struct Minimal {}
impl Minimal {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message)]
pub fn minimal_message(&self) {}
#[ink(message)]
pub fn minimal_message_mut(&mut self) {}
}
}
},
quote! {
mod minimal {
#[ink(storage)]
pub struct Minimal {}
#[ink(event, anonymous)]
pub struct MinimalEvent {
#[ink(topic)]
value: i32,
}
impl Minimal {
#[ink(constructor, payable, default, selector=1)]
pub fn new() -> Self {}
#[ink(message, default, selector=1)]
pub fn minimal_message(&self) {}
#[ink(message, payable, selector=2)]
pub fn payble_message(&mut self) {}
}
}
},
quote! {
mod minimal {
#[ink(storage)]
pub struct Minimal {}
#[ink(event, anonymous)]
pub struct MinimalEvent {
#[ink(topic)]
value: i32,
}
impl Minimal {
#[ink(constructor, payable, default, selector=0x1)]
pub fn new() -> Self {}
#[ink(message, default, selector=0x1)]
pub fn minimal_message(&self) {}
#[ink(message, payable, selector=0x2)]
pub fn payble_message(&mut self) {}
}
}
},
quote! {
mod minimal {
#[ink(storage)]
pub struct Minimal {}
#[ink(event, anonymous)]
pub struct MinimalEvent {
#[ink(topic)]
value: i32,
}
impl Minimal {
#[ink(constructor, payable, default, selector=1)]
pub fn new() -> Self {}
#[ink(constructor, payable, selector=2)]
pub fn new2() -> Self {}
#[ink(message, default, selector=1)]
pub fn minimal_message(&self) {}
#[ink(message, payable, selector=2)]
pub fn minimal_message2(&mut self) {}
}
}
},
quote! {
mod minimal {
#[ink(storage)]
pub struct Minimal {}
#[ink(event, anonymous)]
pub struct MinimalEvent {
#[ink(topic)]
value: i32,
}
impl Minimal {
#[ink(constructor, payable, default, selector=0x1)]
pub fn new() -> Self {}
#[ink(constructor, payable, selector=0x2)]
pub fn new2() -> Self {}
#[ink(message, default, selector=0x1)]
pub fn minimal_message(&self) {}
#[ink(message, payable, selector=0x2)]
pub fn minimal_message2(&mut self) {}
}
}
},
quote! {
mod minimal {
#[ink(storage)]
pub struct Minimal {}
#[ink(event, anonymous)]
pub struct MinimalEvent {
#[ink(topic)]
value: i32,
}
impl Minimal {
#[ink(constructor, payable, default)]
pub fn new() -> Self {}
#[ink(constructor, payable)]
pub fn new2() -> Self {}
#[ink(constructor, payable, selector=3)]
pub fn new3() -> Self {}
#[ink(constructor, payable, selector=0x4)]
pub fn new4() -> Self {}
#[ink(message, default)]
pub fn minimal_message(&self) {}
#[ink(message, payable)]
pub fn minimal_message2(&mut self) {}
#[ink(message, selector=3)]
pub fn minimal_message3(&self) {}
#[ink(message, payable, selector=0x4)]
pub fn minimal_message4(&mut self) {}
}
impl MyTrait for Minimal {
#[ink(message)]
fn minimal_message5(&self) {}
}
impl ::my_full::long_path::MyTrait for Minimal {
#[ink(message, payable)]
fn minimal_message6(&mut self) {}
}
impl relative_path::MyTrait for Minimal {
#[ink(message)]
fn minimal_message7(&self) {}
}
#[ink(namespace="my_namespace")]
impl Minimal {
#[ink(constructor)]
pub fn new8() -> Self {}
#[ink(message)]
pub fn minimal_message8(&self) {}
}
#[ink(impl)]
impl Minimal {
#[ink(constructor)]
pub fn new9() -> Self {}
#[ink(message)]
pub fn minimal_message9(&self) {}
}
#[ink(impl, namespace="my_namespace")]
impl Minimal {
#[ink(constructor)]
pub fn new10() -> Self {}
#[ink(message, payable)]
pub fn minimal_message10(&mut self) {}
}
#[ink(impl)]
impl Minimal {
}
}
},
quote! {
mod minimal {
#[ink(storage)]
pub struct Minimal {}
impl Minimal {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message)]
pub fn minimal_message(&self) {}
}
#[cfg(test)]
mod tests {
#[ink::test]
fn it_works() {
}
}
}
},
quote! {
mod flipper {
#[ink(storage)]
pub struct Flipper {
value: bool,
}
impl Default for Flipper {
#[ink(constructor)]
fn default() -> Self {
Self { value: false }
}
}
impl Flipper {
#[ink(message)]
pub fn flip(&mut self) {
self.value = !self.value
}
#[ink(message)]
pub fn get(&self) -> bool {
self.value
}
}
}
},
quote! {
mod flipper {
#[ink(storage)]
pub struct Flipper {
value: bool,
}
#[ink(event, anonymous)]
pub struct Flip {
#[ink(topic)]
flipped: bool,
}
impl Default for Flipper {
#[ink(constructor, payable, default, selector=1)]
fn default() -> Self {
Self { value: false }
}
}
impl Flipper {
#[ink(message, payable, default, selector=1)]
pub fn flip(&mut self) {
self.value = !self.value
}
#[ink(message, selector=2)]
pub fn get(&self) -> bool {
self.value
}
}
}
},
]
.into_iter()
.chain(valid_contracts_versioned!($version))
.flat_map(|code| {
let env = quote! {
#[derive(Clone)]
pub struct MyEnvironment;
impl ink::env::Environment for MyEnvironment {
const MAX_EVENT_TOPICS: usize = 3;
type AccountId = [u8; 16];
type Balance = u128;
type Hash = [u8; 32];
type Timestamp = u64;
type BlockNumber = u32;
type ChainExtension = ::ink::env::NoChainExtension;
}
};
[
quote! {
#[ink::contract]
#code
},
quote! {
#[ink::contract(env=crate::MyEnvironment)]
#code
#env
},
quote! {
#[ink::contract(keep_attr="foo,bar")]
#code
},
quote! {
#[ink::contract(env=crate::MyEnvironment, keep_attr="foo,bar")]
#code
#env
},
]
})
};
}
#[test]
fn inline_mod_works() {
for code in valid_contracts!() {
let contract = parse_first_contract(quote_as_str! {
#code
});
let result = ensure_inline_module(&contract);
assert!(result.is_none(), "contract: {code}");
}
}
#[test]
fn out_of_line_mod_fails() {
let code = quote_as_pretty_string! {
#[ink::contract]
mod my_contract;
};
let contract = parse_first_contract(&code);
let result = ensure_inline_module(&contract);
assert!(result.is_some());
assert_eq!(result.as_ref().unwrap().severity, Severity::Error);
let expected_quickfixes = [TestResultAction {
label: "inline body",
edits: vec![TestResultTextRange {
text: "{}",
start_pat: Some("<-;"),
end_pat: Some(";"),
}],
}];
let quickfixes = result.as_ref().unwrap().quickfixes.as_ref().unwrap();
verify_actions(&code, quickfixes, &expected_quickfixes);
}
#[test]
fn non_mod_fails() {
for code in [
quote! {
fn my_contract() {
}
},
quote! {
struct MyContract {}
},
quote! {
enum MyContract {
}
},
quote! {
trait MyContract {
}
},
] {
let code = quote_as_pretty_string! {
#[ink::contract]
#code
};
let contract = parse_first_contract(&code);
let result = ensure_inline_module(&contract);
assert!(result.is_some(), "contract: {code}");
assert_eq!(
result.as_ref().unwrap().severity,
Severity::Error,
"contract: {code}"
);
let expected_quickfixes = vec![
TestResultAction {
label: "Remove `#[ink::contract]`",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-#[ink::contract]"),
end_pat: Some("#[ink::contract]"),
}],
},
TestResultAction {
label: "Remove item",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-#[ink::contract]"),
end_pat: Some("}"),
}],
},
];
let quickfixes = result.as_ref().unwrap().quickfixes.as_ref().unwrap();
verify_actions(&code, quickfixes, &expected_quickfixes);
}
}
#[test]
fn one_storage_item_works() {
for code in valid_contracts!() {
let contract = parse_first_contract(quote_as_str! {
#code
});
let mut results = Vec::new();
ensure_storage_quantity(&mut results, &contract);
assert!(results.is_empty(), "contract: {code}");
}
}
#[test]
fn missing_storage_fails() {
let code = quote_as_pretty_string! {
#[ink::contract]
mod my_contract {
}
};
let contract = parse_first_contract(&code);
let mut results = Vec::new();
ensure_storage_quantity(&mut results, &contract);
assert_eq!(results.len(), 1);
assert_eq!(results[0].severity, Severity::Error);
let expected_quickfixes = vec![TestResultAction {
label: "Add",
edits: vec![TestResultTextRange {
text: "#[ink(storage)]",
start_pat: Some("mod my_contract {"),
end_pat: Some("mod my_contract {"),
}],
}];
let quickfixes = results[0].quickfixes.as_ref().unwrap();
verify_actions(&code, quickfixes, &expected_quickfixes);
}
#[test]
fn multiple_storage_items_fails() {
for idx in 2..=5 {
let storage_items = (1..=idx).map(|i| {
let name = format_ident!("MyContract{}", i);
quote! {
#[ink(storage)]
pub struct #name {
}
}
});
let contract = parse_first_contract(quote_as_str! {
#[ink::contract]
mod my_contract {
#( #storage_items )*
}
});
let mut results = Vec::new();
ensure_storage_quantity(&mut results, &contract);
assert_eq!(results.len(), idx - 1);
assert_eq!(
results
.iter()
.filter(|item| item.severity == Severity::Error)
.count(),
idx - 1
);
for item in results {
let fix = &item.quickfixes.as_ref().unwrap()[0];
assert!(fix.label.contains("Remove"));
for edit in &fix.edits {
assert!(edit.text.is_empty());
}
}
}
}
#[test]
fn one_or_multiple_constructors_works() {
for code in valid_contracts!() {
let contract = parse_first_contract(quote_as_str! {
#code
});
let result = ensure_contains_constructor(&contract);
assert!(result.is_none(), "contract: {code}");
}
}
#[test]
fn missing_constructor_fails() {
let code = quote_as_pretty_string! {
#[ink::contract]
mod my_contract {
}
};
let contract = parse_first_contract(&code);
let result = ensure_contains_constructor(&contract);
assert!(result.is_some());
assert_eq!(result.as_ref().unwrap().severity, Severity::Error);
let expected_quickfixes = vec![TestResultAction {
label: "Add",
edits: vec![TestResultTextRange {
text: "#[ink(constructor)]",
start_pat: Some("mod my_contract {"),
end_pat: Some("mod my_contract {"),
}],
}];
let quickfixes = result.as_ref().unwrap().quickfixes.as_ref().unwrap();
verify_actions(&code, quickfixes, &expected_quickfixes);
}
#[test]
fn one_or_multiple_messages_works() {
for code in valid_contracts!() {
let contract = parse_first_contract(quote_as_str! {
#code
});
let result = ensure_contains_message(&contract);
assert!(result.is_none(), "contract: {code}");
}
}
#[test]
fn missing_message_fails() {
let code = quote_as_pretty_string! {
#[ink::contract]
mod my_contract {
}
};
let contract = parse_first_contract(&code);
let result = ensure_contains_message(&contract);
assert!(result.is_some());
assert_eq!(result.as_ref().unwrap().severity, Severity::Error);
let expected_quickfixes = vec![TestResultAction {
label: "Add",
edits: vec![TestResultTextRange {
text: "#[ink(message)]",
start_pat: Some("mod my_contract {"),
end_pat: Some("mod my_contract {"),
}],
}];
let quickfixes = result.as_ref().unwrap().quickfixes.as_ref().unwrap();
verify_actions(&code, quickfixes, &expected_quickfixes);
}
#[test]
fn non_overlapping_selectors_works() {
for code in valid_contracts!() {
let contract = parse_first_contract(quote_as_str! {
#code
});
let mut results = Vec::new();
ensure_no_overlapping_selectors(&mut results, &contract);
assert!(results.is_empty(), "contract: {code}");
}
}
#[test]
fn overlapping_selectors_fails() {
for code in [
quote! {
#[ink(constructor, selector=1)]
pub fn my_constructor() -> Self {
}
#[ink(constructor, selector=1)]
pub fn my_constructor2() -> Self {
}
#[ink(message, selector=2)]
pub fn my_message(&mut self) {
}
#[ink(message, selector=2)]
pub fn my_message2(&mut self) {
}
},
quote! {
#[ink(constructor, selector=0xA)]
pub fn my_constructor() -> Self {
}
#[ink(constructor, selector=0xA)]
pub fn my_constructor2() -> Self {
}
#[ink(message, selector=0xB)]
pub fn my_message(&mut self) {
}
#[ink(message, selector=0xB)]
pub fn my_message2(&mut self) {
}
},
quote! {
#[ink(constructor, selector=10)]
pub fn my_constructor() -> Self {
}
#[ink(constructor, selector=0xA)]
pub fn my_constructor2() -> Self {
}
#[ink(message, selector=11)]
pub fn my_message(&mut self) {
}
#[ink(message, selector=0xB)]
pub fn my_message2(&mut self) {
}
},
]
.iter()
.map(|item| {
quote! {
impl MyContract {
#item
}
}
})
.chain([
quote! {
impl first::MyTrait for MyContract {
#[ink(constructor)]
fn my_constructor() -> Self {}
#[ink(message)]
fn my_message(&self) {}
}
impl second::MyTrait for MyContract {
#[ink(constructor)]
fn my_constructor() -> Self {}
#[ink(message)]
fn my_message(&self) {}
}
},
]) {
let contract = parse_first_contract(quote_as_str! {
#[ink::contract]
mod my_contract {
#code
}
});
let mut results = Vec::new();
ensure_no_overlapping_selectors(&mut results, &contract);
assert_eq!(results.len(), 2);
assert_eq!(
results
.iter()
.filter(|item| item.severity == Severity::Error)
.count(),
2
);
if let Some(quickfixes) = &results[0].quickfixes {
for fix in quickfixes {
assert!(
fix.label.contains("Replace")
&& (fix.label.contains("unique selector")
|| fix.label.contains("unique name"))
);
}
}
}
}
#[test]
fn one_or_no_wildcard_selectors_works() {
for code in valid_contracts!() {
let contract = parse_first_contract(quote_as_str! {
#code
});
let mut results = Vec::new();
ensure_at_most_one_wildcard_selector(&mut results, &contract);
assert!(results.is_empty(), "contract: {code}");
}
}
#[test]
fn multiple_wildcard_selectors_fails() {
let code = quote_as_pretty_string! {
#[ink::contract]
mod my_contract {
impl MyContract {
#[ink(constructor, selector = _)]
pub fn my_constructor() -> Self {
}
#[ink(constructor, selector = _)]
pub fn my_constructor2() -> Self {
}
#[ink(message, selector = _)]
pub fn my_message(&mut self) {
}
#[ink(message, selector = _)]
pub fn my_message2(&mut self) {
}
}
}
};
let contract = parse_first_contract(&code);
let mut results = Vec::new();
ensure_at_most_one_wildcard_selector(&mut results, &contract);
assert_eq!(results.len(), 2);
assert_eq!(
results
.iter()
.filter(|item| item.severity == Severity::Error)
.count(),
2
);
let expected_quickfixes = [
vec![TestResultAction {
label: "Remove wildcard",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-, selector = _)]\n pub fn my_constructor2"),
end_pat: Some("<-)]\n pub fn my_constructor2"),
}],
}],
vec![TestResultAction {
label: "Remove wildcard",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-, selector = _)]\n pub fn my_message2"),
end_pat: Some("<-)]\n pub fn my_message2"),
}],
}],
];
for (idx, item) in results.iter().enumerate() {
let quickfixes = item.quickfixes.as_ref().unwrap();
verify_actions(&code, quickfixes, &expected_quickfixes[idx]);
}
}
#[test]
fn valid_wildcard_complement_selectors_works() {
for code in valid_contracts!() {
let contract = parse_first_contract(quote_as_str! {
#code
});
let mut results = Vec::new();
validate_wildcard_complement_selector(&mut results, &contract);
assert!(results.is_empty(), "contract: {code}");
}
}
#[test]
fn invalid_or_missing_wildcard_complement_selectors_fails() {
for (items, expected_quickfixes) in [
(
quote! {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message, selector = _)]
pub fn fallback() {}
},
vec![vec![TestResultAction {
label: "Add wildcard complement",
edits: vec![TestResultTextRange {
text: ", selector = @",
start_pat: Some("pub fn fallback() {}"),
end_pat: Some("pub fn fallback() {}"),
}],
}]],
),
(
quote! {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message, selector = _)]
pub fn fallback() {}
#[ink(message)]
pub fn handler() {}
},
vec![vec![TestResultAction {
label: "Add wildcard complement",
edits: vec![TestResultTextRange {
text: ", selector = @",
start_pat: Some("#[ink(message->"),
end_pat: Some("#[ink(message->"),
}],
}]],
),
(
quote! {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message, selector = _)]
pub fn fallback() {}
#[ink(message, selector = 0x1)]
pub fn handler() {}
},
vec![vec![TestResultAction {
label: "Replace with wildcard complement",
edits: vec![TestResultTextRange {
text: "selector = @",
start_pat: Some("<-selector = 0x1"),
end_pat: Some("selector = 0x1"),
}],
}]],
),
(
quote! {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message, selector = _)]
pub fn fallback() {}
#[ink(message, selector = @)]
pub fn handler() {}
#[ink(message)]
pub fn message() {}
},
vec![vec![TestResultAction {
label: "Remove item",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(message)]"),
end_pat: Some("pub fn message() {}"),
}],
}]],
),
(
quote! {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message, selector = @)]
pub fn handler() {}
},
vec![vec![TestResultAction {
label: "Remove wildcard complement",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-, selector = @"),
end_pat: Some(", selector = @"),
}],
}]],
),
(
quote! {
#[ink(constructor)]
pub fn new() -> Self {}
#[ink(message, selector = 0x9BAE9D5E)]
pub fn handler() {}
},
vec![vec![TestResultAction {
label: "Remove wildcard complement",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-, selector = 0x9BAE9D5E"),
end_pat: Some(", selector = 0x9BAE9D5E"),
}],
}]],
),
] {
let code = quote_as_pretty_string! {
#[ink::contract]
mod my_contract {
impl MyContract {
#items
}
}
};
let contract = parse_first_contract(&code);
let mut results = Vec::new();
validate_wildcard_complement_selector(&mut results, &contract);
assert_eq!(results.len(), expected_quickfixes.len());
assert_eq!(
results
.iter()
.filter(|item| item.severity == Severity::Error)
.count(),
expected_quickfixes.len()
);
for (idx, item) in results.iter().enumerate() {
let quickfixes = item.quickfixes.as_ref().unwrap();
verify_actions(&code, quickfixes, &expected_quickfixes[idx]);
}
}
}
#[test]
fn impl_parent_for_callables_works() {
for code in valid_contracts!() {
let contract = parse_first_contract(quote_as_str! {
#code
});
let mut results = Vec::new();
ensure_impl_parent_for_callables(&mut results, &contract);
assert!(results.is_empty(), "contract: {code}");
}
}
#[test]
fn non_impl_parent_for_callables_fails() {
for (items, expected_quickfixes) in [
(
quote! {
#[ink(constructor)]
pub fn my_constructor() -> Self {}
#[ink(message)]
pub fn my_message() {}
impl MyContract {}
},
vec![
vec![TestResultAction {
label: "Move item",
edits: vec![
TestResultTextRange {
text: "#[ink(constructor)]",
start_pat: Some("impl MyContract {"),
end_pat: Some("impl MyContract {"),
},
TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(constructor)]"),
end_pat: Some("pub fn my_constructor() -> Self {}"),
},
],
}],
vec![TestResultAction {
label: "Move item",
edits: vec![
TestResultTextRange {
text: "#[ink(message)]",
start_pat: Some("impl MyContract {"),
end_pat: Some("impl MyContract {"),
},
TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(message)]"),
end_pat: Some("pub fn my_message() {}"),
},
],
}],
],
),
(
quote! {
#[ink(constructor)]
pub fn my_constructor() -> Self {}
#[ink(message)]
pub fn my_message() {}
},
vec![
vec![TestResultAction {
label: "Move item",
edits: vec![
TestResultTextRange {
text: "#[ink(constructor)]",
start_pat: Some("pub fn my_message() {}"),
end_pat: Some("pub fn my_message() {}"),
},
TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(constructor)]"),
end_pat: Some("pub fn my_constructor() -> Self {}"),
},
],
}],
vec![TestResultAction {
label: "Move item",
edits: vec![
TestResultTextRange {
text: "#[ink(message)]",
start_pat: Some("pub fn my_message() {}"),
end_pat: Some("pub fn my_message() {}"),
},
TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(message)]"),
end_pat: Some("pub fn my_message() {}"),
},
],
}],
],
),
] {
let code = quote_as_pretty_string! {
#[ink::contract]
mod my_contract {
#items
}
};
let contract = parse_first_contract(&code);
let mut results = Vec::new();
ensure_impl_parent_for_callables(&mut results, &contract);
assert_eq!(results.len(), 2);
assert_eq!(
results
.iter()
.filter(|item| item.severity == Severity::Error)
.count(),
2
);
for (idx, item) in results.iter().enumerate() {
let quickfixes = item.quickfixes.as_ref().unwrap();
verify_actions(&code, quickfixes, &expected_quickfixes[idx]);
}
}
}
#[test]
fn root_items_in_root_works() {
for code in valid_contracts!() {
let contract = parse_first_contract(quote_as_str! {
#code
});
let mut results = Vec::new();
ensure_root_items(&mut results, &contract);
assert!(results.is_empty(), "contract: {code}");
}
}
#[test]
fn root_items_not_in_root_fails() {
let code = quote_as_pretty_string! {
#[ink::contract]
mod my_contract {
fn my_contract_container() {
#[ink(storage)]
pub struct MyContract {}
#[ink(event)]
pub struct MyEvent {}
impl MyContract {
#[ink(constructor)]
pub fn my_constructor() -> Self {
}
#[ink(message)]
pub fn my_message() {}
}
#[ink(impl)]
impl MyContract {}
}
}
};
let contract = parse_first_contract(&code);
let mut results = Vec::new();
ensure_root_items(&mut results, &contract);
assert_eq!(results.len(), 4);
assert_eq!(
results
.iter()
.filter(|item| item.severity == Severity::Error)
.count(),
4
);
let expected_quickfixes = [
vec![TestResultAction {
label: "Move item",
edits: vec![
TestResultTextRange {
text: "#[ink(storage)]",
start_pat: Some("mod my_contract {"),
end_pat: Some("mod my_contract {"),
},
TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(storage)]"),
end_pat: Some("pub struct MyContract {}"),
},
],
}],
vec![TestResultAction {
label: "Move item",
edits: vec![
TestResultTextRange {
text: "#[ink(event)]",
start_pat: Some("mod my_contract {"),
end_pat: Some("mod my_contract {"),
},
TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(event)]"),
end_pat: Some("pub struct MyEvent {}"),
},
],
}],
vec![TestResultAction {
label: "Move item",
edits: vec![
TestResultTextRange {
text: "#[ink(constructor)]",
start_pat: Some("impl MyContract {}\n }"),
end_pat: Some("impl MyContract {}\n }"),
},
TestResultTextRange {
text: "",
start_pat: Some("<-impl MyContract {"),
end_pat: Some("pub fn my_message() {}\n }"),
},
],
}],
vec![TestResultAction {
label: "Move item",
edits: vec![
TestResultTextRange {
text: "#[ink(impl)]",
start_pat: Some("impl MyContract {}\n }"),
end_pat: Some("impl MyContract {}\n }"),
},
TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(impl)]"),
end_pat: Some("impl MyContract {}"),
},
],
}],
];
for (idx, item) in results.iter().enumerate() {
let quickfixes = item.quickfixes.as_ref().unwrap();
verify_actions(&code, quickfixes, &expected_quickfixes[idx]);
}
}
#[test]
fn valid_quasi_direct_descendant_works() {
for (version, contracts) in versioned_fixtures!(valid_contracts) {
for code in contracts {
let contract = parse_first_contract(quote_as_str! {
#code
});
let mut results = Vec::new();
ensure_valid_quasi_direct_ink_descendants(&mut results, &contract, version);
assert!(results.is_empty(), "contract: {code}");
}
}
}
#[test]
fn invalid_quasi_direct_descendant_fails() {
let code = quote_as_pretty_string! {
#[ink::contract]
mod my_contract {
pub struct MyEvent {
#[ink(topic)]
value: bool,
}
#[ink(extension = 1, handle_status = false)]
fn my_extension();
}
};
let contract = parse_first_contract(&code);
for version in [Version::Legacy, Version::V5(MinorVersion::Base)] {
let mut results = Vec::new();
ensure_valid_quasi_direct_ink_descendants(&mut results, &contract, version);
assert_eq!(results.len(), 2);
assert_eq!(
results
.iter()
.filter(|item| item.severity == Severity::Error)
.count(),
2
);
let expected_quickfixes = [
vec![TestResultAction {
label: "Remove `#[ink(topic)]`",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(topic)]"),
end_pat: Some("#[ink(topic)]"),
}],
}],
vec![
TestResultAction {
label: "Remove `#[ink(extension = 1, handle_status = false)]`",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(extension = 1, handle_status = false)]"),
end_pat: Some("#[ink(extension = 1, handle_status = false)]"),
}],
},
TestResultAction {
label: "Remove item",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(extension = 1, handle_status = false)]"),
end_pat: Some("fn my_extension();"),
}],
},
],
];
for (idx, item) in results.iter().enumerate() {
let quickfixes = item.quickfixes.as_ref().unwrap();
verify_actions(&code, quickfixes, &expected_quickfixes[idx]);
}
}
}
#[test]
fn compound_diagnostic_works() {
for (version, contracts) in versioned_fixtures!(valid_contracts) {
for code in contracts {
let contract = parse_first_contract(quote_as_str! {
#code
});
let mut results = Vec::new();
diagnostics(&mut results, &contract, version);
assert!(
results.is_empty(),
"contract: {code}, version: {:?}",
version
);
}
}
}
}