use crate::schema::{
Deprecation, Documentation, Example, OptionEntry, Parameter, SeeReference, Throws,
TypeReference, YieldBlock,
};
#[derive(Debug, Default)]
pub struct ParsedDocComment {
pub summary: Option<String>,
pub description: Option<String>,
pub params: Vec<ParsedParam>,
pub options: Vec<ParsedOption>,
pub returns: Option<ParsedReturn>,
pub yields: Option<ParsedYield>,
pub throws: Vec<ParsedThrow>,
pub examples: Vec<ParsedExample>,
pub see: Vec<String>,
pub notes: Vec<String>,
pub todos: Vec<String>,
pub deprecated: Option<String>,
pub since: Option<String>,
pub is_private: bool,
pub tags: Vec<(String, String)>,
}
#[derive(Debug)]
pub struct ParsedParam {
pub name: String,
pub type_str: Option<String>,
pub description: Option<String>,
pub optional: bool,
pub default: Option<String>,
}
#[derive(Debug)]
pub struct ParsedOption {
pub param_name: String,
pub option_name: String,
pub type_str: Option<String>,
pub description: Option<String>,
pub required: bool,
pub default: Option<String>,
}
#[derive(Debug)]
pub struct ParsedReturn {
pub type_str: Option<String>,
pub description: Option<String>,
}
#[derive(Debug)]
pub struct ParsedYield {
pub description: Option<String>,
pub params: Vec<ParsedParam>,
pub return_type: Option<String>,
}
#[derive(Debug)]
pub struct ParsedThrow {
pub type_str: Option<String>,
pub description: Option<String>,
}
#[derive(Debug)]
pub struct ParsedExample {
pub title: Option<String>,
pub code: String,
pub language: Option<String>,
}
impl ParsedDocComment {
pub fn to_documentation(&self) -> Documentation {
Documentation {
summary: self.summary.clone(),
description: self.description.clone(),
examples: self
.examples
.iter()
.map(|e| Example {
title: e.title.clone(),
code: e.code.clone(),
language: e.language.clone(),
})
.collect(),
see: self
.see
.iter()
.map(|s| SeeReference {
text: s.clone(),
reference: None,
})
.collect(),
notes: self.notes.clone(),
todos: self.todos.clone(),
deprecated: self.deprecated.as_ref().map(|msg| Deprecation {
message: Some(msg.clone()),
since: None,
replacement: None,
}),
since: self.since.clone(),
tags: self.tags.iter().cloned().collect(),
}
}
pub fn to_parameters(&self) -> Vec<Parameter> {
self.params
.iter()
.map(|p| {
let options: Vec<OptionEntry> = self
.options
.iter()
.filter(|o| o.param_name == p.name)
.map(|o| OptionEntry {
name: o.option_name.clone(),
option_type: o.type_str.as_ref().map(|t| parse_type_string(t)),
description: o.description.clone(),
required: Some(o.required),
default: o.default.clone(),
})
.collect();
Parameter {
name: p.name.clone(),
param_type: p.type_str.as_ref().map(|t| parse_type_string(t)),
description: p.description.clone(),
default: p.default.clone(),
optional: if p.optional { Some(true) } else { None },
rest: None,
options,
}
})
.collect()
}
pub fn to_throws(&self) -> Vec<Throws> {
self.throws
.iter()
.map(|t| Throws {
throw_type: t.type_str.as_ref().map(|s| parse_type_string(s)),
description: t.description.clone(),
})
.collect()
}
pub fn to_yield_block(&self) -> Option<YieldBlock> {
self.yields.as_ref().map(|y| YieldBlock {
description: y.description.clone(),
params: y
.params
.iter()
.map(|p| Parameter {
name: p.name.clone(),
param_type: p.type_str.as_ref().map(|t| parse_type_string(t)),
description: p.description.clone(),
default: None,
optional: None,
rest: None,
options: Vec::new(),
})
.collect(),
returns: y.return_type.as_ref().map(|t| parse_type_string(t)),
})
}
pub fn return_type(&self) -> Option<TypeReference> {
self.returns
.as_ref()
.and_then(|r| r.type_str.as_ref())
.map(|t| parse_type_string(t))
}
pub fn return_description(&self) -> Option<String> {
self.returns.as_ref().and_then(|r| r.description.clone())
}
}
pub fn parse_yardoc(comment: &str) -> ParsedDocComment {
let mut doc = ParsedDocComment::default();
let lines: Vec<&str> = comment.lines().collect();
let mut current_tag: Option<&str> = None;
let mut current_content = String::new();
let mut description_lines = Vec::new();
let mut in_example = false;
let mut example_title: Option<String> = None;
let mut example_code = String::new();
for line in lines {
let line = line.trim();
let line = line
.trim_start_matches('#')
.trim_start_matches('*')
.trim_start();
if let Some(after_at) = line.strip_prefix('@') {
if in_example {
doc.examples.push(ParsedExample {
title: example_title.take(),
code: example_code.trim().to_string(),
language: Some("ruby".to_string()),
});
example_code.clear();
in_example = false;
}
if let Some(tag) = current_tag {
process_yardoc_tag(&mut doc, tag, ¤t_content);
}
let tag_end = after_at
.find(|c: char| c.is_whitespace())
.unwrap_or(after_at.len());
let tag = &after_at[..tag_end];
let content = after_at[tag_end..].trim();
if tag == "example" {
in_example = true;
example_title = if content.is_empty() {
None
} else {
Some(content.to_string())
};
current_tag = None;
} else {
current_tag = Some(tag);
current_content = content.to_string();
}
} else if in_example {
example_code.push_str(line);
example_code.push('\n');
} else if current_tag.is_some() {
if !current_content.is_empty() {
current_content.push(' ');
}
current_content.push_str(line);
} else if !line.is_empty() {
description_lines.push(line);
}
}
if in_example && !example_code.is_empty() {
doc.examples.push(ParsedExample {
title: example_title,
code: example_code.trim().to_string(),
language: Some("ruby".to_string()),
});
}
if let Some(tag) = current_tag {
process_yardoc_tag(&mut doc, tag, ¤t_content);
}
if !description_lines.is_empty() {
let full_desc = description_lines.join(" ");
let first_sentence_end = full_desc
.find(". ")
.or_else(|| full_desc.find(".\n"))
.map(|i| i + 1);
if let Some(end) = first_sentence_end {
doc.summary = Some(full_desc[..end].trim().to_string());
if end < full_desc.len() {
doc.description = Some(full_desc.trim().to_string());
}
} else {
doc.summary = Some(full_desc.trim().to_string());
}
}
doc
}
fn process_yardoc_tag(doc: &mut ParsedDocComment, tag: &str, content: &str) {
match tag {
"param" => {
if let Some(param) = parse_yardoc_param(content) {
doc.params.push(param);
}
}
"option" => {
if let Some(opt) = parse_yardoc_option(content) {
doc.options.push(opt);
}
}
"return" | "returns" => {
doc.returns = Some(parse_yardoc_return(content));
}
"yield" => {
let yield_doc = doc.yields.get_or_insert(ParsedYield {
description: None,
params: Vec::new(),
return_type: None,
});
yield_doc.description = Some(content.to_string());
}
"yieldparam" => {
if let Some(param) = parse_yardoc_param(content) {
let yield_doc = doc.yields.get_or_insert(ParsedYield {
description: None,
params: Vec::new(),
return_type: None,
});
yield_doc.params.push(param);
}
}
"yieldreturn" => {
let yield_doc = doc.yields.get_or_insert(ParsedYield {
description: None,
params: Vec::new(),
return_type: None,
});
let ret = parse_yardoc_return(content);
yield_doc.return_type = ret.type_str;
}
"raise" | "raises" | "throw" | "throws" => {
doc.throws.push(parse_yardoc_throw(content));
}
"see" => {
doc.see.push(content.to_string());
}
"note" => {
doc.notes.push(content.to_string());
}
"todo" => {
doc.todos.push(content.to_string());
}
"deprecated" => {
doc.deprecated = Some(content.to_string());
}
"since" => {
doc.since = Some(content.to_string());
}
"private" => {
doc.is_private = true;
}
"api" if content == "private" => {
doc.is_private = true;
}
_ => {
doc.tags.push((tag.to_string(), content.to_string()));
}
}
}
fn parse_yardoc_param(content: &str) -> Option<ParsedParam> {
let content = content.trim();
let (type_str, rest) = if content.starts_with('[') {
let end = content.find(']')?;
(Some(content[1..end].to_string()), content[end + 1..].trim())
} else {
(None, content)
};
let mut parts = rest.splitn(2, char::is_whitespace);
let name = parts.next()?.to_string();
let description = parts.next().map(|s| s.trim().to_string());
Some(ParsedParam {
name,
type_str,
description,
optional: false,
default: None,
})
}
fn parse_yardoc_option(content: &str) -> Option<ParsedOption> {
let content = content.trim();
let mut parts = content.splitn(2, char::is_whitespace);
let param_name = parts.next()?.to_string();
let rest = parts.next()?.trim();
let (type_str, rest) = if rest.starts_with('[') {
let end = rest.find(']')?;
(Some(rest[1..end].to_string()), rest[end + 1..].trim())
} else {
(None, rest)
};
let mut parts = rest.splitn(2, char::is_whitespace);
let option_name = parts.next()?.trim_start_matches(':').to_string();
let description = parts.next().map(|s| s.trim().to_string());
Some(ParsedOption {
param_name,
option_name,
type_str,
description,
required: false,
default: None,
})
}
fn parse_yardoc_return(content: &str) -> ParsedReturn {
let content = content.trim();
let (type_str, description) = if content.starts_with('[') {
if let Some(end) = content.find(']') {
let type_s = content[1..end].to_string();
let desc = content[end + 1..].trim();
(
Some(type_s),
if desc.is_empty() {
None
} else {
Some(desc.to_string())
},
)
} else {
(None, Some(content.to_string()))
}
} else {
(None, Some(content.to_string()))
};
ParsedReturn {
type_str,
description,
}
}
fn parse_yardoc_throw(content: &str) -> ParsedThrow {
let content = content.trim();
let (type_str, description) = if content.starts_with('[') {
if let Some(end) = content.find(']') {
let type_s = content[1..end].to_string();
let desc = content[end + 1..].trim();
(
Some(type_s),
if desc.is_empty() {
None
} else {
Some(desc.to_string())
},
)
} else {
(None, Some(content.to_string()))
}
} else {
let mut parts = content.splitn(2, char::is_whitespace);
let first = parts.next().unwrap_or("");
let rest = parts.next();
if first
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
{
(Some(first.to_string()), rest.map(|s| s.to_string()))
} else {
(None, Some(content.to_string()))
}
};
ParsedThrow {
type_str,
description,
}
}
pub fn parse_jsdoc(comment: &str) -> ParsedDocComment {
let mut doc = ParsedDocComment::default();
let lines: Vec<&str> = comment.lines().collect();
let mut current_tag: Option<&str> = None;
let mut current_content = String::new();
let mut description_lines = Vec::new();
let mut in_example = false;
let mut example_code = String::new();
for line in lines {
let line = line.trim();
let line = line
.trim_start_matches("/**")
.trim_start_matches("*/")
.trim_start_matches('*')
.trim_start_matches("//")
.trim_start();
if line.is_empty() {
continue;
}
if let Some(after_at) = line.strip_prefix('@') {
if in_example {
doc.examples.push(ParsedExample {
title: None,
code: example_code.trim().to_string(),
language: Some("typescript".to_string()),
});
example_code.clear();
in_example = false;
}
if let Some(tag) = current_tag {
process_jsdoc_tag(&mut doc, tag, ¤t_content);
}
let tag_end = after_at
.find(|c: char| c.is_whitespace())
.unwrap_or(after_at.len());
let tag = &after_at[..tag_end];
let content = after_at[tag_end..].trim();
if tag == "example" {
in_example = true;
current_tag = None;
} else {
current_tag = Some(tag);
current_content = content.to_string();
}
} else if in_example {
let code_line = line.trim_start_matches("```").trim_end_matches("```");
if !code_line.starts_with("typescript")
&& !code_line.starts_with("javascript")
&& !code_line.starts_with("ts")
&& !code_line.starts_with("js")
&& !code_line.is_empty()
{
example_code.push_str(code_line);
example_code.push('\n');
}
} else if current_tag.is_some() {
if !current_content.is_empty() {
current_content.push(' ');
}
current_content.push_str(line);
} else {
description_lines.push(line);
}
}
if in_example && !example_code.is_empty() {
doc.examples.push(ParsedExample {
title: None,
code: example_code.trim().to_string(),
language: Some("typescript".to_string()),
});
}
if let Some(tag) = current_tag {
process_jsdoc_tag(&mut doc, tag, ¤t_content);
}
if !description_lines.is_empty() {
let full_desc = description_lines.join(" ");
let first_sentence_end = full_desc.find(". ").or_else(|| full_desc.find(".\n"));
if let Some(end) = first_sentence_end {
doc.summary = Some(full_desc[..=end].trim().to_string());
doc.description = Some(full_desc.trim().to_string());
} else {
doc.summary = Some(full_desc.trim().to_string());
}
}
doc
}
fn process_jsdoc_tag(doc: &mut ParsedDocComment, tag: &str, content: &str) {
match tag {
"param" | "arg" | "argument" => {
if let Some(param) = parse_jsdoc_param(content) {
doc.params.push(param);
}
}
"returns" | "return" => {
doc.returns = Some(parse_jsdoc_return(content));
}
"throws" | "throw" | "exception" => {
doc.throws.push(parse_jsdoc_throw(content));
}
"see" => {
doc.see.push(content.to_string());
}
"deprecated" => {
doc.deprecated = Some(if content.is_empty() {
"Deprecated".to_string()
} else {
content.to_string()
});
}
"since" | "version" => {
doc.since = Some(content.to_string());
}
"todo" => {
doc.todos.push(content.to_string());
}
"private" | "internal" => {
doc.is_private = true;
}
_ => {
doc.tags.push((tag.to_string(), content.to_string()));
}
}
}
fn parse_jsdoc_param(content: &str) -> Option<ParsedParam> {
let content = content.trim();
let (type_str, rest) = if content.starts_with('{') {
let end = content.find('}')?;
(Some(content[1..end].to_string()), content[end + 1..].trim())
} else {
(None, content)
};
let (name, optional, default, rest) = if rest.starts_with('[') {
let end = rest.find(']')?;
let inner = &rest[1..end];
let rest = rest[end + 1..].trim();
if let Some(eq_pos) = inner.find('=') {
let name = inner[..eq_pos].trim().to_string();
let default = inner[eq_pos + 1..].trim().to_string();
(name, true, Some(default), rest)
} else {
(inner.trim().to_string(), true, None, rest)
}
} else {
let mut parts = rest.splitn(2, |c: char| c.is_whitespace() || c == '-');
let name = parts.next()?.trim().to_string();
let rest = parts.next().unwrap_or("").trim_start_matches('-').trim();
(name, false, None, rest)
};
let description = if rest.is_empty() {
None
} else {
Some(rest.trim_start_matches('-').trim().to_string())
};
Some(ParsedParam {
name,
type_str,
description,
optional,
default,
})
}
fn parse_jsdoc_return(content: &str) -> ParsedReturn {
let content = content.trim();
let (type_str, description) = if content.starts_with('{') {
if let Some(end) = content.find('}') {
let type_s = content[1..end].to_string();
let desc = content[end + 1..].trim();
(
Some(type_s),
if desc.is_empty() {
None
} else {
Some(desc.to_string())
},
)
} else {
(None, Some(content.to_string()))
}
} else {
(None, Some(content.to_string()))
};
ParsedReturn {
type_str,
description,
}
}
fn parse_jsdoc_throw(content: &str) -> ParsedThrow {
let content = content.trim();
let (type_str, description) = if content.starts_with('{') {
if let Some(end) = content.find('}') {
let type_s = content[1..end].to_string();
let desc = content[end + 1..].trim();
(
Some(type_s),
if desc.is_empty() {
None
} else {
Some(desc.to_string())
},
)
} else {
(None, Some(content.to_string()))
}
} else {
(None, Some(content.to_string()))
};
ParsedThrow {
type_str,
description,
}
}
fn split_type_string(s: &str, delimiter: char) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut depth = 0;
for c in s.chars() {
match c {
'<' | '(' | '[' | '{' => {
depth += 1;
current.push(c);
}
'>' | ')' | ']' | '}' => {
depth -= 1;
current.push(c);
}
c if c == delimiter && depth == 0 => {
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
current = String::new();
}
_ => current.push(c),
}
}
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
parts
}
pub fn parse_type_string(type_str: &str) -> TypeReference {
let type_str = type_str.trim();
if type_str.is_empty() {
return TypeReference::simple("unknown");
}
let union_parts = split_type_string(type_str, '|');
if union_parts.len() > 1 {
let members: Vec<TypeReference> =
union_parts.iter().map(|s| parse_type_string(s)).collect();
return TypeReference::union(members);
}
if let Some(lt_pos) = type_str.find('<') {
if let Some(gt_pos) = type_str.rfind('>') {
if lt_pos < gt_pos {
let name = type_str[..lt_pos].trim().to_string();
if !name.is_empty() && !name.contains(' ') {
let args_str = &type_str[lt_pos + 1..gt_pos];
let arg_parts = split_type_string(args_str, ',');
let args: Vec<TypeReference> =
arg_parts.iter().map(|s| parse_type_string(s)).collect();
return TypeReference::generic(name, args);
}
}
}
}
if let Some(inner_type) = type_str.strip_suffix("[]") {
let inner = parse_type_string(inner_type);
return TypeReference::generic("Array", vec![inner]);
}
if let Some(inner_type) = type_str.strip_suffix('?') {
let inner = parse_type_string(inner_type);
return inner.nullable();
}
TypeReference::simple(type_str)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::TypeKind;
#[test]
fn test_parse_type_simple() {
let t = parse_type_string("string");
assert_eq!(t.kind, TypeKind::Simple);
assert_eq!(t.name, "string");
}
#[test]
fn test_parse_type_simple_number() {
let t = parse_type_string("number");
assert_eq!(t.kind, TypeKind::Simple);
assert_eq!(t.name, "number");
}
#[test]
fn test_parse_type_simple_boolean() {
let t = parse_type_string("boolean");
assert_eq!(t.kind, TypeKind::Simple);
assert_eq!(t.name, "boolean");
}
#[test]
fn test_parse_type_simple_nil() {
let t = parse_type_string("nil");
assert_eq!(t.kind, TypeKind::Simple);
assert_eq!(t.name, "nil");
}
#[test]
fn test_parse_type_empty_returns_unknown() {
let t = parse_type_string("");
assert_eq!(t.kind, TypeKind::Simple);
assert_eq!(t.name, "unknown");
}
#[test]
fn test_parse_type_whitespace_only() {
let t = parse_type_string(" ");
assert_eq!(t.name, "unknown");
}
#[test]
fn test_parse_type_string_union() {
let t = parse_type_string("String | nil");
assert_eq!(t.kind, TypeKind::Union);
assert_eq!(t.members.len(), 2);
assert_eq!(t.members[0].name, "String");
assert_eq!(t.members[1].name, "nil");
}
#[test]
fn test_parse_type_string_triple_union() {
let t = parse_type_string("string | number | boolean");
assert_eq!(t.kind, TypeKind::Union);
assert_eq!(t.members.len(), 3);
}
#[test]
fn test_parse_type_string_generic() {
let t = parse_type_string("Array<String>");
assert_eq!(t.kind, TypeKind::Generic);
assert_eq!(t.name, "Array");
assert_eq!(t.args.len(), 1);
assert_eq!(t.args[0].name, "String");
}
#[test]
fn test_parse_type_string_generic_multiple_args() {
let t = parse_type_string("Map<string, number>");
assert_eq!(t.kind, TypeKind::Generic);
assert_eq!(t.name, "Map");
assert_eq!(t.args.len(), 2);
assert_eq!(t.args[0].name, "string");
assert_eq!(t.args[1].name, "number");
}
#[test]
fn test_parse_type_string_nested_generic() {
let t = parse_type_string("Map<string, Array<number>>");
assert_eq!(t.kind, TypeKind::Generic);
assert_eq!(t.name, "Map");
assert_eq!(t.args.len(), 2);
assert_eq!(t.args[0].name, "string");
assert_eq!(t.args[1].kind, TypeKind::Generic);
assert_eq!(t.args[1].name, "Array");
}
#[test]
fn test_parse_type_string_array_shorthand() {
let t = parse_type_string("string[]");
assert_eq!(t.kind, TypeKind::Generic);
assert_eq!(t.name, "Array");
assert_eq!(t.args.len(), 1);
assert_eq!(t.args[0].name, "string");
}
#[test]
fn test_parse_type_string_nullable() {
let t = parse_type_string("string?");
assert!(t.nullable.unwrap_or(false));
}
#[test]
fn test_parse_type_string_promise() {
let t = parse_type_string("Promise<string>");
assert_eq!(t.kind, TypeKind::Generic);
assert_eq!(t.name, "Promise");
assert_eq!(t.args[0].name, "string");
}
#[test]
fn test_parse_type_string_complex_union_with_generics() {
let t = parse_type_string("Array<string> | Map<string, number> | null");
assert_eq!(t.kind, TypeKind::Union);
assert_eq!(t.members.len(), 3);
assert_eq!(t.members[0].kind, TypeKind::Generic);
assert_eq!(t.members[1].kind, TypeKind::Generic);
assert_eq!(t.members[2].name, "null");
}
#[test]
fn test_parse_type_string_deeply_nested() {
let t = parse_type_string("Promise<Result<Array<Item>, Error>>");
assert_eq!(t.kind, TypeKind::Generic);
assert_eq!(t.name, "Promise");
assert_eq!(t.args.len(), 1);
assert_eq!(t.args[0].kind, TypeKind::Generic);
assert_eq!(t.args[0].name, "Result");
}
#[test]
fn test_split_type_string_respects_brackets() {
let parts = split_type_string("Array<A, B> | Map<C, D>", '|');
assert_eq!(parts.len(), 2);
assert_eq!(parts[0], "Array<A, B>");
assert_eq!(parts[1], "Map<C, D>");
}
#[test]
fn test_split_type_string_comma() {
let parts = split_type_string("string, number, boolean", ',');
assert_eq!(parts.len(), 3);
}
#[test]
fn test_split_type_string_nested_commas() {
let parts = split_type_string("Map<string, number>, Array<boolean>", ',');
assert_eq!(parts.len(), 2);
assert_eq!(parts[0], "Map<string, number>");
assert_eq!(parts[1], "Array<boolean>");
}
#[test]
fn test_parse_yardoc_param() {
let param = parse_yardoc_param("[String] name The user's name").unwrap();
assert_eq!(param.name, "name");
assert_eq!(param.type_str, Some("String".to_string()));
assert_eq!(param.description, Some("The user's name".to_string()));
}
#[test]
fn test_parse_yardoc_param_without_type() {
let param = parse_yardoc_param("name The user's name").unwrap();
assert_eq!(param.name, "name");
assert_eq!(param.type_str, None);
assert_eq!(param.description, Some("The user's name".to_string()));
}
#[test]
fn test_parse_yardoc_param_union_type() {
let param = parse_yardoc_param("[String, nil] name The name or nil").unwrap();
assert_eq!(param.name, "name");
assert_eq!(param.type_str, Some("String, nil".to_string()));
}
#[test]
fn test_parse_yardoc_option() {
let opt = parse_yardoc_option("options [Integer] :timeout The timeout").unwrap();
assert_eq!(opt.param_name, "options");
assert_eq!(opt.option_name, "timeout");
assert_eq!(opt.type_str, Some("Integer".to_string()));
assert_eq!(opt.description, Some("The timeout".to_string()));
}
#[test]
fn test_parse_yardoc_return() {
let ret = parse_yardoc_return("[String] The result");
assert_eq!(ret.type_str, Some("String".to_string()));
assert_eq!(ret.description, Some("The result".to_string()));
}
#[test]
fn test_parse_yardoc_return_no_type() {
let ret = parse_yardoc_return("The result value");
assert_eq!(ret.type_str, None);
assert_eq!(ret.description, Some("The result value".to_string()));
}
#[test]
fn test_parse_yardoc_throw_with_type() {
let th = parse_yardoc_throw("[ArgumentError] if value is invalid");
assert_eq!(th.type_str, Some("ArgumentError".to_string()));
assert_eq!(th.description, Some("if value is invalid".to_string()));
}
#[test]
fn test_parse_yardoc_throw_capitalized() {
let th = parse_yardoc_throw("RuntimeError when something fails");
assert_eq!(th.type_str, Some("RuntimeError".to_string()));
assert_eq!(th.description, Some("when something fails".to_string()));
}
#[test]
fn test_parse_yardoc_full_comment() {
let comment = r#"
# A test method.
#
# This method does something important.
#
# @param name [String] the name
# @param count [Integer] the count
# @return [Boolean] whether it succeeded
# @raise [ArgumentError] if name is empty
# @example Basic usage
# test_method("foo", 5)
# @see #other_method
# @note This is important
# @todo Add better error handling
# @todo Support more types
# @deprecated Use new_method instead
# @since 1.0.0
"#;
let doc = parse_yardoc(comment);
assert_eq!(doc.summary, Some("A test method.".to_string()));
assert!(doc.description.unwrap().contains("important"));
assert_eq!(doc.params.len(), 2);
assert_eq!(doc.params[0].name, "name");
assert_eq!(doc.params[1].name, "count");
assert!(doc.returns.is_some());
assert_eq!(doc.throws.len(), 1);
assert_eq!(doc.examples.len(), 1);
assert_eq!(doc.see.len(), 1);
assert_eq!(doc.notes.len(), 1);
assert_eq!(doc.todos.len(), 2);
assert_eq!(doc.todos[0], "Add better error handling");
assert_eq!(doc.todos[1], "Support more types");
assert!(doc.deprecated.is_some());
assert_eq!(doc.since, Some("1.0.0".to_string()));
}
#[test]
fn test_parse_yardoc_yield() {
let comment = r#"
# @yield [item, index] Yields each item
# @yieldparam item [Object] the item
# @yieldparam index [Integer] the index
# @yieldreturn [Boolean] whether to continue
"#;
let doc = parse_yardoc(comment);
let yields = doc.yields.unwrap();
assert!(yields.description.is_some());
assert_eq!(yields.params.len(), 2);
assert!(yields.return_type.is_some());
}
#[test]
fn test_parse_jsdoc_param() {
let param = parse_jsdoc_param("{string} name - The user's name").unwrap();
assert_eq!(param.name, "name");
assert_eq!(param.type_str, Some("string".to_string()));
assert_eq!(param.description, Some("The user's name".to_string()));
}
#[test]
fn test_parse_jsdoc_param_optional() {
let param = parse_jsdoc_param("{string} [name] - Optional name").unwrap();
assert_eq!(param.name, "name");
assert!(param.optional);
assert_eq!(param.default, None);
}
#[test]
fn test_parse_jsdoc_param_with_default() {
let param = parse_jsdoc_param("{number} [count=0] - The count").unwrap();
assert_eq!(param.name, "count");
assert!(param.optional);
assert_eq!(param.default, Some("0".to_string()));
}
#[test]
fn test_parse_jsdoc_param_without_type() {
let param = parse_jsdoc_param("name - The user's name").unwrap();
assert_eq!(param.name, "name");
assert_eq!(param.type_str, None);
}
#[test]
fn test_parse_jsdoc_return() {
let ret = parse_jsdoc_return("{string} The result");
assert_eq!(ret.type_str, Some("string".to_string()));
assert_eq!(ret.description, Some("The result".to_string()));
}
#[test]
fn test_parse_jsdoc_throw() {
let th = parse_jsdoc_throw("{Error} If something fails");
assert_eq!(th.type_str, Some("Error".to_string()));
assert_eq!(th.description, Some("If something fails".to_string()));
}
#[test]
fn test_parse_jsdoc_full_comment() {
let comment = r#"
/**
* A test function.
*
* This function does something important.
*
* @param {string} name - The name
* @param {number} count - The count
* @returns {boolean} Whether it succeeded
* @throws {Error} If something fails
* @example
* testFunction("foo", 5);
* @see otherFunction
* @todo Implement caching
* @todo Add validation
* @deprecated Use newFunction instead
* @since 2.0.0
*/
"#;
let doc = parse_jsdoc(comment);
assert_eq!(doc.summary, Some("A test function.".to_string()));
assert_eq!(doc.params.len(), 2);
assert!(doc.returns.is_some());
assert_eq!(doc.throws.len(), 1);
assert_eq!(doc.examples.len(), 1);
assert_eq!(doc.see.len(), 1);
assert_eq!(doc.todos.len(), 2);
assert_eq!(doc.todos[0], "Implement caching");
assert_eq!(doc.todos[1], "Add validation");
assert!(doc.deprecated.is_some());
assert_eq!(doc.since, Some("2.0.0".to_string()));
}
#[test]
fn test_parse_jsdoc_private() {
let comment = "/** @private */";
let doc = parse_jsdoc(comment);
assert!(doc.is_private);
}
#[test]
fn test_parse_jsdoc_internal() {
let comment = "/** @internal */";
let doc = parse_jsdoc(comment);
assert!(doc.is_private);
}
#[test]
fn test_to_documentation() {
let mut doc = ParsedDocComment::default();
doc.summary = Some("Test summary".to_string());
doc.deprecated = Some("Use other".to_string());
doc.since = Some("1.0".to_string());
let documentation = doc.to_documentation();
assert_eq!(documentation.summary, Some("Test summary".to_string()));
assert!(documentation.deprecated.is_some());
assert_eq!(documentation.since, Some("1.0".to_string()));
}
#[test]
fn test_to_parameters() {
let mut doc = ParsedDocComment::default();
doc.params.push(ParsedParam {
name: "foo".to_string(),
type_str: Some("string".to_string()),
description: Some("A param".to_string()),
optional: true,
default: Some("bar".to_string()),
});
doc.options.push(ParsedOption {
param_name: "foo".to_string(),
option_name: "opt".to_string(),
type_str: Some("number".to_string()),
description: Some("An option".to_string()),
required: false,
default: None,
});
let params = doc.to_parameters();
assert_eq!(params.len(), 1);
assert_eq!(params[0].name, "foo");
assert_eq!(params[0].optional, Some(true));
assert_eq!(params[0].default, Some("bar".to_string()));
assert_eq!(params[0].options.len(), 1);
assert_eq!(params[0].options[0].name, "opt");
}
#[test]
fn test_to_throws() {
let mut doc = ParsedDocComment::default();
doc.throws.push(ParsedThrow {
type_str: Some("Error".to_string()),
description: Some("Something bad".to_string()),
});
let throws = doc.to_throws();
assert_eq!(throws.len(), 1);
assert!(throws[0].throw_type.is_some());
assert_eq!(throws[0].description, Some("Something bad".to_string()));
}
#[test]
fn test_to_yield_block() {
let mut doc = ParsedDocComment::default();
doc.yields = Some(ParsedYield {
description: Some("Yields items".to_string()),
params: vec![ParsedParam {
name: "item".to_string(),
type_str: Some("T".to_string()),
description: None,
optional: false,
default: None,
}],
return_type: Some("boolean".to_string()),
});
let yb = doc.to_yield_block();
assert!(yb.is_some());
let yb = yb.unwrap();
assert_eq!(yb.description, Some("Yields items".to_string()));
assert_eq!(yb.params.len(), 1);
assert!(yb.returns.is_some());
}
}