use ink_analyzer_ir::{InkEntity, StorageItem};
use super::common;
use crate::{Action, Diagnostic, Severity, Version};
const SCOPE_NAME: &str = "storage_item";
pub fn diagnostics(results: &mut Vec<Diagnostic>, storage_item: &StorageItem, version: Version) {
common::run_generic_diagnostics(results, storage_item, version);
if let Some(diagnostic) = ensure_adt(storage_item) {
results.push(diagnostic);
}
common::ensure_no_ink_descendants(results, storage_item, SCOPE_NAME, false);
}
fn ensure_adt(storage_item: &StorageItem) -> Option<Diagnostic> {
storage_item.adt().is_none().then(|| Diagnostic {
message: format!(
"`{}` can only be applied to an `enum`, `struct` or `union` item.",
storage_item
.ink_attr()
.map(|attr| attr.syntax().to_string())
.as_deref()
.unwrap_or("#[ink::storage_item]")
),
range: storage_item.syntax().text_range(),
severity: Severity::Error,
quickfixes: storage_item
.ink_attr()
.map(|attr| vec![Action::remove_attribute(attr)]),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use ink_analyzer_ir::syntax::{TextRange, TextSize};
use quote::quote;
use test_utils::{
parse_offset_at, quote_as_pretty_string, quote_as_str, TestResultAction,
TestResultTextRange,
};
fn parse_first_storage_item(code: &str) -> StorageItem {
parse_first_ink_entity_of_type(code)
}
#[test]
fn adt_works() {
for code in [
quote! {
struct MyStorageItem {
}
},
quote! {
enum MyStorageItem {
}
},
quote! {
union MyStorageItem {
}
},
] {
let storage_item = parse_first_storage_item(quote_as_str! {
#[ink::storage_item]
#code
});
let result = ensure_adt(&storage_item);
assert!(result.is_none());
}
}
#[test]
fn non_adt_fails() {
for code in [
quote! {
fn my_storage_item() {
}
},
quote! {
mod my_storage_item;
},
quote! {
trait MyStorageItem {
}
},
] {
let code = quote_as_pretty_string! {
#[ink::storage_item]
#code
};
let storage_item = parse_first_storage_item(&code);
let result = ensure_adt(&storage_item);
assert!(result.is_some(), "storage item: {code}");
assert_eq!(
result.as_ref().unwrap().severity,
Severity::Error,
"storage item: {code}"
);
let fix = &result.as_ref().unwrap().quickfixes.as_ref().unwrap()[0];
assert!(fix.label.contains("Remove `#[ink::storage_item]`"));
assert!(fix.edits[0].text.is_empty());
assert_eq!(
fix.edits[0].range,
TextRange::new(
TextSize::from(
parse_offset_at(&code, Some("<-#[ink::storage_item]")).unwrap() as u32
),
TextSize::from(
parse_offset_at(&code, Some("#[ink::storage_item]")).unwrap() as u32
)
)
);
}
}
#[test]
fn no_ink_descendants_works() {
let storage_item = parse_first_storage_item(quote_as_str! {
#[ink::storage_item]
struct MyStorageItem {
}
});
let mut results = Vec::new();
common::ensure_no_ink_descendants(&mut results, &storage_item, SCOPE_NAME, false);
assert!(results.is_empty());
}
#[test]
fn ink_descendants_fails() {
let code = quote_as_pretty_string! {
#[ink::storage_item]
struct MyStorageItem {
#[ink(event)]
field_1: (u32, bool),
#[ink(topic)]
field_2: String,
}
};
let storage_item = parse_first_storage_item(&code);
let mut results = Vec::new();
common::ensure_no_ink_descendants(&mut results, &storage_item, SCOPE_NAME, false);
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(event)]`",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(event)]"),
end_pat: Some("#[ink(event)]"),
}],
}],
vec![TestResultAction {
label: "Remove `#[ink(topic)]`",
edits: vec![TestResultTextRange {
text: "",
start_pat: Some("<-#[ink(topic)]"),
end_pat: Some("#[ink(topic)]"),
}],
}],
];
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 code in [
quote_as_str! {
#[ink::storage_item]
#[derive(Default, Debug)]
struct NonPacked {
s1: Mapping<u32, u128>,
s2: Lazy<u128>,
}
},
quote_as_str! {
#[ink::storage_item(derive = false)]
#[derive(Storable, StorableHint, StorageKey)]
#[cfg_attr(
feature = "std",
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
)]
#[derive(Default, Debug)]
struct NonPackedGeneric<T>
where
T: Default + core::fmt::Debug,
T: ink::storage::traits::Packed,
{
s1: u32,
s2: T,
s3: Mapping<u128, T>,
}
},
quote_as_str! {
#[ink::storage_item]
#[derive(scale::Decode, scale::Encode)]
#[cfg_attr(
feature = "std",
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
)]
#[derive(Default, Debug)]
struct PackedComplex {
s1: u128,
s2: Vec<u128>,
s3: Vec<Packed>,
}
},
quote_as_str! {
#[ink::storage_item]
#[derive(Default, Debug)]
struct NonPackedComplex<KEY: StorageKey> {
s1: (String, u128, Packed),
s2: Mapping<u128, u128>,
s3: Lazy<u128>,
s4: Mapping<u128, Packed>,
s5: Lazy<NonPacked>,
s6: PackedGeneric<Packed>,
s7: NonPackedGeneric<Packed>,
}
},
] {
let storage_item = parse_first_storage_item(code);
let mut results = Vec::new();
diagnostics(&mut results, &storage_item, Version::Legacy);
assert!(results.is_empty(), "storage_item: {code}");
}
}
}