enum DocstringFormat {
Google,
NumPy,
ReStructuredText,
Plain,
}
fn detect_docstring_format(docstring: &str) -> DocstringFormat {
let lines: Vec<&str> = docstring.lines().collect();
if docstring.contains(":param ")
|| docstring.contains(":returns:")
|| docstring.contains(":type ")
|| docstring.contains(":raises ")
|| docstring.contains(":rtype:")
{
return DocstringFormat::ReStructuredText;
}
for i in 0..lines.len().saturating_sub(1) {
let line = lines[i].trim();
let next_line = lines[i + 1].trim();
if !line.is_empty()
&& (next_line.chars().all(|c| c == '-') || next_line.chars().all(|c| c == '='))
&& (line.eq_ignore_ascii_case("parameters")
|| line.eq_ignore_ascii_case("returns")
|| line.eq_ignore_ascii_case("raises")
|| line.eq_ignore_ascii_case("yields")
|| line.eq_ignore_ascii_case("notes")
|| line.eq_ignore_ascii_case("attributes"))
{
return DocstringFormat::NumPy;
}
}
for line in &lines {
let trimmed = line.trim();
if trimmed == "Args:"
|| trimmed == "Arguments:"
|| trimmed == "Returns:"
|| trimmed == "Return:"
|| trimmed == "Raises:"
|| trimmed == "Yields:"
|| trimmed == "Examples:"
|| trimmed == "Example:"
|| trimmed == "Note:"
|| trimmed == "Notes:"
|| trimmed == "Warning:"
|| trimmed == "Warnings:"
|| trimmed == "Attributes:"
{
return DocstringFormat::Google;
}
}
DocstringFormat::Plain
}
pub(super) fn parse_python_docstring(docstring: &str) -> String {
if docstring.is_empty() {
return String::new();
}
let format = detect_docstring_format(docstring);
match format {
DocstringFormat::Google => parse_google_docstring(docstring),
DocstringFormat::NumPy => parse_numpy_docstring(docstring),
DocstringFormat::ReStructuredText => parse_rst_docstring(docstring),
DocstringFormat::Plain => docstring.to_string(),
}
}
fn parse_google_docstring(docstring: &str) -> String {
let lines: Vec<&str> = docstring.lines().collect();
let mut result = String::new();
let mut i = 0;
while i < lines.len() {
let trimmed = lines[i].trim();
if trimmed == "Args:"
|| trimmed == "Arguments:"
|| trimmed == "Returns:"
|| trimmed == "Return:"
|| trimmed == "Raises:"
|| trimmed == "Yields:"
|| trimmed == "Examples:"
|| trimmed == "Example:"
|| trimmed == "Note:"
|| trimmed == "Notes:"
|| trimmed == "Warning:"
|| trimmed == "Warnings:"
|| trimmed == "Attributes:"
{
break;
}
if !trimmed.is_empty() {
if !result.is_empty() {
result.push('\n');
}
result.push_str(trimmed);
}
i += 1;
}
while i < lines.len() {
let trimmed = lines[i].trim();
if trimmed.ends_with(':') && !trimmed.contains(' ') {
let section_name = if trimmed == "Args:" || trimmed == "Arguments:" {
"Parameters"
} else if trimmed == "Return:" {
"Returns"
} else if trimmed == "Example:" {
"Examples"
} else if trimmed == "Note:" {
"Notes"
} else if trimmed == "Warning:" {
"Warnings"
} else {
trimmed.trim_end_matches(':')
};
let current_section = section_name.to_string();
result.push_str("\n\n");
result.push_str(section_name);
result.push_str(":\n");
i += 1;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim();
if trimmed.ends_with(':') && !trimmed.contains(' ') {
break;
}
if !trimmed.is_empty() {
let is_list_section = current_section == "Parameters"
|| current_section == "Attributes"
|| current_section == "Raises"
|| current_section == "Yields";
if is_list_section && line.starts_with(" ") {
result.push_str("- ");
result.push_str(trimmed);
result.push('\n');
} else if !trimmed.is_empty() {
result.push_str(line);
result.push('\n');
}
}
i += 1;
}
} else {
i += 1;
}
}
result.trim().to_string()
}
fn parse_numpy_docstring(docstring: &str) -> String {
let lines: Vec<&str> = docstring.lines().collect();
let mut result = String::new();
let mut i = 0;
while i < lines.len() {
if i + 1 < lines.len() {
let line = lines[i].trim();
let next_line = lines[i + 1].trim();
if !line.is_empty()
&& (next_line.chars().all(|c| c == '-') || next_line.chars().all(|c| c == '='))
&& (line.eq_ignore_ascii_case("parameters")
|| line.eq_ignore_ascii_case("returns")
|| line.eq_ignore_ascii_case("raises")
|| line.eq_ignore_ascii_case("yields")
|| line.eq_ignore_ascii_case("notes")
|| line.eq_ignore_ascii_case("examples")
|| line.eq_ignore_ascii_case("attributes"))
{
break;
}
}
let trimmed = lines[i].trim();
if !trimmed.is_empty() {
if !result.is_empty() {
result.push('\n');
}
result.push_str(trimmed);
}
i += 1;
}
while i < lines.len() {
if i + 1 < lines.len() {
let line = lines[i].trim();
let next_line = lines[i + 1].trim();
if !line.is_empty()
&& (next_line.chars().all(|c| c == '-') || next_line.chars().all(|c| c == '='))
{
let current_section = line.to_string();
result.push_str("\n\n");
result.push_str(line);
result.push_str(":\n");
i += 2;
while i < lines.len() {
if i + 1 < lines.len() {
let content_line = lines[i].trim();
let next = lines[i + 1].trim();
if !content_line.is_empty()
&& (next.chars().all(|c| c == '-') || next.chars().all(|c| c == '='))
{
break;
}
}
let content_line = lines[i];
let trimmed = content_line.trim();
if !trimmed.is_empty() {
if (current_section.eq_ignore_ascii_case("parameters")
|| current_section.eq_ignore_ascii_case("attributes"))
&& !content_line.starts_with(" ")
&& trimmed.contains(" : ")
{
result.push_str("- ");
}
result.push_str(trimmed);
result.push('\n');
}
i += 1;
}
} else {
i += 1;
}
} else {
i += 1;
}
}
result.trim().to_string()
}
fn parse_rst_docstring(docstring: &str) -> String {
let lines: Vec<&str> = docstring.lines().collect();
let mut result = String::new();
let mut params = Vec::new();
let mut returns = Vec::new();
let mut raises = Vec::new();
let mut types = std::collections::HashMap::new();
let mut return_type = None;
let mut i = 0;
while i < lines.len() {
let trimmed = lines[i].trim();
if trimmed.starts_with(':') {
break;
}
if !trimmed.is_empty() {
if !result.is_empty() {
result.push('\n');
}
result.push_str(trimmed);
}
i += 1;
}
while i < lines.len() {
let line = lines[i].trim();
if line.starts_with(":param ") {
if let Some(rest) = line.strip_prefix(":param ") {
if let Some(colon_pos) = rest.find(':') {
let param_name = rest[..colon_pos].trim();
let description = rest[colon_pos + 1..].trim();
let mut full_desc = description.to_string();
i += 1;
while i < lines.len() {
let next_line = lines[i].trim();
if next_line.starts_with(':') || next_line.is_empty() {
break;
}
full_desc.push(' ');
full_desc.push_str(next_line);
i += 1;
}
params.push((param_name.to_string(), full_desc));
continue;
}
}
} else if line.starts_with(":type ") {
if let Some(rest) = line.strip_prefix(":type ") {
if let Some(colon_pos) = rest.find(':') {
let param_name = rest[..colon_pos].trim();
let type_info = rest[colon_pos + 1..].trim();
types.insert(param_name.to_string(), type_info.to_string());
}
}
} else if line.starts_with(":returns:") || line.starts_with(":return:") {
let prefix = if line.starts_with(":returns:") {
":returns:"
} else {
":return:"
};
let description = line.strip_prefix(prefix).unwrap_or("").trim();
let mut full_desc = description.to_string();
i += 1;
while i < lines.len() {
let next_line = lines[i].trim();
if next_line.starts_with(':') || next_line.is_empty() {
break;
}
if !full_desc.is_empty() {
full_desc.push(' ');
}
full_desc.push_str(next_line);
i += 1;
}
returns.push(full_desc);
continue;
} else if line.starts_with(":rtype:") {
let type_info = line.strip_prefix(":rtype:").unwrap_or("").trim();
return_type = Some(type_info.to_string());
} else if line.starts_with(":raises ") {
if let Some(rest) = line.strip_prefix(":raises ") {
if let Some(colon_pos) = rest.find(':') {
let exc_type = rest[..colon_pos].trim();
let description = rest[colon_pos + 1..].trim();
let mut full_desc = description.to_string();
i += 1;
while i < lines.len() {
let next_line = lines[i].trim();
if next_line.starts_with(':') || next_line.is_empty() {
break;
}
full_desc.push(' ');
full_desc.push_str(next_line);
i += 1;
}
raises.push((exc_type.to_string(), full_desc));
continue;
}
}
}
i += 1;
}
if !params.is_empty() {
result.push_str("\n\nParameters:\n");
for (name, desc) in params {
result.push_str("- ");
result.push_str(&name);
if let Some(type_info) = types.get(&name) {
result.push_str(" (");
result.push_str(type_info);
result.push(')');
}
result.push_str(": ");
result.push_str(&desc);
result.push('\n');
}
}
if !returns.is_empty() {
result.push_str("\nReturns:\n");
for ret in returns {
if let Some(rtype) = &return_type {
result.push_str("- ");
result.push_str(rtype);
result.push_str(": ");
}
result.push_str(&ret);
result.push('\n');
}
}
if !raises.is_empty() {
result.push_str("\nRaises:\n");
for (exc_type, desc) in raises {
result.push_str("- ");
result.push_str(&exc_type);
result.push_str(": ");
result.push_str(&desc);
result.push('\n');
}
}
result.trim().to_string()
}