From bac5d21d70f41d036ed055ab34ea2f02a8a8c36a Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 25 Jun 2025 21:40:10 +0200 Subject: [PATCH 01/13] feat: add support for `///` comments Inspired from #621 --- rust/candid/src/pretty/candid.rs | 24 ++-- rust/candid/src/types/syntax.rs | 9 +- rust/candid/src/types/type_env.rs | 2 + rust/candid_parser/src/bindings/javascript.rs | 9 +- rust/candid_parser/src/bindings/motoko.rs | 4 +- rust/candid_parser/src/bindings/rust.rs | 113 +++++++++++------- rust/candid_parser/src/bindings/typescript.rs | 22 ++-- rust/candid_parser/src/grammar.lalrpop | 28 +++-- rust/candid_parser/src/lib.rs | 20 ++-- rust/candid_parser/src/random.rs | 13 +- rust/candid_parser/src/test.rs | 2 +- rust/candid_parser/src/token.rs | 47 ++++++-- rust/candid_parser/src/typing.rs | 13 +- rust/candid_parser/src/utils.rs | 1 + 14 files changed, 214 insertions(+), 93 deletions(-) diff --git a/rust/candid/src/pretty/candid.rs b/rust/candid/src/pretty/candid.rs index c231d4c06..3ccbbb848 100644 --- a/rust/candid/src/pretty/candid.rs +++ b/rust/candid/src/pretty/candid.rs @@ -191,21 +191,27 @@ pub fn pp_modes(modes: &[FuncMode]) -> RcDoc { fn pp_service(serv: &[Binding]) -> RcDoc { let doc = concat( - serv.iter().map(|Binding { id, typ }| { - let func_doc = match typ { - IDLType::FuncT(ref f) => pp_function(f), - IDLType::VarT(_) => pp_ty(typ), - _ => unreachable!(), - }; - pp_text(id).append(kwd(" :")).append(func_doc) - }), + serv.iter().map( + |Binding { + id, + typ, + doc_comment: _, + }| { + let func_doc = match typ { + IDLType::FuncT(ref f) => pp_function(f), + IDLType::VarT(_) => pp_ty(typ), + _ => unreachable!(), + }; + pp_text(id).append(kwd(" :")).append(func_doc) + }, + ), ";", ); enclose_space("{", doc, "}") } fn pp_defs(env: &IDLMergedProg) -> RcDoc { - lines(env.get_types().iter().map(|(id, typ)| { + lines(env.get_types().iter().map(|(id, typ, _)| { kwd("type") .append(ident(id)) .append(kwd("=")) diff --git a/rust/candid/src/types/syntax.rs b/rust/candid/src/types/syntax.rs index 28b15dae2..d6d394542 100644 --- a/rust/candid/src/types/syntax.rs +++ b/rust/candid/src/types/syntax.rs @@ -189,6 +189,7 @@ impl IDLArgType { pub struct TypeField { pub label: Label, pub typ: IDLType, + pub doc_comment: Option>, } impl From for Field { @@ -211,6 +212,7 @@ pub enum Dec { pub struct Binding { pub id: String, pub typ: IDLType, + pub doc_comment: Option>, } #[derive(Debug, Default, Clone)] @@ -285,8 +287,11 @@ impl IDLMergedProg { } } - pub fn get_types(&self) -> Vec<(&str, &IDLType)> { - self.types.iter().map(|t| (t.id.as_str(), &t.typ)).collect() + pub fn get_types(&self) -> Vec<(&str, &IDLType, Option<&Vec>)> { + self.types + .iter() + .map(|t| (t.id.as_str(), &t.typ, t.doc_comment.as_ref())) + .collect() } pub fn get_bindings(&self) -> Vec { diff --git a/rust/candid/src/types/type_env.rs b/rust/candid/src/types/type_env.rs index b39d8543e..5955dbf02 100644 --- a/rust/candid/src/types/type_env.rs +++ b/rust/candid/src/types/type_env.rs @@ -234,6 +234,7 @@ impl TypeEnv { TypeField { label: field.id.as_ref().clone(), typ: self.as_idl_type(&field.ty), + doc_comment: None, } } @@ -250,6 +251,7 @@ impl TypeEnv { .map(|(id, t)| Binding { id: id.clone(), typ: self.as_idl_type(t), + doc_comment: None, }) .collect() } diff --git a/rust/candid_parser/src/bindings/javascript.rs b/rust/candid_parser/src/bindings/javascript.rs index 9f4cc197a..c13ab39bd 100644 --- a/rust/candid_parser/src/bindings/javascript.rs +++ b/rust/candid_parser/src/bindings/javascript.rs @@ -192,8 +192,13 @@ fn pp_modes(modes: &[candid::types::FuncMode]) -> RcDoc { fn pp_service(serv: &[Binding]) -> RcDoc { let doc = concat( - serv.iter() - .map(|Binding { id, typ }| quote_ident(id).append(kwd(":")).append(pp_ty(typ))), + serv.iter().map( + |Binding { + id, + typ, + doc_comment: _, + }| quote_ident(id).append(kwd(":")).append(pp_ty(typ)), + ), ",", ); enclose_space("({", doc, "})") diff --git a/rust/candid_parser/src/bindings/motoko.rs b/rust/candid_parser/src/bindings/motoko.rs index e9a3e6ad4..94cbf8fc4 100644 --- a/rust/candid_parser/src/bindings/motoko.rs +++ b/rust/candid_parser/src/bindings/motoko.rs @@ -253,8 +253,8 @@ fn pp_service(serv: &[Binding]) -> RcDoc { kwd("actor").append(enclose_space("{", doc, "}")) } -fn pp_defs<'a>(bindings: &[(&'a str, &'a IDLType)]) -> RcDoc<'a> { - lines(bindings.iter().map(|(id, typ)| { +fn pp_defs<'a>(bindings: &[(&'a str, &'a IDLType, Option<&'a Vec>)]) -> RcDoc<'a> { + lines(bindings.iter().map(|(id, typ, _)| { kwd("public type") .append(escape(id, false)) .append(" = ") diff --git a/rust/candid_parser/src/bindings/rust.rs b/rust/candid_parser/src/bindings/rust.rs index 002e93bd2..8754a54cf 100644 --- a/rust/candid_parser/src/bindings/rust.rs +++ b/rust/candid_parser/src/bindings/rust.rs @@ -101,18 +101,22 @@ fn as_result(fs: &[TypeField]) -> Option<(&IDLType, &IDLType, bool)> { [TypeField { label: ok, typ: t_ok, + doc_comment: _, }, TypeField { label: err, typ: t_err, + doc_comment: _, }] if *ok == Label::Named("Ok".to_string()) && *err == Label::Named("Err".to_string()) => { Some((t_ok, t_err, false)) } [TypeField { label: ok, typ: t_ok, + doc_comment: _, }, TypeField { label: err, typ: t_err, + doc_comment: _, }] if *ok == Label::Named("ok".to_string()) && *err == Label::Named("err".to_string()) => { Some((t_ok, t_err, true)) } @@ -869,19 +873,26 @@ impl NominalState<'_> { { let fs: Vec<_> = fs .iter() - .map(|TypeField { label, typ }| { - let lab = label.to_string(); - let elem = StateElem::Label(&lab); - let old = self.state.push_state(&elem); - path.push(TypePath::RecordField(lab.clone())); - let ty = self.nominalize(env, path, typ); - path.pop(); - self.state.pop_state(old, elem); - TypeField { - label: label.clone(), - typ: ty, - } - }) + .map( + |TypeField { + label, + typ, + doc_comment, + }| { + let lab = label.to_string(); + let elem = StateElem::Label(&lab); + let old = self.state.push_state(&elem); + path.push(TypePath::RecordField(lab.clone())); + let ty = self.nominalize(env, path, typ); + path.pop(); + self.state.pop_state(old, elem); + TypeField { + label: label.clone(), + typ: ty, + doc_comment: doc_comment.clone(), + } + }, + ) .collect(); IDLType::RecordT(fs) } else { @@ -900,6 +911,7 @@ impl NominalState<'_> { env.insert_binding(Binding { id: new_var.clone(), typ: ty, + doc_comment: None, }); IDLType::VarT(new_var) } @@ -909,23 +921,30 @@ impl NominalState<'_> { if matches!(path.last(), None | Some(TypePath::Id(_))) || is_result { let fs: Vec<_> = fs .iter() - .map(|TypeField { label, typ }| { - let lab = label.to_string(); - let old = self.state.push_state(&StateElem::Label(&lab)); - if is_result { - // so that inner record gets a new name - path.push(TypePath::ResultField(lab.clone())); - } else { - path.push(TypePath::VariantField(lab.clone())); - } - let ty = self.nominalize(env, path, typ); - path.pop(); - self.state.pop_state(old, StateElem::Label(&lab)); - TypeField { - label: label.clone(), - typ: ty, - } - }) + .map( + |TypeField { + label, + typ, + doc_comment, + }| { + let lab = label.to_string(); + let old = self.state.push_state(&StateElem::Label(&lab)); + if is_result { + // so that inner record gets a new name + path.push(TypePath::ResultField(lab.clone())); + } else { + path.push(TypePath::VariantField(lab.clone())); + } + let ty = self.nominalize(env, path, typ); + path.pop(); + self.state.pop_state(old, StateElem::Label(&lab)); + TypeField { + label: label.clone(), + typ: ty, + doc_comment: doc_comment.clone(), + } + }, + ) .collect(); IDLType::VariantT(fs) } else { @@ -944,6 +963,7 @@ impl NominalState<'_> { env.insert_binding(Binding { id: new_var.clone(), typ: ty, + doc_comment: None, }); IDLType::VarT(new_var) } @@ -1012,6 +1032,7 @@ impl NominalState<'_> { env.insert_binding(Binding { id: new_var.clone(), typ: ty, + doc_comment: None, }); IDLType::VarT(new_var) } @@ -1019,15 +1040,25 @@ impl NominalState<'_> { IDLType::ServT(serv) => match path.last() { None | Some(TypePath::Id(_)) => IDLType::ServT( serv.iter() - .map(|Binding { id, typ }| { - let lab = id.to_string(); - let old = self.state.push_state(&StateElem::Label(&lab)); - path.push(TypePath::Id(lab.clone())); - let ty = self.nominalize(env, path, typ); - path.pop(); - self.state.pop_state(old, StateElem::Label(&lab)); - Binding { id: lab, typ: ty } - }) + .map( + |Binding { + id, + typ, + doc_comment, + }| { + let lab = id.to_string(); + let old = self.state.push_state(&StateElem::Label(&lab)); + path.push(TypePath::Id(lab.clone())); + let ty = self.nominalize(env, path, typ); + path.pop(); + self.state.pop_state(old, StateElem::Label(&lab)); + Binding { + id: lab, + typ: ty, + doc_comment: doc_comment.clone(), + } + }, + ) .collect(), ), Some(_) => { @@ -1046,6 +1077,7 @@ impl NominalState<'_> { env.insert_binding(Binding { id: new_var.clone(), typ: ty, + doc_comment: None, }); IDLType::VarT(new_var) } @@ -1077,13 +1109,14 @@ impl NominalState<'_> { fn nominalize_all(&mut self) -> IDLMergedProg { let mut res = IDLMergedProg::new(); - for (id, typ) in self.state.prog.get_types() { + for (id, typ, doc_comment) in self.state.prog.get_types() { let elem = StateElem::Label(id); let old = self.state.push_state(&elem); let ty = self.nominalize(&mut res, &mut vec![TypePath::Id(id.to_string())], typ); res.insert_binding(Binding { id: id.to_string(), typ: ty, + doc_comment: doc_comment.cloned(), }); self.state.pop_state(old, elem); } diff --git a/rust/candid_parser/src/bindings/typescript.rs b/rust/candid_parser/src/bindings/typescript.rs index f9feeb324..b38360fd7 100644 --- a/rust/candid_parser/src/bindings/typescript.rs +++ b/rust/candid_parser/src/bindings/typescript.rs @@ -151,14 +151,20 @@ fn pp_function<'a>(prog: &'a IDLMergedProg, func: &'a FuncType) -> RcDoc<'a> { fn pp_service<'a>(prog: &'a IDLMergedProg, serv: &'a [Binding]) -> RcDoc<'a> { let doc = concat( - serv.iter().map(|Binding { id, typ }| { - let func = match typ { - IDLType::FuncT(ref func) => pp_function(prog, func), - IDLType::VarT(ref id) => ident(id), - _ => unreachable!(), - }; - quote_ident(id).append(kwd(":")).append(func) - }), + serv.iter().map( + |Binding { + id, + typ, + doc_comment: _, + }| { + let func = match typ { + IDLType::FuncT(ref func) => pp_function(prog, func), + IDLType::VarT(ref id) => ident(id), + _ => unreachable!(), + }; + quote_ident(id).append(kwd(":")).append(func) + }, + ), ",", ); enclose_space("{", doc, "}") diff --git a/rust/candid_parser/src/grammar.lalrpop b/rust/candid_parser/src/grammar.lalrpop index dd22238eb..1d5c2afe4 100644 --- a/rust/candid_parser/src/grammar.lalrpop +++ b/rust/candid_parser/src/grammar.lalrpop @@ -1,12 +1,12 @@ use super::test::{Assert, Input, Test}; -use super::token::{Token, error, error2, LexicalError, Span}; +use super::token::{Token, error, error2, LexicalError, Span, TriviaMap}; use candid::{Principal, types::Label}; use candid::types::syntax::{IDLType, PrimType, TypeField, FuncType, Binding, Dec, IDLProg, IDLTypes, IDLInitArgs, IDLArgType}; use candid::types::value::{IDLField, IDLValue, IDLArgs, VariantValue}; use candid::types::{TypeEnv, FuncMode}; use candid::utils::check_unique; -grammar; +grammar(trivia: Option<&TriviaMap>); extern { type Location = usize; @@ -174,7 +174,7 @@ pub Typ: IDLType = { Label::Unnamed(_) => { id = id + 1; Label::Unnamed(id - 1) }, ref l => { id = l.get_id() + 1; l.clone() }, }; - TypeField { label, typ: f.typ.clone() } + TypeField { label, typ: f.typ.clone(), doc_comment: f.doc_comment.clone() } }).collect(); fs.sort_unstable_by_key(|TypeField { label, .. }| label.get_id()); check_unique(fs.iter().map(|f| &f.label)).map_err(|e| error2(e, span))?; @@ -202,19 +202,19 @@ PrimTyp: IDLType = { } FieldTyp: TypeField = { - ":" =>? Ok(TypeField { label: Label::Id(n), typ: t }), - ":" => TypeField { label: Label::Named(n), typ: t }, + ":" =>? Ok(TypeField { label: Label::Id(id), typ, doc_comment }), + ":" => TypeField { label: Label::Named(n), typ, doc_comment }, } RecordFieldTyp: TypeField = { FieldTyp => <>, - Typ => TypeField { label: Label::Unnamed(0), typ: <> }, + => TypeField { label: Label::Unnamed(0), typ, doc_comment }, } VariantFieldTyp: TypeField = { FieldTyp => <>, - Name => TypeField { label: Label::Named(<>), typ: IDLType::PrimT(PrimType::Null) }, - FieldId =>? Ok(TypeField { label: Label::Id(<>), typ: IDLType::PrimT(PrimType::Null) }), + => TypeField { label: Label::Named(n), typ: IDLType::PrimT(PrimType::Null), doc_comment }, + =>? Ok(TypeField { label: Label::Id(id), typ: IDLType::PrimT(PrimType::Null), doc_comment }), } ArgTupTyp: Vec = "(" > ")" =>? { @@ -253,13 +253,13 @@ ActorTyp: Vec = { } MethTyp: Binding = { - ":" => Binding { id: n, typ: IDLType::FuncT(f) }, - ":" => Binding { id: n, typ: IDLType::VarT(id) }, + ":" => Binding { id: n, typ: IDLType::FuncT(f), doc_comment }, + ":" => Binding { id: n, typ: IDLType::VarT(id), doc_comment }, } // Type declarations Def: Dec = { - "type" "=" => Dec::TypD(Binding { id: id, typ: t }), + "type" "=" => Dec::TypD(Binding { id: id, typ: t, doc_comment }), "import" => Dec::ImportType(<>), "import" "service" => Dec::ImportServ(<>), } @@ -327,3 +327,9 @@ SepBy: Vec = { #[inline] Sp: (T, Span) = => (t, l..r); + +#[inline] +DocComment: Option> = + => { + trivia.and_then(|t| t.borrow().get(&l).cloned()) + }; diff --git a/rust/candid_parser/src/lib.rs b/rust/candid_parser/src/lib.rs index ba0596dd7..2ece94517 100644 --- a/rust/candid_parser/src/lib.rs +++ b/rust/candid_parser/src/lib.rs @@ -119,6 +119,10 @@ #![cfg_attr(docsrs, feature(doc_cfg))] pub mod error; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + pub use error::{ pretty_parse, pretty_parse_idl_prog, pretty_parse_idl_types, pretty_wrap, Error, Result, }; @@ -143,31 +147,33 @@ pub mod random; pub mod test; pub fn parse_idl_prog(str: &str) -> Result { - let lexer = token::Tokenizer::new(str); - Ok(grammar::IDLProgParser::new().parse(lexer)?) + let trivia: token::TriviaMap = Rc::new(RefCell::new(HashMap::new())); + let lexer = token::Tokenizer::new_with_trivia(str, trivia.clone()); + let res = grammar::IDLProgParser::new().parse(Some(&trivia.clone()), lexer)?; + Ok(res) } pub fn parse_idl_init_args(str: &str) -> Result { let lexer = token::Tokenizer::new(str); - Ok(grammar::IDLInitArgsParser::new().parse(lexer)?) + Ok(grammar::IDLInitArgsParser::new().parse(None, lexer)?) } pub fn parse_idl_type(str: &str) -> Result { let lexer = token::Tokenizer::new(str); - Ok(grammar::TypParser::new().parse(lexer)?) + Ok(grammar::TypParser::new().parse(None, lexer)?) } pub fn parse_idl_types(str: &str) -> Result { let lexer = token::Tokenizer::new(str); - Ok(grammar::TypsParser::new().parse(lexer)?) + Ok(grammar::TypsParser::new().parse(None, lexer)?) } pub fn parse_idl_args(s: &str) -> crate::Result { let lexer = token::Tokenizer::new(s); - Ok(grammar::ArgsParser::new().parse(lexer)?) + Ok(grammar::ArgsParser::new().parse(None, lexer)?) } pub fn parse_idl_value(s: &str) -> crate::Result { let lexer = token::Tokenizer::new(s); - Ok(grammar::ArgParser::new().parse(lexer)?) + Ok(grammar::ArgParser::new().parse(None, lexer)?) } diff --git a/rust/candid_parser/src/random.rs b/rust/candid_parser/src/random.rs index 1c3f9e517..75bc6a12b 100644 --- a/rust/candid_parser/src/random.rs +++ b/rust/candid_parser/src/random.rs @@ -217,7 +217,12 @@ impl RandState<'_> { } IDLType::RecordT(fs) => { let mut res = Vec::new(); - for TypeField { label, typ } in fs.iter() { + for TypeField { + label, + typ, + doc_comment: _, + } in fs.iter() + { let lab_str = label.to_string(); let elem = StateElem::Label(&lab_str); let old_config = self.0.push_state(&elem); @@ -245,7 +250,11 @@ impl RandState<'_> { choices.collect() }; let idx = arbitrary_variant(u, &sizes)?; - let TypeField { label, typ } = &fs[idx]; + let TypeField { + label, + typ, + doc_comment: _, + } = &fs[idx]; let lab_str = label.to_string(); let elem = StateElem::Label(&lab_str); let old_config = self.0.push_state(&elem); diff --git a/rust/candid_parser/src/test.rs b/rust/candid_parser/src/test.rs index 974c4768e..e9becf818 100644 --- a/rust/candid_parser/src/test.rs +++ b/rust/candid_parser/src/test.rs @@ -65,7 +65,7 @@ impl std::str::FromStr for Test { type Err = Error; fn from_str(str: &str) -> std::result::Result { let lexer = super::token::Tokenizer::new(str); - Ok(super::grammar::TestParser::new().parse(lexer)?) + Ok(super::grammar::TestParser::new().parse(None, lexer)?) } } diff --git a/rust/candid_parser/src/token.rs b/rust/candid_parser/src/token.rs index db7ae2d61..f3669b1eb 100644 --- a/rust/candid_parser/src/token.rs +++ b/rust/candid_parser/src/token.rs @@ -1,3 +1,5 @@ +use std::{cell::RefCell, collections::HashMap, mem, rc::Rc}; + use lalrpop_util::ParseError; use logos::{Lexer, Logos}; @@ -5,6 +7,8 @@ use logos::{Lexer, Logos}; #[logos(skip r"[ \t\r\n]+")] #[logos(skip r"//[^\n]*")] // line comment pub enum Token { + #[regex(r"///[^\n]*")] + DocComment, #[token("/*")] StartComment, #[token("=")] @@ -118,22 +122,36 @@ fn parse_number(lex: &mut Lexer) -> String { } } +pub type TriviaMap = Rc>>>; + pub struct Tokenizer<'input> { lex: Lexer<'input, Token>, + comment_buffer: Vec, + trivia: Option, } + impl<'input> Tokenizer<'input> { pub fn new(input: &'input str) -> Self { let lex = Token::lexer(input); - Tokenizer { lex } + Tokenizer { + lex, + comment_buffer: vec![], + trivia: None, + } + } + + pub fn new_with_trivia(input: &'input str, trivia: TriviaMap) -> Self { + let lex = Token::lexer(input); + Tokenizer { + lex, + comment_buffer: vec![], + trivia: Some(trivia), + } } } pub type Span = std::ops::Range; -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Spanned { - pub span: Span, - pub value: T, -} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct LexicalError { pub err: String, @@ -179,6 +197,13 @@ impl Iterator for Tokenizer<'_> { let err = format!("Unknown token {}", self.lex.slice()); Some(Err(LexicalError::new(err, span))) } + Ok(Token::DocComment) => { + let content = self.lex.slice(); + if self.trivia.is_some() { + self.comment_buffer.push(content.to_string()); + } + self.next() + } Ok(Token::StartComment) => { let mut lex = self.lex.to_owned().morph::(); let mut nesting = 1; @@ -278,7 +303,15 @@ impl Iterator for Tokenizer<'_> { self.lex = lex.morph::(); Some(Ok((span.start, Token::Text(result), self.lex.span().end))) } - Ok(token) => Some(Ok((span.start, token, span.end))), + Ok(token) => { + if let Some(trivia) = &mut self.trivia { + if !self.comment_buffer.is_empty() { + let content: Vec = mem::take(&mut self.comment_buffer); + trivia.borrow_mut().insert(span.start, content); + } + } + Some(Ok((span.start, token, span.end))) + } } } } diff --git a/rust/candid_parser/src/typing.rs b/rust/candid_parser/src/typing.rs index 84a0d616e..10af6135f 100644 --- a/rust/candid_parser/src/typing.rs +++ b/rust/candid_parser/src/typing.rs @@ -145,7 +145,11 @@ fn check_meths(env: &Env, ms: &[Binding]) -> Result> { fn check_defs(env: &mut Env, decs: &[Dec]) -> Result<()> { for dec in decs.iter() { match dec { - Dec::TypD(Binding { id, typ }) => { + Dec::TypD(Binding { + id, + typ, + doc_comment: _, + }) => { let t = check_type(env, typ)?; env.te.0.insert(id.to_string(), t); } @@ -180,7 +184,12 @@ fn check_cycle(env: &TypeEnv) -> Result<()> { fn check_decs(env: &mut Env, decs: &[Dec]) -> Result<()> { for dec in decs.iter() { - if let Dec::TypD(Binding { id, typ: _ }) = dec { + if let Dec::TypD(Binding { + id, + typ: _, + doc_comment: _, + }) = dec + { let duplicate = env.te.0.insert(id.to_string(), TypeInner::Unknown.into()); if duplicate.is_some() { return Err(Error::msg(format!("duplicate binding for {id}"))); diff --git a/rust/candid_parser/src/utils.rs b/rust/candid_parser/src/utils.rs index 89c870437..3826cac1c 100644 --- a/rust/candid_parser/src/utils.rs +++ b/rust/candid_parser/src/utils.rs @@ -83,6 +83,7 @@ pub fn get_metadata(env: &IDLMergedProg) -> Option { filtered.insert_binding(Binding { id: d.to_string(), typ: typ.clone(), + doc_comment: None, }); } } From 8b5d092de7600a9741526c25cd3d1dab4e29e034 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 25 Jun 2025 22:18:18 +0200 Subject: [PATCH 02/13] feat: support parsing comments from Rust candid methods --- rust/candid/tests/types.rs | 18 +++++++ rust/candid_derive/src/func.rs | 86 ++++++++++++++++++++++------------ rust/candid_derive/src/lib.rs | 25 ++++++++++ 3 files changed, 99 insertions(+), 30 deletions(-) diff --git a/rust/candid/tests/types.rs b/rust/candid/tests/types.rs index 5abf54374..847dbedaf 100644 --- a/rust/candid/tests/types.rs +++ b/rust/candid/tests/types.rs @@ -156,10 +156,12 @@ fn test_struct() { TypeField { label: Label::Named("bar".to_string()), typ: IDLType::PrimT(PrimType::Bool), + doc_comment: None, }, TypeField { label: Label::Named("foo".to_string()), typ: IDLType::PrimT(PrimType::Int), + doc_comment: None, }, ]) ); @@ -181,10 +183,12 @@ fn test_struct() { TypeField { label: Label::Named("g1".to_string()), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, TypeField { label: Label::Named("g2".to_string()), typ: IDLType::PrimT(PrimType::Bool), + doc_comment: None, }, ]) ); @@ -205,10 +209,12 @@ fn test_struct() { TypeField { label: Label::Named("head".to_string()), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, TypeField { label: Label::Named("tail".to_string()), typ: IDLType::OptT(Box::new(IDLType::VarT("List".to_string()))), + doc_comment: None, }, ]) ); @@ -229,10 +235,12 @@ fn test_struct() { TypeField { label: Label::Named("head".to_string()), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, TypeField { label: Label::Named("tail".to_string()), typ: IDLType::OptT(Box::new(IDLType::VarT("GenericList".to_string()))), + doc_comment: None, }, ]) ); @@ -250,10 +258,12 @@ fn test_struct() { TypeField { label: Label::Named("head".to_string()), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, TypeField { label: Label::Named("tail".to_string()), typ: IDLType::OptT(Box::new(IDLType::VarT("GenericList".to_string()))), + doc_comment: None, }, ]) ); @@ -292,12 +302,15 @@ fn test_variant() { TypeField { label: Label::Id(0), typ: IDLType::PrimT(PrimType::Bool), + doc_comment: None, }, TypeField { label: Label::Id(1), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, ]), + doc_comment: None, }, TypeField { label: Label::Named("Baz".to_string()), @@ -305,20 +318,25 @@ fn test_variant() { TypeField { label: Label::Named("a".to_string()), typ: IDLType::PrimT(PrimType::Int32), + doc_comment: None, }, TypeField { label: Label::Named("b".to_string()), typ: IDLType::PrimT(PrimType::Nat32), + doc_comment: None, }, ]), + doc_comment: None, }, TypeField { label: Label::Named("Foo".to_string()), typ: IDLType::PrimT(PrimType::Null), + doc_comment: None, }, TypeField { label: Label::Named("Newtype".to_string()), typ: IDLType::PrimT(PrimType::Bool), + doc_comment: None, }, ]) ); diff --git a/rust/candid_derive/src/func.rs b/rust/candid_derive/src/func.rs index 60133a391..808467fc5 100644 --- a/rust/candid_derive/src/func.rs +++ b/rust/candid_derive/src/func.rs @@ -1,3 +1,5 @@ +use crate::{get_doc_comment_from_lines, get_doc_comment_lines}; + use super::{candid_path, get_lit_str}; use lazy_static::lazy_static; use proc_macro2::TokenStream; @@ -10,11 +12,13 @@ type RawArgs = Vec<(Option, String)>; type RawRets = Vec; type ParsedArgs = Vec<(Option, Type)>; type ParsedRets = Vec; +type CommentLines = Vec; struct Method { args: RawArgs, rets: RawRets, modes: String, + doc_comment: CommentLines, } // There is no official way to communicate information across proc macro invocations. @@ -54,6 +58,7 @@ pub(crate) fn candid_method(attrs: Vec, fun: ItemFn) -> Result { @@ -69,7 +74,15 @@ pub(crate) fn candid_method(attrs: Vec, fun: ItemFn) -> Result) -> TokenStream { } else { unreachable!(); }; - let gen_tys = meths.iter().map(|(name, Method { args, rets, modes })| { - let args = args - .iter() - .map(|t| generate_arg(quote! { args }, t)) - .collect::>(); - let rets = rets - .iter() - .map(|t| generate_ret(quote! { rets }, t)) - .collect::>(); - let modes = match modes.as_ref() { - "query" => quote! { vec![#candid::types::FuncMode::Query] }, - "composite_query" => quote! { vec![#candid::types::FuncMode::CompositeQuery] }, - "oneway" => quote! { vec![#candid::types::FuncMode::Oneway] }, - "update" => quote! { vec![] }, - _ => unreachable!(), - }; - quote! { - { - let mut args: Vec = Vec::new(); - #(#args)* - let mut rets: Vec = Vec::new(); - #(#rets)* - let func = Function { args, rets, modes: #modes }; - service.push((#name.to_string(), TypeInner::Func(func).into())); + let gen_tys = meths.iter().map( + |( + name, + Method { + args, + rets, + modes, + doc_comment, + }, + )| { + let args = args + .iter() + .map(|t| generate_arg(quote! { args }, t)) + .collect::>(); + let rets = rets + .iter() + .map(|t| generate_ret(quote! { rets }, t)) + .collect::>(); + let modes = match modes.as_ref() { + "query" => quote! { vec![#candid::types::FuncMode::Query] }, + "composite_query" => quote! { vec![#candid::types::FuncMode::CompositeQuery] }, + "oneway" => quote! { vec![#candid::types::FuncMode::Oneway] }, + "update" => quote! { vec![] }, + _ => unreachable!(), + }; + let doc_comment = get_doc_comment_from_lines(doc_comment.as_slice()); + quote! { + { + let mut args: Vec = Vec::new(); + #(#args)* + let mut rets: Vec = Vec::new(); + #(#rets)* + let func = Function { args, rets, modes: #modes }; + service.insert(#name.to_string(), (TypeInner::Func(func).into(), #doc_comment)); + } } - } - }); + }, + ); let service = quote! { use #candid::types::{CandidType, Function, Type, ArgType, TypeInner}; use #candid::types::syntax::{Binding, IDLMergedProg}; - let mut service = Vec::<(String, Type)>::new(); + let mut service: std::collections::HashMap)> = std::collections::HashMap::new(); let mut env = #candid::types::internal::TypeContainer::new(); #(#gen_tys)* - service.sort_unstable_by_key(|(name, _)| name.clone()); - let ty = TypeInner::Service(service).into(); + let mut service_methods = service.iter().map(|(id, (typ, _))| (id.clone(), typ.clone())).collect::>(); + service_methods.sort_unstable_by_key(|(name, _)| name.clone()); + let ty = TypeInner::Service(service_methods).into(); }; let actor = if let Some(init) = init { quote! { @@ -145,6 +170,7 @@ pub(crate) fn export_service(path: Option) -> TokenStream { let bindings = env.env.0.iter().map(|(id, t)| Binding { id: id.clone(), typ: env.as_idl_type(t), + doc_comment: service.get(id).map(|(_, c)| c).cloned(), }).collect::>(); let mut idl_merged_prog = IDLMergedProg::from(bindings); idl_merged_prog.set_actor(Some(env.as_idl_type(&actor))); diff --git a/rust/candid_derive/src/lib.rs b/rust/candid_derive/src/lib.rs index eb26e4263..5db103a19 100644 --- a/rust/candid_derive/src/lib.rs +++ b/rust/candid_derive/src/lib.rs @@ -61,6 +61,31 @@ pub(crate) fn get_lit_str(expr: &syn::Expr) -> std::result::Result Vec { + attrs + .iter() + .filter_map(|attr| match &attr.meta { + syn::Meta::NameValue(m) if m.path.is_ident("doc") => { + if let Ok(lit) = get_lit_str(&m.value) { + Some(lit.value().trim().to_string()) + } else { + None + } + } + _ => None, + }) + .collect() +} + +pub(crate) fn get_doc_comment_from_lines(comment_lines: &[String]) -> proc_macro2::TokenStream { + let comment_strings: Vec = comment_lines + .iter() + .map(|s| quote::quote! { #s.to_string() }) + .collect(); + + quote::quote! { vec![#(#comment_strings),*] } +} + fn get_custom_candid_path(input: &syn::DeriveInput) -> Result> { let candid_path_helper_attribute_option = input .attrs From f51d7fb0977c6948cc0f08e775b3f01254644cb5 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 25 Jun 2025 22:22:33 +0200 Subject: [PATCH 03/13] refactor: rename and move function --- rust/candid_derive/src/func.rs | 13 +++++++++++-- rust/candid_derive/src/lib.rs | 9 --------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/rust/candid_derive/src/func.rs b/rust/candid_derive/src/func.rs index 808467fc5..84b2fcdc9 100644 --- a/rust/candid_derive/src/func.rs +++ b/rust/candid_derive/src/func.rs @@ -1,4 +1,4 @@ -use crate::{get_doc_comment_from_lines, get_doc_comment_lines}; +use crate::get_doc_comment_lines; use super::{candid_path, get_lit_str}; use lazy_static::lazy_static; @@ -131,7 +131,7 @@ pub(crate) fn export_service(path: Option) -> TokenStream { "update" => quote! { vec![] }, _ => unreachable!(), }; - let doc_comment = get_doc_comment_from_lines(doc_comment.as_slice()); + let doc_comment = generate_doc_comment(doc_comment.as_slice()); quote! { { let mut args: Vec = Vec::new(); @@ -205,6 +205,15 @@ fn generate_ret(name: TokenStream, ty: &str) -> TokenStream { } } +fn generate_doc_comment(comment_lines: &[String]) -> TokenStream { + let comment_strings: Vec = comment_lines + .iter() + .map(|s| quote::quote! { #s.to_string() }) + .collect(); + + quote::quote! { vec![#(#comment_strings),*] } +} + fn get_args(sig: &Signature) -> Result<(ParsedArgs, ParsedRets)> { let mut args = Vec::new(); for arg in &sig.inputs { diff --git a/rust/candid_derive/src/lib.rs b/rust/candid_derive/src/lib.rs index 5db103a19..78b6d3031 100644 --- a/rust/candid_derive/src/lib.rs +++ b/rust/candid_derive/src/lib.rs @@ -77,15 +77,6 @@ pub(crate) fn get_doc_comment_lines(attrs: &[syn::Attribute]) -> Vec { .collect() } -pub(crate) fn get_doc_comment_from_lines(comment_lines: &[String]) -> proc_macro2::TokenStream { - let comment_strings: Vec = comment_lines - .iter() - .map(|s| quote::quote! { #s.to_string() }) - .collect(); - - quote::quote! { vec![#(#comment_strings),*] } -} - fn get_custom_candid_path(input: &syn::DeriveInput) -> Result> { let candid_path_helper_attribute_option = input .attrs From 526da51943afc0cff80e0425e6fe3c6cfc8479f1 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Thu, 26 Jun 2025 17:24:05 +0200 Subject: [PATCH 04/13] feat: preserve doc comments (wip) --- rust/candid/src/types/syntax.rs | 90 +++++++++++++++++++++++++++++++- rust/candid_parser/src/token.rs | 6 ++- rust/candid_parser/src/typing.rs | 72 +++++++++++++++++-------- 3 files changed, 142 insertions(+), 26 deletions(-) diff --git a/rust/candid/src/types/syntax.rs b/rust/candid/src/types/syntax.rs index d6d394542..debe83f67 100644 --- a/rust/candid/src/types/syntax.rs +++ b/rust/candid/src/types/syntax.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{collections::HashMap, fmt}; use crate::types::{ArgType, Field, FuncMode, Function, Label, Type, TypeInner}; @@ -272,10 +272,96 @@ impl IDLMergedProg { self.actor = other; } + pub fn set_comments_in_type( + &self, + t: &IDLType, + doc_comments: &HashMap>, + ) -> IDLType { + match t { + IDLType::PrimT(prim) => IDLType::PrimT(prim.clone()), + IDLType::VarT(id) => IDLType::VarT(id.clone()), + IDLType::FuncT(func) => IDLType::FuncT(FuncType { + modes: func.modes.clone(), + args: func + .args + .iter() + .map(|a| IDLArgType { + typ: self.set_comments_in_type(&a.typ, doc_comments), + name: a.name.clone(), + }) + .collect(), + rets: func + .rets + .iter() + .map(|r| self.set_comments_in_type(r, doc_comments)) + .collect(), + }), + IDLType::OptT(t) => IDLType::OptT(Box::new(self.set_comments_in_type(t, doc_comments))), + IDLType::VecT(t) => IDLType::VecT(Box::new(self.set_comments_in_type(t, doc_comments))), + IDLType::RecordT(fields) => { + let fields = fields + .iter() + .map(|f| TypeField { + label: f.label.clone(), + typ: self.set_comments_in_type(&f.typ, doc_comments), + doc_comment: doc_comments.get(&f.label.to_string()).cloned(), + }) + .collect(); + IDLType::RecordT(fields) + } + IDLType::ServT(methods) => { + let methods = methods + .iter() + .map(|m| Binding { + id: m.id.clone(), + typ: self.set_comments_in_type(&m.typ, doc_comments), + doc_comment: doc_comments.get(&m.id).cloned(), + }) + .collect(); + IDLType::ServT(methods) + } + IDLType::VariantT(fields) => { + let fields = fields + .iter() + .map(|f| TypeField { + label: f.label.clone(), + typ: self.set_comments_in_type(&f.typ, doc_comments), + doc_comment: doc_comments.get(&f.label.to_string()).cloned(), + }) + .collect(); + IDLType::VariantT(fields) + } + IDLType::ClassT(args, t) => { + let args = args + .iter() + .map(|a| IDLArgType { + typ: self.set_comments_in_type(&a.typ, doc_comments), + name: a.name.clone(), + }) + .collect(); + IDLType::ClassT(args, Box::new(self.set_comments_in_type(t, doc_comments))) + } + IDLType::PrincipalT => IDLType::PrincipalT, + IDLType::FutureT => IDLType::FutureT, + IDLType::UnknownT => IDLType::UnknownT, + } + } + + pub fn set_comments_in_actor(&mut self, doc_comments: &HashMap>) { + self.actor = self + .actor + .as_ref() + .map(|t| self.set_comments_in_type(&t, doc_comments)); + } + pub fn find_type(&self, id: &str) -> Result<&IDLType, String> { + self.find_binding(id).map(|b| &b.typ) + } + + pub fn find_binding(&self, id: &str) -> Result<&Binding, String> { self.types .iter() - .find_map(|t| if t.id == id { Some(&t.typ) } else { None }) + .find(|t| t.id == id) .ok_or(format!("Type identifier not found: {id}")) } diff --git a/rust/candid_parser/src/token.rs b/rust/candid_parser/src/token.rs index f3669b1eb..5dd81c4c9 100644 --- a/rust/candid_parser/src/token.rs +++ b/rust/candid_parser/src/token.rs @@ -122,6 +122,10 @@ fn parse_number(lex: &mut Lexer) -> String { } } +fn parse_doc_comment(lex: &Lexer) -> String { + lex.slice().trim_start_matches("///").trim().to_string() +} + pub type TriviaMap = Rc>>>; pub struct Tokenizer<'input> { @@ -198,7 +202,7 @@ impl Iterator for Tokenizer<'_> { Some(Err(LexicalError::new(err, span))) } Ok(Token::DocComment) => { - let content = self.lex.slice(); + let content = parse_doc_comment(&self.lex); if self.trivia.is_some() { self.comment_buffer.push(content.to_string()); } diff --git a/rust/candid_parser/src/typing.rs b/rust/candid_parser/src/typing.rs index 12f1c46cf..fc819ee09 100644 --- a/rust/candid_parser/src/typing.rs +++ b/rust/candid_parser/src/typing.rs @@ -6,21 +6,44 @@ use candid::types::{ ArgType, Field, Function, Type, TypeEnv, TypeInner, }; use candid::utils::check_unique; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; pub struct Env<'a> { pub te: &'a mut TypeEnv, + doc_comments_in_actor: HashMap>, + can_insert_doc_comments: bool, pub pre: bool, } +impl<'a> Env<'a> { + fn new_with_te(te: &'a mut TypeEnv) -> Self { + Self { + te, + doc_comments_in_actor: HashMap::new(), + can_insert_doc_comments: false, + pre: false, + } + } + + /// Insert doc comments if [Env::can_insert_doc_comments] is true and there is a doc comment. + fn insert_comments_if_allowed(&mut self, id: &str, doc_comment: Option<&Vec>) { + if !self.can_insert_doc_comments { + return; + } + + if let Some(doc_comment) = doc_comment { + self.doc_comments_in_actor + .insert(id.to_string(), doc_comment.to_vec()); + } + } +} + /// Convert candid AST to internal Type pub fn ast_to_type(env: &TypeEnv, ast: &IDLType) -> Result { - let env = Env { - te: &mut env.clone(), - pre: false, - }; - check_type(&env, ast) + let mut te = env.clone(); + let mut env = Env::new_with_te(&mut te); + check_type(&mut env, ast) } fn check_prim(prim: &PrimType) -> Type { @@ -46,7 +69,7 @@ fn check_prim(prim: &PrimType) -> Type { .into() } -pub fn check_type(env: &Env, t: &IDLType) -> Result { +pub fn check_type(env: &mut Env, t: &IDLType) -> Result { match t { IDLType::PrimT(prim) => Ok(check_prim(prim)), IDLType::VarT(id) => { @@ -105,14 +128,14 @@ pub fn check_type(env: &Env, t: &IDLType) -> Result { } } -fn check_arg(env: &Env, arg: &IDLArgType) -> Result { +fn check_arg(env: &mut Env, arg: &IDLArgType) -> Result { Ok(ArgType { name: arg.name.clone(), typ: check_type(env, &arg.typ)?, }) } -fn check_fields(env: &Env, fs: &[TypeField]) -> Result> { +fn check_fields(env: &mut Env, fs: &[TypeField]) -> Result> { // field label duplication is checked in the parser let mut res = Vec::new(); for f in fs.iter() { @@ -122,11 +145,12 @@ fn check_fields(env: &Env, fs: &[TypeField]) -> Result> { ty, }; res.push(field); + env.insert_comments_if_allowed(&f.label.to_string(), f.doc_comment.as_ref()); } Ok(res) } -fn check_meths(env: &Env, ms: &[Binding]) -> Result> { +fn check_meths(env: &mut Env, ms: &[Binding]) -> Result> { // binding duplication is checked in the parser let mut res = Vec::new(); for meth in ms.iter() { @@ -138,6 +162,7 @@ fn check_meths(env: &Env, ms: &[Binding]) -> Result> { ))); } res.push((meth.id.to_owned(), t)); + env.insert_comments_if_allowed(&meth.id, meth.doc_comment.as_ref()); } Ok(res) } @@ -181,8 +206,9 @@ fn check_decs(env: &mut Env, decs: &[Dec]) -> Result<()> { Ok(()) } -fn check_actor(env: &Env, actor: &Option) -> Result> { - match actor { +fn check_actor(env: &mut Env, actor: &Option) -> Result> { + env.can_insert_doc_comments = true; + let res = match actor { None => Ok(None), Some(IDLType::ClassT(ts, t)) => { let mut args = Vec::new(); @@ -198,7 +224,9 @@ fn check_actor(env: &Env, actor: &Option) -> Result> { env.te.as_service(&t)?; Ok(Some(t)) } - } + }; + env.can_insert_doc_comments = false; + res } fn resolve_path(base: &Path, file: &str) -> PathBuf { @@ -246,9 +274,9 @@ fn load_imports( /// Type check IDLProg and adds bindings to type environment. Returns /// the main actor if present. This function ignores the imports. pub fn check_prog(te: &mut TypeEnv, prog: &IDLProg) -> Result> { - let mut env = Env { te, pre: false }; + let mut env = Env::new_with_te(te); check_decs(&mut env, &prog.decs)?; - check_actor(&env, &prog.actor) + check_actor(&mut env, &prog.actor) } /// Type check init args extracted from canister metadata candid:args. /// Need to provide `main_env`, because init args may refer to variables from the main did file. @@ -257,12 +285,12 @@ pub fn check_init_args( main_env: &TypeEnv, prog: &IDLInitArgs, ) -> Result> { - let mut env = Env { te, pre: false }; + let mut env = Env::new_with_te(te); check_decs(&mut env, &prog.decs)?; env.te.merge(main_env)?; let mut args = Vec::new(); for arg in prog.args.iter() { - args.push(check_arg(&env, arg)?); + args.push(check_arg(&mut env, arg)?); } Ok(args) } @@ -336,10 +364,7 @@ fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option, I .collect(); let mut te = TypeEnv::new(); - let mut env = Env { - te: &mut te, - pre: false, - }; + let mut env = Env::new_with_te(&mut te); let mut idl_merged_prog = IDLMergedProg::new(); let mut actor: Option = None; @@ -349,7 +374,7 @@ fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option, I check_decs(&mut env, &code.decs)?; idl_merged_prog.add_decs(&code.decs); if *include_serv { - let t = check_actor(&env, &code.actor)?; + let t = check_actor(&mut env, &code.actor)?; actor = merge_actor(&env, &actor, &t, name)?; } } @@ -357,12 +382,13 @@ fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option, I check_decs(&mut env, &prog.decs)?; idl_merged_prog.add_decs(&prog.decs); - let mut res = check_actor(&env, &prog.actor)?; + let mut res = check_actor(&mut env, &prog.actor)?; if actor.is_some() { res = merge_actor(&env, &res, &actor, "")?; } idl_merged_prog.set_actor(res.clone().map(|t| env.te.as_idl_type(&t))); + idl_merged_prog.set_comments_in_actor(&env.doc_comments_in_actor); Ok((te, res, idl_merged_prog)) } From 00a55770678a7d719723f2370ba12dd49fabf22e Mon Sep 17 00:00:00 2001 From: ilbertt Date: Fri, 27 Jun 2025 12:20:04 +0200 Subject: [PATCH 05/13] feat: support comments in typescript and motoko (rust is wip) --- rust/candid/src/pretty/candid.rs | 36 ++++++-- rust/candid/src/types/syntax.rs | 31 +++++-- rust/candid_parser/src/bindings/analysis.rs | 2 +- rust/candid_parser/src/bindings/javascript.rs | 8 +- rust/candid_parser/src/bindings/motoko.rs | 49 +++++++--- rust/candid_parser/src/bindings/rust.rs | 92 +++++++++++++------ .../candid_parser/src/bindings/rust_agent.hbs | 9 +- rust/candid_parser/src/bindings/rust_call.hbs | 12 ++- rust/candid_parser/src/bindings/rust_stub.hbs | 3 + rust/candid_parser/src/bindings/typescript.rs | 47 ++++++++-- rust/candid_parser/src/grammar.lalrpop | 8 +- rust/candid_parser/src/typing.rs | 26 ++++-- rust/candid_parser/src/utils.rs | 18 ++-- rust/candid_parser/tests/assets/example.did | 45 ++++++++- rust/candid_parser/tests/assets/import/a.did | 4 + .../candid_parser/tests/assets/import/b/b.did | 4 + rust/candid_parser/tests/assets/ok/actor.rs | 3 +- rust/candid_parser/tests/assets/ok/cyclic.rs | 3 +- rust/candid_parser/tests/assets/ok/empty.rs | 3 +- rust/candid_parser/tests/assets/ok/escape.rs | 3 +- .../tests/assets/ok/example.d.ts | 79 +++++++++++++++- .../candid_parser/tests/assets/ok/example.did | 36 +++++++- rust/candid_parser/tests/assets/ok/example.mo | 31 ++++++- .../candid_parser/tests/assets/ok/fieldnat.rs | 3 +- .../tests/assets/ok/inline_methods.rs | 3 +- rust/candid_parser/tests/assets/ok/keyword.rs | 3 +- .../tests/assets/ok/management.rs | 3 +- .../tests/assets/ok/recursion.rs | 3 +- .../tests/assets/ok/recursive_class.rs | 3 +- rust/candid_parser/tests/assets/ok/service.rs | 3 +- rust/candid_parser/tests/assets/ok/unicode.rs | 3 +- tools/didc/src/main.rs | 2 +- 32 files changed, 465 insertions(+), 113 deletions(-) diff --git a/rust/candid/src/pretty/candid.rs b/rust/candid/src/pretty/candid.rs index 3ccbbb848..4b0b6a78f 100644 --- a/rust/candid/src/pretty/candid.rs +++ b/rust/candid/src/pretty/candid.rs @@ -5,6 +5,8 @@ use crate::types::{ }; use pretty::RcDoc; +const DOC_COMMENT_LINE_PREFIX: &str = "/// "; + static KEYWORDS: [&str; 30] = [ "import", "service", @@ -143,7 +145,9 @@ pub(crate) fn pp_field(field: &TypeField, is_variant: bool) -> RcDoc { } else { kwd(" :").append(pp_ty(&field.typ)) }; - pp_label(&field.label).append(ty_doc) + pp_doc_comment(field.doc_comment.as_ref()) + .append(pp_label(&field.label)) + .append(ty_doc) } fn pp_fields(fs: &[TypeField], is_variant: bool) -> RcDoc { @@ -195,14 +199,17 @@ fn pp_service(serv: &[Binding]) -> RcDoc { |Binding { id, typ, - doc_comment: _, + doc_comment, }| { let func_doc = match typ { IDLType::FuncT(ref f) => pp_function(f), IDLType::VarT(_) => pp_ty(typ), _ => unreachable!(), }; - pp_text(id).append(kwd(" :")).append(func_doc) + pp_doc_comment(doc_comment.as_ref()) + .append(pp_text(id)) + .append(kwd(" :")) + .append(func_doc) }, ), ";", @@ -211,8 +218,9 @@ fn pp_service(serv: &[Binding]) -> RcDoc { } fn pp_defs(env: &IDLMergedProg) -> RcDoc { - lines(env.get_types().iter().map(|(id, typ, _)| { - kwd("type") + lines(env.get_types().iter().map(|(id, typ, doc_comment)| { + pp_doc_comment(*doc_comment) + .append(kwd("type")) .append(ident(id)) .append(kwd("=")) .append(pp_ty(typ)) @@ -228,6 +236,20 @@ fn pp_actor(ty: &IDLType) -> RcDoc { } } +fn pp_doc_comment(comment_lines: Option<&Vec>) -> RcDoc { + let mut doc_comment = RcDoc::nil(); + if let Some(comment_lines) = comment_lines { + for line in comment_lines { + doc_comment = doc_comment.append( + RcDoc::text(DOC_COMMENT_LINE_PREFIX) + .append(line) + .append(RcDoc::hardline()), + ); + } + } + doc_comment +} + pub fn pp_init_args<'a>(env: &'a IDLMergedProg, args: &'a [IDLArgType]) -> RcDoc<'a> { pp_defs(env).append(pp_args(args)) } @@ -236,7 +258,9 @@ pub fn compile(env: &IDLMergedProg) -> String { None => pp_defs(env).pretty(LINE_WIDTH).to_string(), Some(actor) => { let defs = pp_defs(env); - let actor = kwd("service :").append(pp_actor(actor)); + let actor = pp_doc_comment(actor.doc_comment.as_ref()) + .append(kwd("service :")) + .append(pp_actor(&actor.typ)); let doc = defs.append(actor); doc.pretty(LINE_WIDTH).to_string() } diff --git a/rust/candid/src/types/syntax.rs b/rust/candid/src/types/syntax.rs index debe83f67..a8ff56429 100644 --- a/rust/candid/src/types/syntax.rs +++ b/rust/candid/src/types/syntax.rs @@ -215,10 +215,16 @@ pub struct Binding { pub doc_comment: Option>, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] +pub struct IDLActorType { + pub typ: IDLType, + pub doc_comment: Option>, +} + +#[derive(Debug, Clone)] pub struct IDLProg { pub decs: Vec, - pub actor: Option, + pub actor: Option, } #[derive(Debug)] @@ -231,7 +237,7 @@ pub struct IDLInitArgs { #[derive(Debug, Default)] pub struct IDLMergedProg { types: Vec, - pub actor: Option, + pub actor: Option, } impl From for IDLMergedProg { @@ -243,6 +249,15 @@ impl From for IDLMergedProg { } } +impl From for IDLMergedProg { + fn from(other_actor: IDLActorType) -> Self { + Self { + types: vec![], + actor: Some(other_actor), + } + } +} + impl From> for IDLMergedProg { fn from(bindings: Vec) -> Self { Self { @@ -268,7 +283,7 @@ impl IDLMergedProg { self.types.extend(types); } - pub fn set_actor(&mut self, other: Option) { + pub fn set_actor(&mut self, other: Option) { self.actor = other; } @@ -348,10 +363,10 @@ impl IDLMergedProg { } pub fn set_comments_in_actor(&mut self, doc_comments: &HashMap>) { - self.actor = self - .actor - .as_ref() - .map(|t| self.set_comments_in_type(&t, doc_comments)); + self.actor = self.actor.as_ref().map(|t| IDLActorType { + typ: self.set_comments_in_type(&t.typ, doc_comments), + doc_comment: t.doc_comment.clone(), + }); } pub fn find_type(&self, id: &str) -> Result<&IDLType, String> { diff --git a/rust/candid_parser/src/bindings/analysis.rs b/rust/candid_parser/src/bindings/analysis.rs index b8c84038c..35172182b 100644 --- a/rust/candid_parser/src/bindings/analysis.rs +++ b/rust/candid_parser/src/bindings/analysis.rs @@ -87,7 +87,7 @@ pub fn chase_actor<'a>(prog: &'a IDLMergedProg, actor: &'a IDLType) -> Result Result>> { let mut res = BTreeMap::new(); let actor = prog.actor.as_ref().ok_or_else(|| Error::msg("no actor"))?; - let actor = prog.trace_type(actor).map_err(Error::msg)?; + let actor = prog.trace_type(&actor.typ).map_err(Error::msg)?; if let IDLType::ClassT(args, _) = &actor { for (i, arg) in args.iter().enumerate() { let mut used = Vec::new(); diff --git a/rust/candid_parser/src/bindings/javascript.rs b/rust/candid_parser/src/bindings/javascript.rs index c13ab39bd..313b0291e 100644 --- a/rust/candid_parser/src/bindings/javascript.rs +++ b/rust/candid_parser/src/bindings/javascript.rs @@ -254,16 +254,18 @@ pub fn compile(prog: &IDLMergedProg) -> String { doc.pretty(LINE_WIDTH).to_string() } Some(actor) => { - let def_list = chase_actor(prog, actor).unwrap(); + let def_list = chase_actor(prog, &actor.typ).unwrap(); let recs = infer_rec(prog, &def_list).unwrap(); let defs = pp_defs(prog, &def_list, &recs); - let init = if let IDLType::ClassT(ref args, _) = actor { + let init = if let IDLType::ClassT(ref args, _) = actor.typ { args.iter().map(|arg| arg.typ.clone()).collect::>() } else { Vec::new() }; let init = init.as_slice(); - let actor = kwd("return").append(pp_actor(actor, &recs)).append(";"); + let actor = kwd("return") + .append(pp_actor(&actor.typ, &recs)) + .append(";"); let body = defs.append(actor); let doc = str("export const idlFactory = ({ IDL }) => ") .append(enclose_space("{", body, "};")); diff --git a/rust/candid_parser/src/bindings/motoko.rs b/rust/candid_parser/src/bindings/motoko.rs index 94cbf8fc4..6dc1a2acb 100644 --- a/rust/candid_parser/src/bindings/motoko.rs +++ b/rust/candid_parser/src/bindings/motoko.rs @@ -10,6 +10,8 @@ use candid::types::{ }; use pretty::RcDoc; +const DOC_COMMENT_LINE_PREFIX: &str = "/// "; + fn is_tuple(t: &IDLType) -> bool { match t { IDLType::RecordT(ref fs) => { @@ -163,17 +165,20 @@ fn pp_label(id: &Label) -> RcDoc { } fn pp_field(field: &TypeField) -> RcDoc { - pp_label(&field.label) + pp_doc_comment(field.doc_comment.as_ref()) + .append(pp_label(&field.label)) .append(" : ") .append(pp_ty(&field.typ)) } + fn pp_variant(field: &TypeField) -> RcDoc { - let doc = str("#").append(pp_label(&field.label)); - if !field.typ.is_null() { - doc.append(" : ").append(pp_ty(&field.typ)) + let label = str("#").append(pp_label(&field.label)); + let doc = if !field.typ.is_null() { + label.append(" : ").append(pp_ty(&field.typ)) } else { - doc - } + label + }; + pp_doc_comment(field.doc_comment.as_ref()).append(doc) } fn pp_function(func: &FuncType) -> RcDoc { @@ -200,6 +205,7 @@ fn pp_function(func: &FuncType) -> RcDoc { } .nest(INDENT_SPACE) } + fn pp_args(args: &[IDLArgType]) -> RcDoc { match args { [ty] => { @@ -246,16 +252,21 @@ fn pp_rets(args: &[IDLType]) -> RcDoc { fn pp_service(serv: &[Binding]) -> RcDoc { let doc = concat( - serv.iter() - .map(|b| escape(&b.id, true).append(" : ").append(pp_ty(&b.typ))), + serv.iter().map(|b| { + pp_doc_comment(b.doc_comment.as_ref()) + .append(escape(&b.id, true)) + .append(" : ") + .append(pp_ty(&b.typ)) + }), ";", ); kwd("actor").append(enclose_space("{", doc, "}")) } fn pp_defs<'a>(bindings: &[(&'a str, &'a IDLType, Option<&'a Vec>)]) -> RcDoc<'a> { - lines(bindings.iter().map(|(id, typ, _)| { - kwd("public type") + lines(bindings.iter().map(|(id, typ, doc_comment)| { + pp_doc_comment(*doc_comment) + .append(kwd("public type")) .append(escape(id, false)) .append(" = ") .append(pp_ty(typ)) @@ -271,6 +282,20 @@ fn pp_actor(ty: &IDLType) -> RcDoc { } } +fn pp_doc_comment(comment_lines: Option<&Vec>) -> RcDoc { + let mut doc_comment = RcDoc::nil(); + if let Some(comment_lines) = comment_lines { + for line in comment_lines { + doc_comment = doc_comment.append( + RcDoc::text(DOC_COMMENT_LINE_PREFIX) + .append(line) + .append(RcDoc::hardline()), + ); + } + } + doc_comment +} + pub fn compile(prog: &IDLMergedProg) -> String { let header = r#"// This is a generated Motoko binding. // Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. @@ -280,7 +305,9 @@ pub fn compile(prog: &IDLMergedProg) -> String { None => pp_defs(&bindings), Some(actor) => { let defs = pp_defs(&bindings); - let actor = kwd("public type Self =").append(pp_actor(actor)); + let actor = pp_doc_comment(actor.doc_comment.as_ref()) + .append(kwd("public type Self =")) + .append(pp_actor(&actor.typ)); defs.append(actor) } }; diff --git a/rust/candid_parser/src/bindings/rust.rs b/rust/candid_parser/src/bindings/rust.rs index 8754a54cf..a870f3ca7 100644 --- a/rust/candid_parser/src/bindings/rust.rs +++ b/rust/candid_parser/src/bindings/rust.rs @@ -4,7 +4,7 @@ use crate::{ Deserialize, }; use candid::types::{ - syntax::{Binding, FuncType, IDLArgType, IDLType, PrimType, TypeField}, + syntax::{Binding, FuncType, IDLActorType, IDLArgType, IDLType, PrimType, TypeField}, Label, }; use candid::{pretty::utils::*, types::syntax::IDLMergedProg}; @@ -14,6 +14,8 @@ use serde::Serialize; use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; +const DOC_COMMENT_LINE_PREFIX: &str = "/// "; + #[derive(Default, Deserialize, Clone, Debug)] pub struct BindingConfig { name: Option, @@ -169,6 +171,20 @@ fn pp_vis<'a>(vis: &Option) -> RcDoc<'a> { } } +fn pp_doc_comment(comment_lines: Option<&Vec>) -> RcDoc { + let mut doc_comment = RcDoc::nil(); + if let Some(comment_lines) = comment_lines { + for line in comment_lines { + doc_comment = doc_comment.append( + RcDoc::text(DOC_COMMENT_LINE_PREFIX) + .append(line) + .append(RcDoc::hardline()), + ); + } + } + doc_comment +} + impl<'a> State<'a> { fn generate_test(&mut self, src: &IDLType, use_type: &str) { if self.tests.contains_key(use_type) { @@ -259,7 +275,7 @@ fn test_{test_name}() {{ RecordT(ref fs) => self.pp_record_fields(fs, false, is_ref), VariantT(ref fs) => { // only possible for result variant - let (ok, err, is_motoko) = as_result(fs).unwrap(); + let (ok_typ, err_typ, is_motoko) = as_result(fs).unwrap(); // This is a hacky way to redirect Result type let old = self .state @@ -277,10 +293,10 @@ fn test_{test_name}() {{ self.state .pop_state(old, StateElem::TypeStr("std::result::Result")); let old = self.state.push_state(&StateElem::Label("Ok")); - let ok = self.pp_ty(ok, is_ref); + let ok = self.pp_ty(ok_typ, is_ref); self.state.pop_state(old, StateElem::Label("Ok")); let old = self.state.push_state(&StateElem::Label("Err")); - let err = self.pp_ty(err, is_ref); + let err = self.pp_ty(err_typ, is_ref); self.state.pop_state(old, StateElem::Label("Err")); let body = ok.append(", ").append(err); RcDoc::text(result).append(enclose("<", body, ">")) @@ -366,10 +382,11 @@ fn test_{test_name}() {{ ) -> RcDoc<'b> { let lab = field.label.to_string(); let old = self.state.push_state(&StateElem::Label(&lab)); - let res = self + let f = self .pp_label(&field.label, false, need_vis) .append(kwd(":")) .append(self.pp_ty(&field.typ, is_ref)); + let res = pp_doc_comment(field.doc_comment.as_ref()).append(f); self.state.pop_state(old, StateElem::Label(&lab)); res } @@ -403,7 +420,7 @@ fn test_{test_name}() {{ fn pp_variant_field<'b>(&mut self, field: &'b TypeField) -> RcDoc<'b> { let lab = field.label.to_string(); let old = self.state.push_state(&StateElem::Label(&lab)); - let res = match &field.typ { + let f = match &field.typ { IDLType::PrimT(PrimType::Null) => self.pp_label(&field.label, true, false), IDLType::RecordT(fs) => self .pp_label(&field.label, true, false) @@ -414,6 +431,7 @@ fn test_{test_name}() {{ ")", )), }; + let res = pp_doc_comment(field.doc_comment.as_ref()).append(f); self.state.pop_state(old, StateElem::Label(&lab)); res } @@ -433,7 +451,7 @@ fn test_{test_name}() {{ self.state.pop_state(old, StateElem::Label(id)); continue; } - let ty = self.state.prog.find_type(id).unwrap(); + let binding = self.state.prog.find_binding(id).unwrap(); let name = self .state .config @@ -452,9 +470,9 @@ fn test_{test_name}() {{ .clone() .map(RcDoc::text) .unwrap_or(RcDoc::text("#[derive(CandidType, Deserialize)]")); - let line = match ty { + let line = match &binding.typ { IDLType::RecordT(fs) => { - let separator = if is_tuple(ty) { + let separator = if is_tuple(&binding.typ) { RcDoc::text(";") } else { RcDoc::nil() @@ -473,7 +491,7 @@ fn test_{test_name}() {{ vis.append(kwd("type")) .append(name) .append(" = ") - .append(self.pp_ty(ty, false)) + .append(self.pp_ty(&binding.typ, false)) .append(";") } else { derive @@ -505,19 +523,23 @@ fn test_{test_name}() {{ .append("struct ") .append(name) // TODO: Unfortunately, the visibility of the inner newtype is also controlled by var.visibility - .append(enclose("(", vis.append(self.pp_ty(ty, false)), ")")) + .append(enclose( + "(", + vis.append(self.pp_ty(&binding.typ, false)), + ")", + )) .append(";") } else { vis.append(kwd("type")) .append(name) .append(" = ") - .append(self.pp_ty(ty, false)) + .append(self.pp_ty(&binding.typ, false)) .append(";") } } }; self.state.pop_state(old, StateElem::Label(id)); - res.push(line) + res.push(pp_doc_comment(binding.doc_comment.as_ref()).append(line)); } lines(res.into_iter()) } @@ -587,8 +609,9 @@ fn test_{test_name}() {{ self.state.pop_state(old, lab); res } - fn pp_function(&mut self, id: &str, func: &FuncType) -> Method { + fn pp_function(&mut self, binding: &Binding, func: &FuncType) -> Method { use candid::types::internal::FuncMode; + let id = &binding.id; let old = self.state.push_state(&StateElem::Label(id)); let name = self .state @@ -647,13 +670,21 @@ fn test_{test_name}() {{ .map(|x| x.pretty(LINE_WIDTH).to_string()) .collect(), mode, + doc_comment_lines: binding.doc_comment.clone().unwrap_or_default(), }; self.state.pop_state(old, StateElem::Label(id)); res } - fn pp_actor(&mut self, actor: &IDLType) -> (Vec, Option>) { - let actor = self.state.prog.trace_type(actor).unwrap(); - let init = if let IDLType::ClassT(args, _) = &actor { + fn pp_actor( + &mut self, + actor: &IDLActorType, + ) -> ( + Vec, + Option>, + Option>, + ) { + let actor_typ = self.state.prog.trace_type(&actor.typ).unwrap(); + let init = if let IDLType::ClassT(args, _) = &actor_typ { let old = self.state.push_state(&StateElem::Label("init")); let args: Vec<_> = args .iter() @@ -678,13 +709,13 @@ fn test_{test_name}() {{ } else { None }; - let serv = self.state.prog.service_methods(&actor).unwrap(); + let serv = self.state.prog.service_methods(&actor_typ).unwrap(); let mut res = Vec::new(); for binding in serv.iter() { let func = self.state.prog.as_func(&binding.typ).unwrap(); - res.push(self.pp_function(&binding.id, func)); + res.push(self.pp_function(&binding, func)); } - (res, init) + (res, init, actor.doc_comment.clone()) } } #[derive(Serialize, Debug)] @@ -692,6 +723,7 @@ pub struct Output { pub type_defs: String, pub methods: Vec, pub init_args: Option>, + pub actor_comment_lines: Vec, pub tests: String, } #[derive(Serialize, Debug)] @@ -701,6 +733,7 @@ pub struct Method { pub args: Vec<(String, String)>, pub rets: Vec, pub mode: String, + pub doc_comment_lines: Vec, } pub fn emit_bindgen(tree: &Config, prog: &IDLMergedProg) -> (Output, Vec) { let mut state = NominalState { @@ -709,7 +742,7 @@ pub fn emit_bindgen(tree: &Config, prog: &IDLMergedProg) -> (Output, Vec let env = state.nominalize_all(); let old_stats = state.state.stats.clone(); let def_list = if let Some(actor) = &env.actor { - chase_actor(&env, actor).unwrap() + chase_actor(&env, &actor.typ).unwrap() } else { env.types_ids() }; @@ -721,10 +754,10 @@ pub fn emit_bindgen(tree: &Config, prog: &IDLMergedProg) -> (Output, Vec }; state.state.stats = old_stats; let defs = state.pp_defs(&def_list); - let (methods, init_args) = if let Some(actor) = &env.actor { + let (methods, init_args, actor_doc_comment) = if let Some(actor) = &env.actor { state.pp_actor(actor) } else { - (Vec::new(), None) + (Vec::new(), None, None) }; let tests = state.tests.into_values().collect::>().join("\n"); let unused = state.state.report_unused(); @@ -733,6 +766,7 @@ pub fn emit_bindgen(tree: &Config, prog: &IDLMergedProg) -> (Output, Vec type_defs: defs.pretty(LINE_WIDTH).to_string(), methods, init_args, + actor_comment_lines: actor_doc_comment.unwrap_or_default(), tests, }, unused, @@ -747,6 +781,7 @@ pub fn output_handlebar(output: Output, config: ExternalConfig, template: &str) type_defs: String, methods: Vec, init_args: Option>, + actor_comment_lines: Vec, tests: String, } let data = HBOutput { @@ -754,6 +789,7 @@ pub fn output_handlebar(output: Output, config: ExternalConfig, template: &str) methods: output.methods, external: config.0, init_args: output.init_args, + actor_comment_lines: output.actor_comment_lines, tests: output.tests, }; hbs.render_template(template, &data).unwrap() @@ -1120,12 +1156,10 @@ impl NominalState<'_> { }); self.state.pop_state(old, elem); } - let actor = self - .state - .prog - .actor - .as_ref() - .map(|ty| self.nominalize(&mut res, &mut vec![], ty)); + let actor = self.state.prog.actor.as_ref().map(|a| IDLActorType { + typ: self.nominalize(&mut res, &mut vec![], &a.typ), + doc_comment: a.doc_comment.clone(), + }); res.set_actor(actor); res } diff --git a/rust/candid_parser/src/bindings/rust_agent.hbs b/rust/candid_parser/src/bindings/rust_agent.hbs index 4e0526254..06782b650 100644 --- a/rust/candid_parser/src/bindings/rust_agent.hbs +++ b/rust/candid_parser/src/bindings/rust_agent.hbs @@ -6,9 +6,15 @@ type Result = std::result::Result; {{type_defs}} {{#if methods}} +{{#each actor_comment_lines}} +/// {{this}} +{{/each}} pub struct {{PascalCase service_name}}<'a>(pub Principal, pub &'a ic_agent::Agent); impl<'a> {{PascalCase service_name}}<'a> { {{#each methods}} + {{#each this.doc_comment_lines}} + /// {{this}} + {{/each}} pub async fn {{this.name}}(&self{{#each this.args}}, {{this.0}}: &{{this.1}}{{/each}}) -> Result<{{vec_to_arity this.rets}}> { let args = Encode!({{#each this.args}}&{{this.0}}{{#unless @last}},{{/unless}}{{/each}})?; let bytes = self.1.{{#if (eq this.mode "update")}}update{{else}}query{{/if}}(&self.0, "{{escape_debug this.original_name}}").with_arg(args).{{#if (eq this.mode "update")}}call_and_wait{{else}}call{{/if}}().await?; @@ -17,7 +23,8 @@ impl<'a> {{PascalCase service_name}}<'a> { {{/each}} } {{#if canister_id}} -pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice canister_id}}]); // {{canister_id}} +/// Canister ID: `{{canister_id}}` +pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice canister_id}}]); {{/if}} {{/if}} {{#if tests}} diff --git a/rust/candid_parser/src/bindings/rust_call.hbs b/rust/candid_parser/src/bindings/rust_call.hbs index 788fc0db3..6697c6747 100644 --- a/rust/candid_parser/src/bindings/rust_call.hbs +++ b/rust/candid_parser/src/bindings/rust_call.hbs @@ -6,16 +6,26 @@ use ic_cdk::api::call::CallResult as Result; {{type_defs}} {{#if methods}} +{{#each actor_comment_lines}} +/// {{this}} +{{/each}} pub struct {{PascalCase service_name}}(pub Principal); impl {{PascalCase service_name}} { {{#each methods}} + {{#each this.doc_comment_lines}} + /// {{this}} + {{/each}} pub async fn {{this.name}}(&self{{#each this.args}}, {{this.0}}: &{{this.1}}{{/each}}) -> Result<({{#each this.rets}}{{this}},{{/each}})> { ic_cdk::call(self.0, "{{escape_debug this.original_name}}", ({{#each this.args}}{{this.0}},{{/each}})).await } {{/each}} } {{#if canister_id}} -pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice canister_id}}]); // {{canister_id}} +/// Canister ID: `{{canister_id}}` +pub const CANISTER_ID : Principal = Principal::from_slice(&[{{principal_slice canister_id}}]); +{{#each actor_comment_lines}} +/// {{this}} +{{/each}} pub const {{snake_case service_name}} : {{PascalCase service_name}} = {{PascalCase service_name}}(CANISTER_ID); {{/if}} {{/if}} diff --git a/rust/candid_parser/src/bindings/rust_stub.hbs b/rust/candid_parser/src/bindings/rust_stub.hbs index a0cf2c4cd..5f23dc1b2 100644 --- a/rust/candid_parser/src/bindings/rust_stub.hbs +++ b/rust/candid_parser/src/bindings/rust_stub.hbs @@ -11,6 +11,9 @@ fn init({{#each init_args}}{{#if (not @first)}}, {{/if}}{{this.0}}: {{this.1}}{{ } {{/if}} {{#each methods}} +{{#each this.doc_comment_lines}} +/// {{this}} +{{/each}} #[ic_cdk::{{cdk_attribute this.mode this.name this.original_name}}] fn {{this.name}}({{#each this.args}}{{#if (not @first)}}, {{/if}}{{this.0}}: {{this.1}}{{/each}}) -> {{vec_to_arity this.rets}} { unimplemented!() diff --git a/rust/candid_parser/src/bindings/typescript.rs b/rust/candid_parser/src/bindings/typescript.rs index b38360fd7..36ea69549 100644 --- a/rust/candid_parser/src/bindings/typescript.rs +++ b/rust/candid_parser/src/bindings/typescript.rs @@ -6,6 +6,10 @@ use candid::types::{ }; use pretty::RcDoc; +const DOC_COMMENT_PREFIX: &str = "/**"; +const DOC_COMMENT_LINE_PREFIX: &str = " * "; +const DOC_COMMENT_SUFFIX: &str = " */"; + fn pp_prim_ty(ty: &PrimType) -> RcDoc { use PrimType::*; match ty { @@ -125,7 +129,8 @@ fn pp_label(id: &Label) -> RcDoc { } fn pp_field<'a>(prog: &'a IDLMergedProg, field: &'a TypeField, is_ref: bool) -> RcDoc<'a> { - pp_label(&field.label) + pp_doc_comment(field.doc_comment.as_ref()) + .append(pp_label(&field.label)) .append(kwd(":")) .append(pp_ty(prog, &field.typ, is_ref)) } @@ -155,14 +160,17 @@ fn pp_service<'a>(prog: &'a IDLMergedProg, serv: &'a [Binding]) -> RcDoc<'a> { |Binding { id, typ, - doc_comment: _, + doc_comment, }| { let func = match typ { IDLType::FuncT(ref func) => pp_function(prog, func), IDLType::VarT(ref id) => ident(id), _ => unreachable!(), }; - quote_ident(id).append(kwd(":")).append(func) + pp_doc_comment(doc_comment.as_ref()) + .append(quote_ident(id)) + .append(kwd(":")) + .append(func) }, ), ",", @@ -172,8 +180,9 @@ fn pp_service<'a>(prog: &'a IDLMergedProg, serv: &'a [Binding]) -> RcDoc<'a> { fn pp_defs<'a>(prog: &'a IDLMergedProg, def_list: &'a [&'a str]) -> RcDoc<'a> { lines(def_list.iter().map(|id| { - let ty = prog.find_type(id).unwrap(); - let export = match ty { + let binding = prog.find_binding(id).unwrap(); + let ty = &binding.typ; + let doc = match ty { IDLType::RecordT(_) if !ty.is_tuple() => kwd("export interface") .append(ident(id)) .append(" ") @@ -198,7 +207,7 @@ fn pp_defs<'a>(prog: &'a IDLMergedProg, def_list: &'a [&'a str]) -> RcDoc<'a> { .append(pp_ty(prog, ty, false)) .append(";"), }; - export + pp_doc_comment(binding.doc_comment.as_ref()).append(doc) })) } @@ -213,6 +222,29 @@ fn pp_actor<'a>(prog: &'a IDLMergedProg, ty: &'a IDLType) -> RcDoc<'a> { } } +fn pp_doc_comment(comment_lines: Option<&Vec>) -> RcDoc { + let mut doc_comment = RcDoc::nil(); + let mut is_empty = true; + if let Some(comment_lines) = comment_lines { + is_empty = comment_lines.is_empty(); + for line in comment_lines { + doc_comment = doc_comment.append( + RcDoc::text(DOC_COMMENT_LINE_PREFIX) + .append(line) + .append(RcDoc::hardline()), + ); + } + } + if !is_empty { + doc_comment = RcDoc::text(DOC_COMMENT_PREFIX) + .append(RcDoc::hardline()) + .append(doc_comment) + .append(RcDoc::text(DOC_COMMENT_SUFFIX)) + .append(RcDoc::hardline()); + } + doc_comment +} + pub fn compile(prog: &IDLMergedProg) -> String { let header = r#"import type { Principal } from '@dfinity/principal'; import type { ActorMethod } from '@dfinity/agent'; @@ -222,7 +254,8 @@ import type { IDL } from '@dfinity/candid'; let defs = pp_defs(prog, &def_list); let actor = match &prog.actor { None => RcDoc::nil(), - Some(actor) => pp_actor(prog, actor) + Some(actor) => pp_doc_comment(actor.doc_comment.as_ref()) + .append(pp_actor(prog, &actor.typ)) .append(RcDoc::line()) .append("export declare const idlFactory: IDL.InterfaceFactory;") .append(RcDoc::line()) diff --git a/rust/candid_parser/src/grammar.lalrpop b/rust/candid_parser/src/grammar.lalrpop index 1d5c2afe4..462efb0c6 100644 --- a/rust/candid_parser/src/grammar.lalrpop +++ b/rust/candid_parser/src/grammar.lalrpop @@ -1,7 +1,7 @@ use super::test::{Assert, Input, Test}; use super::token::{Token, error, error2, LexicalError, Span, TriviaMap}; use candid::{Principal, types::Label}; -use candid::types::syntax::{IDLType, PrimType, TypeField, FuncType, Binding, Dec, IDLProg, IDLTypes, IDLInitArgs, IDLArgType}; +use candid::types::syntax::{IDLType, PrimType, TypeField, FuncType, Binding, Dec, IDLProg, IDLTypes, IDLInitArgs, IDLArgType, IDLActorType}; use candid::types::value::{IDLField, IDLValue, IDLArgs, VariantValue}; use candid::types::{TypeEnv, FuncMode}; use candid::utils::check_unique; @@ -269,9 +269,9 @@ Actor: IDLType = { "id" => IDLType::VarT(<>), } -MainActor: IDLType = { - "service" "id"? ":" ";"? => <>, - "service" "id"? ":" "->" ";"? => IDLType::ClassT(args, Box::new(t)), +MainActor: IDLActorType = { + "service" "id"? ":" ";"? => IDLActorType { typ: t, doc_comment }, + "service" "id"? ":" "->" ";"? => IDLActorType { typ: IDLType::ClassT(args, Box::new(t)), doc_comment }, } pub IDLProg: IDLProg = { diff --git a/rust/candid_parser/src/typing.rs b/rust/candid_parser/src/typing.rs index fc819ee09..016803554 100644 --- a/rust/candid_parser/src/typing.rs +++ b/rust/candid_parser/src/typing.rs @@ -1,7 +1,8 @@ use crate::{parse_idl_prog, pretty_parse_idl_prog, Error, Result}; use candid::types::{ syntax::{ - Binding, Dec, IDLArgType, IDLInitArgs, IDLMergedProg, IDLProg, IDLType, PrimType, TypeField, + Binding, Dec, IDLActorType, IDLArgType, IDLInitArgs, IDLMergedProg, IDLProg, IDLType, + PrimType, TypeField, }, ArgType, Field, Function, Type, TypeEnv, TypeInner, }; @@ -11,7 +12,8 @@ use std::path::{Path, PathBuf}; pub struct Env<'a> { pub te: &'a mut TypeEnv, - doc_comments_in_actor: HashMap>, + doc_comments_in_actor_types: HashMap>, + actor_doc_comment: Option>, can_insert_doc_comments: bool, pub pre: bool, } @@ -20,7 +22,8 @@ impl<'a> Env<'a> { fn new_with_te(te: &'a mut TypeEnv) -> Self { Self { te, - doc_comments_in_actor: HashMap::new(), + doc_comments_in_actor_types: HashMap::new(), + actor_doc_comment: None, can_insert_doc_comments: false, pre: false, } @@ -33,7 +36,7 @@ impl<'a> Env<'a> { } if let Some(doc_comment) = doc_comment { - self.doc_comments_in_actor + self.doc_comments_in_actor_types .insert(id.to_string(), doc_comment.to_vec()); } } @@ -206,9 +209,11 @@ fn check_decs(env: &mut Env, decs: &[Dec]) -> Result<()> { Ok(()) } -fn check_actor(env: &mut Env, actor: &Option) -> Result> { +fn check_actor(env: &mut Env, actor: &Option) -> Result> { env.can_insert_doc_comments = true; - let res = match actor { + env.actor_doc_comment = actor.as_ref().and_then(|a| a.doc_comment.clone()); + + let res = match actor.as_ref().map(|a| &a.typ) { None => Ok(None), Some(IDLType::ClassT(ts, t)) => { let mut args = Vec::new(); @@ -225,7 +230,9 @@ fn check_actor(env: &mut Env, actor: &Option) -> Result> { Ok(Some(t)) } }; + env.can_insert_doc_comments = false; + res } @@ -387,8 +394,11 @@ fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option, I res = merge_actor(&env, &res, &actor, "")?; } - idl_merged_prog.set_actor(res.clone().map(|t| env.te.as_idl_type(&t))); - idl_merged_prog.set_comments_in_actor(&env.doc_comments_in_actor); + idl_merged_prog.set_actor(res.clone().map(|t| IDLActorType { + typ: env.te.as_idl_type(&t), + doc_comment: env.actor_doc_comment.clone(), + })); + idl_merged_prog.set_comments_in_actor(&env.doc_comments_in_actor_types); Ok((te, res, idl_merged_prog)) } diff --git a/rust/candid_parser/src/utils.rs b/rust/candid_parser/src/utils.rs index 3826cac1c..9873c831c 100644 --- a/rust/candid_parser/src/utils.rs +++ b/rust/candid_parser/src/utils.rs @@ -1,7 +1,7 @@ use crate::{check_prog, pretty_check_file, pretty_parse_idl_prog, Error, Result}; use candid::{ types::{ - syntax::{Binding, IDLMergedProg, IDLType}, + syntax::{Binding, IDLActorType, IDLMergedProg, IDLType}, Type, TypeInner, }, TypeEnv, @@ -68,18 +68,21 @@ pub fn instantiate_candid(candid: CandidSource) -> Result<(Vec, (TypeEnv, _ => unreachable!(), }) } -pub fn get_metadata(env: &IDLMergedProg) -> Option { - let serv = env.actor.as_ref()?; - let serv = env.trace_type(serv).ok()?; +pub fn get_metadata(prog: &IDLMergedProg) -> Option { + let serv = prog.actor.as_ref().map(|a| &a.typ)?; + let serv = prog.trace_type(serv).ok()?; let serv = match &serv { IDLType::ClassT(_, ty) => ty.as_ref(), IDLType::ServT(_) => &serv, _ => unreachable!(), }; - let def_list = crate::bindings::analysis::chase_actor(env, serv).ok()?; - let mut filtered = IDLMergedProg::new(); + let def_list = crate::bindings::analysis::chase_actor(prog, serv).ok()?; + let mut filtered = IDLMergedProg::from(IDLActorType { + typ: serv.clone(), + doc_comment: prog.actor.as_ref().and_then(|a| a.doc_comment.clone()), + }); for d in def_list { - if let Ok(typ) = env.find_type(d) { + if let Ok(typ) = prog.find_type(d) { filtered.insert_binding(Binding { id: d.to_string(), typ: typ.clone(), @@ -87,7 +90,6 @@ pub fn get_metadata(env: &IDLMergedProg) -> Option { }); } } - filtered.set_actor(Some(serv.clone())); Some(candid::pretty::candid::compile(&filtered)) } diff --git a/rust/candid_parser/tests/assets/example.did b/rust/candid_parser/tests/assets/example.did index a5a47fee6..3cc8978f8 100644 --- a/rust/candid_parser/tests/assets/example.did +++ b/rust/candid_parser/tests/assets/example.did @@ -1,21 +1,58 @@ import service "recursion.did"; import "import/a.did"; import service "import/b/b.did"; +/// Doc comment for prim type type my_type = principal; +/// Doc comment for List type List = opt record { head: int; tail: List }; type f = func (List, func (int32) -> (int64)) -> (opt List, res); +/// Doc comment for broker service type broker = service { find : (name: text) -> (service {up:() -> (); current:() -> (nat32)}); }; -type nested = record { nat; nat; record {nat;int;}; record { nat; 0x2a:nat; nat8; }; 42:nat; 40:nat; variant{ A; 0x2a; B; C }; }; -type res = variant { Ok: record{int;nat}; Err: record{ error: text } }; -type nested_res = variant { Ok: variant { Ok; Err }; Err: variant { Ok: record { content: text }; Err: record {int} } }; +/// Doc comment for nested type +type nested = record { + nat; + nat; + /// Doc comment for nested record + record {nat;int;}; + record { nat; 0x2a:nat; nat8; }; + 42:nat; + 40:nat; + variant{ A; 0x2a; B; C }; +}; +type res = variant { + /// Doc comment for Ok variant + Ok: record{int;nat}; + /// Doc comment for Err variant + Err: record{ + /// Doc comment for error field in Err variant, + /// on multiple lines + error: text + } +}; +type nested_res = variant { Ok: variant { Ok; Err }; Err: variant { + /// Doc comment for Ok in nested variant + Ok: record { content: text }; + /// Doc comment for Err in nested variant + Err: record { int } +} }; +/// Doc comment for service service server : { + /// Doc comment for f1 method of service f1 : (list, test: blob, opt bool) -> () oneway; g1 : (my_type, List, opt List, nested) -> (int, broker, nested_res) query; - h : (vec opt text, variant { A: nat; B: opt text }, opt List) -> (record { id: nat; 0x2a: record {} }); + h : (vec opt text, variant { A: nat; B: opt text }, opt List) -> ( + record { + /// Doc comment for id field in h method return + id: nat; + /// Doc comment for 0x2a field in h method return + 0x2a: record {}; + } + ); + /// Doc comment for i method of service i : f; x : (a,b) -> (opt a, opt b, variant { Ok: record { result: text }; Err: variant {a;b} }) composite_query; } diff --git a/rust/candid_parser/tests/assets/import/a.did b/rust/candid_parser/tests/assets/import/a.did index ebdcddc77..defa071e4 100644 --- a/rust/candid_parser/tests/assets/import/a.did +++ b/rust/candid_parser/tests/assets/import/a.did @@ -1,5 +1,9 @@ import "b/b.did"; +/// Doc comment for a in imported file type a = variant {a;b:b}; +/// Doc comment for service in imported file, +/// ignored if the service in the importing file has a doc comment service : (a,b) -> { + /// Doc comment for f in imported service f : (b) -> (a); } diff --git a/rust/candid_parser/tests/assets/import/b/b.did b/rust/candid_parser/tests/assets/import/b/b.did index b28cb7773..3d6309f80 100644 --- a/rust/candid_parser/tests/assets/import/b/b.did +++ b/rust/candid_parser/tests/assets/import/b/b.did @@ -1,4 +1,8 @@ +/// Doc comment for b in imported file type b = record { int;nat }; +/// Doc comment for service in imported file, +/// ignored if the service in the importing file has a doc comment service : { + /// Doc comment for bbbbb method in imported service bbbbb : (b) -> (); } diff --git a/rust/candid_parser/tests/assets/ok/actor.rs b/rust/candid_parser/tests/assets/ok/actor.rs index fd5f40acc..616b3680a 100644 --- a/rust/candid_parser/tests/assets/ok/actor.rs +++ b/rust/candid_parser/tests/assets/ok/actor.rs @@ -28,6 +28,7 @@ impl Service { ic_cdk::call(self.0, "o", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/cyclic.rs b/rust/candid_parser/tests/assets/ok/cyclic.rs index 33eaa02fe..cb12343cc 100644 --- a/rust/candid_parser/tests/assets/ok/cyclic.rs +++ b/rust/candid_parser/tests/assets/ok/cyclic.rs @@ -18,6 +18,7 @@ impl Service { ic_cdk::call(self.0, "f", (arg0,arg1,arg2,arg3,arg4,arg5,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/empty.rs b/rust/candid_parser/tests/assets/ok/empty.rs index 4477e9f99..899477cc8 100644 --- a/rust/candid_parser/tests/assets/ok/empty.rs +++ b/rust/candid_parser/tests/assets/ok/empty.rs @@ -27,6 +27,7 @@ impl Service { ic_cdk::call(self.0, "h", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/escape.rs b/rust/candid_parser/tests/assets/ok/escape.rs index 0fde3e5d9..96feb0727 100644 --- a/rust/candid_parser/tests/assets/ok/escape.rs +++ b/rust/candid_parser/tests/assets/ok/escape.rs @@ -22,6 +22,7 @@ impl Service { ic_cdk::call(self.0, "\n\'\"\'\'\"\"\r\t", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/example.d.ts b/rust/candid_parser/tests/assets/ok/example.d.ts index 89620882c..f6c7419df 100644 --- a/rust/candid_parser/tests/assets/ok/example.d.ts +++ b/rust/candid_parser/tests/assets/ok/example.d.ts @@ -13,16 +13,37 @@ export type tree = { export interface s { 'f' : t, 'g' : ActorMethod<[list], [B, tree, stream]> } export type t = ActorMethod<[Principal], undefined>; export type stream = [] | [{ 'head' : bigint, 'next' : [Principal, string] }]; +/** + * Doc comment for b in imported file + */ export type b = [bigint, bigint]; +/** + * Doc comment for a in imported file + */ export type a = { 'a' : null } | { 'b' : b }; +/** + * Doc comment for prim type + */ export type my_type = Principal; +/** + * Doc comment for List + */ export type List = [] | [{ 'head' : bigint, 'tail' : List }]; export type f = ActorMethod<[List, [Principal, string]], [[] | [List], res]>; +/** + * Doc comment for broker service + */ export interface broker { 'find' : ActorMethod<[string], Principal> } +/** + * Doc comment for nested type + */ export interface nested { _0_ : bigint, _1_ : bigint, + /** + * Doc comment for nested record + */ _2_ : [bigint, bigint], _3_ : { _0_ : bigint, _42_ : bigint, _43_ : number }, _40_ : bigint, @@ -32,13 +53,51 @@ export interface nested { { 'C' : null }, _42_ : bigint, } -export type res = { 'Ok' : [bigint, bigint] } | - { 'Err' : { 'error' : string } }; +export type res = { + /** + * Doc comment for Ok variant + */ + 'Ok' : [bigint, bigint] + } | + { + /** + * Doc comment for Err variant + */ + 'Err' : { + /** + * Doc comment for error field in Err variant, + * on multiple lines + */ + 'error' : string, + } + }; export type nested_res = { 'Ok' : { 'Ok' : null } | { 'Err' : null } } | - { 'Err' : { 'Ok' : { 'content' : string } } | { 'Err' : [bigint] } }; + { + 'Err' : { + /** + * Doc comment for Ok in nested variant + */ + 'Ok' : { 'content' : string } + } | + { + /** + * Doc comment for Err in nested variant + */ + 'Err' : [bigint] + } + }; +/** + * Doc comment for service + */ export interface _SERVICE { + /** + * Doc comment for bbbbb method in imported service + */ 'bbbbb' : ActorMethod<[b], undefined>, 'f' : t, + /** + * Doc comment for f1 method of service + */ 'f1' : ActorMethod<[list, Uint8Array | number[], [] | [boolean]], undefined>, 'g' : ActorMethod<[list], [B, tree, stream]>, 'g1' : ActorMethod< @@ -52,8 +111,20 @@ export interface _SERVICE { { 'B' : [] | [string] }, [] | [List], ], - { _42_ : {}, 'id' : bigint } + { + /** + * Doc comment for 0x2a field in h method return + */ + _42_ : {}, + /** + * Doc comment for id field in h method return + */ + 'id' : bigint, + } >, + /** + * Doc comment for i method of service + */ 'i' : f, 'x' : ActorMethod< [a, b], diff --git a/rust/candid_parser/tests/assets/ok/example.did b/rust/candid_parser/tests/assets/ok/example.did index 0767aee59..6f7424573 100644 --- a/rust/candid_parser/tests/assets/ok/example.did +++ b/rust/candid_parser/tests/assets/ok/example.did @@ -9,37 +9,67 @@ type tree = variant { type s = service { f : t; g : (list) -> (B, tree, stream) }; type t = func (server : s) -> (); type stream = opt record { head : nat; next : func () -> (stream) query }; +/// Doc comment for b in imported file type b = record { int; nat }; +/// Doc comment for a in imported file type a = variant { a; b : b }; +/// Doc comment for prim type type my_type = principal; +/// Doc comment for List type List = opt record { head : int; tail : List }; type f = func (List, func (int32) -> (int64)) -> (opt List, res); +/// Doc comment for broker service type broker = service { find : (name : text) -> (service { current : () -> (nat32); up : () -> () }); }; +/// Doc comment for nested type type nested = record { 0 : nat; 1 : nat; + /// Doc comment for nested record 2 : record { nat; int }; 3 : record { 0 : nat; 42 : nat; 43 : nat8 }; 40 : nat; 41 : variant { 42; A; B; C }; 42 : nat; }; -type res = variant { Ok : record { int; nat }; Err : record { error : text } }; +type res = variant { + /// Doc comment for Ok variant + Ok : record { int; nat }; + /// Doc comment for Err variant + Err : record { + /// Doc comment for error field in Err variant, + /// on multiple lines + error : text; + }; +}; type nested_res = variant { Ok : variant { Ok; Err }; - Err : variant { Ok : record { content : text }; Err : record { int } }; + Err : variant { + /// Doc comment for Ok in nested variant + Ok : record { content : text }; + /// Doc comment for Err in nested variant + Err : record { int }; + }; }; +/// Doc comment for service service : { + /// Doc comment for bbbbb method in imported service bbbbb : (b) -> (); f : t; + /// Doc comment for f1 method of service f1 : (list, test : blob, opt bool) -> () oneway; g : (list) -> (B, tree, stream); g1 : (my_type, List, opt List, nested) -> (int, broker, nested_res) query; h : (vec opt text, variant { A : nat; B : opt text }, opt List) -> ( - record { 42 : record {}; id : nat }, + record { + /// Doc comment for 0x2a field in h method return + 42 : record {}; + /// Doc comment for id field in h method return + id : nat; + }, ); + /// Doc comment for i method of service i : f; x : (a, b) -> ( opt a, diff --git a/rust/candid_parser/tests/assets/ok/example.mo b/rust/candid_parser/tests/assets/ok/example.mo index 3a909f1ca..b1385d518 100644 --- a/rust/candid_parser/tests/assets/ok/example.mo +++ b/rust/candid_parser/tests/assets/ok/example.mo @@ -13,37 +13,61 @@ module { public type s = actor { f : t; g : shared list -> async (B, tree, stream) }; public type t = shared (server : s) -> async (); public type stream = ?{ head : Nat; next : shared query () -> async stream }; + /// Doc comment for b in imported file public type b = (Int, Nat); + /// Doc comment for a in imported file public type a = { #a; #b : b }; + /// Doc comment for prim type public type my_type = Principal; + /// Doc comment for List public type List = ?{ head : Int; tail : List }; public type f = shared (List, shared Int32 -> async Int64) -> async ( ?List, res, ); + /// Doc comment for broker service public type broker = actor { find : shared (name : Text) -> async actor { current : shared () -> async Nat32; up : shared () -> async (); }; }; + /// Doc comment for nested type public type nested = { _0_ : Nat; _1_ : Nat; + /// Doc comment for nested record _2_ : (Nat, Int); _3_ : { _0_ : Nat; _42_ : Nat; _43_ : Nat8 }; _40_ : Nat; _41_ : { #_42_ ; #A; #B; #C }; _42_ : Nat; }; - public type res = { #Ok : (Int, Nat); #Err : { error : Text } }; + public type res = { + /// Doc comment for Ok variant + #Ok : (Int, Nat); + /// Doc comment for Err variant + #Err : { + /// Doc comment for error field in Err variant, + /// on multiple lines + error : Text; + }; + }; public type nested_res = { #Ok : { #Ok; #Err }; - #Err : { #Ok : { content : Text }; #Err : { _0_ : Int } }; + #Err : { + /// Doc comment for Ok in nested variant + #Ok : { content : Text }; + /// Doc comment for Err in nested variant + #Err : { _0_ : Int }; + }; }; + /// Doc comment for service public type Self = actor { + /// Doc comment for bbbbb method in imported service bbbbb : shared b -> async (); f : t; + /// Doc comment for f1 method of service f1 : shared (list, test : Blob, ?Bool) -> (); g : shared list -> async (B, tree, stream); g1 : shared query (my_type, List, ?List, nested) -> async ( @@ -52,9 +76,12 @@ module { nested_res, ); h : shared ([?Text], { #A : Nat; #B : ?Text }, ?List) -> async { + /// Doc comment for 0x2a field in h method return _42_ : {}; + /// Doc comment for id field in h method return id : Nat; }; + /// Doc comment for i method of service i : f; x : shared composite query (a, b) -> async ( ?a, diff --git a/rust/candid_parser/tests/assets/ok/fieldnat.rs b/rust/candid_parser/tests/assets/ok/fieldnat.rs index 3d335e76b..43c063fd1 100644 --- a/rust/candid_parser/tests/assets/ok/fieldnat.rs +++ b/rust/candid_parser/tests/assets/ok/fieldnat.rs @@ -51,6 +51,7 @@ impl Service { ic_cdk::call(self.0, "foo", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/inline_methods.rs b/rust/candid_parser/tests/assets/ok/inline_methods.rs index edc700b8d..7fc443b3a 100644 --- a/rust/candid_parser/tests/assets/ok/inline_methods.rs +++ b/rust/candid_parser/tests/assets/ok/inline_methods.rs @@ -46,6 +46,7 @@ impl Service { ic_cdk::call(self.0, "high_order_fn_via_record_inline", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/keyword.rs b/rust/candid_parser/tests/assets/ok/keyword.rs index e0b8ddb7b..d25756994 100644 --- a/rust/candid_parser/tests/assets/ok/keyword.rs +++ b/rust/candid_parser/tests/assets/ok/keyword.rs @@ -76,6 +76,7 @@ impl Service { ic_cdk::call(self.0, "variant", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/management.rs b/rust/candid_parser/tests/assets/ok/management.rs index 311f8e1d6..87b5bcf95 100644 --- a/rust/candid_parser/tests/assets/ok/management.rs +++ b/rust/candid_parser/tests/assets/ok/management.rs @@ -301,5 +301,6 @@ impl<'a> Service<'a> { Ok(Decode!(&bytes)?) } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); diff --git a/rust/candid_parser/tests/assets/ok/recursion.rs b/rust/candid_parser/tests/assets/ok/recursion.rs index 3eb52d95f..239310042 100644 --- a/rust/candid_parser/tests/assets/ok/recursion.rs +++ b/rust/candid_parser/tests/assets/ok/recursion.rs @@ -38,6 +38,7 @@ impl Service { ic_cdk::call(self.0, "g", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/recursive_class.rs b/rust/candid_parser/tests/assets/ok/recursive_class.rs index ce01cd0ea..6678e8ca0 100644 --- a/rust/candid_parser/tests/assets/ok/recursive_class.rs +++ b/rust/candid_parser/tests/assets/ok/recursive_class.rs @@ -12,6 +12,7 @@ impl Service { ic_cdk::call(self.0, "next", ()).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/service.rs b/rust/candid_parser/tests/assets/ok/service.rs index 631c2095a..cbcf60cd7 100644 --- a/rust/candid_parser/tests/assets/ok/service.rs +++ b/rust/candid_parser/tests/assets/ok/service.rs @@ -30,6 +30,7 @@ impl Service { ic_cdk::call(self.0, "asVariant", ()).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/rust/candid_parser/tests/assets/ok/unicode.rs b/rust/candid_parser/tests/assets/ok/unicode.rs index 87c596f46..b5bfbb954 100644 --- a/rust/candid_parser/tests/assets/ok/unicode.rs +++ b/rust/candid_parser/tests/assets/ok/unicode.rs @@ -42,6 +42,7 @@ impl Service { ic_cdk::call(self.0, "👀", (arg0,)).await } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); pub const service : Service = Service(CANISTER_ID); diff --git a/tools/didc/src/main.rs b/tools/didc/src/main.rs index 5615f01c2..59e600510 100644 --- a/tools/didc/src/main.rs +++ b/tools/didc/src/main.rs @@ -142,7 +142,7 @@ impl TypeAnnotation { .actor .as_ref() .ok_or_else(|| Error::msg("Cannot use --method with a non-service did file"))?; - let func = idl_prog.get_method(actor, meth).map_err(Error::msg)?; + let func = idl_prog.get_method(&actor.typ, meth).map_err(Error::msg)?; match mode { Mode::Encode => func.args.iter().map(|arg| arg.typ.clone()).collect(), Mode::Decode => func.rets.clone(), From abea51dd75b1a75d351cff3a05523f6eba3867ac Mon Sep 17 00:00:00 2001 From: ilbertt Date: Fri, 27 Jun 2025 15:18:31 +0200 Subject: [PATCH 06/13] fix: support doc comments in rust bindings --- rust/candid_parser/src/bindings/rust.rs | 138 ++++++++---------- rust/candid_parser/tests/assets/example.did | 28 +++- .../tests/assets/ok/example.d.ts | 54 ++++++- .../candid_parser/tests/assets/ok/example.did | 28 +++- rust/candid_parser/tests/assets/ok/example.js | 12 ++ rust/candid_parser/tests/assets/ok/example.mo | 28 +++- rust/candid_parser/tests/assets/ok/example.rs | 71 ++++++++- 7 files changed, 276 insertions(+), 83 deletions(-) diff --git a/rust/candid_parser/src/bindings/rust.rs b/rust/candid_parser/src/bindings/rust.rs index a870f3ca7..75110077b 100644 --- a/rust/candid_parser/src/bindings/rust.rs +++ b/rust/candid_parser/src/bindings/rust.rs @@ -880,6 +880,7 @@ impl NominalState<'_> { env: &mut IDLMergedProg, path: &mut Vec, t: &IDLType, + parent_doc_comment: Option<&Vec>, ) -> IDLType { let elem = StateElem::Type(t); let old = if matches!(t, IDLType::FuncT(_)) { @@ -891,13 +892,13 @@ impl NominalState<'_> { let res = match t { IDLType::OptT(ty) => { path.push(TypePath::Opt); - let ty = self.nominalize(env, path, ty); + let ty = self.nominalize(env, path, ty, None); path.pop(); IDLType::OptT(Box::new(ty)) } IDLType::VecT(ty) => { path.push(TypePath::Vec); - let ty = self.nominalize(env, path, ty); + let ty = self.nominalize(env, path, ty, None); path.pop(); IDLType::VecT(Box::new(ty)) } @@ -909,26 +910,19 @@ impl NominalState<'_> { { let fs: Vec<_> = fs .iter() - .map( - |TypeField { - label, - typ, - doc_comment, - }| { - let lab = label.to_string(); - let elem = StateElem::Label(&lab); - let old = self.state.push_state(&elem); - path.push(TypePath::RecordField(lab.clone())); - let ty = self.nominalize(env, path, typ); - path.pop(); - self.state.pop_state(old, elem); - TypeField { - label: label.clone(), - typ: ty, - doc_comment: doc_comment.clone(), - } - }, - ) + .map(|field| { + let lab = field.label.to_string(); + let elem = StateElem::Label(&lab); + let old = self.state.push_state(&elem); + path.push(TypePath::RecordField(lab.clone())); + let ty = self.nominalize(env, path, &field.typ, None); + path.pop(); + self.state.pop_state(old, elem); + TypeField { + typ: ty, + ..field.clone() + } + }) .collect(); IDLType::RecordT(fs) } else { @@ -943,11 +937,12 @@ impl NominalState<'_> { env, &mut vec![TypePath::Id(new_var.clone())], &IDLType::RecordT(fs.to_vec()), + None, ); env.insert_binding(Binding { id: new_var.clone(), typ: ty, - doc_comment: None, + doc_comment: parent_doc_comment.cloned(), }); IDLType::VarT(new_var) } @@ -957,30 +952,27 @@ impl NominalState<'_> { if matches!(path.last(), None | Some(TypePath::Id(_))) || is_result { let fs: Vec<_> = fs .iter() - .map( - |TypeField { - label, - typ, - doc_comment, - }| { - let lab = label.to_string(); - let old = self.state.push_state(&StateElem::Label(&lab)); - if is_result { - // so that inner record gets a new name - path.push(TypePath::ResultField(lab.clone())); - } else { - path.push(TypePath::VariantField(lab.clone())); - } - let ty = self.nominalize(env, path, typ); - path.pop(); - self.state.pop_state(old, StateElem::Label(&lab)); - TypeField { - label: label.clone(), - typ: ty, - doc_comment: doc_comment.clone(), - } - }, - ) + .map(|field| { + let lab = field.label.to_string(); + let old = self.state.push_state(&StateElem::Label(&lab)); + let doc_comment = if is_result { + // so that inner record gets a new name + path.push(TypePath::ResultField(lab.clone())); + // Only preserve comments on the field if it's a result, + // because the result cannot have comments on the inner types + field.doc_comment.as_ref() + } else { + path.push(TypePath::VariantField(lab.clone())); + None + }; + let ty = self.nominalize(env, path, &field.typ, doc_comment); + path.pop(); + self.state.pop_state(old, StateElem::Label(&lab)); + TypeField { + typ: ty, + ..field.clone() + } + }) .collect(); IDLType::VariantT(fs) } else { @@ -995,11 +987,12 @@ impl NominalState<'_> { env, &mut vec![TypePath::Id(new_var.clone())], &IDLType::VariantT(fs.to_vec()), + None, ); env.insert_binding(Binding { id: new_var.clone(), typ: ty, - doc_comment: None, + doc_comment: parent_doc_comment.cloned(), }); IDLType::VarT(new_var) } @@ -1022,7 +1015,7 @@ impl NominalState<'_> { i.to_string() }; path.push(TypePath::Func(format!("arg{idx}"))); - let ty = self.nominalize(env, path, &arg.typ); + let ty = self.nominalize(env, path, &arg.typ, None); path.pop(); self.state.pop_state(old, StateElem::Label(&lab)); IDLArgType { @@ -1044,7 +1037,7 @@ impl NominalState<'_> { i.to_string() }; path.push(TypePath::Func(format!("ret{idx}"))); - let ty = self.nominalize(env, path, &ty); + let ty = self.nominalize(env, path, &ty, None); path.pop(); self.state.pop_state(old, StateElem::Label(&lab)); ty @@ -1064,11 +1057,12 @@ impl NominalState<'_> { env, &mut vec![TypePath::Id(new_var.clone())], &IDLType::FuncT(func.clone()), + None, ); env.insert_binding(Binding { id: new_var.clone(), typ: ty, - doc_comment: None, + doc_comment: parent_doc_comment.cloned(), }); IDLType::VarT(new_var) } @@ -1076,25 +1070,18 @@ impl NominalState<'_> { IDLType::ServT(serv) => match path.last() { None | Some(TypePath::Id(_)) => IDLType::ServT( serv.iter() - .map( - |Binding { - id, - typ, - doc_comment, - }| { - let lab = id.to_string(); - let old = self.state.push_state(&StateElem::Label(&lab)); - path.push(TypePath::Id(lab.clone())); - let ty = self.nominalize(env, path, typ); - path.pop(); - self.state.pop_state(old, StateElem::Label(&lab)); - Binding { - id: lab, - typ: ty, - doc_comment: doc_comment.clone(), - } - }, - ) + .map(|binding| { + let lab = binding.id.to_string(); + let old = self.state.push_state(&StateElem::Label(&lab)); + path.push(TypePath::Id(lab.clone())); + let ty = self.nominalize(env, path, &binding.typ, None); + path.pop(); + self.state.pop_state(old, StateElem::Label(&lab)); + Binding { + typ: ty, + ..binding.clone() + } + }) .collect(), ), Some(_) => { @@ -1109,11 +1096,12 @@ impl NominalState<'_> { env, &mut vec![TypePath::Id(new_var.clone())], &IDLType::ServT(serv.clone()), + None, ); env.insert_binding(Binding { id: new_var.clone(), typ: ty, - doc_comment: None, + doc_comment: parent_doc_comment.cloned(), }); IDLType::VarT(new_var) } @@ -1124,7 +1112,7 @@ impl NominalState<'_> { let elem = StateElem::Label("init"); let old = self.state.push_state(&elem); path.push(TypePath::Init); - let ty = self.nominalize(env, path, &arg.typ); + let ty = self.nominalize(env, path, &arg.typ, None); path.pop(); self.state.pop_state(old, elem); IDLArgType { @@ -1133,7 +1121,7 @@ impl NominalState<'_> { } }) .collect(), - Box::new(self.nominalize(env, path, ty)), + Box::new(self.nominalize(env, path, ty, None)), ), t => t.clone(), }; @@ -1148,7 +1136,7 @@ impl NominalState<'_> { for (id, typ, doc_comment) in self.state.prog.get_types() { let elem = StateElem::Label(id); let old = self.state.push_state(&elem); - let ty = self.nominalize(&mut res, &mut vec![TypePath::Id(id.to_string())], typ); + let ty = self.nominalize(&mut res, &mut vec![TypePath::Id(id.to_string())], typ, None); res.insert_binding(Binding { id: id.to_string(), typ: ty, @@ -1157,7 +1145,7 @@ impl NominalState<'_> { self.state.pop_state(old, elem); } let actor = self.state.prog.actor.as_ref().map(|a| IDLActorType { - typ: self.nominalize(&mut res, &mut vec![], &a.typ), + typ: self.nominalize(&mut res, &mut vec![], &a.typ, None), doc_comment: a.doc_comment.clone(), }); res.set_actor(actor); diff --git a/rust/candid_parser/tests/assets/example.did b/rust/candid_parser/tests/assets/example.did index 3cc8978f8..6b7c8da28 100644 --- a/rust/candid_parser/tests/assets/example.did +++ b/rust/candid_parser/tests/assets/example.did @@ -4,7 +4,12 @@ import service "import/b/b.did"; /// Doc comment for prim type type my_type = principal; /// Doc comment for List -type List = opt record { head: int; tail: List }; +type List = opt record { + /// Doc comment for List head + head: int; + /// Doc comment for List tail + tail: List +}; type f = func (List, func (int32) -> (int64)) -> (opt List, res); /// Doc comment for broker service type broker = service { @@ -38,6 +43,26 @@ type nested_res = variant { Ok: variant { Ok; Err }; Err: variant { /// Doc comment for Err in nested variant Err: record { int } } }; +/// Doc comment for nested_records +type nested_records = record { + /// Doc comment for nested_records field nested + nested: opt record { + /// Doc comment for nested_records field nested_field + nested_field: text + } +}; +type my_variant = variant { + /// Doc comment for my_variant field a + a: record { + /// Doc comment for my_variant field a field b + b: text; + }; + /// Doc comment for my_variant field c + c: opt record { + /// Doc comment for my_variant field c field d + d: text; + } +} /// Doc comment for service service server : { @@ -55,5 +80,6 @@ service server : { /// Doc comment for i method of service i : f; x : (a,b) -> (opt a, opt b, variant { Ok: record { result: text }; Err: variant {a;b} }) composite_query; + y : (nested_records) -> (record { nested_records; my_variant }) query; } diff --git a/rust/candid_parser/tests/assets/ok/example.d.ts b/rust/candid_parser/tests/assets/ok/example.d.ts index f6c7419df..42c3c3720 100644 --- a/rust/candid_parser/tests/assets/ok/example.d.ts +++ b/rust/candid_parser/tests/assets/ok/example.d.ts @@ -29,7 +29,18 @@ export type my_type = Principal; /** * Doc comment for List */ -export type List = [] | [{ 'head' : bigint, 'tail' : List }]; +export type List = [] | [ + { + /** + * Doc comment for List head + */ + 'head' : bigint, + /** + * Doc comment for List tail + */ + 'tail' : List, + } +]; export type f = ActorMethod<[List, [Principal, string]], [[] | [List], res]>; /** * Doc comment for broker service @@ -86,6 +97,46 @@ export type nested_res = { 'Ok' : { 'Ok' : null } | { 'Err' : null } } | 'Err' : [bigint] } }; +/** + * Doc comment for nested_records + */ +export interface nested_records { + /** + * Doc comment for nested_records field nested + */ + 'nested' : [] | [ + { + /** + * Doc comment for nested_records field nested_field + */ + 'nested_field' : string, + } + ], +} +export type my_variant = { + /** + * Doc comment for my_variant field a + */ + 'a' : { + /** + * Doc comment for my_variant field a field b + */ + 'b' : string, + } + } | + { + /** + * Doc comment for my_variant field c + */ + 'c' : [] | [ + { + /** + * Doc comment for my_variant field c field d + */ + 'd' : string, + } + ] + }; /** * Doc comment for service */ @@ -135,6 +186,7 @@ export interface _SERVICE { { 'Err' : { 'a' : null } | { 'b' : null } }, ] >, + 'y' : ActorMethod<[nested_records], [nested_records, my_variant]>, } export declare const idlFactory: IDL.InterfaceFactory; export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[]; diff --git a/rust/candid_parser/tests/assets/ok/example.did b/rust/candid_parser/tests/assets/ok/example.did index 6f7424573..adaa2614f 100644 --- a/rust/candid_parser/tests/assets/ok/example.did +++ b/rust/candid_parser/tests/assets/ok/example.did @@ -16,7 +16,12 @@ type a = variant { a; b : b }; /// Doc comment for prim type type my_type = principal; /// Doc comment for List -type List = opt record { head : int; tail : List }; +type List = opt record { + /// Doc comment for List head + head : int; + /// Doc comment for List tail + tail : List; +}; type f = func (List, func (int32) -> (int64)) -> (opt List, res); /// Doc comment for broker service type broker = service { @@ -52,6 +57,26 @@ type nested_res = variant { Err : record { int }; }; }; +/// Doc comment for nested_records +type nested_records = record { + /// Doc comment for nested_records field nested + nested : opt record { + /// Doc comment for nested_records field nested_field + nested_field : text; + }; +}; +type my_variant = variant { + /// Doc comment for my_variant field a + a : record { + /// Doc comment for my_variant field a field b + b : text; + }; + /// Doc comment for my_variant field c + c : opt record { + /// Doc comment for my_variant field c field d + d : text; + }; +}; /// Doc comment for service service : { /// Doc comment for bbbbb method in imported service @@ -76,4 +101,5 @@ service : { opt b, variant { Ok : record { result : text }; Err : variant { a; b } }, ) composite_query; + y : (nested_records) -> (record { nested_records; my_variant }) query; } diff --git a/rust/candid_parser/tests/assets/ok/example.js b/rust/candid_parser/tests/assets/ok/example.js index 4602f2a1b..b289ebe3f 100644 --- a/rust/candid_parser/tests/assets/ok/example.js +++ b/rust/candid_parser/tests/assets/ok/example.js @@ -74,6 +74,13 @@ export const idlFactory = ({ IDL }) => { [], ); const a = IDL.Variant({ 'a' : IDL.Null, 'b' : b }); + const nested_records = IDL.Record({ + 'nested' : IDL.Opt(IDL.Record({ 'nested_field' : IDL.Text })), + }); + const my_variant = IDL.Variant({ + 'a' : IDL.Record({ 'b' : IDL.Text }), + 'c' : IDL.Opt(IDL.Record({ 'd' : IDL.Text })), + }); return IDL.Service({ 'bbbbb' : IDL.Func([b], [], []), 'f' : t, @@ -110,6 +117,11 @@ export const idlFactory = ({ IDL }) => { ], ['composite_query'], ), + 'y' : IDL.Func( + [nested_records], + [IDL.Tuple(nested_records, my_variant)], + ['query'], + ), }); }; export const init = ({ IDL }) => { return []; }; diff --git a/rust/candid_parser/tests/assets/ok/example.mo b/rust/candid_parser/tests/assets/ok/example.mo index b1385d518..eeae24bfc 100644 --- a/rust/candid_parser/tests/assets/ok/example.mo +++ b/rust/candid_parser/tests/assets/ok/example.mo @@ -20,7 +20,12 @@ module { /// Doc comment for prim type public type my_type = Principal; /// Doc comment for List - public type List = ?{ head : Int; tail : List }; + public type List = ?{ + /// Doc comment for List head + head : Int; + /// Doc comment for List tail + tail : List; + }; public type f = shared (List, shared Int32 -> async Int64) -> async ( ?List, res, @@ -62,6 +67,26 @@ module { #Err : { _0_ : Int }; }; }; + /// Doc comment for nested_records + public type nested_records = { + /// Doc comment for nested_records field nested + nested : ?{ + /// Doc comment for nested_records field nested_field + nested_field : Text; + }; + }; + public type my_variant = { + /// Doc comment for my_variant field a + #a : { + /// Doc comment for my_variant field a field b + b : Text; + }; + /// Doc comment for my_variant field c + #c : ?{ + /// Doc comment for my_variant field c field d + d : Text; + }; + }; /// Doc comment for service public type Self = actor { /// Doc comment for bbbbb method in imported service @@ -88,5 +113,6 @@ module { ?b, { #Ok : { result : Text }; #Err : { #a; #b } }, ); + y : shared query nested_records -> async ((nested_records, my_variant)); } } diff --git a/rust/candid_parser/tests/assets/ok/example.rs b/rust/candid_parser/tests/assets/ok/example.rs index 14ec4611d..6e745065b 100644 --- a/rust/candid_parser/tests/assets/ok/example.rs +++ b/rust/candid_parser/tests/assets/ok/example.rs @@ -4,6 +4,7 @@ use candid::{self, CandidType, Deserialize, Principal}; use ic_cdk::api::call::CallResult as Result; +/// Doc comment for b in imported file #[derive(CandidType, Deserialize, Debug)] pub(crate) struct B (pub(crate) candid::Int,pub(crate) u128,); #[derive(CandidType, Deserialize, Debug)] @@ -33,15 +34,19 @@ candid::define_service!(pub(crate) S : { "g" : candid::func!((List) -> (B, Tree, Stream)); }); candid::define_function!(pub(crate) T : (S) -> ()); +/// Doc comment for prim type type CanisterId = Principal; #derive[CandidType, Deserialize, Clone] pub(crate) struct ListInner { + /// Doc comment for List head #[serde(skip_deserializing)] #[serde(rename="head")] HEAD: candid::Int, + /// Doc comment for List tail #[serde(skip_deserializing)] tail: Arc, } +/// Doc comment for List #[derive(CandidType, Deserialize, Debug)] pub(crate) struct MyList(pub(crate) Option); #[derive(CandidType, Deserialize, Debug)] @@ -59,10 +64,12 @@ pub(crate) enum Nested41 { B, C, } +/// Doc comment for nested type #[derive(CandidType, Deserialize, Debug)] pub(crate) struct Nested { pub(crate) _0_: u128, pub(crate) _1_: u128, + /// Doc comment for nested record pub(crate) _2_: (u128,candid::Int,), pub(crate) _3_: Nested3, pub(crate) _40_: u128, @@ -73,9 +80,11 @@ candid::define_service!(pub BrokerReturn : { "current" : candid::func!(() -> (u32)); "up" : candid::func!(() -> ()); }); +/// Doc comment for broker service candid::define_service!(pub(crate) Broker : { "find" : candid::func!((String) -> (BrokerReturn)); }); +/// Doc comment for Ok in nested variant #[derive(CandidType, Deserialize, Debug)] pub(crate) struct NestedResErrOk { pub(crate) content: String } pub(crate) type NestedRes = std::result::Result< @@ -86,30 +95,72 @@ pub(crate) enum HArg1 { A(u128), B(Option) } #[derive(CandidType, Deserialize, Debug)] pub(crate) struct HRet42 {} #[derive(CandidType, Deserialize, Debug)] -pub(crate) struct HRet { pub(crate) _42_: HRet42, pub(crate) id: u128 } +pub(crate) struct HRet { + /// Doc comment for 0x2a field in h method return + pub(crate) _42_: HRet42, + /// Doc comment for id field in h method return + pub(crate) id: u128, +} candid::define_function!(pub(crate) FArg1 : (i32) -> (i64)); +/// Doc comment for Err variant #[derive(CandidType, Deserialize, Debug)] -pub(crate) struct ResErr { pub(crate) error: String } +pub(crate) struct ResErr { + /// Doc comment for error field in Err variant, + /// on multiple lines + pub(crate) error: String, +} pub(crate) type Res = std::result::Result<(candid::Int,u128,), ResErr>; candid::define_function!(pub(crate) F : (MyList, FArg1) -> ( Option, Res, )); +/// Doc comment for a in imported file #[derive(CandidType, Deserialize, Debug)] pub(crate) enum A { #[serde(rename="a")] A, #[serde(rename="b")] B(B) } #[derive(CandidType, Deserialize, Debug)] pub(crate) struct XRet2Ok { pub(crate) result: String } #[derive(CandidType, Deserialize, Debug)] pub(crate) enum Error { #[serde(rename="a")] A, #[serde(rename="b")] B } +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct NestedRecordsNestedInner { + /// Doc comment for nested_records field nested_field + pub(crate) nested_field: String, +} +/// Doc comment for nested_records +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct NestedRecords { + /// Doc comment for nested_records field nested + pub(crate) nested: Option, +} +#[derive(CandidType, Deserialize, Debug)] +pub(crate) struct MyVariantCInner { + /// Doc comment for my_variant field c field d + pub(crate) d: String, +} +#[derive(CandidType, Deserialize, Debug)] +pub(crate) enum MyVariant { + /// Doc comment for my_variant field a + #[serde(rename="a")] + A{ + /// Doc comment for my_variant field a field b + b: String, + }, + /// Doc comment for my_variant field c + #[serde(rename="c")] + C(Option), +} +/// Doc comment for service pub struct Service(pub Principal); impl Service { + /// Doc comment for bbbbb method in imported service pub async fn bbbbb(&self, arg0: &B) -> Result<()> { ic_cdk::call(self.0, "bbbbb", (arg0,)).await } pub async fn f(&self, server: &S) -> Result<()> { ic_cdk::call(self.0, "f", (server,)).await } + /// Doc comment for f1 method of service pub async fn f_1(&self, arg0: &List, test: &serde_bytes::ByteBuf, arg2: &Option) -> Result<()> { ic_cdk::call(self.0, "f1", (arg0,test,arg2,)).await } @@ -122,19 +173,31 @@ impl Service { pub async fn h(&self, arg0: &Vec>, arg1: &HArg1, arg2: &Option) -> Result<(HRet,)> { ic_cdk::call(self.0, "h", (arg0,arg1,arg2,)).await } + /// Doc comment for i method of service pub async fn i(&self, arg0: &MyList, arg1: &FArg1) -> Result<(Option,Res,)> { ic_cdk::call(self.0, "i", (arg0,arg1,)).await } pub async fn x(&self, arg0: &A, arg1: &B) -> Result<(Option,Option,std::result::Result,)> { ic_cdk::call(self.0, "x", (arg0,arg1,)).await } + pub async fn y(&self, arg0: &NestedRecords) -> Result<((NestedRecords,MyVariant,),)> { + ic_cdk::call(self.0, "y", (arg0,)).await + } } -pub const CANISTER_ID : Principal = Principal::from_slice(&[]); // aaaaa-aa +/// Canister ID: `aaaaa-aa` +pub const CANISTER_ID : Principal = Principal::from_slice(&[]); +/// Doc comment for service pub const service : Service = Service(CANISTER_ID); #[test] fn test_Arc_MyList_() { // Generated from ListInner.record.tail.use_type = "Arc" - let candid_src = r#"type ListInner = record { head : int; tail : List }; + let candid_src = r#"type ListInner = record { + /// Doc comment for List head + head : int; + /// Doc comment for List tail + tail : List; +}; +/// Doc comment for List type List = opt ListInner; (List)"#; candid_parser::utils::check_rust_type::>(candid_src).unwrap(); From 078282d3f26c500756e8be10468fc6d72fc23887 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Fri, 27 Jun 2025 17:00:46 +0200 Subject: [PATCH 07/13] feat: canister methods doc comments from Rust --- rust/candid/tests/types.rs | 12 ++++++++++++ rust/candid_derive/src/func.rs | 24 ++++++++++++++++-------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/rust/candid/tests/types.rs b/rust/candid/tests/types.rs index 847dbedaf..d5a0d3721 100644 --- a/rust/candid/tests/types.rs +++ b/rust/candid/tests/types.rs @@ -350,6 +350,7 @@ pub struct List { #[test] fn test_func() { + /// Doc comment for 🐂 method #[candid_method(query, rename = "🐂")] fn test(a: String, b: i32) -> (String, i32) { (a, b) @@ -382,24 +383,29 @@ fn test_func() { } } use internal::A; + /// Doc comment for id_variant method #[candid::candid_method] fn id_variant(_: &[internal::A]) -> Result<((A,), A), String> { unreachable!() } + /// Doc comment for oneway method #[candid_method(oneway)] fn oneway(_: &str) { unreachable!() } + /// Doc comment for id_struct query method #[candid_method(query)] fn id_struct(_: (List,)) -> Result, candid::Empty> { unreachable!() } + /// Doc comment for id_struct_composite composite_query method #[candid_method(composite_query)] fn id_struct_composite(_: (List,)) -> Result, candid::Empty> { unreachable!() } + /// Doc comment for id_tuple_destructure method #[candid_method] fn id_tuple_destructure((a, b): (u8, u8)) -> (u8, u8) { (a, b) @@ -437,13 +443,19 @@ type Result = variant { Ok : List; Err : empty }; type Result_1 = variant { Ok : record { record { A }; A }; Err : text }; type Wrap = record { head : int8; tail : opt Box }; service : (List_2) -> { + /// Doc comment for id_struct query method id_struct : (record { List }) -> (Result) query; + /// Doc comment for id_struct_composite composite_query method id_struct_composite : (record { List }) -> (Result) composite_query; id_struct_destructure : (NamedStruct) -> (nat16, int32); + /// Doc comment for id_tuple_destructure method id_tuple_destructure : (record { nat8; nat8 }) -> (nat8, nat8); id_unused_arg : (nat8) -> (Result); + /// Doc comment for id_variant method id_variant : (vec A) -> (Result_1); + /// Doc comment for oneway method "oneway" : (text) -> () oneway; + /// Doc comment for 🐂 method "🐂" : (a : text, b : int32) -> (text, int32) query; }"#; assert_eq!(expected, __export_service()); diff --git a/rust/candid_derive/src/func.rs b/rust/candid_derive/src/func.rs index 84b2fcdc9..40760f2c8 100644 --- a/rust/candid_derive/src/func.rs +++ b/rust/candid_derive/src/func.rs @@ -139,20 +139,24 @@ pub(crate) fn export_service(path: Option) -> TokenStream { let mut rets: Vec = Vec::new(); #(#rets)* let func = Function { args, rets, modes: #modes }; - service.insert(#name.to_string(), (TypeInner::Func(func).into(), #doc_comment)); + service.push((#name.to_string(), TypeInner::Func(func).into())); + #doc_comment + if !doc_comment.is_empty() { + doc_comments.insert(#name.to_string(), doc_comment); + } } } }, ); let service = quote! { use #candid::types::{CandidType, Function, Type, ArgType, TypeInner}; - use #candid::types::syntax::{Binding, IDLMergedProg}; - let mut service: std::collections::HashMap)> = std::collections::HashMap::new(); + use #candid::types::syntax::{Binding, IDLMergedProg, IDLActorType}; + let mut service = Vec::<(String, Type)>::new(); + let mut doc_comments: std::collections::HashMap> = std::collections::HashMap::new(); let mut env = #candid::types::internal::TypeContainer::new(); #(#gen_tys)* - let mut service_methods = service.iter().map(|(id, (typ, _))| (id.clone(), typ.clone())).collect::>(); - service_methods.sort_unstable_by_key(|(name, _)| name.clone()); - let ty = TypeInner::Service(service_methods).into(); + service.sort_unstable_by_key(|(name, _)| name.clone()); + let ty = TypeInner::Service(service).into(); }; let actor = if let Some(init) = init { quote! { @@ -170,10 +174,14 @@ pub(crate) fn export_service(path: Option) -> TokenStream { let bindings = env.env.0.iter().map(|(id, t)| Binding { id: id.clone(), typ: env.as_idl_type(t), - doc_comment: service.get(id).map(|(_, c)| c).cloned(), + doc_comment: doc_comments.get(id).cloned(), }).collect::>(); let mut idl_merged_prog = IDLMergedProg::from(bindings); - idl_merged_prog.set_actor(Some(env.as_idl_type(&actor))); + idl_merged_prog.set_actor(Some(IDLActorType { + typ: env.as_idl_type(&actor), + doc_comment: None, + })); + idl_merged_prog.set_comments_in_actor(&doc_comments); let result = #candid::pretty::candid::compile(&idl_merged_prog); format!("{}", result) From d3c26e3350aaf31621192bc7f1c991664c700c2e Mon Sep 17 00:00:00 2001 From: ilbertt Date: Fri, 27 Jun 2025 19:03:53 +0200 Subject: [PATCH 08/13] test: add more methods tests --- rust/candid/tests/types.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rust/candid/tests/types.rs b/rust/candid/tests/types.rs index d5a0d3721..356c7b176 100644 --- a/rust/candid/tests/types.rs +++ b/rust/candid/tests/types.rs @@ -470,14 +470,17 @@ fn test_counter() { fn init() -> Self { Service { counter: 0 } } + /// Doc comment for inc method #[candid_method] fn inc(&mut self) { self.counter += 1; } + /// Doc comment for read method #[candid_method(query)] fn read(&self) -> usize { self.counter } + /// Doc comment for set method #[candid_method] fn set(&mut self, value: usize) { self.counter = value; @@ -485,8 +488,11 @@ fn test_counter() { } candid::export_service!(); let expected = r#"service : { + /// Doc comment for inc method inc : () -> (); + /// Doc comment for read method read : () -> (nat64) query; + /// Doc comment for set method set : (value : nat64) -> (); }"#; assert_eq!(expected, __export_service()); From cbcd959971cea5970b9286ebfe5fd7cf0a91e6a6 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Fri, 27 Jun 2025 19:18:56 +0200 Subject: [PATCH 09/13] style: clippy --- rust/candid/src/types/syntax.rs | 148 ++++++++++++------------ rust/candid_derive/src/func.rs | 6 +- rust/candid_parser/src/bindings/rust.rs | 18 +-- 3 files changed, 84 insertions(+), 88 deletions(-) diff --git a/rust/candid/src/types/syntax.rs b/rust/candid/src/types/syntax.rs index a8ff56429..ae205703e 100644 --- a/rust/candid/src/types/syntax.rs +++ b/rust/candid/src/types/syntax.rs @@ -287,84 +287,9 @@ impl IDLMergedProg { self.actor = other; } - pub fn set_comments_in_type( - &self, - t: &IDLType, - doc_comments: &HashMap>, - ) -> IDLType { - match t { - IDLType::PrimT(prim) => IDLType::PrimT(prim.clone()), - IDLType::VarT(id) => IDLType::VarT(id.clone()), - IDLType::FuncT(func) => IDLType::FuncT(FuncType { - modes: func.modes.clone(), - args: func - .args - .iter() - .map(|a| IDLArgType { - typ: self.set_comments_in_type(&a.typ, doc_comments), - name: a.name.clone(), - }) - .collect(), - rets: func - .rets - .iter() - .map(|r| self.set_comments_in_type(r, doc_comments)) - .collect(), - }), - IDLType::OptT(t) => IDLType::OptT(Box::new(self.set_comments_in_type(t, doc_comments))), - IDLType::VecT(t) => IDLType::VecT(Box::new(self.set_comments_in_type(t, doc_comments))), - IDLType::RecordT(fields) => { - let fields = fields - .iter() - .map(|f| TypeField { - label: f.label.clone(), - typ: self.set_comments_in_type(&f.typ, doc_comments), - doc_comment: doc_comments.get(&f.label.to_string()).cloned(), - }) - .collect(); - IDLType::RecordT(fields) - } - IDLType::ServT(methods) => { - let methods = methods - .iter() - .map(|m| Binding { - id: m.id.clone(), - typ: self.set_comments_in_type(&m.typ, doc_comments), - doc_comment: doc_comments.get(&m.id).cloned(), - }) - .collect(); - IDLType::ServT(methods) - } - IDLType::VariantT(fields) => { - let fields = fields - .iter() - .map(|f| TypeField { - label: f.label.clone(), - typ: self.set_comments_in_type(&f.typ, doc_comments), - doc_comment: doc_comments.get(&f.label.to_string()).cloned(), - }) - .collect(); - IDLType::VariantT(fields) - } - IDLType::ClassT(args, t) => { - let args = args - .iter() - .map(|a| IDLArgType { - typ: self.set_comments_in_type(&a.typ, doc_comments), - name: a.name.clone(), - }) - .collect(); - IDLType::ClassT(args, Box::new(self.set_comments_in_type(t, doc_comments))) - } - IDLType::PrincipalT => IDLType::PrincipalT, - IDLType::FutureT => IDLType::FutureT, - IDLType::UnknownT => IDLType::UnknownT, - } - } - pub fn set_comments_in_actor(&mut self, doc_comments: &HashMap>) { self.actor = self.actor.as_ref().map(|t| IDLActorType { - typ: self.set_comments_in_type(&t.typ, doc_comments), + typ: set_comments_in_type(&t.typ, doc_comments), doc_comment: t.doc_comment.clone(), }); } @@ -440,3 +365,74 @@ impl IDLMergedProg { Err(format!("cannot find method {id}")) } } + +fn set_comments_in_type(t: &IDLType, doc_comments: &HashMap>) -> IDLType { + match t { + IDLType::PrimT(prim) => IDLType::PrimT(prim.clone()), + IDLType::VarT(id) => IDLType::VarT(id.clone()), + IDLType::FuncT(func) => IDLType::FuncT(FuncType { + modes: func.modes.clone(), + args: func + .args + .iter() + .map(|a| IDLArgType { + typ: set_comments_in_type(&a.typ, doc_comments), + name: a.name.clone(), + }) + .collect(), + rets: func + .rets + .iter() + .map(|r| set_comments_in_type(r, doc_comments)) + .collect(), + }), + IDLType::OptT(t) => IDLType::OptT(Box::new(set_comments_in_type(t, doc_comments))), + IDLType::VecT(t) => IDLType::VecT(Box::new(set_comments_in_type(t, doc_comments))), + IDLType::RecordT(fields) => { + let fields = fields + .iter() + .map(|f| TypeField { + label: f.label.clone(), + typ: set_comments_in_type(&f.typ, doc_comments), + doc_comment: doc_comments.get(&f.label.to_string()).cloned(), + }) + .collect(); + IDLType::RecordT(fields) + } + IDLType::ServT(methods) => { + let methods = methods + .iter() + .map(|m| Binding { + id: m.id.clone(), + typ: set_comments_in_type(&m.typ, doc_comments), + doc_comment: doc_comments.get(&m.id).cloned(), + }) + .collect(); + IDLType::ServT(methods) + } + IDLType::VariantT(fields) => { + let fields = fields + .iter() + .map(|f| TypeField { + label: f.label.clone(), + typ: set_comments_in_type(&f.typ, doc_comments), + doc_comment: doc_comments.get(&f.label.to_string()).cloned(), + }) + .collect(); + IDLType::VariantT(fields) + } + IDLType::ClassT(args, t) => { + let args = args + .iter() + .map(|a| IDLArgType { + typ: set_comments_in_type(&a.typ, doc_comments), + name: a.name.clone(), + }) + .collect(); + IDLType::ClassT(args, Box::new(set_comments_in_type(t, doc_comments))) + } + IDLType::PrincipalT => IDLType::PrincipalT, + IDLType::FutureT => IDLType::FutureT, + IDLType::UnknownT => IDLType::UnknownT, + } +} diff --git a/rust/candid_derive/src/func.rs b/rust/candid_derive/src/func.rs index 40760f2c8..7cfb7d984 100644 --- a/rust/candid_derive/src/func.rs +++ b/rust/candid_derive/src/func.rs @@ -140,9 +140,9 @@ pub(crate) fn export_service(path: Option) -> TokenStream { #(#rets)* let func = Function { args, rets, modes: #modes }; service.push((#name.to_string(), TypeInner::Func(func).into())); - #doc_comment - if !doc_comment.is_empty() { - doc_comments.insert(#name.to_string(), doc_comment); + let function_doc_comment = #doc_comment; + if !function_doc_comment.is_empty() { + doc_comments.insert(#name.to_string(), function_doc_comment); } } } diff --git a/rust/candid_parser/src/bindings/rust.rs b/rust/candid_parser/src/bindings/rust.rs index 75110077b..b1d34f0d5 100644 --- a/rust/candid_parser/src/bindings/rust.rs +++ b/rust/candid_parser/src/bindings/rust.rs @@ -185,6 +185,13 @@ fn pp_doc_comment(comment_lines: Option<&Vec>) -> RcDoc { doc_comment } +/// (actor methods, init args, actor comment lines) +type PpActorRet = ( + Vec, + Option>, + Option>, +); + impl<'a> State<'a> { fn generate_test(&mut self, src: &IDLType, use_type: &str) { if self.tests.contains_key(use_type) { @@ -675,14 +682,7 @@ fn test_{test_name}() {{ self.state.pop_state(old, StateElem::Label(id)); res } - fn pp_actor( - &mut self, - actor: &IDLActorType, - ) -> ( - Vec, - Option>, - Option>, - ) { + fn pp_actor(&mut self, actor: &IDLActorType) -> PpActorRet { let actor_typ = self.state.prog.trace_type(&actor.typ).unwrap(); let init = if let IDLType::ClassT(args, _) = &actor_typ { let old = self.state.push_state(&StateElem::Label("init")); @@ -713,7 +713,7 @@ fn test_{test_name}() {{ let mut res = Vec::new(); for binding in serv.iter() { let func = self.state.prog.as_func(&binding.typ).unwrap(); - res.push(self.pp_function(&binding, func)); + res.push(self.pp_function(binding, func)); } (res, init, actor.doc_comment.clone()) } From efafa21c66a295317b518396b89d46686520f7c1 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Fri, 27 Jun 2025 19:37:42 +0200 Subject: [PATCH 10/13] chore: disable format rule in clippy --- Cargo.toml | 9 +++++++++ rust/bench/Cargo.toml | 3 +++ rust/candid/Cargo.toml | 3 +++ rust/candid/fuzz/Cargo.toml | 5 ++++- rust/candid_derive/Cargo.toml | 3 +++ rust/candid_parser/Cargo.toml | 8 +++++++- rust/ic_principal/Cargo.toml | 3 +++ tools/didc-js/wasm-package/Cargo.toml | 6 ++---- tools/didc-js/wasm-package/src/lib.rs | 1 - tools/didc/Cargo.toml | 2 ++ 10 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bd87832de..6ec93b657 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,12 @@ anyhow = "1.0" rand = "0.8" arbitrary = "1.3" console = "0.15" + +[workspace.lints.clippy] +all = { level = "deny", priority = 0 } +uninlined_format_args = { level = "allow", priority = 1 } + +[workspace.lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(wasm_bindgen_unstable_test_coverage)', +] } diff --git a/rust/bench/Cargo.toml b/rust/bench/Cargo.toml index fbe32c4cc..b84d2a353 100644 --- a/rust/bench/Cargo.toml +++ b/rust/bench/Cargo.toml @@ -21,3 +21,6 @@ hex = "0.4.3" candid = { path = "../candid" } candid_derive = { path = "../candid_derive" } ic_principal = { path = "../ic_principal" } + +[lints] +workspace = true diff --git a/rust/candid/Cargo.toml b/rust/candid/Cargo.toml index d6bb9140a..a7925dec9 100644 --- a/rust/candid/Cargo.toml +++ b/rust/candid/Cargo.toml @@ -67,3 +67,6 @@ required-features = ["bignum"] features = ["all"] # defines the configuration attribute `docsrs` rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/rust/candid/fuzz/Cargo.toml b/rust/candid/fuzz/Cargo.toml index 47e0514ea..af76b3532 100644 --- a/rust/candid/fuzz/Cargo.toml +++ b/rust/candid/fuzz/Cargo.toml @@ -21,6 +21,9 @@ features = ["all"] [workspace] members = ["."] +[lints] +workspace = true + [[bin]] name = "parser" path = "fuzz_targets/parser.rs" @@ -31,4 +34,4 @@ doc = false name = "type_decoder" path = "fuzz_targets/type_decoder.rs" test = false -doc = false \ No newline at end of file +doc = false diff --git a/rust/candid_derive/Cargo.toml b/rust/candid_derive/Cargo.toml index d71d157d3..9caeadbe3 100644 --- a/rust/candid_derive/Cargo.toml +++ b/rust/candid_derive/Cargo.toml @@ -24,3 +24,6 @@ quote = "1.0.7" syn = { version = "2.0", features = ["visit", "full"] } proc-macro2 = "1.0.19" lazy_static = "1.4.0" + +[lints] +workspace = true diff --git a/rust/candid_parser/Cargo.toml b/rust/candid_parser/Cargo.toml index e648989ca..445750de4 100644 --- a/rust/candid_parser/Cargo.toml +++ b/rust/candid_parser/Cargo.toml @@ -38,7 +38,10 @@ arbitrary = { workspace = true, optional = true } fake = { version = "2.4", optional = true } rand = { version = "0.8", optional = true } num-traits = { workspace = true, optional = true } -dialoguer = { version = "0.11", default-features = false, features = ["editor", "completion"], optional = true } +dialoguer = { version = "0.11", default-features = false, features = [ + "editor", + "completion", +], optional = true } ctrlc = { version = "3.4", optional = true } console = { workspace = true, optional = true } @@ -58,3 +61,6 @@ all = ["random", "assist"] features = ["all"] # defines the configuration attribute `docsrs` rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/rust/ic_principal/Cargo.toml b/rust/ic_principal/Cargo.toml index 297d42c52..1e5f3ed48 100644 --- a/rust/ic_principal/Cargo.toml +++ b/rust/ic_principal/Cargo.toml @@ -35,3 +35,6 @@ arbitrary = ['dep:arbitrary', 'serde'] convert = ['dep:crc32fast', 'dep:data-encoding', 'dep:thiserror'] self_authenticating = ['dep:sha2'] serde = ['dep:serde', 'convert'] + +[lints] +workspace = true diff --git a/tools/didc-js/wasm-package/Cargo.toml b/tools/didc-js/wasm-package/Cargo.toml index 8aa41ecf8..9f25622c8 100644 --- a/tools/didc-js/wasm-package/Cargo.toml +++ b/tools/didc-js/wasm-package/Cargo.toml @@ -32,7 +32,5 @@ getrandom = { version = "0.2", features = ["js"] } serde-wasm-bindgen = "0.5" js-sys = "0.3" -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(wasm_bindgen_unstable_test_coverage)', -] } +[lints] +workspace = true diff --git a/tools/didc-js/wasm-package/src/lib.rs b/tools/didc-js/wasm-package/src/lib.rs index fb1a41ccf..17f408f45 100644 --- a/tools/didc-js/wasm-package/src/lib.rs +++ b/tools/didc-js/wasm-package/src/lib.rs @@ -1,4 +1,3 @@ -#![deny(clippy::all)] use types::{JsDecodeArgs, JsEncodeArgs}; use wasm_bindgen::prelude::*; diff --git a/tools/didc/Cargo.toml b/tools/didc/Cargo.toml index 19ab3a7fc..af7508c7f 100644 --- a/tools/didc/Cargo.toml +++ b/tools/didc/Cargo.toml @@ -13,3 +13,5 @@ anyhow.workspace = true rand.workspace = true console.workspace = true +[lints] +workspace = true From 8256e5391dc05a91f2d6777192fb97da183dce3d Mon Sep 17 00:00:00 2001 From: ilbertt Date: Fri, 27 Jun 2025 19:52:23 +0200 Subject: [PATCH 11/13] chore: remove packages that are not in the workspace --- rust/bench/Cargo.toml | 3 --- rust/candid/fuzz/Cargo.toml | 3 --- 2 files changed, 6 deletions(-) diff --git a/rust/bench/Cargo.toml b/rust/bench/Cargo.toml index b84d2a353..fbe32c4cc 100644 --- a/rust/bench/Cargo.toml +++ b/rust/bench/Cargo.toml @@ -21,6 +21,3 @@ hex = "0.4.3" candid = { path = "../candid" } candid_derive = { path = "../candid_derive" } ic_principal = { path = "../ic_principal" } - -[lints] -workspace = true diff --git a/rust/candid/fuzz/Cargo.toml b/rust/candid/fuzz/Cargo.toml index af76b3532..6fbbf38e6 100644 --- a/rust/candid/fuzz/Cargo.toml +++ b/rust/candid/fuzz/Cargo.toml @@ -21,9 +21,6 @@ features = ["all"] [workspace] members = ["."] -[lints] -workspace = true - [[bin]] name = "parser" path = "fuzz_targets/parser.rs" From 46df4fbe49cf2d7aac92a3f2021bbe6a2d448c77 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Mon, 30 Jun 2025 11:42:26 +0200 Subject: [PATCH 12/13] refactor: remove doc comments from derive We'll introduce them in a follow-up PR --- rust/candid/tests/types.rs | 18 ------- rust/candid_derive/src/func.rs | 95 ++++++++++------------------------ rust/candid_derive/src/lib.rs | 16 ------ 3 files changed, 28 insertions(+), 101 deletions(-) diff --git a/rust/candid/tests/types.rs b/rust/candid/tests/types.rs index 356c7b176..847dbedaf 100644 --- a/rust/candid/tests/types.rs +++ b/rust/candid/tests/types.rs @@ -350,7 +350,6 @@ pub struct List { #[test] fn test_func() { - /// Doc comment for 🐂 method #[candid_method(query, rename = "🐂")] fn test(a: String, b: i32) -> (String, i32) { (a, b) @@ -383,29 +382,24 @@ fn test_func() { } } use internal::A; - /// Doc comment for id_variant method #[candid::candid_method] fn id_variant(_: &[internal::A]) -> Result<((A,), A), String> { unreachable!() } - /// Doc comment for oneway method #[candid_method(oneway)] fn oneway(_: &str) { unreachable!() } - /// Doc comment for id_struct query method #[candid_method(query)] fn id_struct(_: (List,)) -> Result, candid::Empty> { unreachable!() } - /// Doc comment for id_struct_composite composite_query method #[candid_method(composite_query)] fn id_struct_composite(_: (List,)) -> Result, candid::Empty> { unreachable!() } - /// Doc comment for id_tuple_destructure method #[candid_method] fn id_tuple_destructure((a, b): (u8, u8)) -> (u8, u8) { (a, b) @@ -443,19 +437,13 @@ type Result = variant { Ok : List; Err : empty }; type Result_1 = variant { Ok : record { record { A }; A }; Err : text }; type Wrap = record { head : int8; tail : opt Box }; service : (List_2) -> { - /// Doc comment for id_struct query method id_struct : (record { List }) -> (Result) query; - /// Doc comment for id_struct_composite composite_query method id_struct_composite : (record { List }) -> (Result) composite_query; id_struct_destructure : (NamedStruct) -> (nat16, int32); - /// Doc comment for id_tuple_destructure method id_tuple_destructure : (record { nat8; nat8 }) -> (nat8, nat8); id_unused_arg : (nat8) -> (Result); - /// Doc comment for id_variant method id_variant : (vec A) -> (Result_1); - /// Doc comment for oneway method "oneway" : (text) -> () oneway; - /// Doc comment for 🐂 method "🐂" : (a : text, b : int32) -> (text, int32) query; }"#; assert_eq!(expected, __export_service()); @@ -470,17 +458,14 @@ fn test_counter() { fn init() -> Self { Service { counter: 0 } } - /// Doc comment for inc method #[candid_method] fn inc(&mut self) { self.counter += 1; } - /// Doc comment for read method #[candid_method(query)] fn read(&self) -> usize { self.counter } - /// Doc comment for set method #[candid_method] fn set(&mut self, value: usize) { self.counter = value; @@ -488,11 +473,8 @@ fn test_counter() { } candid::export_service!(); let expected = r#"service : { - /// Doc comment for inc method inc : () -> (); - /// Doc comment for read method read : () -> (nat64) query; - /// Doc comment for set method set : (value : nat64) -> (); }"#; assert_eq!(expected, __export_service()); diff --git a/rust/candid_derive/src/func.rs b/rust/candid_derive/src/func.rs index 7cfb7d984..2fff7b72b 100644 --- a/rust/candid_derive/src/func.rs +++ b/rust/candid_derive/src/func.rs @@ -1,5 +1,3 @@ -use crate::get_doc_comment_lines; - use super::{candid_path, get_lit_str}; use lazy_static::lazy_static; use proc_macro2::TokenStream; @@ -12,13 +10,11 @@ type RawArgs = Vec<(Option, String)>; type RawRets = Vec; type ParsedArgs = Vec<(Option, Type)>; type ParsedRets = Vec; -type CommentLines = Vec; struct Method { args: RawArgs, rets: RawRets, modes: String, - doc_comment: CommentLines, } // There is no official way to communicate information across proc macro invocations. @@ -58,7 +54,6 @@ pub(crate) fn candid_method(attrs: Vec, fun: ItemFn) -> Result { @@ -74,15 +69,7 @@ pub(crate) fn candid_method(attrs: Vec, fun: ItemFn) -> Result) -> TokenStream { } else { unreachable!(); }; - let gen_tys = meths.iter().map( - |( - name, - Method { - args, - rets, - modes, - doc_comment, - }, - )| { - let args = args - .iter() - .map(|t| generate_arg(quote! { args }, t)) - .collect::>(); - let rets = rets - .iter() - .map(|t| generate_ret(quote! { rets }, t)) - .collect::>(); - let modes = match modes.as_ref() { - "query" => quote! { vec![#candid::types::FuncMode::Query] }, - "composite_query" => quote! { vec![#candid::types::FuncMode::CompositeQuery] }, - "oneway" => quote! { vec![#candid::types::FuncMode::Oneway] }, - "update" => quote! { vec![] }, - _ => unreachable!(), - }; - let doc_comment = generate_doc_comment(doc_comment.as_slice()); - quote! { - { - let mut args: Vec = Vec::new(); - #(#args)* - let mut rets: Vec = Vec::new(); - #(#rets)* - let func = Function { args, rets, modes: #modes }; - service.push((#name.to_string(), TypeInner::Func(func).into())); - let function_doc_comment = #doc_comment; - if !function_doc_comment.is_empty() { - doc_comments.insert(#name.to_string(), function_doc_comment); - } - } + let gen_tys = meths.iter().map(|(name, Method { args, rets, modes })| { + let args = args + .iter() + .map(|t| generate_arg(quote! { args }, t)) + .collect::>(); + let rets = rets + .iter() + .map(|t| generate_ret(quote! { rets }, t)) + .collect::>(); + let modes = match modes.as_ref() { + "query" => quote! { vec![#candid::types::FuncMode::Query] }, + "composite_query" => quote! { vec![#candid::types::FuncMode::CompositeQuery] }, + "oneway" => quote! { vec![#candid::types::FuncMode::Oneway] }, + "update" => quote! { vec![] }, + _ => unreachable!(), + }; + quote! { + { + let mut args: Vec = Vec::new(); + #(#args)* + let mut rets: Vec = Vec::new(); + #(#rets)* + let func = Function { args, rets, modes: #modes }; + service.push((#name.to_string(), TypeInner::Func(func).into())); } - }, - ); + } + }); let service = quote! { use #candid::types::{CandidType, Function, Type, ArgType, TypeInner}; use #candid::types::syntax::{Binding, IDLMergedProg, IDLActorType}; let mut service = Vec::<(String, Type)>::new(); - let mut doc_comments: std::collections::HashMap> = std::collections::HashMap::new(); let mut env = #candid::types::internal::TypeContainer::new(); #(#gen_tys)* service.sort_unstable_by_key(|(name, _)| name.clone()); @@ -174,14 +145,13 @@ pub(crate) fn export_service(path: Option) -> TokenStream { let bindings = env.env.0.iter().map(|(id, t)| Binding { id: id.clone(), typ: env.as_idl_type(t), - doc_comment: doc_comments.get(id).cloned(), + doc_comment: None, }).collect::>(); let mut idl_merged_prog = IDLMergedProg::from(bindings); idl_merged_prog.set_actor(Some(IDLActorType { typ: env.as_idl_type(&actor), doc_comment: None, })); - idl_merged_prog.set_comments_in_actor(&doc_comments); let result = #candid::pretty::candid::compile(&idl_merged_prog); format!("{}", result) @@ -213,15 +183,6 @@ fn generate_ret(name: TokenStream, ty: &str) -> TokenStream { } } -fn generate_doc_comment(comment_lines: &[String]) -> TokenStream { - let comment_strings: Vec = comment_lines - .iter() - .map(|s| quote::quote! { #s.to_string() }) - .collect(); - - quote::quote! { vec![#(#comment_strings),*] } -} - fn get_args(sig: &Signature) -> Result<(ParsedArgs, ParsedRets)> { let mut args = Vec::new(); for arg in &sig.inputs { diff --git a/rust/candid_derive/src/lib.rs b/rust/candid_derive/src/lib.rs index 78b6d3031..eb26e4263 100644 --- a/rust/candid_derive/src/lib.rs +++ b/rust/candid_derive/src/lib.rs @@ -61,22 +61,6 @@ pub(crate) fn get_lit_str(expr: &syn::Expr) -> std::result::Result Vec { - attrs - .iter() - .filter_map(|attr| match &attr.meta { - syn::Meta::NameValue(m) if m.path.is_ident("doc") => { - if let Ok(lit) = get_lit_str(&m.value) { - Some(lit.value().trim().to_string()) - } else { - None - } - } - _ => None, - }) - .collect() -} - fn get_custom_candid_path(input: &syn::DeriveInput) -> Result> { let candid_path_helper_attribute_option = input .attrs From 910fd3b77312865b4d3aa468e10e95eb353d6829 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Mon, 30 Jun 2025 21:47:44 +0200 Subject: [PATCH 13/13] fix: remove variants --- rust/candid/src/types/syntax.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/rust/candid/src/types/syntax.rs b/rust/candid/src/types/syntax.rs index bcb7c6404..d237b38f1 100644 --- a/rust/candid/src/types/syntax.rs +++ b/rust/candid/src/types/syntax.rs @@ -426,7 +426,5 @@ fn set_comments_in_type(t: &IDLType, doc_comments: &HashMap> IDLType::ClassT(args, Box::new(set_comments_in_type(t, doc_comments))) } IDLType::PrincipalT => IDLType::PrincipalT, - IDLType::FutureT => IDLType::FutureT, - IDLType::UnknownT => IDLType::UnknownT, } }