use super::*;
#[test]
fn test_php_option_param_emits_nullable_with_default() {
let backend = PhpBackend;
let api = ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![],
functions: vec![FunctionDef {
name: "do_thing".to_string(),
rust_path: "test_lib::do_thing".to_string(),
original_rust_path: String::new(),
params: vec![
ParamDef {
name: "required_str".to_string(),
ty: TypeRef::String,
optional: false,
..ParamDef::default()
},
ParamDef {
name: "optional_str".to_string(),
ty: TypeRef::Optional(Box::new(TypeRef::String)),
..ParamDef::default()
},
],
return_type: TypeRef::String,
is_async: false,
error_type: None,
doc: "Do a thing with strings".to_string(),
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(),
}],
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 = make_config();
let files = backend.generate_public_api(&api, &config).expect("generate ok");
let facade_file = files
.iter()
.find(|f| f.path.to_string_lossy().ends_with(".php"))
.expect("facade file exists");
let content = &facade_file.content;
assert!(
content.contains("string $required_str"),
"required parameter must be non-nullable; got:\n{content}"
);
assert!(
content.contains("?string $optional_str = null"),
"optional parameter must be ?string with = null default; got:\n{content}"
);
assert!(
!content.contains("??string"),
"must not have double-nullable ??string; got:\n{content}"
);
}
#[test]
fn test_php_required_str_param_not_nullable_with_optional_tail() {
let backend = PhpBackend;
let api = ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![],
functions: vec![FunctionDef {
name: "process_document".to_string(),
rust_path: "test_lib::process_document".to_string(),
original_rust_path: String::new(),
params: vec![
ParamDef {
name: "content_type".to_string(),
ty: TypeRef::String,
optional: false,
is_ref: true, ..ParamDef::default()
},
ParamDef {
name: "hint".to_string(),
ty: TypeRef::Optional(Box::new(TypeRef::String)),
optional: true,
is_ref: true,
..ParamDef::default()
},
],
return_type: TypeRef::String,
is_async: false,
error_type: None,
doc: "Process a document with optional hint".to_string(),
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(),
}],
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 = make_config();
let files = backend.generate_public_api(&api, &config).expect("generate ok");
let facade_file = files
.iter()
.find(|f| f.path.to_string_lossy().ends_with(".php"))
.expect("facade file exists");
let content = &facade_file.content;
assert!(
content.contains("string $content_type") && !content.contains("?string $content_type"),
"required &str parameter must be non-nullable string; got:\n{content}"
);
assert!(
content.contains("?string $hint = null"),
"optional parameter must be ?string with = null default; got:\n{content}"
);
assert!(
!content.contains("??string"),
"must not have double-nullable ??string; got:\n{content}"
);
}
#[test]
fn test_php_source_files_have_blank_line_after_opening_tag() {
let backend = PhpBackend;
let api = ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![
TypeDef {
name: "Config".to_string(),
rust_path: "test_lib::Config".to_string(),
original_rust_path: String::new(),
fields: vec![make_field("timeout", TypeRef::Primitive(PrimitiveType::U32), true)],
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
is_trait: false,
has_default: true,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
doc: "Config".to_string(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
},
TypeDef {
name: "Handle".to_string(),
rust_path: "test_lib::Handle".to_string(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![MethodDef {
name: "close".to_string(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: false,
error_type: None,
doc: "Close the handle".to_string(),
receiver: Some(ReceiverKind::Owned),
sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
trait_source: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}],
is_opaque: true,
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: false,
super_traits: vec![],
doc: "Opaque handle".to_string(),
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 = make_config();
let mut php_files: Vec<alef::core::backend::GeneratedFile> = Vec::new();
php_files.extend(backend.generate_public_api(&api, &config).expect("public api ok"));
php_files.extend(backend.generate_type_stubs(&api, &config).expect("type stubs ok"));
php_files.retain(|f| f.path.extension().and_then(|e| e.to_str()) == Some("php"));
assert!(!php_files.is_empty(), "expected at least one generated .php file");
for file in &php_files {
let name = file.path.to_string_lossy().to_string();
assert!(
file.content.starts_with("<?php\n\n"),
"{name} must have a blank line after `<?php` (PSR-12 blank_line_after_opening_tag). got:\n{}",
&file.content[..file.content.len().min(120)],
);
}
use std::process::Command;
let tools_available = Command::new("php").arg("--version").output().is_ok()
&& Command::new("php-cs-fixer").arg("--version").output().is_ok();
if !tools_available {
eprintln!("skipping php-cs-fixer no-op check: php or php-cs-fixer not installed");
return;
}
let dir = tempfile::tempdir().unwrap();
for file in php_files.iter().filter(|f| !f.path.to_string_lossy().contains("stubs")) {
let php_path = dir.path().join("subject.php");
std::fs::write(&php_path, &file.content).unwrap();
let output = Command::new("php-cs-fixer")
.arg("fix")
.arg("--using-cache=no")
.arg("--rules=@PSR12")
.arg(&php_path)
.output()
.expect("run php-cs-fixer");
let after = std::fs::read_to_string(&php_path).unwrap();
assert_eq!(
after,
file.content,
"php-cs-fixer rewrote {}; stderr:\n{}",
file.path.display(),
String::from_utf8_lossy(&output.stderr),
);
}
}
#[test]
fn facade_emits_nullable_marker_for_non_tail_optional_param() {
let backend = PhpBackend;
let api = ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![TypeDef {
name: "ExtractionConfig".to_string(),
rust_path: "test_lib::ExtractionConfig".to_string(),
original_rust_path: String::new(),
fields: vec![make_field("timeout", TypeRef::Primitive(PrimitiveType::U32), false)],
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
is_trait: false,
has_default: true,
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(),
}],
functions: vec![FunctionDef {
name: "extract_file".to_string(),
rust_path: "test_lib::extract_file".to_string(),
original_rust_path: String::new(),
params: vec![
ParamDef {
name: "path".to_string(),
ty: TypeRef::Path,
..ParamDef::default()
},
ParamDef {
name: "mime_type".to_string(),
ty: TypeRef::String,
optional: true,
..ParamDef::default()
},
ParamDef {
name: "config".to_string(),
ty: TypeRef::Named("ExtractionConfig".to_string()),
is_ref: true,
..ParamDef::default()
},
],
return_type: TypeRef::String,
is_async: false,
error_type: Some("Error".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(),
}],
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 files = backend.generate_public_api(&api, &make_config()).unwrap();
let facade = files.first().expect("facade file generated");
assert!(
facade.content.contains("?string $mime_type"),
"facade must keep the nullable marker on non-tail Option<T> params; got:\n{}",
facade.content
);
assert!(
!facade.content.contains(" string $mime_type"),
"facade must not emit a non-nullable `string $mime_type`; got:\n{}",
facade.content
);
}
#[test]
fn module_entry_uses_explicit_extension_name_not_cargo_pkg_name() {
let backend = PhpBackend;
let api = ApiSurface {
crate_name: "test-lib".to_string(),
version: "1.2.3".to_string(),
types: vec![],
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 = make_config_with_extension("tree_sitter_language_pack");
let files = backend.generate_bindings(&api, &config).unwrap();
let lib_rs = files
.iter()
.find(|f| f.path.ends_with("lib.rs"))
.expect("lib.rs generated");
assert!(
lib_rs.content.contains("ModuleBuilder::new(") && lib_rs.content.contains("tree_sitter_language_pack"),
"module entry must use explicit extension name in ModuleBuilder::new(); got:\n{}",
lib_rs.content
);
assert!(
!lib_rs.content.contains("CARGO_PKG_NAME"),
"module entry must not rely on CARGO_PKG_NAME macro; got:\n{}",
lib_rs.content
);
assert!(
lib_rs.content.contains("extern \"C\" fn get_module()"),
"module entry must export get_module extern function; got:\n{}",
lib_rs.content
);
assert!(
lib_rs.content.contains("StaticModuleEntry"),
"module entry must use StaticModuleEntry for thread-safe singleton; got:\n{}",
lib_rs.content
);
}