use crate::ast::{
IdlArrayElementSnapshot, IdlDefinedInnerSnapshot, IdlEnumVariantFieldSnapshot, IdlSnapshot,
IdlTypeDefKindSnapshot, IdlTypeDefSnapshot, IdlTypeSnapshot, ResolvedStructType,
SerializableStreamSpec,
};
use crate::identifiers::{typescript as ts_ident, IdentifierCase};
use std::collections::{BTreeMap, BTreeSet, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ResolvedTypeClaim {
Declare(String),
Shared(String),
}
impl ResolvedTypeClaim {
pub(crate) fn name(&self) -> &str {
match self {
Self::Declare(name) | Self::Shared(name) => name,
}
}
pub(crate) fn into_name(self) -> String {
match self {
Self::Declare(name) | Self::Shared(name) => name,
}
}
pub(crate) fn is_shared(&self) -> bool {
matches!(self, Self::Shared(_))
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct StackResolvedTypes {
declared: BTreeMap<String, String>,
reserved: HashSet<String>,
}
impl StackResolvedTypes {
pub(crate) fn reserve(&mut self, names: impl IntoIterator<Item = String>) {
self.reserved.extend(names);
}
pub(crate) fn is_declared(&self, name: &str) -> bool {
self.declared.contains_key(name)
}
pub(crate) fn declare(&mut self, name: &str, resolved: &ResolvedStructType) {
self.declared
.entry(name.to_string())
.or_insert_with(|| resolved_type_shape(resolved));
}
pub(crate) fn claim(
&self,
resolved: &ResolvedStructType,
chosen: String,
base_name: &str,
namespaces: &[String],
taken: &mut HashSet<String>,
) -> ResolvedTypeClaim {
let Some(declared) = self.declared.get(&chosen) else {
return ResolvedTypeClaim::Declare(chosen);
};
let shape = resolved_type_shape(resolved);
if *declared == shape {
return ResolvedTypeClaim::Shared(chosen);
}
let mut namespaces = namespaces
.iter()
.filter(|namespace| !namespace.is_empty())
.collect::<Vec<_>>();
namespaces.dedup();
let numbered_stem = namespaces
.last()
.map(|namespace| format!("{namespace}{base_name}"))
.unwrap_or_else(|| base_name.to_string());
let candidates = namespaces
.iter()
.map(|namespace| format!("{namespace}{base_name}"))
.chain((2usize..).map(|index| format!("{numbered_stem}{index}")));
for candidate in candidates {
if taken.contains(&candidate) || self.reserved.contains(&candidate) {
continue;
}
match self.declared.get(&candidate) {
Some(declared) if *declared == shape => {
taken.insert(candidate.clone());
return ResolvedTypeClaim::Shared(candidate);
}
Some(_) => continue,
None => {
taken.insert(candidate.clone());
return ResolvedTypeClaim::Declare(candidate);
}
}
}
unreachable!("the numbered candidates are unbounded")
}
}
pub(crate) fn entity_program_name<'a>(
entity: &'a SerializableStreamSpec,
idls: &'a [IdlSnapshot],
) -> Option<&'a str> {
entity
.program_id
.as_deref()
.and_then(|program_id| {
idls.iter()
.find(|idl| idl.program_id.as_deref() == Some(program_id))
})
.or(entity.idl.as_ref())
.map(|idl| idl.name.as_str())
}
pub(crate) fn entity_idl<'a>(
entity: &'a SerializableStreamSpec,
idls: &'a [IdlSnapshot],
) -> Option<&'a IdlSnapshot> {
entity
.program_id
.as_deref()
.and_then(|program_id| {
idls.iter()
.find(|idl| idl.program_id.as_deref() == Some(program_id))
})
.or(entity.idl.as_ref())
.or(match idls {
[only] => Some(only),
_ => None,
})
}
#[derive(Debug, Clone, Default)]
pub(crate) struct AccountModels {
by_program: BTreeMap<(String, String), String>,
first: BTreeMap<String, String>,
}
impl AccountModels {
pub(crate) fn first_mut(&mut self) -> &mut BTreeMap<String, String> {
&mut self.first
}
pub(crate) fn record(&mut self, program: Option<&str>, account_type: &str, model: &str) {
if let Some(program) = program {
self.by_program
.entry((program.to_string(), account_type.to_string()))
.or_insert_with(|| model.to_string());
}
}
pub(crate) fn get(&self, program: Option<&str>, account: &str) -> Option<&String> {
let own = program.and_then(|program| {
self.by_program
.get(&(program.to_string(), account.to_string()))
.or_else(|| {
self.by_program
.iter()
.find(|((owner, name), _)| {
owner == program && name.eq_ignore_ascii_case(account)
})
.map(|(_, model)| model)
})
});
own.or_else(|| self.first.get(account)).or_else(|| {
self.first
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(account))
.map(|(_, model)| model)
})
}
}
pub(crate) fn resolved_type_namespaces(
program_name: Option<&str>,
entity_name: &str,
) -> Vec<String> {
program_name
.map(|name| ts_ident::identifier_stem(name, IdentifierCase::Pascal))
.into_iter()
.chain(std::iter::once(ts_ident::identifier_stem(
entity_name,
IdentifierCase::Pascal,
)))
.collect()
}
pub(crate) fn resolved_type_shape(resolved: &ResolvedStructType) -> String {
serde_json::to_string(&(resolved.is_enum, &resolved.enum_variants, &resolved.fields))
.expect("resolved types serialize")
}
pub(crate) struct ProgramTypeDefs<'a> {
first: BTreeMap<String, (&'a IdlTypeDefSnapshot, usize)>,
names: BTreeSet<String>,
programs: Vec<ProgramDefs<'a>>,
program_names: Vec<String>,
scope: Option<usize>,
}
struct ProgramDefs<'a> {
by_name: BTreeMap<&'a str, &'a IdlTypeDefSnapshot>,
lower: BTreeMap<String, &'a str>,
own: BTreeSet<&'a str>,
}
impl<'a> ProgramDefs<'a> {
fn get(&self, name: &str) -> Option<&'a IdlTypeDefSnapshot> {
self.by_name.get(name).copied().or_else(|| {
self.lower
.get(&name.to_lowercase())
.map(|spelling| self.by_name[spelling])
})
}
}
#[derive(Clone, Copy)]
pub(crate) struct ProgramTypeDef<'a> {
pub(crate) name: &'a str,
pub(crate) def: &'a IdlTypeDefSnapshot,
pub(crate) program: Option<usize>,
}
impl<'a> ProgramTypeDefs<'a> {
pub(crate) fn new(idls: &'a [IdlSnapshot]) -> Self {
let mut first = BTreeMap::new();
let mut names = BTreeSet::new();
for (program, idl) in idls.iter().enumerate() {
for def in &idl.types {
names.insert(def.name.clone());
first
.entry(def.name.to_lowercase())
.or_insert((def, program));
}
}
let mut programs = idls
.iter()
.map(|idl| {
let mut lower = BTreeMap::new();
for def in &idl.types {
lower
.entry(def.name.to_lowercase())
.or_insert(def.name.as_str());
}
ProgramDefs {
by_name: idl
.types
.iter()
.map(|def| (def.name.as_str(), def))
.collect(),
lower,
own: BTreeSet::new(),
}
})
.collect::<Vec<_>>();
for program in &mut programs {
let mut differs = BTreeMap::new();
program.own = program
.by_name
.values()
.filter(|def| {
definition_differs(def, program, &first, &mut differs, &mut Vec::new())
})
.map(|def| def.name.as_str())
.collect();
}
Self {
first,
names,
programs,
program_names: idls.iter().map(|idl| idl.name.clone()).collect(),
scope: None,
}
}
pub(crate) fn set_scope(&mut self, program: Option<usize>) {
self.scope = program;
}
pub(crate) fn program_name(&self, program: usize) -> &str {
&self.program_names[program]
}
pub(crate) fn names(&self) -> impl Iterator<Item = &String> {
self.names.iter()
}
pub(crate) fn lookup(&self, name: &str) -> Option<ProgramTypeDef<'a>> {
let scoped = self.scope.and_then(|program| {
let def = self.programs.get(program)?.get(name)?;
Some((program, def))
});
if let Some((program, def)) = scoped {
if self.programs[program].own.contains(def.name.as_str()) {
return Some(ProgramTypeDef {
name: &def.name,
def,
program: Some(program),
});
}
}
let key = scoped.map_or(name, |(_, def)| def.name.as_str());
let (def, _) = self.first.get(&key.to_lowercase())?;
Some(ProgramTypeDef {
name: &def.name,
def,
program: None,
})
}
pub(crate) fn conflicts(&self) -> Vec<(String, String, String)> {
self.programs
.iter()
.enumerate()
.flat_map(|(program, defs)| {
defs.own.iter().map(move |name| {
let (_, first_program) = self.first[&name.to_lowercase()];
(
name.to_string(),
self.program_names[first_program].clone(),
self.program_names[program].clone(),
)
})
})
.collect()
}
}
fn definition_differs<'a>(
own: &'a IdlTypeDefSnapshot,
program: &ProgramDefs<'a>,
first: &BTreeMap<String, (&'a IdlTypeDefSnapshot, usize)>,
differs: &mut BTreeMap<String, bool>,
visiting: &mut Vec<String>,
) -> bool {
if let Some(known) = differs.get(&own.name) {
return *known;
}
if visiting.contains(&own.name) {
return false;
}
let Some((first_def, _)) = first.get(&own.name.to_lowercase()) else {
return false;
};
visiting.push(own.name.clone());
let result = definition_shape(own) != definition_shape(first_def)
|| referenced_type_names(own).iter().any(|referenced| {
program.get(referenced).is_some_and(|referenced| {
definition_differs(referenced, program, first, differs, visiting)
})
});
visiting.pop();
differs.insert(own.name.clone(), result);
result
}
fn definition_shape(def: &IdlTypeDefSnapshot) -> serde_json::Value {
serde_json::json!({
"serialization": def.serialization,
"type": def.type_def,
})
}
fn referenced_type_names(def: &IdlTypeDefSnapshot) -> BTreeSet<String> {
fn words(text: &str, names: &mut BTreeSet<String>) {
names.extend(
text.split(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
.filter(|word| !word.is_empty())
.map(str::to_string),
);
}
fn walk(ty: &IdlTypeSnapshot, names: &mut BTreeSet<String>) {
match ty {
IdlTypeSnapshot::Simple(simple) => words(simple, names),
IdlTypeSnapshot::Array(array) => {
for element in &array.array {
match element {
IdlArrayElementSnapshot::Type(inner) => walk(inner, names),
IdlArrayElementSnapshot::TypeName(name) => words(name, names),
IdlArrayElementSnapshot::Size(_) => {}
}
}
}
IdlTypeSnapshot::Option(option) => walk(&option.option, names),
IdlTypeSnapshot::Vec(vec) => walk(&vec.vec, names),
IdlTypeSnapshot::HashMap(map) => {
walk(&map.hash_map.0, names);
walk(&map.hash_map.1, names);
}
IdlTypeSnapshot::Tuple(tuple) => {
for element in &tuple.tuple {
walk(element, names);
}
}
IdlTypeSnapshot::Defined(defined) => match &defined.defined {
IdlDefinedInnerSnapshot::Named { name } => {
names.insert(name.clone());
}
IdlDefinedInnerSnapshot::Simple(name) => {
names.insert(name.clone());
}
},
}
}
let mut names = BTreeSet::new();
match &def.type_def {
IdlTypeDefKindSnapshot::Struct { fields, .. } => {
for field in fields {
walk(&field.type_, &mut names);
}
}
IdlTypeDefKindSnapshot::TupleStruct { fields, .. } => {
for field in fields {
walk(field, &mut names);
}
}
IdlTypeDefKindSnapshot::Enum { variants, .. } => {
for field in variants.iter().flat_map(|variant| &variant.fields) {
match field {
IdlEnumVariantFieldSnapshot::Named(named) => walk(&named.type_, &mut names),
IdlEnumVariantFieldSnapshot::Tuple(tuple) => walk(tuple, &mut names),
}
}
}
}
names
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BaseType, ResolvedField};
fn resolved(name: &str, fields: &[(&str, BaseType)]) -> ResolvedStructType {
ResolvedStructType {
type_name: name.to_string(),
fields: fields
.iter()
.map(|(field_name, base_type)| ResolvedField {
field_name: field_name.to_string(),
raw_name: None,
canonical_name: None,
field_type: format!("{base_type:?}"),
base_type: base_type.clone(),
integer_kind: None,
is_optional: false,
is_array: false,
})
.collect(),
is_instruction: false,
is_account: false,
is_event: false,
is_enum: false,
enum_variants: Vec::new(),
}
}
fn namespaces() -> Vec<String> {
vec!["Beta".to_string(), "Vault".to_string()]
}
#[test]
fn undeclared_names_are_kept() {
let types = StackResolvedTypes::default();
let header = resolved("header", &[("version", BaseType::Integer)]);
let mut taken = HashSet::from(["Header".to_string()]);
assert_eq!(
types.claim(
&header,
"Header".into(),
"Header",
&namespaces(),
&mut taken
),
ResolvedTypeClaim::Declare("Header".into())
);
}
#[test]
fn identical_definitions_are_shared_whatever_their_flags() {
let mut types = StackResolvedTypes::default();
types.declare(
"Header",
&resolved("header", &[("version", BaseType::Integer)]),
);
let mut account = resolved("Header", &[("version", BaseType::Integer)]);
account.is_account = true;
let mut taken = HashSet::from(["Header".to_string()]);
assert_eq!(
types.claim(
&account,
"Header".into(),
"Header",
&namespaces(),
&mut taken
),
ResolvedTypeClaim::Shared("Header".into())
);
}
#[test]
fn different_definitions_are_namespaced_then_numbered() {
let mut types = StackResolvedTypes::default();
types.declare(
"Header",
&resolved("header", &[("version", BaseType::Integer)]),
);
let other = resolved("header", &[("owner", BaseType::Pubkey)]);
let mut taken = HashSet::from(["Header".to_string()]);
let claim = types.claim(&other, "Header".into(), "Header", &namespaces(), &mut taken);
assert_eq!(claim, ResolvedTypeClaim::Declare("BetaHeader".into()));
assert!(taken.contains("BetaHeader"));
types.declare("BetaHeader", &other);
let mut taken = HashSet::from(["Header".to_string()]);
assert_eq!(
types.claim(&other, "Header".into(), "Header", &namespaces(), &mut taken),
ResolvedTypeClaim::Shared("BetaHeader".into())
);
let third = resolved("header", &[("flag", BaseType::Boolean)]);
types.reserve(["VaultHeader".to_string()]);
let mut taken = HashSet::from(["Header".to_string()]);
assert_eq!(
types.claim(&third, "Header".into(), "Header", &namespaces(), &mut taken),
ResolvedTypeClaim::Declare("VaultHeader2".into())
);
}
fn idl(name: &str, types: serde_json::Value) -> IdlSnapshot {
serde_json::from_value(serde_json::json!({
"name": name,
"version": "0.1.0",
"accounts": [],
"instructions": [],
"types": types,
"discriminant_size": 8
}))
.expect("test IDL")
}
fn program_of(defs: &ProgramTypeDefs<'_>, name: &str) -> Option<Option<usize>> {
defs.lookup(name).map(|found| found.program)
}
#[test]
fn program_type_defs_resolve_each_programs_own_different_definition() {
let struct_of =
|fields: serde_json::Value| serde_json::json!({ "kind": "struct", "fields": fields });
let inner_a = struct_of(serde_json::json!([{ "name": "a", "type": "u8" }]));
let inner_b = struct_of(serde_json::json!([{ "name": "a", "type": "u16" }]));
let outer =
struct_of(serde_json::json!([{ "name": "inner", "type": { "defined": "Inner" } }]));
let same = struct_of(serde_json::json!([{ "name": "x", "type": "bool" }]));
let idls = vec![
idl(
"alpha",
serde_json::json!([
{ "name": "Inner", "type": inner_a },
{ "name": "Outer", "type": outer },
{ "name": "Same", "type": same }
]),
),
idl(
"beta",
serde_json::json!([
{ "name": "Inner", "docs": ["documented differently"], "type": inner_b },
{ "name": "Outer", "type": outer },
{ "name": "Same", "docs": ["documented differently"], "type": same }
]),
),
];
let mut defs = ProgramTypeDefs::new(&idls);
for scope in [None, Some(0)] {
defs.set_scope(scope);
assert_eq!(program_of(&defs, "Inner"), Some(None));
assert_eq!(program_of(&defs, "Outer"), Some(None));
}
defs.set_scope(Some(1));
assert_eq!(program_of(&defs, "Inner"), Some(Some(1)));
assert_eq!(program_of(&defs, "inner"), Some(Some(1)));
assert_eq!(program_of(&defs, "Outer"), Some(Some(1)));
assert_eq!(program_of(&defs, "Same"), Some(None));
assert_eq!(program_of(&defs, "Missing"), None);
assert_eq!(
defs.conflicts(),
vec![
("Inner".to_string(), "alpha".to_string(), "beta".to_string()),
("Outer".to_string(), "alpha".to_string(), "beta".to_string()),
]
);
}
#[test]
fn program_type_defs_match_names_case_insensitively_across_programs() {
let struct_of = |ty: &str| serde_json::json!({ "kind": "struct", "fields": [{ "name": "a", "type": ty }] });
let idls = vec![
idl(
"alpha",
serde_json::json!([{ "name": "Header", "type": struct_of("u8") }]),
),
idl(
"beta",
serde_json::json!([{ "name": "header", "type": struct_of("u16") }]),
),
idl(
"gamma",
serde_json::json!([{ "name": "HEADER", "type": struct_of("u8") }]),
),
];
let mut defs = ProgramTypeDefs::new(&idls);
defs.set_scope(Some(1));
for spelling in ["Header", "header", "HEADER"] {
let found = defs.lookup(spelling).unwrap();
assert_eq!((found.name, found.program), ("header", Some(1)));
}
defs.set_scope(Some(2));
let found = defs.lookup("header").unwrap();
assert_eq!((found.name, found.program), ("Header", None));
assert_eq!(
defs.conflicts(),
vec![(
"header".to_string(),
"alpha".to_string(),
"beta".to_string()
)]
);
}
}