use crate::core::config::{Language, ResolvedCrateConfig};
use crate::core::ir::{ApiSurface, FunctionDef, ParamDef, TypeRef, VersionAnnotation};
use crate::docs::descriptions::generate_param_description;
use crate::docs::doc_cleaning::{clean_doc_inline, demote_headings_to_start_at, extract_param_docs};
use crate::docs::examples::render_function_example;
use crate::docs::formatting::{doc_type_with_optional, escape_table_cell, format_error_phrase};
use crate::docs::naming::{field_name, func_name, lang_code_fence};
use crate::docs::rust_types::rust_param_type;
use crate::docs::signatures::render_function_signature;
use crate::docs::{clean_doc, doc_type, template_env, version_labels};
pub(super) fn push_version_annotation(out: &mut String, version: &VersionAnnotation) {
if let Some(ref since) = version.since {
let since = version_labels::major_minor(since);
out.push_str(&template_env::render(
"since_badge.jinja",
minijinja::context! { since => since },
));
out.push('\n');
out.push('\n');
}
if let Some(ref dep) = version.deprecated {
let since = dep
.since
.as_deref()
.map(version_labels::major_minor)
.unwrap_or_default();
out.push_str(&template_env::render(
"deprecated_notice.jinja",
minijinja::context! {
since => since,
note => dep.note.as_deref().unwrap_or(""),
},
));
out.push('\n');
out.push('\n');
}
}
fn mut_writeback_return<'a>(func: &'a FunctionDef, api: &ApiSurface) -> std::borrow::Cow<'a, FunctionDef> {
let opaque_types: ahash::AHashSet<String> = api
.types
.iter()
.filter(|t| t.is_opaque)
.map(|t| t.name.clone())
.collect();
match crate::codegen::mut_writeback::effective_return_type(&func.params, &func.return_type, &opaque_types) {
Some(return_type) => {
let mut bound = func.clone();
bound.return_type = return_type;
std::borrow::Cow::Owned(bound)
}
None => std::borrow::Cow::Borrowed(func),
}
}
pub(super) fn render_function(
func: &FunctionDef,
lang: Language,
_config: &ResolvedCrateConfig,
api: &ApiSurface,
ffi_prefix: &str,
) -> String {
let with_writeback = mut_writeback_return(func, api);
let func = with_writeback.as_ref();
let mut out = String::new();
let fn_name = func_name(&func.name, lang, ffi_prefix);
out.push_str(&template_env::render(
"heading.jinja",
minijinja::context! { marker => "####", title => format!("{fn_name}()") },
));
push_version_annotation(&mut out, &func.version);
let param_docs = extract_param_docs(&func.doc);
if !func.doc.is_empty() {
let doc = clean_doc(&func.doc, lang);
let doc = demote_headings_to_start_at(&doc, 5);
out.push_str(&doc);
out.push('\n');
out.push('\n');
}
out.push_str("**Signature:**\n\n");
let lang_code = lang_code_fence(lang);
let sig = render_function_signature(func, lang, ffi_prefix, &api.crate_name);
out.push_str(&template_env::render(
"code_block.jinja",
minijinja::context! { lang_code => lang_code, body => sig },
));
out.push('\n');
out.push_str(&render_function_example(func, lang, ffi_prefix, &api.crate_name));
push_parameters_table(&mut out, &func.params, ¶m_docs, lang, ffi_prefix);
push_returns(
&mut out,
&func.return_type,
func.error_type.as_deref(),
lang,
ffi_prefix,
);
push_errors(
&mut out,
func.error_type.as_deref(),
&func.return_type,
lang,
&api.crate_name,
);
out
}
pub(super) fn push_parameters_table(
out: &mut String,
params: &[ParamDef],
param_docs: &std::collections::HashMap<String, String>,
lang: Language,
ffi_prefix: &str,
) {
if params.is_empty() {
return;
}
out.push_str("**Parameters:**\n\n");
out.push_str("| Name | Type | Required | Description |\n");
out.push_str("|------|------|----------|-------------|\n");
for param in params {
let pname = field_name(¶m.name, lang);
let pty = if lang == Language::Rust {
rust_param_type(param, ffi_prefix)
} else {
doc_type_with_optional(¶m.ty, lang, param.optional, ffi_prefix)
};
let required = if param.optional { "No" } else { "Yes" };
let pdoc = param_docs
.get(param.name.as_str())
.map(|s| clean_doc_inline(s, lang))
.unwrap_or_else(|| generate_param_description(¶m.name, ¶m.ty));
out.push_str(&template_env::render(
"param_row.jinja",
minijinja::context! {
name => escape_table_cell(&pname),
ty => escape_table_cell(&pty),
required => required,
doc => escape_table_cell(&pdoc),
},
));
}
out.push('\n');
}
pub(super) fn push_returns(
out: &mut String,
return_type: &TypeRef,
error_type: Option<&str>,
lang: Language,
ffi_prefix: &str,
) {
push_returns_with_override(out, return_type, None, error_type, lang, ffi_prefix);
}
pub(super) fn push_returns_with_override(
out: &mut String,
return_type: &TypeRef,
return_type_override: Option<&str>,
error_type: Option<&str>,
lang: Language,
ffi_prefix: &str,
) {
if matches!(return_type, TypeRef::Unit) {
if let Some(override_ty) = return_type_override {
out.push_str(&template_env::render(
"returns.jinja",
minijinja::context! { ty => override_ty },
));
out.push('\n');
return;
}
if matches!(lang, Language::Ffi | Language::C) && error_type.is_some() {
out.push_str("**Returns:** `int32_t` status code -- `0` on success, `-1` on error.\n");
} else {
out.push_str("**Returns:** No return value.\n");
}
out.push('\n');
return;
}
let ret_ty = return_type_override
.map(str::to_string)
.unwrap_or_else(|| doc_type(return_type, lang, ffi_prefix));
if ret_ty.is_empty() {
out.push_str("**Returns:** No return value.\n");
out.push('\n');
} else {
out.push_str(&template_env::render(
"returns.jinja",
minijinja::context! { ty => ret_ty },
));
out.push('\n');
}
}
pub(super) fn push_errors(
out: &mut String,
error_type: Option<&str>,
return_type: &TypeRef,
lang: Language,
crate_name: &str,
) {
if let Some(err) = error_type {
let error_phrase = format_error_phrase(err, return_type, lang, crate_name);
out.push_str(&template_env::render(
"errors_phrase.jinja",
minijinja::context! { phrase => error_phrase },
));
out.push('\n');
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::docs::test_helpers::{TEST_CRATE_NAME, TEST_PREFIX, make_function, make_param};
#[test]
fn test_note_section_after_examples_is_not_emitted_twice() {
let mut func = make_function("count_tokens", vec![], TypeRef::String, false, None);
func.doc = r#"Count the number of tokens in `text`.
# Arguments
* `text` - The text to tokenize.
# Example
```rust,no_run
let n = count_tokens("hello");
```
# Note
This function is intentionally excluded from language bindings."#
.to_string();
let doc_body = {
let doc = clean_doc(&func.doc, Language::Rust);
demote_headings_to_start_at(&doc, 5)
};
let example = render_function_example(&func, Language::Rust, TEST_PREFIX, TEST_CRATE_NAME);
let rendered = format!("{doc_body}\n\n{example}");
assert_eq!(
rendered.matches("**Note:**").count(),
1,
"the Note section must be converted to a bold label exactly once: {rendered}"
);
assert!(
!rendered
.lines()
.any(|line| line.trim_start().trim_start_matches('#').trim() == "Note"
&& line.trim_start().starts_with('#')),
"no raw, unconverted `# Note` heading (at any level) may survive in the rendered \
output: {rendered}"
);
}
#[test]
fn test_c_signature_and_returns_prose_agree_on_fallible_void_status_code() {
let func = make_function(
"init",
vec![make_param("config", TypeRef::Named("ClientConfig".to_string()), false)],
TypeRef::Unit,
false,
Some("InitError"),
);
let signature = render_function_signature(&func, Language::C, TEST_PREFIX, TEST_CRATE_NAME);
let mut returns_prose = String::new();
push_returns(
&mut returns_prose,
&func.return_type,
func.error_type.as_deref(),
Language::C,
TEST_PREFIX,
);
assert!(signature.starts_with("int32_t "), "signature: {signature}");
assert!(
returns_prose.contains("int32_t"),
"returns prose must mention the same status-code type the signature declares: {returns_prose}"
);
assert!(
!returns_prose.contains("No return value"),
"must not still claim there's no return value once the signature says int32_t: {returns_prose}"
);
}
#[test]
fn test_c_signature_and_returns_prose_agree_infallible_void_stays_silent() {
let func = make_function("touch", vec![], TypeRef::Unit, false, None);
let signature = render_function_signature(&func, Language::C, TEST_PREFIX, TEST_CRATE_NAME);
let mut returns_prose = String::new();
push_returns(
&mut returns_prose,
&func.return_type,
func.error_type.as_deref(),
Language::C,
TEST_PREFIX,
);
assert!(signature.starts_with("void "), "signature: {signature}");
assert!(returns_prose.contains("No return value"), "{returns_prose}");
assert!(!returns_prose.contains("int32_t"), "{returns_prose}");
}
#[test]
fn test_returns_with_override_wins_over_status_code_inference_for_unit_return() {
let mut out = String::new();
push_returns_with_override(
&mut out,
&TypeRef::Unit,
Some("StreamHandle"),
Some("StreamError"),
Language::C,
TEST_PREFIX,
);
assert!(out.contains("StreamHandle"), "{out}");
assert!(!out.contains("int32_t"), "{out}");
}
#[test]
fn test_errors_prose_names_the_class_exception_class_name_derives() {
let mut out = String::new();
push_errors(
&mut out,
Some("Error"),
&TypeRef::String,
Language::Java,
TEST_CRATE_NAME,
);
let expected_class = crate::backends::java::naming::exception_class_name(TEST_CRATE_NAME);
assert_eq!(
out.trim(),
format!("**Errors:** Throws `{expected_class}`."),
"the Errors: prose must name the class the Java binding actually declares, not a \
pascal-cased spelling of the error type's own short name: {out}"
);
}
}