diff --git a/rs/client-gen/src/lib.rs b/rs/client-gen/src/lib.rs index a26c37ea6..a999f9769 100644 --- a/rs/client-gen/src/lib.rs +++ b/rs/client-gen/src/lib.rs @@ -7,6 +7,7 @@ mod ctor_generators; mod events_generator; mod helpers; mod mock_generator; +mod resolution; mod root_generator; mod service_generators; mod type_generators; @@ -102,8 +103,7 @@ impl<'ast> ClientGenerator<'ast, IdlPath<'ast>> { let client_path = self.client_path.context("client path not set")?; let idl_path = self.idl.0; - let idl = fs::read_to_string(idl_path) - .with_context(|| format!("Failed to open {} for reading", idl_path.display()))?; + let idl = resolution::resolve_idl_from_path(idl_path)?; self.with_idl(&idl) .generate_to(client_path) @@ -114,8 +114,7 @@ impl<'ast> ClientGenerator<'ast, IdlPath<'ast>> { pub fn generate_to(self, out_path: impl AsRef) -> Result<()> { let idl_path = self.idl.0; - let idl = fs::read_to_string(idl_path) - .with_context(|| format!("Failed to open {} for reading", idl_path.display()))?; + let idl = resolution::resolve_idl_from_path(idl_path)?; self.with_idl(&idl) .generate_to(out_path) diff --git a/rs/client-gen/src/resolution.rs b/rs/client-gen/src/resolution.rs new file mode 100644 index 000000000..4ab34c078 --- /dev/null +++ b/rs/client-gen/src/resolution.rs @@ -0,0 +1,62 @@ +use anyhow::Result; +use sails_idl_parser_v2::preprocess::{self, IdlLoader}; +use std::fs; +use std::path::{Path, PathBuf}; + +struct FsLoader { + base_dir: PathBuf, +} + +impl IdlLoader for FsLoader { + fn load(&self, path: &str) -> Result<(String, String), String> { + let full_path = self.base_dir.join(path); + + let canonical_path = full_path + .canonicalize() + .map_err(|e| format!("Failed to resolve path '{}': {}", full_path.display(), e))?; + + let content = fs::read_to_string(&canonical_path).map_err(|e| { + format!( + "Failed to read include '{}': {}", + canonical_path.display(), + e + ) + })?; + + let unique_id = canonical_path.to_string_lossy().into_owned(); + Ok((content, unique_id)) + } +} + +pub fn resolve_idl_from_path(path: &Path) -> Result { + // Resolve initial path to get correct base dir and filename + let canonical_path = path.canonicalize()?; + + let parent_dir = canonical_path.parent().unwrap_or(Path::new(".")); + let loader = FsLoader { + base_dir: parent_dir.to_path_buf(), + }; + + let filename = canonical_path + .file_name() + .and_then(|s| s.to_str()) + .ok_or_else(|| anyhow::anyhow!("Invalid IDL path"))?; + + preprocess::preprocess(filename, &loader).map_err(|e| anyhow::anyhow!(e)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn test_resolve_idl_from_path() { + let path = Path::new("tests/idls/recursive_main.idl"); + let result = resolve_idl_from_path(path).unwrap(); + + assert!(result.contains("service Leaf")); + assert!(result.contains("service Middle")); + assert!(result.contains("service Main")); + } +} diff --git a/rs/client-gen/src/type_generators.rs b/rs/client-gen/src/type_generators.rs index d009e57ca..819ef0a5f 100644 --- a/rs/client-gen/src/type_generators.rs +++ b/rs/client-gen/src/type_generators.rs @@ -73,6 +73,13 @@ impl<'ast> Visitor<'ast> for TopLevelTypeGenerator<'ast> { enum_def_generator.visit_enum_def(enum_def); self.tokens.extend(enum_def_generator.finalize()); } + ast::TypeDef::Alias(alias_def) => { + let target_type_code = generate_type_decl_with_path(&alias_def.target, ""); + quote_in! { self.tokens => + $['\r'] + pub type $(self.type_name) $(self.type_params_tokens.clone()) = $target_type_code; + }; + } } } } diff --git a/rs/client-gen/tests/generator.rs b/rs/client-gen/tests/generator.rs index 8e1ddedba..d9f12f83d 100644 --- a/rs/client-gen/tests/generator.rs +++ b/rs/client-gen/tests/generator.rs @@ -42,6 +42,13 @@ fn test_events_works() { insta::assert_snapshot!(gen_client(idl)); } +#[test] +fn test_aliases_works() { + let idl = include_str!("idls/aliases_works.idl"); + + insta::assert_snapshot!(gen_client(idl)); +} + #[test] fn full_with_sails_path() { const IDL: &str = include_str!("idls/full_coverage.idl"); diff --git a/rs/client-gen/tests/idls/aliases_works.idl b/rs/client-gen/tests/idls/aliases_works.idl new file mode 100644 index 000000000..9b9108a51 --- /dev/null +++ b/rs/client-gen/tests/idls/aliases_works.idl @@ -0,0 +1,12 @@ +service Aliases { + types { + struct MyStruct { f: u32 } + alias MyU32 = u32; + alias MyStructAlias = MyStruct; + alias MyGeneric = Result; + } + functions { + DoSomething(p: MyU32) -> MyStructAlias; + Gen() -> MyGeneric; + } +} \ No newline at end of file diff --git a/rs/client-gen/tests/idls/leaf.idl b/rs/client-gen/tests/idls/leaf.idl new file mode 100644 index 000000000..88d4d55d2 --- /dev/null +++ b/rs/client-gen/tests/idls/leaf.idl @@ -0,0 +1,5 @@ +service Leaf { + functions { + DoLeaf(); + } +} diff --git a/rs/client-gen/tests/idls/middle.idl b/rs/client-gen/tests/idls/middle.idl new file mode 100644 index 000000000..224d91db9 --- /dev/null +++ b/rs/client-gen/tests/idls/middle.idl @@ -0,0 +1,7 @@ +!@include: leaf.idl + +service Middle { + functions { + DoMiddle(); + } +} diff --git a/rs/client-gen/tests/idls/recursive_main.idl b/rs/client-gen/tests/idls/recursive_main.idl new file mode 100644 index 000000000..a57ec0a42 --- /dev/null +++ b/rs/client-gen/tests/idls/recursive_main.idl @@ -0,0 +1,7 @@ +!@include: middle.idl + +service Main { + functions { + DoMain(); + } +} diff --git a/rs/client-gen/tests/snapshots/generator__aliases_works.snap b/rs/client-gen/tests/snapshots/generator__aliases_works.snap new file mode 100644 index 000000000..33da6e2a2 --- /dev/null +++ b/rs/client-gen/tests/snapshots/generator__aliases_works.snap @@ -0,0 +1,59 @@ +--- +source: rs/client-gen/tests/generator.rs +expression: gen_client(idl) +--- +// Code generated by sails-client-gen. DO NOT EDIT. +#[cfg(feature = "with_mocks")] +#[cfg(not(target_arch = "wasm32"))] +extern crate std; +#[allow(unused_imports)] +use sails_rs::{client::*, collections::*, prelude::*}; + +pub mod aliases { + use super::*; + #[derive(PartialEq, Clone, Debug, Encode, Decode, TypeInfo, ReflectHash)] + #[codec(crate = sails_rs::scale_codec)] + #[scale_info(crate = sails_rs::scale_info)] + #[reflect_hash(crate = sails_rs)] + pub struct MyStruct { + pub f: u32, + } + pub type MyU32 = u32; + pub type MyStructAlias = MyStruct; + pub type MyGeneric = Result; + pub trait Aliases { + type Env: sails_rs::client::GearEnv; + fn do_something( + &mut self, + p: MyU32, + ) -> sails_rs::client::PendingCall; + fn gen(&mut self) -> sails_rs::client::PendingCall; + } + pub struct AliasesImpl; + impl Aliases for sails_rs::client::Service { + type Env = E; + fn do_something( + &mut self, + p: MyU32, + ) -> sails_rs::client::PendingCall { + self.pending_call((p,)) + } + fn gen(&mut self) -> sails_rs::client::PendingCall { + self.pending_call(()) + } + } + + pub mod io { + use super::*; + sails_rs::io_struct_impl!(DoSomething (p: super::MyU32) -> super::MyStructAlias); + sails_rs::io_struct_impl!(Gen () -> super::MyGeneric); + } + + #[cfg(feature = "with_mocks")] + #[cfg(not(target_arch = "wasm32"))] + pub mod mockall { + use super::*; + use sails_rs::mockall::*; + mock! { pub Aliases {} #[allow(refining_impl_trait)] #[allow(clippy::type_complexity)] impl aliases::Aliases for Aliases { type Env = sails_rs::client::GstdEnv; fn do_something (&mut self, p: MyU32) -> sails_rs::client::PendingCall;fn gen (&mut self, ) -> sails_rs::client::PendingCall; } } + } +} diff --git a/rs/idl-gen/src/type_resolver.rs b/rs/idl-gen/src/type_resolver.rs index f251d8db0..30b9bb8a2 100644 --- a/rs/idl-gen/src/type_resolver.rs +++ b/rs/idl-gen/src/type_resolver.rs @@ -58,6 +58,7 @@ impl UserDefinedEntry { }); fields } + sails_idl_meta::TypeDef::Alias(_) => Vec::new(), } } } diff --git a/rs/idl-meta/src/ast.rs b/rs/idl-meta/src/ast.rs index 025466087..53748030d 100644 --- a/rs/idl-meta/src/ast.rs +++ b/rs/idl-meta/src/ast.rs @@ -222,8 +222,8 @@ pub type ServiceEvent = EnumVariant; /// - tuples (`Tuple`), /// - named types (e.g. `Point`) /// - container types like `Option`, `Result` -/// - user-defined types with generics (`UserDefined`), -/// - bare generic parameters (`T`). +/// - user-defined named type +/// - generic type parameter (e.g. `T`) used in type definitions. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum TypeDecl { /// Slice type `[T]`. @@ -493,6 +493,7 @@ impl Display for TypeParameter { pub enum TypeDef { Struct(StructDef), Enum(EnumDef), + Alias(AliasDef), } /// Struct definition backing a named type or an enum variant payload. @@ -574,3 +575,9 @@ pub struct EnumVariant { pub docs: Vec, pub annotations: Vec<(String, Option)>, } + +/// Alias definition backing a named alias type. +#[derive(Debug, Clone, PartialEq)] +pub struct AliasDef { + pub target: TypeDecl, +} diff --git a/rs/idl-meta/src/hash.rs b/rs/idl-meta/src/hash.rs index e82474e68..7b04bd97f 100644 --- a/rs/idl-meta/src/hash.rs +++ b/rs/idl-meta/src/hash.rs @@ -81,6 +81,7 @@ fn hash_type( } hash.finalize() } + TypeDef::Alias(alias_def) => hash_type_decl(&alias_def.target, type_map, type_params)?, }; Ok(bytes) } @@ -530,4 +531,63 @@ mod tests { map ); } + + #[test] + fn hash_alias_identical_to_target() { + let mut map = BTreeMap::new(); + + // 1. Define target struct + let struct_ty = Type { + name: "TargetStruct".to_string(), + type_params: vec![], + def: TypeDef::Struct(StructDef { + fields: vec![StructField { + name: Some("f1".to_string()), + type_decl: TypeDecl::Primitive(PrimitiveType::U32), + docs: vec![], + annotations: vec![], + }], + }), + docs: vec![], + annotations: vec![], + }; + map.insert("TargetStruct", &struct_ty); + + // 2. Define alias to that struct + let alias_ty = Type { + name: "MyAlias".to_string(), + type_params: vec![], + def: TypeDef::Alias(AliasDef { + target: TypeDecl::Named { + name: "TargetStruct".to_string(), + generics: vec![], + }, + }), + docs: vec![], + annotations: vec![], + }; + map.insert("MyAlias", &alias_ty); + + let struct_hash = hash_type_decl( + &TypeDecl::Named { + name: "TargetStruct".to_string(), + generics: vec![], + }, + &map, + None, + ) + .unwrap(); + + let alias_hash = hash_type_decl( + &TypeDecl::Named { + name: "MyAlias".to_string(), + generics: vec![], + }, + &map, + None, + ) + .unwrap(); + + assert_eq!(struct_hash, alias_hash); + } } diff --git a/rs/idl-meta/templates/type.askama b/rs/idl-meta/templates/type.askama index 295db3f2d..913367d56 100644 --- a/rs/idl-meta/templates/type.askama +++ b/rs/idl-meta/templates/type.askama @@ -4,8 +4,7 @@ {%- for (k, v) in annotations %} @{{ k }} {%- if v.is_some() -%}: {{ v.as_ref().unwrap() }}{% endif %} {%- endfor %} -{% match def %}{% when TypeDef::Struct(_) %}struct{% when TypeDef::Enum(_) %}enum{% endmatch %} {{ name }} - +{% match def %}{% when TypeDef::Struct(_) %}struct{% when TypeDef::Enum(_) %}enum{% when TypeDef::Alias(_) %}alias{% endmatch %} {{ name }} {%- if type_params.len() > 0 -%} < {%- for type_param in type_params -%} @@ -22,4 +21,5 @@ {{ variant | indent(4) }}, {%- endfor %} } -{%- endmatch -%} +{%- when TypeDef::Alias(alias_def) -%} = {{ alias_def.target }}; +{%- endmatch -%} \ No newline at end of file diff --git a/rs/idl-meta/tests/snapshots/templates__type_alias.snap b/rs/idl-meta/tests/snapshots/templates__type_alias.snap new file mode 100644 index 000000000..95781c853 --- /dev/null +++ b/rs/idl-meta/tests/snapshots/templates__type_alias.snap @@ -0,0 +1,6 @@ +--- +source: rs/idl-meta/tests/templates.rs +expression: idl +--- +/// My alias docs +alias MyAlias= u32; diff --git a/rs/idl-meta/tests/templates.rs b/rs/idl-meta/tests/templates.rs index 696700bf3..48084afda 100644 --- a/rs/idl-meta/tests/templates.rs +++ b/rs/idl-meta/tests/templates.rs @@ -10,6 +10,21 @@ fn type_enum() { insta::assert_snapshot!(idl); } +#[test] +fn type_alias() { + let ty = Type { + name: "MyAlias".to_string(), + type_params: vec![], + def: TypeDef::Alias(AliasDef { + target: TypeDecl::Primitive(PrimitiveType::U32), + }), + docs: vec!["My alias docs".to_string()], + annotations: vec![], + }; + let idl = ty.render().unwrap(); + insta::assert_snapshot!(idl); +} + #[test] fn idl_globals() { let doc = IdlDoc { diff --git a/rs/idl-parser-v2/src/ffi/ast/mod.rs b/rs/idl-parser-v2/src/ffi/ast/mod.rs index 3f107d82c..9a78d56ba 100644 --- a/rs/idl-parser-v2/src/ffi/ast/mod.rs +++ b/rs/idl-parser-v2/src/ffi/ast/mod.rs @@ -681,3 +681,17 @@ impl TypeDef { } } } + +/// FFI-safe representation of a [crate::ast::AliasDef]. +#[repr(C)] +pub struct AliasDef { + pub raw_ptr: Ptr, +} + +impl AliasDef { + pub fn from_ast(alias_def: &ast::AliasDef, _allocations: &mut Allocations) -> Self { + AliasDef { + raw_ptr: Ptr::from(alias_def), + } + } +} diff --git a/rs/idl-parser-v2/src/ffi/ast/visitor.rs b/rs/idl-parser-v2/src/ffi/ast/visitor.rs index 41573e024..42d665740 100644 --- a/rs/idl-parser-v2/src/ffi/ast/visitor.rs +++ b/rs/idl-parser-v2/src/ffi/ast/visitor.rs @@ -1,7 +1,7 @@ use super::{ - Allocations, Annotation, CtorFunc, EnumDef, EnumVariant, ErrorCode, FuncParam, IdlDoc, - ProgramUnit, ServiceEvent, ServiceExpo, ServiceFunc, ServiceUnit, StructDef, StructField, Type, - TypeDecl, TypeDef, TypeParameter, + AliasDef, Allocations, Annotation, CtorFunc, EnumDef, EnumVariant, ErrorCode, FuncParam, + IdlDoc, ProgramUnit, ServiceEvent, ServiceExpo, ServiceFunc, ServiceUnit, StructDef, + StructField, Type, TypeDecl, TypeDef, TypeParameter, }; use crate::ast; use crate::visitor::Visitor as RawVisitor; @@ -48,6 +48,7 @@ pub struct Visitor { pub visit_enum_def: Option, pub visit_enum_variant: Option, + pub visit_alias_def: Option, pub visit_service_expo: Option, pub visit_type_parameter: @@ -80,6 +81,7 @@ unsafe extern "C" { fn visit_struct_field(context: *const (), field: *const StructField); fn visit_enum_def(context: *const (), def: *const EnumDef); fn visit_enum_variant(context: *const (), variant: *const EnumVariant); + fn visit_alias_def(context: *const (), def: *const AliasDef); fn visit_service_expo(context: *const (), service_item: *const ServiceExpo); fn visit_type_parameter(context: *const (), type_param: *const TypeParameter); fn visit_type_def(context: *const (), type_def: *const TypeDef); @@ -104,6 +106,7 @@ static VISITOR: Visitor = Visitor { visit_struct_field: Some(visit_struct_field), visit_enum_def: Some(visit_enum_def), visit_enum_variant: Some(visit_enum_variant), + visit_alias_def: Some(visit_alias_def), visit_service_expo: Some(visit_service_expo), visit_type_parameter: Some(visit_type_parameter), visit_type_def: Some(visit_type_def), @@ -305,6 +308,15 @@ impl<'a, 'ast> RawVisitor<'ast> for VisitorWrapper<'a> { crate::visitor::accept_enum_variant(variant, self); } + fn visit_alias_def(&mut self, def: &'ast ast::AliasDef) { + if let Some(visit) = self.visitor.visit_alias_def { + let ffi_def = AliasDef::from_ast(def, &mut self.allocations); + unsafe { visit(self.context, &ffi_def) }; + return; + } + crate::visitor::accept_alias_def(def, self); + } + fn visit_service_expo(&mut self, service_item: &'ast ast::ServiceExpo) { if let Some(visit) = self.visitor.visit_service_expo { let ffi_item = ServiceExpo::from_ast(service_item, &mut self.allocations); @@ -389,6 +401,7 @@ accept_impl!(struct_def, StructDef, ast::StructDef); accept_impl!(struct_field, StructField, ast::StructField); accept_impl!(enum_def, EnumDef, ast::EnumDef); accept_impl!(enum_variant, EnumVariant, ast::EnumVariant); +accept_impl!(alias_def, AliasDef, ast::AliasDef); accept_impl!(service_expo, ServiceExpo, ast::ServiceExpo); accept_impl!(type_parameter, TypeParameter, ast::TypeParameter); accept_impl!(type_def, TypeDef, ast::TypeDef); diff --git a/rs/idl-parser-v2/src/lib.rs b/rs/idl-parser-v2/src/lib.rs index f2870d023..6405a0050 100644 --- a/rs/idl-parser-v2/src/lib.rs +++ b/rs/idl-parser-v2/src/lib.rs @@ -18,6 +18,7 @@ pub mod ffi { pub mod ast; } mod post_process; +pub mod preprocess; pub mod visitor; // Sails IDL v2 — parser using `pest-rs` @@ -203,16 +204,45 @@ pub fn parse_type(p: Pair) -> Result { match p.as_rule() { Rule::StructDecl => parse_struct_type(p), Rule::EnumDecl => parse_enum_type(p), - Rule::AliasDecl => { - // TODO: Alias is not implemented - Err(Error::Validation("unimplemented AliasDecl".to_string())) - } + Rule::AliasDecl => parse_alias_decl(p), _ => Err(Error::Rule( "expected StructDecl | EnumDecl | AliasDecl".to_string(), )), } } +fn parse_alias_decl(p: Pair) -> Result { + let mut it = p.into_inner(); + let name = expect_next(&mut it, parse_ident)?; + let mut type_params = Vec::new(); + let mut target = None; + + for part in it { + match part.as_rule() { + Rule::TypeParams => { + for i in part.into_inner().filter(|x| x.as_rule() == Rule::Ident) { + let name = i.as_str().to_string(); + type_params.push(TypeParameter { name, ty: None }); + } + } + Rule::Primitive | Rule::Named | Rule::Tuple | Rule::Slice | Rule::Array => { + target = Some(parse_type_decl(part)?); + } + _ => {} + } + } + + let target = target.ok_or(Error::Rule("expected TypeDecl".to_string()))?; + + Ok(Type { + name, + type_params, + def: TypeDef::Alias(AliasDef { target }), + docs: Vec::new(), + annotations: Vec::new(), + }) +} + fn parse_struct_type(p: Pair) -> Result { let mut it = p.into_inner(); let (docs, annotations) = parse_docs_and_annotations(&mut it)?; @@ -713,12 +743,27 @@ mod tests { } #[test] - fn parse_alias_decl_not_supported() { + fn parse_alias_decl_works() { const SRC: &str = r#"alias AliasName = u32;"#; let mut pairs = IdlParser::parse(Rule::AliasDecl, SRC).expect("parse alias"); - let err = - parse_type(pairs.next().expect("alias")).expect_err("alias should not be supported"); - assert!(err.to_string().contains("unimplemented AliasDecl")); + let ty = parse_type(pairs.next().expect("alias")).expect("alias should be supported"); + assert_eq!(ty.name, "AliasName"); + if let TypeDef::Alias(def) = ty.def { + assert_eq!(def.target, TypeDecl::Primitive(PrimitiveType::U32)); + } else { + panic!("Expected AliasDef"); + } + } + + #[test] + fn parse_generic_alias() { + const SRC: &str = r#"alias Result = Result;"#; + let mut pairs = IdlParser::parse(Rule::AliasDecl, SRC).expect("parse alias"); + let ty = parse_type(pairs.next().expect("alias")).expect("parse alias type"); + + assert_eq!(ty.name, "Result"); + assert_eq!(ty.type_params.len(), 1); + assert_eq!(ty.type_params[0].name, "T"); } } diff --git a/rs/idl-parser-v2/src/preprocess.rs b/rs/idl-parser-v2/src/preprocess.rs new file mode 100644 index 000000000..2d3667d40 --- /dev/null +++ b/rs/idl-parser-v2/src/preprocess.rs @@ -0,0 +1,189 @@ +use alloc::collections::BTreeSet; +use alloc::string::String; + +/// Trait for loading IDL content from a path. +pub trait IdlLoader { + /// Load the content of the IDL file at the given path. + /// Returns a tuple of (content, unique_id), where unique_id is used + /// to detect cycles and prevent duplicate includes (e.g. canonical path). + fn load(&self, path: &str) -> Result<(String, String), String>; +} + +/// Preprocesses the IDL source, starting from the given path, +/// resolving `!@include` directives recursively. +/// +/// Each file is included at most once (behavior similar to `#pragma once`). +pub fn preprocess(path: &str, loader: &impl IdlLoader) -> Result { + let mut visited = BTreeSet::new(); + preprocess_recursive(path, loader, &mut visited) +} + +fn preprocess_recursive( + path: &str, + loader: &impl IdlLoader, + visited: &mut BTreeSet, +) -> Result { + let (src, unique_id) = loader.load(path)?; + + if visited.contains(&unique_id) { + // If already visited, we return empty string to prevent duplication/cycle + return Ok(String::new()); + } + visited.insert(unique_id.clone()); + + let mut result = String::new(); + let mut brace_level: i32 = 0; + + for line in src.lines() { + let trimmed = line.trim(); + + if brace_level == 0 && trimmed.starts_with("!@include:") { + let next_path = trimmed.strip_prefix("!@include:").unwrap().trim(); + let next_path = next_path.trim_matches('"').trim_matches('\''); + + let processed_content = preprocess_recursive(next_path, loader, visited)?; + + result.push_str(&processed_content); + if !processed_content.is_empty() && !processed_content.ends_with('\n') { + result.push('\n'); + } + continue; + } + + result.push_str(line); + result.push('\n'); + + let change = calculate_brace_change(line); + brace_level += change; + + if brace_level < 0 { + brace_level = 0; + } + } + + Ok(result) +} + +fn calculate_brace_change(line: &str) -> i32 { + let mut change = 0; + let mut in_string = false; // inside "..." + let mut in_char = false; // inside '...' + let mut chars = line.chars().peekable(); + + while let Some(c) = chars.next() { + if in_string { + if c == '"' { + in_string = false; + } else if c == '\\' { + // Skip escaped char + chars.next(); + } + } else if in_char { + if c == '\'' { + in_char = false; + } else if c == '\\' { + chars.next(); + } + } else { + match c { + '{' => change += 1, + '}' => change -= 1, + '"' => in_string = true, + '\'' => in_char = true, + '/' => { + if let Some('/') = chars.peek() { + // Found comment start '//', ignore rest of line + break; + } + } + _ => {} // Ignore other characters + } + } + } + change +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::collections::BTreeMap; + use alloc::format; + + struct MapLoader(BTreeMap); + + impl IdlLoader for MapLoader { + fn load(&self, path: &str) -> Result<(String, String), String> { + let content = self + .0 + .get(path) + .cloned() + .ok_or_else(|| format!("File not found: {path}"))?; + // In tests, path is its own unique_id + Ok((content, String::from(path))) + } + } + + #[test] + fn test_preprocess_recursive() { + let mut files = BTreeMap::new(); + files.insert("leaf.idl".into(), "service Leaf {}".into()); + files.insert( + "middle.idl".into(), + "!@include: leaf.idl\nservice Middle {}".into(), + ); + files.insert( + "main.idl".into(), + "!@include: middle.idl\nservice Main {}".into(), + ); + + let loader = MapLoader(files); + let result = preprocess("main.idl", &loader).unwrap(); + + assert!(result.contains("service Leaf")); + assert!(result.contains("service Middle")); + assert!(result.contains("service Main")); + } + + #[test] + fn test_preprocess_duplicate_prevented() { + let mut files = BTreeMap::new(); + files.insert("common.idl".into(), "struct Common {}".into()); + files.insert("a.idl".into(), "!@include: common.idl\nservice A {}".into()); + files.insert("b.idl".into(), "!@include: common.idl\nservice B {}".into()); + files.insert( + "main.idl".into(), + "!@include: a.idl\n!@include: b.idl".into(), + ); + + let loader = MapLoader(files); + let result = preprocess("main.idl", &loader).unwrap(); + + // Count occurrences of "struct Common" + let count = result.matches("struct Common").count(); + assert_eq!(count, 1); // Should be included only once + } + + #[test] + fn test_brace_counting_robustness() { + // Case 1: Braces in comments + // { -> +1, // starts comment, rest ignored. Total 1. + assert_eq!(calculate_brace_change("service { // { }"), 1); + + // Case 2: Braces in strings + // { -> +1 + // " { " -> string, braces inside ignored + // } -> -1 + // Total 0. + assert_eq!(calculate_brace_change(r#"service { " { " }"#), 0); + + // Case 3: Escaped quotes in strings + // { -> +1 + // " -> start string + // \" -> escaped quote (ignored) + // { -> inside string (ignored) + // " -> end string + // } -> -1 + // Total 0. + assert_eq!(calculate_brace_change(r#"{ " \" { " }"#), 0); + } +} diff --git a/rs/idl-parser-v2/src/visitor.rs b/rs/idl-parser-v2/src/visitor.rs index 47f67e43d..c563df9ac 100644 --- a/rs/idl-parser-v2/src/visitor.rs +++ b/rs/idl-parser-v2/src/visitor.rs @@ -82,6 +82,11 @@ pub trait Visitor<'ast> { accept_enum_variant(enum_variant, self); } + /// Visits an alias definition, [ast::AliasDef]. + fn visit_alias_def(&mut self, alias_def: &'ast ast::AliasDef) { + accept_alias_def(alias_def, self); + } + // ----- TypeDecl variants ----- /// Visits a slice type declaration, `[T]`, from [ast::TypeDecl::Slice]. @@ -273,6 +278,9 @@ pub fn accept_type_def<'ast>( ast::TypeDef::Enum(enum_def) => { visitor.visit_enum_def(enum_def); } + ast::TypeDef::Alias(alias_def) => { + visitor.visit_alias_def(alias_def); + } } } @@ -317,6 +325,15 @@ pub fn accept_enum_variant<'ast>( visitor.visit_struct_def(&enum_variant.def); } +/// Traverses the children of an [ast::AliasDef]. +/// It visits the target type of the alias. +pub fn accept_alias_def<'ast>( + alias_def: &'ast ast::AliasDef, + visitor: &mut (impl Visitor<'ast> + ?Sized), +) { + accept_type_decl(&alias_def.target, visitor); +} + #[cfg(test)] mod tests { use super::*;