use std::collections::HashSet;
use std::fmt;
use boltffi_ffi_rules::naming;
use crate::ir::ids::{
EnumId, FieldName, FunctionId, MethodId, ParamName, RecordId, StreamId, VariantName,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct CSharpClassName(String);
impl CSharpClassName {
pub(crate) fn from_source(source: &str) -> Self {
Self(naming::to_upper_camel_case(source))
}
pub(crate) fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub(crate) fn wire_helper(base: &CSharpClassName) -> Self {
Self(format!("{}Wire", base.0))
}
pub(crate) fn methods_companion(base: &CSharpClassName) -> Self {
Self(format!("{}Methods", base.0))
}
pub(crate) fn exception_for(base: &CSharpClassName) -> Self {
Self(format!("{}Exception", base.0))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CSharpClassName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&RecordId> for CSharpClassName {
fn from(id: &RecordId) -> Self {
Self::from_source(id.as_str())
}
}
impl From<&EnumId> for CSharpClassName {
fn from(id: &EnumId) -> Self {
Self::from_source(id.as_str())
}
}
impl From<&VariantName> for CSharpClassName {
fn from(name: &VariantName) -> Self {
Self::from_source(name.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) enum CSharpTypeReference {
Plain(CSharpClassName),
Qualified {
namespace: CSharpNamespace,
name: CSharpClassName,
},
}
impl CSharpTypeReference {
pub(crate) fn qualify_if_shadowed(
self,
shadowed: &HashSet<CSharpClassName>,
namespace: &CSharpNamespace,
) -> Self {
match self {
Self::Plain(name) if shadowed.contains(&name) => Self::Qualified {
namespace: namespace.clone(),
name,
},
other => other,
}
}
pub(crate) fn qualify_if_shadowed_opt(
self,
shadowed: Option<&HashSet<CSharpClassName>>,
namespace: &CSharpNamespace,
) -> Self {
match shadowed {
Some(sh) => self.qualify_if_shadowed(sh, namespace),
None => self,
}
}
}
impl fmt::Display for CSharpTypeReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Plain(name) => write!(f, "{name}"),
Self::Qualified { namespace, name } => write!(f, "global::{namespace}.{name}"),
}
}
}
impl From<CSharpClassName> for CSharpTypeReference {
fn from(name: CSharpClassName) -> Self {
Self::Plain(name)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct CSharpMethodName(String);
impl CSharpMethodName {
pub(crate) fn from_source(source: &str) -> Self {
Self(naming::to_upper_camel_case(source))
}
pub(crate) fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub(crate) fn native_for_owner(owner: &CSharpClassName, method: &CSharpMethodName) -> Self {
Self(format!("{}{}", owner.as_str(), method.as_str()))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CSharpMethodName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&FunctionId> for CSharpMethodName {
fn from(id: &FunctionId) -> Self {
Self::from_source(id.as_str())
}
}
impl From<&MethodId> for CSharpMethodName {
fn from(id: &MethodId) -> Self {
Self::from_source(id.as_str())
}
}
impl From<&StreamId> for CSharpMethodName {
fn from(id: &StreamId) -> Self {
Self::from_source(id.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct CSharpPropertyName(String);
impl CSharpPropertyName {
pub(crate) fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub(crate) fn from_source(source: &str) -> Self {
Self(naming::to_upper_camel_case(source))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CSharpPropertyName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&FieldName> for CSharpPropertyName {
fn from(name: &FieldName) -> Self {
Self::from_source(name.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct CSharpParamName(String);
impl CSharpParamName {
pub(crate) fn from_source(source: &str) -> Self {
Self(escape_if_keyword(naming::snake_to_camel(source)))
}
pub(crate) fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
fn stripped(&self) -> &str {
self.0.strip_prefix('@').unwrap_or(&self.0)
}
}
impl fmt::Display for CSharpParamName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&ParamName> for CSharpParamName {
fn from(name: &ParamName) -> Self {
Self::from_source(name.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct CSharpLocalName(String);
impl CSharpLocalName {
pub(crate) fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub(crate) fn for_wire_writer(param: &CSharpParamName) -> Self {
Self(format!("_wire_{}", param.stripped()))
}
pub(crate) fn for_bytes(param: &CSharpParamName) -> Self {
Self(format!("_{}Bytes", param.stripped()))
}
pub(crate) fn for_pinned_ptr(param: &CSharpParamName) -> Self {
Self(format!("_{}Ptr", param.stripped()))
}
pub(crate) fn for_inline_callback_scope(param: &CSharpParamName) -> Self {
Self(format!("_{}Callback", param.stripped()))
}
pub(crate) fn size_option_binding(n: usize) -> Self {
Self(format!("sizeOpt{n}"))
}
pub(crate) fn size_loop_var(n: usize) -> Self {
Self(format!("sizeItem{n}"))
}
pub(crate) fn encode_option_binding(n: usize) -> Self {
Self(format!("opt{n}"))
}
pub(crate) fn encode_loop_var(n: usize) -> Self {
Self(format!("item{n}"))
}
pub(crate) fn decode_closure_var(n: usize) -> Self {
Self(format!("r{n}"))
}
}
impl fmt::Display for CSharpLocalName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct CSharpNamespace(String);
impl CSharpNamespace {
pub(crate) fn from_source(source: &str) -> Self {
Self(naming::to_upper_camel_case(source))
}
}
impl fmt::Display for CSharpNamespace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
fn escape_if_keyword(name: String) -> String {
if is_csharp_keyword(&name) {
format!("@{}", name)
} else {
name
}
}
fn is_csharp_keyword(name: &str) -> bool {
matches!(
name,
"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"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn csharp_class_name_from_snake_case_produces_pascal_case() {
let name = CSharpClassName::from_source("my_record");
assert_eq!(name.as_str(), "MyRecord");
assert_eq!(name.to_string(), "MyRecord");
}
#[test]
fn csharp_class_name_wire_helper_appends_wire_suffix() {
let base = CSharpClassName::from_source("status");
let helper = CSharpClassName::wire_helper(&base);
assert_eq!(helper.as_str(), "StatusWire");
}
#[test]
fn csharp_class_name_methods_companion_appends_methods_suffix() {
let base = CSharpClassName::from_source("log_level");
let companion = CSharpClassName::methods_companion(&base);
assert_eq!(companion.as_str(), "LogLevelMethods");
}
#[test]
fn csharp_class_name_from_record_id_converts_through_source() {
let id = RecordId::new("point");
let name: CSharpClassName = (&id).into();
assert_eq!(name.as_str(), "Point");
}
#[test]
fn csharp_class_name_exception_for_appends_exception_suffix() {
let base = CSharpClassName::from_source("math_error");
let exception = CSharpClassName::exception_for(&base);
assert_eq!(exception.as_str(), "MathErrorException");
}
#[test]
fn csharp_method_name_from_snake_case_produces_pascal_case() {
let name = CSharpMethodName::from_source("do_thing");
assert_eq!(name.as_str(), "DoThing");
}
#[test]
fn csharp_method_name_from_function_id_converts_through_source() {
let id = FunctionId::new("echo_i32");
let name: CSharpMethodName = (&id).into();
assert_eq!(name.as_str(), "EchoI32");
}
#[test]
fn csharp_property_name_from_snake_case_produces_pascal_case() {
let name = CSharpPropertyName::from_source("my_prop");
assert_eq!(name.to_string(), "MyProp");
}
#[test]
fn csharp_property_name_new_wraps_pre_formed_name_verbatim() {
let name = CSharpPropertyName::new("handle");
assert_eq!(name.to_string(), "handle");
}
#[test]
fn csharp_property_name_from_field_name_converts_through_source() {
let field = FieldName::new("radius");
let name: CSharpPropertyName = (&field).into();
assert_eq!(name.to_string(), "Radius");
}
#[test]
fn csharp_param_name_from_snake_case_produces_camel_case() {
let name = CSharpParamName::from_source("my_param");
assert_eq!(name.as_str(), "myParam");
}
#[test]
fn csharp_param_name_escapes_csharp_keyword() {
let name = CSharpParamName::from_source("class");
assert_eq!(name.as_str(), "@class");
}
#[test]
fn csharp_param_name_from_ir_param_name_converts_through_source() {
let param = ParamName::new("my_param");
let name: CSharpParamName = (¶m).into();
assert_eq!(name.as_str(), "myParam");
}
#[test]
fn csharp_local_name_for_wire_writer_prefixes_param() {
let param = CSharpParamName::from_source("point");
let local = CSharpLocalName::for_wire_writer(¶m);
assert_eq!(local.to_string(), "_wire_point");
}
#[test]
fn csharp_local_name_for_bytes_strips_keyword_escape() {
let param = CSharpParamName::from_source("class");
let local = CSharpLocalName::for_bytes(¶m);
assert_eq!(local.to_string(), "_classBytes");
}
#[test]
fn csharp_local_name_for_bytes_uses_param_directly_when_not_escaped() {
let param = CSharpParamName::from_source("value");
let local = CSharpLocalName::for_bytes(¶m);
assert_eq!(local.to_string(), "_valueBytes");
}
#[rstest::rstest]
#[case::first(0, "sizeOpt0")]
#[case::second(1, "sizeOpt1")]
#[case::later(3, "sizeOpt3")]
fn csharp_local_name_size_option_binding_uses_sizeopt_prefix(
#[case] n: usize,
#[case] expected: &str,
) {
assert_eq!(
CSharpLocalName::size_option_binding(n).to_string(),
expected
);
}
#[rstest::rstest]
#[case::first(0, "sizeItem0")]
#[case::second(1, "sizeItem1")]
#[case::later(2, "sizeItem2")]
fn csharp_local_name_size_loop_var_uses_sizeitem_prefix(
#[case] n: usize,
#[case] expected: &str,
) {
assert_eq!(CSharpLocalName::size_loop_var(n).to_string(), expected);
}
#[rstest::rstest]
#[case::first(0, "opt0")]
#[case::second(1, "opt1")]
#[case::later(5, "opt5")]
fn csharp_local_name_encode_option_binding_uses_opt_prefix(
#[case] n: usize,
#[case] expected: &str,
) {
assert_eq!(
CSharpLocalName::encode_option_binding(n).to_string(),
expected
);
}
#[rstest::rstest]
#[case::first(0, "item0")]
#[case::second(1, "item1")]
#[case::later(4, "item4")]
fn csharp_local_name_encode_loop_var_uses_item_prefix(
#[case] n: usize,
#[case] expected: &str,
) {
assert_eq!(CSharpLocalName::encode_loop_var(n).to_string(), expected);
}
#[rstest::rstest]
#[case::first(0, "r0")]
#[case::second(1, "r1")]
#[case::later(3, "r3")]
fn csharp_local_name_decode_closure_var_uses_r_prefix(
#[case] n: usize,
#[case] expected: &str,
) {
assert_eq!(CSharpLocalName::decode_closure_var(n).to_string(), expected);
}
#[test]
fn csharp_method_name_new_wraps_pre_formed_name_verbatim() {
assert_eq!(
CSharpMethodName::new("WriteNIntArray").as_str(),
"WriteNIntArray"
);
}
#[test]
fn csharp_namespace_from_snake_case_produces_pascal_case() {
let ns = CSharpNamespace::from_source("demo_lib");
assert_eq!(ns.to_string(), "DemoLib");
}
mod csharp_type_reference {
use super::*;
fn shadowed(names: &[&str]) -> HashSet<CSharpClassName> {
names
.iter()
.map(|n| CSharpClassName::from_source(n))
.collect()
}
#[test]
fn plain_display_uses_bare_class_name() {
let r = CSharpTypeReference::Plain(CSharpClassName::from_source("point"));
assert_eq!(r.to_string(), "Point");
}
#[test]
fn qualified_display_uses_global_prefix() {
let r = CSharpTypeReference::Qualified {
namespace: CSharpNamespace::from_source("demo"),
name: CSharpClassName::from_source("point"),
};
assert_eq!(r.to_string(), "global::Demo.Point");
}
#[test]
fn qualify_promotes_plain_when_name_is_in_shadowed_set() {
let r: CSharpTypeReference = CSharpClassName::from_source("point").into();
let ns = CSharpNamespace::from_source("demo");
let qualified = r.qualify_if_shadowed(&shadowed(&["point"]), &ns);
assert_eq!(qualified.to_string(), "global::Demo.Point");
}
#[test]
fn qualify_leaves_plain_when_name_is_not_shadowed() {
let r: CSharpTypeReference = CSharpClassName::from_source("point").into();
let ns = CSharpNamespace::from_source("demo");
let qualified = r.qualify_if_shadowed(&shadowed(&["circle"]), &ns);
assert_eq!(qualified.to_string(), "Point");
}
#[test]
fn qualify_is_a_no_op_on_already_qualified_reference() {
let r = CSharpTypeReference::Qualified {
namespace: CSharpNamespace::from_source("demo"),
name: CSharpClassName::from_source("point"),
};
let ns = CSharpNamespace::from_source("demo");
let qualified = r.qualify_if_shadowed(&shadowed(&["point"]), &ns);
assert_eq!(qualified.to_string(), "global::Demo.Point");
}
#[test]
fn from_csharp_class_name_produces_plain_variant() {
let name = CSharpClassName::from_source("point");
let r: CSharpTypeReference = name.into();
assert!(matches!(r, CSharpTypeReference::Plain(_)));
}
}
}