use crate::language::Language;
#[derive(Debug, Clone, PartialEq)]
pub struct DocFormat {
pub prefix: &'static str,
pub line_prefix: &'static str,
pub suffix: &'static str,
pub position: InsertionPosition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InsertionPosition {
BeforeFunction,
InsideBody,
}
pub fn doc_format_for(language: Language) -> DocFormat {
let tag = language
.try_def()
.map(|d| d.doc_format)
.unwrap_or("default");
doc_format_from_tag(tag)
}
fn doc_format_from_tag(tag: &str) -> DocFormat {
match tag {
"triple_slash" => DocFormat {
prefix: "",
line_prefix: "/// ",
suffix: "",
position: InsertionPosition::BeforeFunction,
},
"python_docstring" => DocFormat {
prefix: "\"\"\"",
line_prefix: "",
suffix: "\"\"\"",
position: InsertionPosition::InsideBody,
},
"go_comment" => DocFormat {
prefix: "",
line_prefix: "// ",
suffix: "",
position: InsertionPosition::BeforeFunction,
},
"javadoc" => DocFormat {
prefix: "/**",
line_prefix: " * ",
suffix: " */",
position: InsertionPosition::BeforeFunction,
},
"hash_comment" => DocFormat {
prefix: "",
line_prefix: "# ",
suffix: "",
position: InsertionPosition::BeforeFunction,
},
"elixir_doc" => DocFormat {
prefix: "@doc \"\"\"",
line_prefix: "",
suffix: "\"\"\"",
position: InsertionPosition::BeforeFunction,
},
"lua_ldoc" => DocFormat {
prefix: "",
line_prefix: "--- ",
suffix: "",
position: InsertionPosition::BeforeFunction,
},
"haskell_haddock" => DocFormat {
prefix: "",
line_prefix: "-- | ",
suffix: "",
position: InsertionPosition::BeforeFunction,
},
"ocaml_doc" => DocFormat {
prefix: "(** ",
line_prefix: "",
suffix: " *)",
position: InsertionPosition::BeforeFunction,
},
"erlang_edoc" => DocFormat {
prefix: "",
line_prefix: "%% ",
suffix: "",
position: InsertionPosition::BeforeFunction,
},
"r_roxygen" => DocFormat {
prefix: "",
line_prefix: "#' ",
suffix: "",
position: InsertionPosition::BeforeFunction,
},
"block_comment" => DocFormat {
prefix: "(*",
line_prefix: "",
suffix: "*)",
position: InsertionPosition::BeforeFunction,
},
_ => DocFormat {
prefix: "",
line_prefix: "// ",
suffix: "",
position: InsertionPosition::BeforeFunction,
},
}
}
pub fn format_doc_comment(text: &str, language: Language, indent: &str, func_name: &str) -> String {
let _span = tracing::debug_span!("format_doc_comment", func_name, ?language).entered();
let format = doc_format_for(language);
let lines: Vec<&str> = text.lines().collect();
if lines.is_empty() {
return String::new();
}
let mut result = String::new();
let go_first_line: String;
let effective_lines: Vec<&str> = if language == Language::Go {
if let Some(&first) = lines.first() {
go_first_line = format!("{func_name} {first}");
let mut v = vec![go_first_line.as_str()];
v.extend_from_slice(&lines[1..]);
v
} else {
lines.clone()
}
} else {
lines.clone()
};
if !format.prefix.is_empty() {
result.push_str(indent);
result.push_str(format.prefix);
result.push('\n');
}
for line in &effective_lines {
result.push_str(indent);
result.push_str(format.line_prefix);
result.push_str(line);
result.push('\n');
}
if !format.suffix.is_empty() {
result.push_str(indent);
result.push_str(format.suffix);
result.push('\n');
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rust_format() {
let fmt = doc_format_for(Language::Rust);
assert_eq!(fmt.prefix, "");
assert_eq!(fmt.line_prefix, "/// ");
assert_eq!(fmt.suffix, "");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_python_format() {
let fmt = doc_format_for(Language::Python);
assert_eq!(fmt.prefix, "\"\"\"");
assert_eq!(fmt.line_prefix, "");
assert_eq!(fmt.suffix, "\"\"\"");
assert_eq!(fmt.position, InsertionPosition::InsideBody);
}
#[test]
fn test_go_format() {
let fmt = doc_format_for(Language::Go);
assert_eq!(fmt.prefix, "");
assert_eq!(fmt.line_prefix, "// ");
assert_eq!(fmt.suffix, "");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_java_format() {
let fmt = doc_format_for(Language::Java);
assert_eq!(fmt.prefix, "/**");
assert_eq!(fmt.line_prefix, " * ");
assert_eq!(fmt.suffix, " */");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_typescript_format() {
let fmt = doc_format_for(Language::TypeScript);
assert_eq!(fmt.prefix, "/**");
assert_eq!(fmt.line_prefix, " * ");
assert_eq!(fmt.suffix, " */");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_fsharp_format() {
let fmt = doc_format_for(Language::FSharp);
assert_eq!(fmt.prefix, "");
assert_eq!(fmt.line_prefix, "/// ");
assert_eq!(fmt.suffix, "");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_ruby_format() {
let fmt = doc_format_for(Language::Ruby);
assert_eq!(fmt.prefix, "");
assert_eq!(fmt.line_prefix, "# ");
assert_eq!(fmt.suffix, "");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_elixir_format() {
let fmt = doc_format_for(Language::Elixir);
assert_eq!(fmt.prefix, "@doc \"\"\"");
assert_eq!(fmt.line_prefix, "");
assert_eq!(fmt.suffix, "\"\"\"");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_lua_format() {
let fmt = doc_format_for(Language::Lua);
assert_eq!(fmt.prefix, "");
assert_eq!(fmt.line_prefix, "--- ");
assert_eq!(fmt.suffix, "");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_haskell_format() {
let fmt = doc_format_for(Language::Haskell);
assert_eq!(fmt.prefix, "");
assert_eq!(fmt.line_prefix, "-- | ");
assert_eq!(fmt.suffix, "");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_ocaml_format() {
let fmt = doc_format_for(Language::OCaml);
assert_eq!(fmt.prefix, "(** ");
assert_eq!(fmt.line_prefix, "");
assert_eq!(fmt.suffix, " *)");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_default_format_for_unknown_language() {
let fmt = doc_format_for(Language::Bash);
assert_eq!(fmt.prefix, "");
assert_eq!(fmt.line_prefix, "// ");
assert_eq!(fmt.suffix, "");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_format_rust_single_line() {
let result =
format_doc_comment("Returns the sum of two numbers.", Language::Rust, "", "add");
assert_eq!(result, "/// Returns the sum of two numbers.\n");
}
#[test]
fn test_format_rust_multiline() {
let text = "Returns the sum of two numbers.\n\nPanics if overflow occurs.";
let result = format_doc_comment(text, Language::Rust, "", "add");
assert_eq!(
result,
"/// Returns the sum of two numbers.\n/// \n/// Panics if overflow occurs.\n"
);
}
#[test]
fn test_format_rust_with_indent() {
let result =
format_doc_comment("Does something useful.", Language::Rust, " ", "process");
assert_eq!(result, " /// Does something useful.\n");
}
#[test]
fn test_format_python_single_line() {
let result = format_doc_comment(
"Returns the sum of two numbers.",
Language::Python,
" ",
"add",
);
assert_eq!(
result,
" \"\"\"\n Returns the sum of two numbers.\n \"\"\"\n"
);
}
#[test]
fn test_format_python_multiline() {
let text = "Calculate the sum.\n\nArgs:\n a: First number.\n b: Second number.";
let result = format_doc_comment(text, Language::Python, " ", "add");
let expected = " \"\"\"\n Calculate the sum.\n \n Args:\n a: First number.\n b: Second number.\n \"\"\"\n";
assert_eq!(result, expected);
}
#[test]
fn test_format_go_prepends_func_name() {
let result = format_doc_comment("returns the sum of two numbers.", Language::Go, "", "Add");
assert_eq!(result, "// Add returns the sum of two numbers.\n");
}
#[test]
fn test_format_go_multiline() {
let text = "returns the sum of two numbers.\n\nIt panics on overflow.";
let result = format_doc_comment(text, Language::Go, "", "Add");
assert_eq!(
result,
"// Add returns the sum of two numbers.\n// \n// It panics on overflow.\n"
);
}
#[test]
fn test_format_java_single_line() {
let result =
format_doc_comment("Returns the sum of two numbers.", Language::Java, "", "add");
assert_eq!(result, "/**\n * Returns the sum of two numbers.\n */\n");
}
#[test]
fn test_format_java_multiline_with_indent() {
let text = "Returns the sum.\n\n@param a first number\n@param b second number";
let result = format_doc_comment(text, Language::Java, " ", "add");
let expected = " /**\n * Returns the sum.\n * \n * @param a first number\n * @param b second number\n */\n";
assert_eq!(result, expected);
}
#[test]
fn test_format_typescript_single_line() {
let result = format_doc_comment(
"Fetches data from the API.",
Language::TypeScript,
"",
"fetchData",
);
assert_eq!(result, "/**\n * Fetches data from the API.\n */\n");
}
#[test]
fn test_format_empty_text() {
let result = format_doc_comment("", Language::Rust, "", "foo");
assert_eq!(result, "");
}
#[test]
fn test_format_default_language() {
let result = format_doc_comment("Runs the deploy script.", Language::Bash, "", "deploy");
assert_eq!(result, "// Runs the deploy script.\n");
}
#[test]
fn test_format_elixir() {
let result =
format_doc_comment("Adds two numbers together.", Language::Elixir, " ", "add");
assert_eq!(
result,
" @doc \"\"\"\n Adds two numbers together.\n \"\"\"\n"
);
}
#[test]
fn test_format_ocaml() {
let result =
format_doc_comment("Computes the factorial.", Language::OCaml, "", "factorial");
assert_eq!(result, "(** \nComputes the factorial.\n *)\n");
}
#[test]
fn test_format_haskell() {
let result = format_doc_comment(
"Maps a function over a list.",
Language::Haskell,
"",
"mapF",
);
assert_eq!(result, "-- | Maps a function over a list.\n");
}
#[test]
fn test_format_ruby() {
let result = format_doc_comment(
"Initializes the connection pool.",
Language::Ruby,
" ",
"initialize",
);
assert_eq!(result, " # Initializes the connection pool.\n");
}
#[test]
fn all_language_doc_formats_are_valid() {
let valid = [
"triple_slash",
"python_docstring",
"go_comment",
"javadoc",
"hash_comment",
"elixir_doc",
"lua_ldoc",
"haskell_haddock",
"ocaml_doc",
"erlang_edoc",
"r_roxygen",
"block_comment",
"default",
];
for lang in Language::all_variants() {
if let Some(def) = lang.try_def() {
assert!(
valid.contains(&def.doc_format),
"Language {:?} has unknown doc_format: {:?}",
lang,
def.doc_format
);
}
}
}
#[test]
fn test_block_comment_format() {
let fmt = doc_format_from_tag("block_comment");
assert_eq!(fmt.prefix, "(*");
assert_eq!(fmt.line_prefix, "");
assert_eq!(fmt.suffix, "*)");
assert_eq!(fmt.position, InsertionPosition::BeforeFunction);
}
#[test]
fn test_format_block_comment_single_line() {
let fmt = doc_format_from_tag("block_comment");
let text = "Moves the axis to home position.";
let indent = " ";
let mut result = String::new();
result.push_str(indent);
result.push_str(fmt.prefix);
result.push('\n');
for line in text.lines() {
result.push_str(indent);
result.push_str(fmt.line_prefix);
result.push_str(line);
result.push('\n');
}
result.push_str(indent);
result.push_str(fmt.suffix);
result.push('\n');
assert_eq!(result, " (*\n Moves the axis to home position.\n *)\n");
}
#[test]
fn test_format_preserves_internal_indentation() {
let text = "Short summary.\n\nArgs:\n x: The input value.";
let result = format_doc_comment(text, Language::Rust, "", "foo");
assert_eq!(
result,
"/// Short summary.\n/// \n/// Args:\n/// x: The input value.\n"
);
}
}