use crate::core::config::Language;
use crate::core::ir::{ApiSurface, DefaultValue, FieldDef, PrimitiveType, TypeRef};
use crate::docs::enum_variant_ref::format_enum_variant_ref;
use crate::docs::type_mapping::doc_type;
use crate::docs::type_mapping::java_boxed_type;
use heck::ToPascalCase;
pub(crate) fn escape_table_cell(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
let mut code_delimiter = 0;
let mut chars = value.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '`' {
let mut run = 1;
while chars.peek() == Some(&'`') {
chars.next();
run += 1;
}
if code_delimiter == 0 {
code_delimiter = run;
} else if code_delimiter == run {
code_delimiter = 0;
}
escaped.extend(std::iter::repeat_n('`', run));
} else if ch == '|' {
escaped.push_str("\\|");
} else if code_delimiter == 0 && matches!(ch, '[' | ']') {
escaped.push('\\');
escaped.push(ch);
} else {
escaped.push(ch);
}
}
escaped
}
#[cfg(test)]
mod table_escape_tests {
use super::escape_table_cell;
#[test]
fn preserves_brackets_in_code_spans_and_escapes_table_syntax() {
assert_eq!(
escape_table_cell("Use `[Item]` or [Item] | none"),
"Use `[Item]` or \\[Item\\] \\| none"
);
}
#[test]
fn preserves_brackets_in_multi_backtick_code_spans() {
assert_eq!(escape_table_cell("``value[`key`]``"), "``value[`key`]``");
}
}
pub(crate) fn format_field_default(field: &FieldDef, lang: Language, api: &ApiSurface, ffi_prefix: &str) -> String {
if let Some(typed) = &field.typed_default {
return format_typed_default(typed, &field.ty, lang, api, ffi_prefix, field.optional);
}
if let Some(raw) = &field.default
&& !raw.is_empty()
{
let collapsed = crate::docs::doc_cleaning::collapse_whitespace(raw);
return format!("`{collapsed}`");
}
if field.optional {
return match lang {
Language::Python => "`None`".to_string(),
Language::Node | Language::Wasm => "`null`".to_string(),
Language::Go => "`nil`".to_string(),
Language::Java => "`null`".to_string(),
Language::Csharp => "`null`".to_string(),
Language::Ruby => "`nil`".to_string(),
Language::Php => "`null`".to_string(),
Language::Elixir => "`nil`".to_string(),
Language::R => "`NULL`".to_string(),
Language::Rust => "`None`".to_string(),
Language::Ffi | Language::C | Language::Jni => "`NULL`".to_string(),
Language::Kotlin
| Language::KotlinAndroid
| Language::Swift
| Language::Dart
| Language::Gleam
| Language::Zig => "`null`".to_string(),
};
}
"—".to_string()
}
fn enum_variant_doc_label(variant: &str, field_ty: &TypeRef, lang: Language, ffi_prefix: &str) -> String {
let enum_type_name_str = match field_ty {
TypeRef::Named(n) => Some(n.as_str()),
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(n) => Some(n.as_str()),
_ => None,
},
_ => None,
};
match enum_type_name_str {
Some(type_str) => format_enum_variant_ref(type_str, variant, lang, ffi_prefix),
None => variant.to_string(),
}
}
pub(crate) fn format_typed_default(
val: &DefaultValue,
field_ty: &TypeRef,
lang: Language,
api: &ApiSurface,
ffi_prefix: &str,
optional: bool,
) -> String {
match val {
DefaultValue::BoolLiteral(b) => match lang {
Language::Python => format!("`{}`", if *b { "True" } else { "False" }),
_ => format!("`{b}`"),
},
DefaultValue::StringLiteral(s) => {
let normalized = crate::docs::doc_cleaning::collapse_whitespace(s);
format!("`\"{normalized}\"`")
}
DefaultValue::ListLiteral(items) => {
let rendered: Vec<String> = items
.iter()
.map(|item| {
format_typed_default(item, field_ty, lang, api, ffi_prefix, optional)
.trim_matches('`')
.to_string()
})
.collect();
format!("`[{}]`", rendered.join(", "))
}
DefaultValue::IntLiteral(n) => {
if matches!(field_ty, TypeRef::Duration)
|| matches!(field_ty, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Duration))
{
return format!("`{n}ms`");
}
format!("`{n}`")
}
DefaultValue::FloatLiteral(f) => format!("`{f}`"),
DefaultValue::EnumVariant(v) => {
let parts: Vec<&str> = v.splitn(2, "::").collect();
if parts.len() == 2 {
format!("`{}`", format_enum_variant_ref(parts[0], parts[1], lang, ffi_prefix))
} else {
let enum_type_name_str = match field_ty {
TypeRef::Named(n) => Some(n.as_str()),
TypeRef::Optional(inner) => {
if let TypeRef::Named(n) = inner.as_ref() {
Some(n.as_str())
} else {
None
}
}
_ => None,
};
if let Some(type_str) = enum_type_name_str {
format!("`{}`", format_enum_variant_ref(type_str, v, lang, ffi_prefix))
} else {
format!("`{v}`")
}
}
}
DefaultValue::TupleVariant(variant, values) => {
let rendered: Vec<String> = values
.iter()
.map(|item| {
format_typed_default(item, field_ty, lang, api, ffi_prefix, optional)
.trim_matches('`')
.to_string()
})
.collect();
format!(
"`{}({})`",
enum_variant_doc_label(variant, field_ty, lang, ffi_prefix),
rendered.join(", ")
)
}
DefaultValue::StructVariant(variant, fields) => {
let rendered: Vec<String> = fields
.iter()
.map(|(name, item)| {
let value = format_typed_default(item, field_ty, lang, api, ffi_prefix, optional)
.trim_matches('`')
.to_string();
format!("{name}: {value}")
})
.collect();
format!(
"`{} {{ {} }}`",
enum_variant_doc_label(variant, field_ty, lang, ffi_prefix),
rendered.join(", ")
)
}
DefaultValue::Empty | DefaultValue::Unresolved(_) => {
let inner_for_dur = match field_ty {
TypeRef::Optional(inner) => Some(inner.as_ref()),
other => Some(other),
};
if matches!(inner_for_dur, Some(TypeRef::Duration)) {
return match lang {
Language::Rust => "`Duration::default()`".to_string(),
_ => "`0ms`".to_string(),
};
}
if !optional
&& let TypeRef::Named(type_name_str) = field_ty
&& let Some(enum_def) = api.enums.iter().find(|e| &e.name == type_name_str)
{
let variant = enum_def
.variants
.iter()
.find(|v| v.is_default)
.or_else(|| enum_def.variants.first());
if let Some(v) = variant {
return format!(
"`{}`",
format_enum_variant_ref(type_name_str, &v.name, lang, ffi_prefix)
);
}
}
let inner_ty = match field_ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
if matches!(inner_ty, TypeRef::Vec(_)) {
return match lang {
Language::Python => "`[]`".to_string(),
Language::Node | Language::Wasm => "`[]`".to_string(),
Language::Go => "`nil`".to_string(),
Language::Java => "`Collections.emptyList()`".to_string(),
Language::Csharp => {
let elem_ty = if let TypeRef::Vec(elem) = inner_ty {
doc_type(elem, lang, ffi_prefix)
} else {
String::new()
};
format!("`new List<{elem_ty}>()`")
}
Language::Ruby | Language::Elixir => "`[]`".to_string(),
Language::Php => "`[]`".to_string(),
Language::Rust => "`vec![]`".to_string(),
Language::Ffi | Language::C | Language::Jni => "`NULL`".to_string(),
Language::R => "`list()`".to_string(),
Language::Kotlin
| Language::KotlinAndroid
| Language::Swift
| Language::Dart
| Language::Gleam
| Language::Zig => "`[]`".to_string(),
};
}
if matches!(inner_ty, TypeRef::Map(_, _)) {
return match lang {
Language::Python | Language::Ruby | Language::Php => "`{}`".to_string(),
Language::Node | Language::Wasm => "`{}`".to_string(),
Language::Go => "`nil`".to_string(),
Language::Elixir => "`%{}`".to_string(),
Language::Java => "`Collections.emptyMap()`".to_string(),
Language::Csharp => {
if let TypeRef::Map(k, v) = inner_ty {
let kty = doc_type(k, lang, ffi_prefix);
let vty = doc_type(v, lang, ffi_prefix);
format!("`new Dictionary<{kty}, {vty}>()`")
} else {
"`new Dictionary<>()`".to_string()
}
}
Language::Rust => "`HashMap::new()`".to_string(),
Language::Ffi | Language::C | Language::Jni => "`NULL`".to_string(),
Language::R => "`list()`".to_string(),
Language::Kotlin
| Language::KotlinAndroid
| Language::Swift
| Language::Dart
| Language::Gleam
| Language::Zig => "`{}`".to_string(),
};
}
if !optional {
return "—".to_string();
}
match lang {
Language::Python => "`None`".to_string(),
Language::Node | Language::Wasm => "`null`".to_string(),
Language::Go => "`nil`".to_string(),
Language::Java => "`null`".to_string(),
Language::Csharp => "`null`".to_string(),
Language::Ruby => "`nil`".to_string(),
Language::Php => "`null`".to_string(),
Language::Elixir => "`nil`".to_string(),
Language::R => "`NULL`".to_string(),
Language::Rust => "`Default::default()`".to_string(),
Language::Ffi | Language::C | Language::Jni => "`NULL`".to_string(),
Language::Kotlin
| Language::KotlinAndroid
| Language::Swift
| Language::Dart
| Language::Gleam
| Language::Zig => "`null`".to_string(),
}
}
DefaultValue::None => {
if !optional {
return "—".to_string();
}
match lang {
Language::Python => "`None`".to_string(),
Language::Node | Language::Wasm => "`null`".to_string(),
Language::Go => "`nil`".to_string(),
Language::Java => "`null`".to_string(),
Language::Csharp => "`null`".to_string(),
Language::Ruby => "`nil`".to_string(),
Language::Php => "`null`".to_string(),
Language::Elixir => "`nil`".to_string(),
Language::R => "`NULL`".to_string(),
Language::Rust => "`None`".to_string(),
Language::Ffi | Language::C | Language::Jni => "`NULL`".to_string(),
Language::Kotlin
| Language::KotlinAndroid
| Language::Swift
| Language::Dart
| Language::Gleam
| Language::Zig => "`null`".to_string(),
}
}
DefaultValue::FunctionCall(path) | DefaultValue::PublicFunctionCall(path) => format!("`{path}()`"),
}
}
fn ffi_error_return_phrase(return_type: &TypeRef) -> String {
match return_type {
TypeRef::Unit | TypeRef::Bytes => "Returns `-1` on error.".to_string(),
TypeRef::Named(_) => "Returns the sentinel handle `0` on error.".to_string(),
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
"Returns `NULL` on error.".to_string()
}
TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => "Returns `0.0` on error.".to_string(),
TypeRef::Primitive(_) | TypeRef::Duration => "Returns `0` on error.".to_string(),
TypeRef::Optional(inner) => ffi_error_return_phrase(inner),
}
}
pub(crate) fn format_error_phrase(error_type: &str, return_type: &TypeRef, lang: Language, crate_name: &str) -> String {
let short = error_type.rsplit("::").next().unwrap_or(error_type);
match lang {
Language::Python => {
let ename = short.to_pascal_case();
format!("Raises `{ename}`.")
}
Language::Go => "Returns `error`.".to_string(),
Language::Java => {
let ename = crate::backends::java::naming::exception_class_name(crate_name);
format!("Throws `{ename}`.")
}
Language::Node | Language::Wasm => "Throws `Error` with a descriptive message.".to_string(),
Language::Ruby => {
let ename = short.to_pascal_case();
format!("Raises `{ename}`.")
}
Language::Csharp => {
let ename = short.to_pascal_case();
format!("Throws `{ename}`.")
}
Language::Elixir => "Returns `{:error, reason}`".to_string(),
Language::Php => {
let ename = short.to_pascal_case();
format!("Throws `{ename}`.")
}
Language::Ffi | Language::C => ffi_error_return_phrase(return_type),
Language::Jni => "Returns `NULL` on error.".to_string(),
Language::R => "Stops with error message.".to_string(),
Language::Rust => {
let ename = short.to_pascal_case();
format!("Returns `Err({ename})`.")
}
Language::Kotlin
| Language::KotlinAndroid
| Language::Swift
| Language::Dart
| Language::Gleam
| Language::Zig => {
let ename = short.to_pascal_case();
format!("Throws `{ename}`.")
}
}
}
pub(crate) fn doc_type_with_optional(ty: &TypeRef, lang: Language, optional: bool, ffi_prefix: &str) -> String {
if optional && !matches!(ty, TypeRef::Optional(_)) {
let inner = doc_type(ty, lang, ffi_prefix);
return match lang {
Language::Python => format!("{inner} | None"),
Language::Node | Language::Wasm => format!("{inner} | null"),
Language::Go => format!("*{inner}"),
Language::Java => {
let boxed = java_boxed_type(ty);
crate::backends::java::gen_bindings::helpers::render_nullable_type(&boxed, true)
}
Language::Csharp => format!("{inner}?"),
Language::Ruby => format!("{inner}?"),
Language::Php => format!("?{inner}"),
Language::Elixir => format!("{inner} | nil"),
Language::R => format!("{inner} or NULL"),
Language::Rust => format!("Option<{inner}>"),
Language::Ffi | Language::C if matches!(ty, TypeRef::Named(_)) || inner.ends_with('*') => inner,
Language::Ffi | Language::C | Language::Jni => format!("{inner}*"),
Language::Kotlin
| Language::KotlinAndroid
| Language::Swift
| Language::Dart
| Language::Gleam
| Language::Zig => {
format!("{inner}?")
}
};
}
doc_type(ty, lang, ffi_prefix)
}
fn reserved_words(lang: Language) -> &'static [&'static str] {
match lang {
Language::Python => &[
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", "def",
"del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda",
"nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield",
],
Language::Node | Language::Wasm => &[
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"export",
"extends",
"false",
"finally",
"for",
"function",
"if",
"import",
"in",
"instanceof",
"new",
"null",
"return",
"super",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
"let",
"static",
"enum",
"await",
"implements",
"package",
"protected",
"interface",
"private",
"public",
],
Language::Ruby => &[
"__ENCODING__",
"__LINE__",
"__FILE__",
"BEGIN",
"END",
"alias",
"and",
"begin",
"break",
"case",
"class",
"def",
"defined?",
"do",
"else",
"elsif",
"end",
"ensure",
"false",
"for",
"if",
"in",
"module",
"next",
"nil",
"not",
"or",
"redo",
"rescue",
"retry",
"return",
"self",
"super",
"then",
"true",
"undef",
"unless",
"until",
"when",
"while",
"yield",
],
Language::Php => &[
"abstract",
"and",
"array",
"as",
"break",
"callable",
"case",
"catch",
"class",
"clone",
"const",
"continue",
"declare",
"default",
"do",
"echo",
"else",
"elseif",
"empty",
"enddeclare",
"endfor",
"endforeach",
"endif",
"endswitch",
"endwhile",
"extends",
"final",
"finally",
"fn",
"for",
"foreach",
"function",
"global",
"goto",
"if",
"implements",
"include",
"include_once",
"instanceof",
"insteadof",
"interface",
"isset",
"list",
"match",
"namespace",
"new",
"or",
"print",
"private",
"protected",
"public",
"readonly",
"require",
"require_once",
"return",
"static",
"switch",
"throw",
"trait",
"try",
"unset",
"use",
"var",
"while",
"xor",
"yield",
],
Language::Elixir => &[
"true", "false", "nil", "when", "and", "or", "not", "in", "fn", "do", "end", "catch", "rescue", "after",
"else",
],
Language::Go => &[
"break",
"default",
"func",
"interface",
"select",
"case",
"defer",
"go",
"map",
"struct",
"chan",
"else",
"goto",
"package",
"switch",
"const",
"fallthrough",
"if",
"range",
"type",
"continue",
"for",
"import",
"return",
"var",
],
Language::Java => &[
"abstract",
"assert",
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"class",
"const",
"continue",
"default",
"do",
"double",
"else",
"enum",
"extends",
"final",
"finally",
"float",
"for",
"goto",
"if",
"implements",
"import",
"instanceof",
"int",
"interface",
"long",
"native",
"new",
"package",
"private",
"protected",
"public",
"return",
"short",
"static",
"strictfp",
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"try",
"void",
"volatile",
"while",
"true",
"false",
"null",
],
Language::Csharp => &[
"abstract",
"as",
"base",
"bool",
"break",
"byte",
"case",
"catch",
"char",
"checked",
"class",
"const",
"continue",
"decimal",
"default",
"delegate",
"do",
"double",
"else",
"enum",
"event",
"explicit",
"extern",
"false",
"finally",
"fixed",
"float",
"for",
"foreach",
"goto",
"if",
"implicit",
"in",
"int",
"interface",
"internal",
"is",
"lock",
"long",
"namespace",
"new",
"null",
"object",
"operator",
"out",
"override",
"params",
"private",
"protected",
"public",
"readonly",
"ref",
"return",
"sbyte",
"sealed",
"short",
"sizeof",
"stackalloc",
"static",
"string",
"struct",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"uint",
"ulong",
"unchecked",
"unsafe",
"ushort",
"using",
"virtual",
"void",
"volatile",
"while",
],
Language::Rust => &[
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if", "impl",
"in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static",
"struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "async", "await", "dyn",
"abstract", "become", "box", "do", "final", "macro", "override", "priv", "try", "typeof", "unsized",
"virtual", "yield",
],
Language::Kotlin | Language::KotlinAndroid => &[
"as",
"break",
"class",
"continue",
"do",
"else",
"false",
"for",
"fun",
"if",
"in",
"interface",
"is",
"null",
"object",
"package",
"return",
"super",
"this",
"throw",
"true",
"try",
"typealias",
"typeof",
"val",
"var",
"when",
"while",
"by",
"catch",
"constructor",
"delegate",
"dynamic",
"field",
"file",
"finally",
"get",
"import",
"init",
"param",
"property",
"receiver",
"set",
"setparam",
"where",
"actual",
"abstract",
"annotation",
"companion",
"const",
"crossinline",
"data",
"enum",
"expect",
"external",
"final",
"infix",
"inline",
"inner",
"internal",
"lateinit",
"noinline",
"open",
"operator",
"out",
"override",
"private",
"protected",
"public",
"reified",
"sealed",
"suspend",
"tailrec",
"vararg",
],
Language::Swift => &[
"associatedtype",
"class",
"deinit",
"enum",
"extension",
"fileprivate",
"func",
"import",
"init",
"inout",
"internal",
"let",
"open",
"operator",
"private",
"protocol",
"public",
"rethrows",
"static",
"struct",
"subscript",
"typealias",
"var",
"break",
"case",
"continue",
"default",
"defer",
"do",
"else",
"fallthrough",
"for",
"guard",
"if",
"in",
"repeat",
"return",
"switch",
"where",
"while",
"as",
"Any",
"catch",
"false",
"is",
"nil",
"self",
"Self",
"super",
"throw",
"throws",
"true",
"try",
],
Language::Dart => &[
"abstract",
"as",
"assert",
"async",
"await",
"break",
"case",
"catch",
"class",
"const",
"continue",
"covariant",
"default",
"deferred",
"do",
"dynamic",
"else",
"enum",
"export",
"extends",
"extension",
"external",
"factory",
"false",
"final",
"finally",
"for",
"Function",
"get",
"hide",
"if",
"implements",
"import",
"in",
"interface",
"is",
"late",
"library",
"mixin",
"new",
"null",
"on",
"operator",
"part",
"required",
"rethrow",
"return",
"set",
"show",
"static",
"super",
"switch",
"sync",
"this",
"throw",
"true",
"try",
"typedef",
"var",
"void",
"while",
"with",
"yield",
],
Language::Gleam => &[
"as", "assert", "case", "const", "external", "fn", "if", "import", "let", "opaque", "pub", "todo", "type",
"use", "panic", "echo",
],
Language::Zig => &[
"addrspace",
"align",
"allowzero",
"and",
"anyframe",
"anytype",
"asm",
"async",
"await",
"break",
"callconv",
"catch",
"comptime",
"const",
"continue",
"defer",
"else",
"enum",
"errdefer",
"error",
"export",
"extern",
"fn",
"for",
"if",
"inline",
"noalias",
"noinline",
"nosuspend",
"opaque",
"or",
"orelse",
"packed",
"pub",
"resume",
"return",
"linksection",
"struct",
"suspend",
"switch",
"test",
"threadlocal",
"try",
"union",
"unreachable",
"usingnamespace",
"var",
"volatile",
"while",
],
Language::R => &[
"if",
"else",
"repeat",
"while",
"function",
"for",
"in",
"next",
"break",
"TRUE",
"FALSE",
"NULL",
"Inf",
"NaN",
"NA",
"NA_integer_",
"NA_real_",
"NA_character_",
"NA_complex_",
],
Language::Ffi | Language::C | Language::Jni => &[
"auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern",
"float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", "signed",
"sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while",
],
}
}
fn has_valid_identifier_syntax(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum IdentifierPosition {
Declaration,
Member,
}
impl IdentifierPosition {
fn label(self) -> &'static str {
match self {
Self::Declaration => "declaration",
Self::Member => "member",
}
}
}
fn reserved_words_bind_in_member_position(lang: Language) -> bool {
!matches!(lang, Language::Node | Language::Wasm | Language::Php)
}
pub(crate) fn identifier_violation(name: &str, lang: Language, position: IdentifierPosition) -> Option<&'static str> {
if name.is_empty() {
return Some("empty identifier");
}
let reserved_applies = position == IdentifierPosition::Declaration || reserved_words_bind_in_member_position(lang);
if reserved_applies && reserved_words(lang).contains(&name) {
return Some("reserved word");
}
if !has_valid_identifier_syntax(name) {
return Some("not a syntactically valid identifier");
}
None
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct IdentifierViolation {
pub(crate) name: String,
pub(crate) lang: Language,
pub(crate) position: IdentifierPosition,
pub(crate) reason: &'static str,
pub(crate) context: String,
}
impl std::fmt::Display for IdentifierViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
name,
lang,
position,
reason,
context,
} = self;
write!(
f,
"generated {lang:?} identifier `{name}` is invalid ({reason}) in {} position while rendering {context} \
-- the docs pipeline must not emit an identifier a {lang:?} toolchain would reject",
position.label()
)
}
}
impl std::error::Error for IdentifierViolation {}
pub(crate) fn check_identifier(
name: &str,
lang: Language,
position: IdentifierPosition,
context: &str,
) -> Result<(), IdentifierViolation> {
match identifier_violation(name, lang, position) {
None => Ok(()),
Some(reason) => Err(IdentifierViolation {
name: name.to_string(),
lang,
position,
reason,
context: context.to_string(),
}),
}
}
pub(crate) fn report_identifier_violation(name: &str, lang: Language, position: IdentifierPosition, context: &str) {
if let Err(violation) = check_identifier(name, lang, position, context) {
tracing::error!(
identifier = %violation.name,
language = ?violation.lang,
position = violation.position.label(),
reason = violation.reason,
render_context = %violation.context,
"{}",
violation
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::Language;
use crate::core::ir::{DefaultValue, PrimitiveType, TypeRef};
use crate::docs::test_helpers::{TEST_CRATE_NAME, TEST_PREFIX, empty_api, make_field};
#[test]
fn test_identifier_violation_rejects_new_in_declaration_position_wherever_it_is_reserved() {
for lang in [
Language::Java,
Language::Dart,
Language::Php,
Language::Node,
Language::Csharp,
] {
assert_eq!(
identifier_violation("new", lang, IdentifierPosition::Declaration),
Some("reserved word"),
"`new` must be rejected in declaration position for {lang:?}"
);
}
}
#[test]
fn test_identifier_violation_accepts_new_in_member_position_where_the_grammar_allows_it() {
for lang in [Language::Node, Language::Wasm, Language::Php] {
assert_eq!(
identifier_violation("new", lang, IdentifierPosition::Member),
None,
"`new` is a legal member name in {lang:?}"
);
}
}
#[test]
fn test_identifier_violation_still_rejects_new_in_member_position_where_the_grammar_forbids_it() {
for lang in [Language::Java, Language::Csharp, Language::Dart] {
assert_eq!(
identifier_violation("new", lang, IdentifierPosition::Member),
Some("reserved word"),
"`new` is not a legal member name in {lang:?}"
);
}
}
#[test]
fn test_identifier_violation_node_relaxation_does_not_reach_declaration_position() {
assert_eq!(
identifier_violation("class", Language::Node, IdentifierPosition::Member),
None
);
assert_eq!(
identifier_violation("class", Language::Node, IdentifierPosition::Declaration),
Some("reserved word")
);
}
#[test]
fn test_identifier_violation_rejects_malformed_names_in_member_position_too() {
for lang in [Language::Node, Language::Wasm, Language::Php] {
assert_eq!(
identifier_violation("1invalid", lang, IdentifierPosition::Member),
Some("not a syntactically valid identifier"),
"{lang:?}"
);
assert_eq!(
identifier_violation("", lang, IdentifierPosition::Member),
Some("empty identifier"),
"{lang:?}"
);
}
}
#[test]
fn test_identifier_violation_accepts_new_in_languages_where_it_is_not_reserved() {
for lang in [
Language::Rust,
Language::Go,
Language::Kotlin,
Language::Zig,
Language::Elixir,
] {
for position in [IdentifierPosition::Declaration, IdentifierPosition::Member] {
assert_eq!(
identifier_violation("new", lang, position),
None,
"`new` must be accepted for {lang:?} in {position:?} position"
);
}
}
}
#[test]
fn test_identifier_violation_csharp_new_only_collides_lowercase() {
assert_eq!(
identifier_violation("new", Language::Csharp, IdentifierPosition::Declaration),
Some("reserved word")
);
assert_eq!(
identifier_violation("New", Language::Csharp, IdentifierPosition::Declaration),
None
);
}
#[test]
fn test_identifier_violation_rejects_python_default_arg_style_keywords() {
assert_eq!(
identifier_violation("class", Language::Python, IdentifierPosition::Declaration),
Some("reserved word")
);
assert_eq!(
identifier_violation("import", Language::Python, IdentifierPosition::Declaration),
Some("reserved word")
);
}
#[test]
fn test_identifier_violation_kotlin_default_is_not_reserved() {
assert_eq!(
identifier_violation("default", Language::Kotlin, IdentifierPosition::Declaration),
None
);
}
#[test]
fn test_identifier_violation_rejects_empty_identifier() {
assert_eq!(
identifier_violation("", Language::Python, IdentifierPosition::Declaration),
Some("empty identifier")
);
}
#[test]
fn test_identifier_violation_rejects_identifier_starting_with_digit() {
assert_eq!(
identifier_violation("1invalid", Language::Python, IdentifierPosition::Declaration),
Some("not a syntactically valid identifier")
);
}
#[test]
fn test_identifier_violation_accepts_ordinary_identifier_in_every_language() {
for lang in [
Language::Python,
Language::Node,
Language::Wasm,
Language::Ruby,
Language::Php,
Language::Elixir,
Language::Go,
Language::Java,
Language::Csharp,
Language::Rust,
Language::Kotlin,
Language::KotlinAndroid,
Language::Swift,
Language::Dart,
Language::Gleam,
Language::Zig,
Language::R,
Language::Ffi,
Language::C,
Language::Jni,
] {
assert_eq!(
identifier_violation("classifyLink", lang, IdentifierPosition::Declaration),
None,
"an ordinary camelCase identifier must be accepted for {lang:?}"
);
}
}
#[test]
fn test_check_identifier_returns_a_structured_error_instead_of_panicking() {
let violation = check_identifier("new", Language::Java, IdentifierPosition::Member, "a method signature")
.expect_err("`new` is not a legal Java member name");
assert_eq!(violation.name, "new");
assert_eq!(violation.lang, Language::Java);
assert_eq!(violation.position, IdentifierPosition::Member);
assert_eq!(violation.reason, "reserved word");
assert_eq!(violation.context, "a method signature");
let rendered = violation.to_string();
assert!(rendered.contains("`new`"), "{rendered}");
assert!(rendered.contains("reserved word"), "{rendered}");
assert!(rendered.contains("member position"), "{rendered}");
assert!(rendered.contains("a method signature"), "{rendered}");
assert!(rendered.contains("Java"), "{rendered}");
}
#[test]
fn test_check_identifier_accepts_the_napi_static_new_that_used_to_abort_the_docs_run() {
assert_eq!(
check_identifier("new", Language::Node, IdentifierPosition::Member, "a method signature"),
Ok(())
);
report_identifier_violation("new", Language::Node, IdentifierPosition::Member, "a method signature");
}
#[test]
fn test_report_identifier_violation_does_not_unwind_on_a_violation() {
report_identifier_violation("new", Language::Java, IdentifierPosition::Member, "a test context");
report_identifier_violation(
"createClient",
Language::Java,
IdentifierPosition::Member,
"a test context",
);
}
#[test]
fn test_doc_type_with_optional_true_wraps_correctly() {
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Python, true, TEST_PREFIX),
"str | None"
);
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Node, true, TEST_PREFIX),
"string | null"
);
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Go, true, TEST_PREFIX),
"*string"
);
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Csharp, true, TEST_PREFIX),
"string?"
);
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Ruby, true, TEST_PREFIX),
"String?"
);
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Php, true, TEST_PREFIX),
"?string"
);
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Elixir, true, TEST_PREFIX),
"String.t() | nil"
);
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::R, true, TEST_PREFIX),
"character or NULL"
);
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Rust, true, TEST_PREFIX),
"Option<String>"
);
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Ffi, true, TEST_PREFIX),
"const char*"
);
}
#[test]
fn test_doc_type_with_optional_false_is_identity() {
for lang in [
Language::Python,
Language::Node,
Language::Go,
Language::Java,
Language::Rust,
] {
assert_eq!(
doc_type_with_optional(&TypeRef::String, lang, false, TEST_PREFIX),
crate::docs::doc_type(&TypeRef::String, lang, TEST_PREFIX),
"optional=false should be identity for {lang:?}"
);
}
}
#[test]
fn test_doc_type_with_optional_does_not_double_wrap_already_optional_type() {
let already_optional = TypeRef::Optional(Box::new(TypeRef::String));
assert_eq!(
doc_type_with_optional(&already_optional, Language::Python, true, TEST_PREFIX),
"str | None"
);
assert_eq!(
doc_type_with_optional(&already_optional, Language::Rust, true, TEST_PREFIX),
"Option<String>"
);
}
#[test]
fn test_doc_type_with_optional_java_boxes_primitive_i32() {
assert_eq!(
doc_type_with_optional(
&TypeRef::Primitive(PrimitiveType::I32),
Language::Java,
true,
TEST_PREFIX
),
"@Nullable Integer"
);
}
#[test]
fn test_doc_type_with_optional_java_boxes_primitive_bool() {
assert_eq!(
doc_type_with_optional(
&TypeRef::Primitive(PrimitiveType::Bool),
Language::Java,
true,
TEST_PREFIX
),
"@Nullable Boolean"
);
}
#[test]
fn test_doc_type_with_optional_java_boxes_primitive_f64() {
assert_eq!(
doc_type_with_optional(
&TypeRef::Primitive(PrimitiveType::F64),
Language::Java,
true,
TEST_PREFIX
),
"@Nullable Double"
);
}
#[test]
fn test_doc_type_with_optional_java_non_primitive_not_double_boxed() {
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Java, true, TEST_PREFIX),
"@Nullable String"
);
}
#[test]
fn test_doc_type_with_optional_named_ffi_is_bare_scalar_handle_not_a_pointer() {
let ty = TypeRef::Named("ClientConfig".to_string());
let rendered = doc_type_with_optional(&ty, Language::Ffi, true, TEST_PREFIX);
assert_eq!(rendered, "HTMAlefHandle");
assert!(
!rendered.contains('*'),
"handle must not be pointer-suffixed: {rendered}"
);
assert!(
!rendered.contains("ClientConfig"),
"must not name the concrete Rust type: {rendered}"
);
}
#[test]
fn test_doc_type_with_optional_string_ffi_is_single_pointer_not_double() {
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Ffi, true, TEST_PREFIX),
"const char*"
);
}
#[test]
fn test_doc_type_with_optional_string_jni_currently_emits_c_double_pointer() {
assert_eq!(
doc_type_with_optional(&TypeRef::String, Language::Jni, true, TEST_PREFIX),
"const char**"
);
}
#[test]
fn test_doc_type_with_optional_agrees_with_doc_type_on_handle_token() {
let ty = TypeRef::Named("ClientConfig".to_string());
let required = doc_type(&ty, Language::Ffi, TEST_PREFIX);
let optional = doc_type_with_optional(&ty, Language::Ffi, true, TEST_PREFIX);
assert_eq!(
required, optional,
"a Named type must render identically whether required or optional"
);
assert_eq!(required, "HTMAlefHandle");
}
#[test]
fn test_format_error_phrase_ffi_unit_return_reports_negative_one() {
assert_eq!(
format_error_phrase("InitError", &TypeRef::Unit, Language::Ffi, TEST_CRATE_NAME),
"Returns `-1` on error."
);
}
#[test]
fn test_format_error_phrase_ffi_bytes_return_reports_negative_one() {
assert_eq!(
format_error_phrase("ReadError", &TypeRef::Bytes, Language::Ffi, TEST_CRATE_NAME),
"Returns `-1` on error."
);
}
#[test]
fn test_format_error_phrase_ffi_named_return_reports_integer_sentinel_not_null() {
let phrase = format_error_phrase(
"ParseError",
&TypeRef::Named("ConversionResult".to_string()),
Language::C,
TEST_CRATE_NAME,
);
assert_eq!(phrase, "Returns the sentinel handle `0` on error.");
assert!(
!phrase.contains("NULL"),
"a scalar handle has no pointer to compare: {phrase}"
);
}
#[test]
fn test_format_error_phrase_ffi_string_return_stays_null() {
assert_eq!(
format_error_phrase("ReadError", &TypeRef::String, Language::C, TEST_CRATE_NAME),
"Returns `NULL` on error."
);
assert_eq!(
format_error_phrase(
"ReadError",
&TypeRef::Vec(Box::new(TypeRef::String)),
Language::C,
TEST_CRATE_NAME
),
"Returns `NULL` on error."
);
assert_eq!(
format_error_phrase(
"ReadError",
&TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
Language::C,
TEST_CRATE_NAME
),
"Returns `NULL` on error."
);
}
#[test]
fn test_format_error_phrase_ffi_numeric_primitive_reports_zero() {
assert_eq!(
format_error_phrase(
"ComputeError",
&TypeRef::Primitive(PrimitiveType::I32),
Language::C,
TEST_CRATE_NAME
),
"Returns `0` on error."
);
assert_eq!(
format_error_phrase(
"ComputeError",
&TypeRef::Primitive(PrimitiveType::F64),
Language::C,
TEST_CRATE_NAME
),
"Returns `0.0` on error."
);
assert_eq!(
format_error_phrase("ComputeError", &TypeRef::Duration, Language::C, TEST_CRATE_NAME),
"Returns `0` on error."
);
}
#[test]
fn test_format_error_phrase_ffi_optional_named_recurses_to_inner_shape() {
let ty = TypeRef::Optional(Box::new(TypeRef::Named("ClientConfig".to_string())));
assert_eq!(
format_error_phrase("AttachError", &ty, Language::C, TEST_CRATE_NAME),
"Returns the sentinel handle `0` on error."
);
}
#[test]
fn test_format_error_phrase_jni_currently_emits_fixed_null_phrase() {
assert_eq!(
format_error_phrase("InitError", &TypeRef::Unit, Language::Jni, TEST_CRATE_NAME),
"Returns `NULL` on error."
);
assert_eq!(
format_error_phrase(
"ParseError",
&TypeRef::Named("ConversionResult".to_string()),
Language::Jni,
TEST_CRATE_NAME
),
"Returns `NULL` on error."
);
}
#[test]
fn test_format_default_bool_literal_python_uses_capitalised_form() {
let api = empty_api();
let field_true = make_field(
"flag",
TypeRef::Primitive(PrimitiveType::Bool),
false,
Some(DefaultValue::BoolLiteral(true)),
);
let field_false = make_field(
"flag",
TypeRef::Primitive(PrimitiveType::Bool),
false,
Some(DefaultValue::BoolLiteral(false)),
);
assert_eq!(
format_field_default(&field_true, Language::Python, &api, TEST_PREFIX),
"`True`"
);
assert_eq!(
format_field_default(&field_false, Language::Python, &api, TEST_PREFIX),
"`False`"
);
}
#[test]
fn test_format_default_bool_literal_non_python_uses_lowercase_form() {
let api = empty_api();
let field_true = make_field(
"flag",
TypeRef::Primitive(PrimitiveType::Bool),
false,
Some(DefaultValue::BoolLiteral(true)),
);
for lang in [Language::Rust, Language::Java, Language::Go, Language::Node] {
assert_eq!(
format_field_default(&field_true, lang, &api, TEST_PREFIX),
"`true`",
"bool literal for {lang:?}"
);
}
}
#[test]
fn test_format_default_string_literal_all_languages_produce_quoted_form() {
let api = empty_api();
let field = make_field(
"name",
TypeRef::String,
false,
Some(DefaultValue::StringLiteral("hello".to_string())),
);
for lang in [
Language::Python,
Language::Rust,
Language::Java,
Language::Go,
Language::Node,
] {
assert_eq!(
format_field_default(&field, lang, &api, TEST_PREFIX),
"`\"hello\"`",
"string literal for {lang:?}"
);
}
}
#[test]
fn test_format_default_int_literal() {
let api = empty_api();
let field = make_field(
"count",
TypeRef::Primitive(PrimitiveType::U32),
false,
Some(DefaultValue::IntLiteral(42)),
);
for lang in [Language::Python, Language::Rust, Language::Java, Language::Node] {
assert_eq!(
format_field_default(&field, lang, &api, TEST_PREFIX),
"`42`",
"int literal for {lang:?}"
);
}
}
#[test]
fn test_format_default_int_literal_on_duration_field_shows_ms_suffix() {
let api = empty_api();
let field = make_field(
"timeout",
TypeRef::Duration,
false,
Some(DefaultValue::IntLiteral(5000)),
);
for lang in [Language::Python, Language::Rust, Language::Java, Language::Go] {
assert_eq!(
format_field_default(&field, lang, &api, TEST_PREFIX),
"`5000ms`",
"duration field should show ms suffix for {lang:?}"
);
}
}
#[test]
fn test_format_default_float_literal() {
let api = empty_api();
let field = make_field(
"confidence",
TypeRef::Primitive(PrimitiveType::F32),
false,
Some(DefaultValue::FloatLiteral(0.85)),
);
for lang in [Language::Python, Language::Rust, Language::Java] {
assert_eq!(
format_field_default(&field, lang, &api, TEST_PREFIX),
"`0.85`",
"float literal for {lang:?}"
);
}
}
#[test]
fn test_format_default_enum_variant_qualified_python_and_rust() {
let api = empty_api();
let field = make_field(
"style",
TypeRef::Named("HeadingStyle".to_string()),
false,
Some(DefaultValue::EnumVariant("HeadingStyle::Atx".to_string())),
);
assert_eq!(
format_field_default(&field, Language::Python, &api, TEST_PREFIX),
"`HeadingStyle.ATX`"
);
assert_eq!(
format_field_default(&field, Language::Rust, &api, TEST_PREFIX),
"`HeadingStyle::Atx`"
);
assert_eq!(
format_field_default(&field, Language::Java, &api, TEST_PREFIX),
"`HeadingStyle.Atx`"
);
assert_eq!(
format_field_default(&field, Language::Ruby, &api, TEST_PREFIX),
"`:atx`"
);
assert_eq!(
format_field_default(&field, Language::Php, &api, TEST_PREFIX),
"`HeadingStyle::ATX`"
);
}
#[test]
fn test_format_default_tuple_variant_qualifies_variant_and_renders_arguments() {
let api = empty_api();
let field = make_field(
"kind",
TypeRef::Named("Kind".to_string()),
false,
Some(DefaultValue::TupleVariant(
"Scaled".to_string(),
vec![
DefaultValue::IntLiteral(5),
DefaultValue::StringLiteral("x".to_string()),
],
)),
);
assert_eq!(
format_field_default(&field, Language::Rust, &api, TEST_PREFIX),
"`Kind::Scaled(5, \"x\")`"
);
assert_eq!(
format_field_default(&field, Language::Python, &api, TEST_PREFIX),
"`Kind.SCALED(5, \"x\")`"
);
}
#[test]
fn test_format_default_struct_variant_qualifies_variant_and_renders_named_fields() {
let api = empty_api();
let field = make_field(
"kind",
TypeRef::Named("Kind".to_string()),
false,
Some(DefaultValue::StructVariant(
"Curated".to_string(),
vec![
("label".to_string(), DefaultValue::StringLiteral("balanced".to_string())),
("weight".to_string(), DefaultValue::IntLiteral(3)),
],
)),
);
assert_eq!(
format_field_default(&field, Language::Rust, &api, TEST_PREFIX),
"`Kind::Curated { label: \"balanced\", weight: 3 }`"
);
assert_eq!(
format_field_default(&field, Language::Python, &api, TEST_PREFIX),
"`Kind.CURATED { label: \"balanced\", weight: 3 }`"
);
}
#[test]
fn test_format_default_empty_vec_field() {
let api = empty_api();
let field = make_field(
"items",
TypeRef::Vec(Box::new(TypeRef::String)),
false,
Some(DefaultValue::Empty),
);
assert_eq!(
format_field_default(&field, Language::Python, &api, TEST_PREFIX),
"`[]`"
);
assert_eq!(
format_field_default(&field, Language::Rust, &api, TEST_PREFIX),
"`vec![]`"
);
assert_eq!(
format_field_default(&field, Language::Java, &api, TEST_PREFIX),
"`Collections.emptyList()`"
);
assert_eq!(format_field_default(&field, Language::Go, &api, TEST_PREFIX), "`nil`");
assert_eq!(
format_field_default(&field, Language::Csharp, &api, TEST_PREFIX),
"`new List<string>()`"
);
assert_eq!(format_field_default(&field, Language::R, &api, TEST_PREFIX), "`list()`");
assert_eq!(format_field_default(&field, Language::Ruby, &api, TEST_PREFIX), "`[]`");
assert_eq!(
format_field_default(&field, Language::Elixir, &api, TEST_PREFIX),
"`[]`"
);
assert_eq!(format_field_default(&field, Language::Ffi, &api, TEST_PREFIX), "`NULL`");
}
#[test]
fn test_format_default_empty_map_field() {
let api = empty_api();
let field = make_field(
"attributes",
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
false,
Some(DefaultValue::Empty),
);
assert_eq!(
format_field_default(&field, Language::Python, &api, TEST_PREFIX),
"`{}`"
);
assert_eq!(
format_field_default(&field, Language::Rust, &api, TEST_PREFIX),
"`HashMap::new()`"
);
assert_eq!(
format_field_default(&field, Language::Java, &api, TEST_PREFIX),
"`Collections.emptyMap()`"
);
assert_eq!(format_field_default(&field, Language::Go, &api, TEST_PREFIX), "`nil`");
assert_eq!(
format_field_default(&field, Language::Elixir, &api, TEST_PREFIX),
"`%{}`"
);
assert_eq!(
format_field_default(&field, Language::Csharp, &api, TEST_PREFIX),
"`new Dictionary<string, string>()`"
);
}
#[test]
fn test_format_default_none_on_optional_field() {
let api = empty_api();
let field = make_field("label", TypeRef::String, true, Some(DefaultValue::None));
assert_eq!(
format_field_default(&field, Language::Python, &api, TEST_PREFIX),
"`None`"
);
assert_eq!(
format_field_default(&field, Language::Node, &api, TEST_PREFIX),
"`null`"
);
assert_eq!(format_field_default(&field, Language::Go, &api, TEST_PREFIX), "`nil`");
assert_eq!(
format_field_default(&field, Language::Rust, &api, TEST_PREFIX),
"`None`"
);
assert_eq!(format_field_default(&field, Language::Ffi, &api, TEST_PREFIX), "`NULL`");
assert_eq!(format_field_default(&field, Language::R, &api, TEST_PREFIX), "`NULL`");
}
#[test]
fn test_format_default_none_on_non_optional_field_returns_dash() {
let api = empty_api();
let field = make_field(
"count",
TypeRef::Primitive(PrimitiveType::U32),
false,
Some(DefaultValue::None),
);
assert_eq!(format_field_default(&field, Language::Python, &api, TEST_PREFIX), "—");
}
#[test]
fn test_format_default_empty_duration_shows_zero_ms_for_non_rust() {
let api = empty_api();
let field = make_field("timeout", TypeRef::Duration, false, Some(DefaultValue::Empty));
assert_eq!(
format_field_default(&field, Language::Python, &api, TEST_PREFIX),
"`0ms`"
);
assert_eq!(format_field_default(&field, Language::Java, &api, TEST_PREFIX), "`0ms`");
assert_eq!(format_field_default(&field, Language::Go, &api, TEST_PREFIX), "`0ms`");
}
#[test]
fn test_format_default_empty_duration_rust_shows_duration_default() {
let api = empty_api();
let field = make_field("timeout", TypeRef::Duration, false, Some(DefaultValue::Empty));
assert_eq!(
format_field_default(&field, Language::Rust, &api, TEST_PREFIX),
"`Duration::default()`"
);
}
#[test]
fn test_format_default_multiline_raw_string_collapsed() {
let api = empty_api();
let mut field = make_field("config", TypeRef::String, false, None);
field.default = Some("line1\nline2\nline3".to_string());
let result = format_field_default(&field, Language::Python, &api, TEST_PREFIX);
assert_eq!(result, "`line1 line2 line3`", "multiline defaults must be collapsed");
assert!(!result.contains('\n'), "backtick code span must be single-line");
}
#[test]
fn test_format_default_raw_string_with_extra_spaces_collapsed() {
let api = empty_api();
let mut field = make_field("config", TypeRef::String, false, None);
field.default = Some("value with spaces".to_string());
let result = format_field_default(&field, Language::Python, &api, TEST_PREFIX);
assert!(result.contains("value"), "default should be wrapped");
assert!(!result.contains(" "), "extra spaces should be normalized");
}
#[test]
fn test_format_default_string_literal_with_newlines_collapsed() {
let api = empty_api();
let field = make_field(
"marker_format",
TypeRef::String,
false,
Some(DefaultValue::StringLiteral("line1\nline2\nline3".to_string())),
);
let result = format_field_default(&field, Language::Python, &api, TEST_PREFIX);
assert_eq!(result, "`\"line1 line2 line3\"`");
assert!(!result.contains('\n'), "newlines must be removed");
}
#[test]
fn test_format_default_string_literal_with_pipes_and_newlines() {
let api = empty_api();
let field = make_field(
"marker_format",
TypeRef::String,
false,
Some(DefaultValue::StringLiteral(
"\n\n<!-- PAGE {page_num} -->\n\n".to_string(),
)),
);
let result = format_field_default(&field, Language::Python, &api, TEST_PREFIX);
assert!(!result.contains('\n'), "newlines must be collapsed");
let without_backticks = result.trim_matches('`').trim_matches('"');
assert!(
without_backticks.starts_with("<!-- PAGE"),
"normalized: {}",
without_backticks
);
}
#[test]
fn test_format_default_string_literal_with_extra_spaces() {
let api = empty_api();
let field = make_field(
"options",
TypeRef::String,
false,
Some(DefaultValue::StringLiteral("option1 option2 option3".to_string())),
);
let result = format_field_default(&field, Language::Python, &api, TEST_PREFIX);
assert!(!result.contains(" "), "multiple spaces must be normalized: {}", result);
}
}