use super::super::FfiBackend;
use super::common::*;
use crate::core::backend::Backend;
use crate::core::ir::*;
#[test]
fn test_option_option_primitive_getter_returns_primitive_type() {
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![TypeDef {
name: "ConfigUpdate".to_string(),
rust_path: "my_lib::ConfigUpdate".to_string(),
original_rust_path: String::new(),
fields: vec![FieldDef {
name: "max_depth".to_string(),
ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
optional: true,
default: None,
doc: String::new(),
sanitized: false,
is_boxed: false,
type_rust_path: None,
cfg: None,
typed_default: None,
core_wrapper: crate::core::ir::CoreWrapper::None,
vec_inner_core_wrapper: crate::core::ir::CoreWrapper::None,
newtype_wrapper: None,
serde_rename: None,
serde_flatten: false,
original_type: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: true,
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
}],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
};
let config = sample_config();
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
lib.content.contains("-> usize"),
"expected `-> usize` in getter but got:\n{}",
lib.content
);
assert!(
!lib.content.contains("-> *mut std::ffi::c_char"),
"getter must not return *mut c_char for Option<Option<usize>>"
);
assert!(
lib.content.contains("None => 0"),
"expected `None => 0` sentinel in generated getter"
);
assert!(
lib.content.contains("*inner_val"),
"expected `*inner_val` deref for inner primitive in generated getter"
);
}
fn api_with_named_field(field_type: &str, is_clone: bool) -> ApiSurface {
let holder = TypeDef {
name: "Holder".to_string(),
rust_path: "my_lib::Holder".to_string(),
original_rust_path: String::new(),
fields: vec![FieldDef {
name: "inner".to_string(),
ty: TypeRef::Named(field_type.to_string()),
optional: false,
default: None,
doc: String::new(),
sanitized: false,
is_boxed: false,
type_rust_path: None,
cfg: None,
typed_default: None,
core_wrapper: crate::core::ir::CoreWrapper::None,
vec_inner_core_wrapper: crate::core::ir::CoreWrapper::None,
newtype_wrapper: None,
serde_rename: None,
serde_flatten: false,
original_type: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
methods: vec![],
is_opaque: false,
is_clone: false,
is_copy: false,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
};
let named_type = TypeDef {
name: field_type.to_string(),
rust_path: format!("my_lib::{field_type}"),
original_rust_path: String::new(),
fields: vec![],
methods: vec![],
is_opaque: true,
is_clone,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
doc: String::new(),
cfg: None,
is_copy: false,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
};
ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![holder, named_type],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
}
}
#[test]
fn test_named_field_non_clone_no_clone_call() {
let api = api_with_named_field("LanguageRegistry", false);
let config = sample_config();
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
!lib.content.contains(".clone()"),
"non-Clone opaque Named field must not emit .clone() in accessor:\n{}",
lib.content
);
}
#[test]
fn test_named_field_clone_capable_emits_clone() {
let api = api_with_named_field("ConversionOptions", true);
let config = sample_config();
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
lib.content.contains(".clone()"),
"Clone-capable Named field must emit .clone() in accessor:\n{}",
lib.content
);
}
#[test]
fn test_options_field_visitor_callbacks_use_configured_renderer_setter() {
let config = resolved_one(
r#"
[workspace]
languages = ["ffi"]
[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
[crates.ffi]
prefix = "syn"
visitor_callbacks = true
[[crates.trait_bridges]]
trait_name = "SyntaxWalker"
type_alias = "SyntaxWalkerHandle"
param_name = "renderer"
bind_via = "options_field"
options_type = "ParseOptions"
options_field = "renderer"
context_type = "SyntaxContext"
result_type = "WalkOutcome"
"#,
);
let mut api = sample_api();
api.types.push(TypeDef {
name: "SyntaxWalker".to_string(),
rust_path: "my_lib::syntax::SyntaxWalker".to_string(),
methods: vec![MethodDef {
name: "visit_token".to_string(),
params: vec![ParamDef {
name: "context".to_string(),
ty: TypeRef::Named("SyntaxContext".to_string()),
is_ref: true,
..ParamDef::default()
}],
return_type: TypeRef::Named("WalkOutcome".to_string()),
receiver: Some(ReceiverKind::RefMut),
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}],
is_trait: true,
..TypeDef::default()
});
api.types.push(TypeDef {
name: "SyntaxContext".to_string(),
rust_path: "my_lib::syntax::SyntaxContext".to_string(),
fields: vec![FieldDef {
name: "rule_name".to_string(),
ty: TypeRef::String,
..FieldDef::default()
}],
..TypeDef::default()
});
api.types.push(TypeDef {
name: "ParseOptions".to_string(),
rust_path: "my_lib::ParseOptions".to_string(),
is_clone: true,
..TypeDef::default()
});
api.types.push(TypeDef {
name: "ParseResult".to_string(),
rust_path: "my_lib::ParseResult".to_string(),
is_clone: true,
is_return_type: true,
..TypeDef::default()
});
api.enums.push(EnumDef {
name: "WalkOutcome".to_string(),
rust_path: "my_lib::syntax::WalkOutcome".to_string(),
variants: vec![
EnumVariant {
name: "Continue".to_string(),
is_default: true,
..EnumVariant::default()
},
EnumVariant {
name: "Stop".to_string(),
..EnumVariant::default()
},
],
has_serde: true,
..EnumDef::default()
});
api.functions.push(FunctionDef {
name: "parse".to_string(),
rust_path: "my_lib::parse".to_string(),
params: vec![
ParamDef {
name: "source".to_string(),
ty: TypeRef::String,
is_ref: true,
..ParamDef::default()
},
ParamDef {
name: "options".to_string(),
ty: TypeRef::Optional(Box::new(TypeRef::Named("ParseOptions".to_string()))),
optional: true,
..ParamDef::default()
},
],
return_type: TypeRef::Named("ParseResult".to_string()),
error_type: Some("ParseError".to_string()),
..FunctionDef::default()
});
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
lib.content.contains("syn_options_set_renderer"),
"options-field setter must derive from configured renderer field"
);
assert!(
!lib.content.contains("syn_options_set_visitor_handle"),
"options-field mode must not emit the legacy visitor_handle setter"
);
assert!(
lib.content.contains("pub struct SynVisitorCallbacks"),
"Java callback lifecycle support should remain available"
);
assert!(
lib.content.contains("syn_visitor_create") && lib.content.contains("syn_visitor_free"),
"visitor create/free symbols should remain available"
);
let convert_count = lib.content.matches("fn syn_parse(").count();
assert_eq!(convert_count, 1, "syn_parse must appear exactly once");
assert!(
!lib.content.contains("syn_parse_with_visitor"),
"options-field mode must not emit the legacy with_visitor wrapper"
);
assert!(
lib.content.contains("visitor: *mut SynVisitor"),
"options-field setter must accept *mut SynVisitor when visitor_callbacks is enabled"
);
assert!(
!lib.content.contains("visitor: *mut SynSyntaxWalkerBridge"),
"options-field setter must not require the trait-bridge handle when visitor_callbacks is enabled"
);
}
#[test]
fn test_options_field_bridge_generates_non_convert_function_from_ir() {
let config = resolved_one(
r#"
[workspace]
languages = ["ffi"]
[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
[crates.ffi]
prefix = "doc"
[[crates.trait_bridges]]
trait_name = "HtmlVisitor"
type_alias = "RenderHandle"
param_name = "renderer"
bind_via = "options_field"
options_type = "RenderSettings"
options_field = "renderer"
"#,
);
let mut api = visitor_api();
api.types.push(TypeDef {
name: "RenderSettings".to_string(),
rust_path: "my_lib::RenderSettings".to_string(),
fields: vec![],
is_clone: true,
..TypeDef::default()
});
api.types.push(TypeDef {
name: "RenderedDocument".to_string(),
rust_path: "my_lib::RenderedDocument".to_string(),
fields: vec![],
is_clone: true,
..TypeDef::default()
});
api.functions.push(FunctionDef {
name: "render_document".to_string(),
rust_path: "my_lib::render_document".to_string(),
original_rust_path: String::new(),
params: vec![
ParamDef {
name: "source".to_string(),
ty: TypeRef::String,
is_ref: true,
..ParamDef::default()
},
ParamDef {
name: "settings".to_string(),
ty: TypeRef::Optional(Box::new(TypeRef::Named("RenderSettings".to_string()))),
optional: true,
..ParamDef::default()
},
],
return_type: TypeRef::Named("RenderedDocument".to_string()),
is_async: false,
error_type: Some("RenderError".to_string()),
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
});
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
lib.content.contains("fn doc_render_document("),
"must generate IR-derived symbol"
);
assert!(
lib.content.contains("settings: *const my_lib::RenderSettings"),
"must use configured options type"
);
assert!(
lib.content.contains(") -> *mut my_lib::RenderedDocument"),
"must use actual return type"
);
assert!(
lib.content
.contains("match my_lib::render_document(source_rs, settings_rs)"),
"must call actual core function with actual parameters"
);
assert!(
!lib.content.contains("my_lib::convert("),
"must not hardcode conversion call"
);
assert!(
!lib.content.contains("ConversionOptions") && !lib.content.contains("ConversionResult"),
"must not leak conversion-shaped type names in generic wrapper"
);
}