Skip to content

Commit 4ae5a12

Browse files
Avoid silent failure for associated types. (#16)
* dedup bug and error handling * clippy and fmt * add test for no skip case * fix tricky situation with multiple params that share a type id * Update typegen/src/utils.rs Co-authored-by: Niklas Adolfsson <[email protected]> --------- Co-authored-by: Niklas Adolfsson <[email protected]>
1 parent 99065e6 commit 4ae5a12

File tree

8 files changed

+208
-33
lines changed

8 files changed

+208
-33
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typegen/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ quote = { workspace = true }
1414
scale-info = { workspace = true }
1515
syn = { workspace = true }
1616
thiserror = { workspace = true }
17+
smallvec = { workspace = true }
1718

1819
[dev-dependencies]
1920
scale-bits = { workspace = true }

typegen/src/tests/mod.rs

+114-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
},
1616
},
1717
utils::ensure_unique_type_paths,
18-
DerivesRegistry, TypeSubstitutes,
18+
DerivesRegistry, TypeSubstitutes, TypegenError,
1919
};
2020

2121
mod utils;
@@ -852,7 +852,7 @@ fn range_fields() {
852852
}
853853

854854
#[test]
855-
fn generics() {
855+
fn generics_foo() {
856856
#[allow(unused)]
857857
#[derive(TypeInfo)]
858858
struct Foo<T> {
@@ -1584,3 +1584,115 @@ fn find_similar_type_paths() {
15841584
];
15851585
assert_eq!(similar, expected);
15861586
}
1587+
1588+
#[test]
1589+
fn assoc_types_skip_params() {
1590+
pub trait Config {
1591+
type Inner: TypeInfo;
1592+
}
1593+
1594+
pub struct A;
1595+
pub struct B;
1596+
1597+
impl Config for A {
1598+
type Inner = ();
1599+
}
1600+
1601+
impl Config for B {
1602+
type Inner = u32;
1603+
}
1604+
1605+
#[derive(TypeInfo)]
1606+
#[scale_info(skip_type_params(T))]
1607+
pub struct X<T: Config> {
1608+
pub inner: T::Inner,
1609+
}
1610+
1611+
let res_no_dedup = Testgen::new()
1612+
.with::<X<A>>()
1613+
.with::<X<B>>()
1614+
.try_gen_tests_mod(subxt_settings(), false);
1615+
assert!(matches!(
1616+
res_no_dedup,
1617+
Err(TypegenError::DuplicateTypePath(_))
1618+
));
1619+
1620+
let dedup_code = Testgen::new()
1621+
.with::<X<A>>()
1622+
.with::<X<B>>()
1623+
.try_gen_tests_mod(Default::default(), true)
1624+
.unwrap();
1625+
1626+
let expected_code = quote! {
1627+
pub mod tests {
1628+
use super::types;
1629+
pub struct X1 {
1630+
pub inner: (),
1631+
}
1632+
pub struct X2 {
1633+
pub inner: ::core::primitive::u32,
1634+
}
1635+
}
1636+
};
1637+
1638+
assert_eq!(dedup_code.to_string(), expected_code.to_string());
1639+
}
1640+
1641+
#[test]
1642+
fn assoc_types_no_skip_params() {
1643+
pub trait Config {
1644+
type Inner: TypeInfo;
1645+
}
1646+
1647+
#[derive(TypeInfo)]
1648+
pub struct A;
1649+
1650+
#[derive(TypeInfo)]
1651+
pub struct B;
1652+
1653+
impl Config for A {
1654+
type Inner = ();
1655+
}
1656+
impl Config for B {
1657+
type Inner = u32;
1658+
}
1659+
1660+
#[derive(TypeInfo)]
1661+
pub struct X<T: Config> {
1662+
pub inner: T::Inner,
1663+
}
1664+
1665+
let res_no_dedup = Testgen::new()
1666+
.with::<X<A>>()
1667+
.with::<X<B>>()
1668+
.try_gen_tests_mod(subxt_settings(), false);
1669+
assert!(matches!(
1670+
res_no_dedup,
1671+
Err(TypegenError::DuplicateTypePath(_))
1672+
));
1673+
1674+
let dedup_code = Testgen::new()
1675+
.with::<X<A>>()
1676+
.with::<X<B>>()
1677+
.try_gen_tests_mod(Default::default(), true)
1678+
.unwrap();
1679+
1680+
// Unfortunately the structs A and B are still part of the type registry and X1 and X2 have generic parameters, linking to them.
1681+
let expected_code = quote! {
1682+
pub mod tests {
1683+
use super::types;
1684+
pub struct A;
1685+
pub struct B;
1686+
pub struct X1<_0> {
1687+
pub inner: (),
1688+
pub __ignore: ::core::marker::PhantomData<_0>
1689+
}
1690+
pub struct X2<_0> {
1691+
pub inner: ::core::primitive::u32,
1692+
pub __ignore: ::core::marker::PhantomData<_0>
1693+
}
1694+
}
1695+
};
1696+
1697+
assert_eq!(dedup_code.to_string(), expected_code.to_string());
1698+
}

typegen/src/tests/utils.rs

+20-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use syn::parse_quote;
77

88
use crate::typegen::ir::module_ir::ModuleIR;
99
use crate::utils::ensure_unique_type_paths;
10+
use crate::TypegenError;
1011
use crate::{
1112
typegen::settings::substitutes::absolute_path, DerivesRegistry, TypeGenerator,
1213
TypeGeneratorSettings, TypeSubstitutes,
@@ -41,12 +42,25 @@ impl Testgen {
4142
module.to_token_stream()
4243
}
4344

44-
pub fn gen_tests_mod(self, settings: TypeGeneratorSettings) -> TokenStream {
45-
let registry: PortableRegistry = self.registry.into();
45+
pub fn try_gen_tests_mod(
46+
self,
47+
settings: TypeGeneratorSettings,
48+
deduplicate: bool,
49+
) -> Result<TokenStream, TypegenError> {
50+
let mut registry: PortableRegistry = self.registry.into();
51+
if deduplicate {
52+
ensure_unique_type_paths(&mut registry)
53+
}
4654
let type_gen = TypeGenerator::new(&registry, &settings);
47-
let module = type_gen.generate_types_mod().unwrap();
48-
let module = get_mod(&module, TESTS_MOD_PATH).unwrap();
49-
module.to_token_stream()
55+
type_gen
56+
.generate_types_mod()
57+
.map(|module| get_mod(&module, TESTS_MOD_PATH).unwrap().to_token_stream())
58+
}
59+
60+
pub fn gen_tests_mod(self, settings: TypeGeneratorSettings) -> TokenStream {
61+
self.try_gen_tests_mod(settings, false)
62+
.unwrap()
63+
.to_token_stream()
5064
}
5165
}
5266

@@ -160,7 +174,7 @@ pub(super) fn subxt_default_substitutes() -> TypeSubstitutes {
160174

161175
const TESTS_MOD_PATH: &[&str] = &["scale_typegen", "tests"];
162176

163-
fn get_mod<'a>(module: &'a ModuleIR, path_segs: &[&'static str]) -> Option<&'a ModuleIR> {
177+
fn get_mod<'a>(module: &'a ModuleIR, path_segs: &[&'static str]) -> Option<&'a ModuleIR<'a>> {
164178
let (mod_name, rest) = path_segs.split_first()?;
165179
let mod_ident: Ident = syn::parse_str(mod_name).unwrap();
166180
let module = module.children.get(&mod_ident)?;

typegen/src/typegen/error.rs

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ pub enum TypegenError {
3131
/// The settings do not fit the given type registry.
3232
#[error("Settings do not fit the given type registry: {0}")]
3333
SettingsValidation(SettingsValidationError),
34+
/// There are two types with the the same type path but a different structure.
35+
/// Use [`crate::utils::ensure_unique_type_paths`] on your [`scale_info::PortableRegistry`] to deduplicate type paths.
36+
#[error("There are two types with the the same type path {0} but different structure. Use `scale_typegen::utils::ensure_unique_type_paths` on your `PortableRegistry` before, to avoid this error.")]
37+
DuplicateTypePath(String),
3438
}
3539

3640
/// Error attempting to do type substitution.

typegen/src/typegen/ir/module_ir.rs

+9-8
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,24 @@ use scale_info::form::PortableForm;
99

1010
/// Represents a Rust `mod`, containing generated types and child `mod`s.
1111
#[derive(Debug, Clone)]
12-
pub struct ModuleIR {
12+
pub struct ModuleIR<'a> {
1313
/// Name of this module.
1414
pub name: Ident,
1515
/// Root module identifier.
1616
pub root_mod: Ident,
1717
/// Submodules of this module.
18-
pub children: BTreeMap<Ident, ModuleIR>,
18+
pub children: BTreeMap<Ident, ModuleIR<'a>>,
1919
/// Types in this module.
20-
pub types: BTreeMap<scale_info::Path<PortableForm>, TypeIR>,
20+
pub types:
21+
BTreeMap<scale_info::Path<PortableForm>, (&'a scale_info::Type<PortableForm>, TypeIR)>,
2122
}
2223

23-
impl ToTokens for ModuleIR {
24+
impl<'a> ToTokens for ModuleIR<'a> {
2425
fn to_tokens(&self, tokens: &mut TokenStream) {
2526
let name = &self.name;
2627
let root_mod = &self.root_mod;
2728
let modules = self.children.values();
28-
let types = self.types.values().clone();
29+
let types = self.types.values().map(|e| &e.1).clone();
2930

3031
tokens.extend(quote! {
3132
pub mod #name {
@@ -38,7 +39,7 @@ impl ToTokens for ModuleIR {
3839
}
3940
}
4041

41-
impl ModuleIR {
42+
impl<'a> ModuleIR<'a> {
4243
/// Create a new [`Module`], with a reference to the root `mod` for resolving type paths.
4344
pub(crate) fn new(name: Ident, root_mod: Ident) -> Self {
4445
Self {
@@ -61,7 +62,7 @@ impl ModuleIR {
6162

6263
/// Returns the generated types.
6364
pub fn types(&self) -> impl Iterator<Item = (&scale_info::Path<PortableForm>, &TypeIR)> {
64-
self.types.iter()
65+
self.types.iter().map(|(k, v)| (k, &v.1))
6566
}
6667

6768
/// Returns the root `mod` used for resolving type paths.
@@ -71,7 +72,7 @@ impl ModuleIR {
7172

7273
/// Recursively creates submodules for the given namespace and returns a mutable reference to the innermost module created this way.
7374
/// Returns itself, if the namespace is empty.
74-
pub fn get_or_insert_submodule(&mut self, namespace: &[String]) -> &mut ModuleIR {
75+
pub fn get_or_insert_submodule(&mut self, namespace: &[String]) -> &mut ModuleIR<'a> {
7576
if namespace.is_empty() {
7677
return self;
7778
}

typegen/src/typegen/mod.rs

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::TypegenError;
1+
use std::collections::btree_map::Entry;
2+
3+
use crate::{utils::types_equal_extended_to_params, TypegenError};
24

35
use self::{
46
ir::module_ir::ModuleIR,
@@ -89,10 +91,24 @@ impl<'a> TypeGenerator<'a> {
8991
}
9092

9193
// if the type is not a builtin type, insert it into the respective module
92-
if let Some(type_ir) = self.create_type_ir(&ty.ty, &flat_derives_registry)? {
94+
let ty = &ty.ty;
95+
if let Some(type_ir) = self.create_type_ir(ty, &flat_derives_registry)? {
9396
// Create the module this type should go into
9497
let innermost_module = root_mod.get_or_insert_submodule(namespace);
95-
innermost_module.types.insert(path.clone(), type_ir);
98+
match innermost_module.types.entry(path.clone()) {
99+
Entry::Vacant(e) => {
100+
e.insert((ty, type_ir));
101+
}
102+
Entry::Occupied(e) => {
103+
// There is already a type with the same type path present.
104+
// We do not just want to override it, so we check if the two types are semantically similar (structure + generics).
105+
// If not, return an error, if yes, just keep the first one.
106+
let other_ty = &e.get().0;
107+
if !types_equal_extended_to_params(ty, other_ty) {
108+
return Err(TypegenError::DuplicateTypePath(ty.path.to_string()));
109+
}
110+
}
111+
};
96112
}
97113
}
98114

typegen/src/utils.rs

+40-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use scale_info::{form::PortableForm, Field, PortableRegistry, Type, TypeDef, TypeParameter};
2-
use std::collections::HashMap;
2+
use smallvec::{smallvec, SmallVec};
3+
use std::collections::{hash_map::Entry, HashMap};
34

45
use crate::TypegenError;
56

@@ -81,25 +82,50 @@ pub fn ensure_unique_type_paths(types: &mut PortableRegistry) {
8182
/// all type ids mentioned in the TypeDef are either:
8283
/// - equal
8384
/// - or different, but map essentially to the same generic type parameter
84-
fn types_equal_extended_to_params(a: &Type<PortableForm>, b: &Type<PortableForm>) -> bool {
85-
let collect_params = |type_params: &[TypeParameter<PortableForm>]| {
86-
type_params
87-
.iter()
88-
.filter_map(|p| p.ty.map(|ty| (ty.id, p.name.clone())))
89-
.collect::<HashMap<u32, String>>()
90-
};
85+
pub(crate) fn types_equal_extended_to_params(
86+
a: &Type<PortableForm>,
87+
b: &Type<PortableForm>,
88+
) -> bool {
89+
// We map each type ID to all type params if could refer to. Type IDs can refer to multiple parameters:
90+
// E.g. Foo<A,B> can be parameterized as Foo<u8,u8>, so if 42 is the type id of u8, a field with id=42 could refer to either A or B.
91+
fn collect_params(
92+
type_params: &[TypeParameter<PortableForm>],
93+
) -> HashMap<u32, SmallVec<[&str; 2]>> {
94+
let mut hm: HashMap<u32, SmallVec<[&str; 2]>> = HashMap::new();
95+
for p in type_params {
96+
if let Some(ty) = &p.ty {
97+
match hm.entry(ty.id) {
98+
Entry::Occupied(mut e) => {
99+
e.get_mut().push(p.name.as_str());
100+
}
101+
Entry::Vacant(e) => {
102+
e.insert(smallvec![p.name.as_str()]);
103+
}
104+
}
105+
}
106+
}
107+
hm
108+
}
91109

92110
let type_params_a = collect_params(&a.type_params);
93-
let type_params_b = collect_params(&a.type_params);
111+
let type_params_b = collect_params(&b.type_params);
94112

95-
if type_params_a.len() != type_params_b.len() {
113+
if a.type_params.len() != b.type_params.len() {
96114
return false;
97115
}
98-
99-
// returns true if the ids are the same OR if they point to the same generic parameter.
116+
// Returns true if the ids are the same OR if they point to the same generic parameter.
100117
let ids_equal = |a: u32, b: u32| -> bool {
101-
a == b
102-
|| matches!((type_params_a.get(&a), type_params_b.get(&b)), (Some(a_name), Some(b_name)) if a_name == b_name)
118+
if a == b {
119+
return true;
120+
}
121+
let Some(a_param_names) = type_params_a.get(&a) else {
122+
return false;
123+
};
124+
let Some(b_param_names) = type_params_b.get(&b) else {
125+
return false;
126+
};
127+
// Check if there is any intersection, meaning that both IDs map to the same generic type param:
128+
a_param_names.iter().any(|a_p| b_param_names.contains(a_p))
103129
};
104130

105131
let fields_equal = |a: &[Field<PortableForm>], b: &[Field<PortableForm>]| -> bool {

0 commit comments

Comments
 (0)