diff --git a/src/NodeApi.DotNetHost/JSMarshaller.cs b/src/NodeApi.DotNetHost/JSMarshaller.cs index 5060c5e7..612e3d20 100644 --- a/src/NodeApi.DotNetHost/JSMarshaller.cs +++ b/src/NodeApi.DotNetHost/JSMarshaller.cs @@ -131,13 +131,59 @@ public JSMarshaller() /// public bool AutoCamelCase { get; set; } - private string ToCamelCase(string name) + public static string ToCamelCase(string name) { - if (!AutoCamelCase) return name; + if (name.Length == 0) + { + return name; + } + + // Skip leading underscores. + int firstLetterIndex = 0; + while (name[firstLetterIndex] == '_') + { + firstLetterIndex++; + if (firstLetterIndex == name.Length) + { + // Only underscores. + return name; + } + } + + // Only convert if it looks like title-case. (Avoid converting ALLCAPS.) + if (char.IsUpper(name[firstLetterIndex])) + { + for (int i = firstLetterIndex + 1; i < name.Length; i++) + { + if (char.IsLower(name[i])) + { + // Found at least one lowercase letter. Convert to camel-case and return. + char[] chars = name.ToCharArray(); + chars[firstLetterIndex] = char.ToLower(name[firstLetterIndex]); + return new string(chars); + } + } + } + + return name; + } + + private UnaryExpression JSMemberNameExpression(MemberInfo member) + { + string name = member.Name; + int lastDotIndex = name.LastIndexOf('.'); + if (lastDotIndex > 0) + { + // For explicit interface implementations, use the simple name. + name = name.Substring(lastDotIndex + 1); + } + + string jsName = AutoCamelCase ? ToCamelCase(name) : name; - StringBuilder sb = new(name); - sb[0] = char.ToLowerInvariant(sb[0]); - return sb.ToString(); + return Expression.Convert( + Expression.Constant(jsName), + typeof(JSValue), + typeof(JSValue).GetImplicitConversion(typeof(string), typeof(JSValue))); } /// @@ -603,14 +649,7 @@ public LambdaExpression BuildToJSMethodExpression(MethodInfo method) * } */ - // If the method is an explicit interface implementation, parse off the simple name. - // Then convert to JSValue for use as a JS property name. - int dotIndex = method.Name.LastIndexOf('.'); - Expression methodName = Expression.Convert( - Expression.Constant(ToCamelCase( - dotIndex >= 0 ? method.Name.Substring(dotIndex + 1) : method.Name)), - typeof(JSValue), - typeof(JSValue).GetImplicitConversion(typeof(string), typeof(JSValue))); + Expression methodName = JSMemberNameExpression(method); Expression ParameterToJSValue(int index) => InlineOrInvoke( GetToJSValueExpression(methodParameters[index].ParameterType), @@ -1114,10 +1153,7 @@ public LambdaExpression BuildToJSPropertyGetExpression(PropertyInfo property) * } */ - Expression propertyName = Expression.Convert( - Expression.Constant(ToCamelCase(property.Name)), - typeof(JSValue), - typeof(JSValue).GetImplicitConversion(typeof(string), typeof(JSValue))); + Expression propertyName = JSMemberNameExpression(property); Expression getStatement = Expression.Assign( resultVariable, @@ -1189,10 +1225,7 @@ public LambdaExpression BuildToJSPropertySetExpression(PropertyInfo property) * } */ - Expression propertyName = Expression.Convert( - Expression.Constant(ToCamelCase(property.Name)), - typeof(JSValue), - typeof(JSValue).GetImplicitConversion(typeof(string), typeof(JSValue))); + Expression propertyName = JSMemberNameExpression(property); Expression convertStatement = Expression.Assign( jsValueVariable, @@ -2962,7 +2995,8 @@ private IEnumerable BuildFromJSToStructExpressions( continue; } - Expression propertyName = Expression.Constant(ToCamelCase(property.Name)); + Expression propertyName = Expression.Constant( + AutoCamelCase ? ToCamelCase(property.Name) : property.Name); memberBindings.Add(Expression.Bind(property, InlineOrInvoke( GetFromJSValueExpression(property.PropertyType), Expression.Property(valueVariable, s_valueItem, propertyName), @@ -3021,7 +3055,8 @@ private IEnumerable BuildToJSFromStructExpressions( continue; } - Expression propertyName = Expression.Constant(ToCamelCase(property.Name)); + Expression propertyName = Expression.Constant( + AutoCamelCase ? ToCamelCase(property.Name) : property.Name); yield return Expression.Assign( Expression.Property(jsValueVariable, s_valueItem, propertyName), InlineOrInvoke( diff --git a/src/NodeApi.Generator/ModuleGenerator.cs b/src/NodeApi.Generator/ModuleGenerator.cs index 6671796c..d4c3bd87 100644 --- a/src/NodeApi.Generator/ModuleGenerator.cs +++ b/src/NodeApi.Generator/ModuleGenerator.cs @@ -859,7 +859,7 @@ public static string GetExportName(ISymbol symbol) } // Member names are automatically formatted as camelCase; type names are not. - return symbol is ITypeSymbol ? symbol.Name : ToCamelCase(symbol.Name); + return symbol is ITypeSymbol ? symbol.Name : JSMarshaller.ToCamelCase(symbol.Name); } /// diff --git a/src/NodeApi.Generator/SourceGenerator.cs b/src/NodeApi.Generator/SourceGenerator.cs index 4995435f..613a7e58 100644 --- a/src/NodeApi.Generator/SourceGenerator.cs +++ b/src/NodeApi.Generator/SourceGenerator.cs @@ -77,13 +77,6 @@ public static string GetFullName(ISymbol symbol) return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; } - public static string ToCamelCase(string name) - { - StringBuilder sb = new(name); - sb[0] = char.ToLowerInvariant(sb[0]); - return sb.ToString(); - } - public void ReportException(Exception ex) { // The compiler diagnostic will only show up to the first \r or \n. diff --git a/src/NodeApi.Generator/TypeDefinitionsGenerator.cs b/src/NodeApi.Generator/TypeDefinitionsGenerator.cs index f69f7bd3..2c749cb9 100644 --- a/src/NodeApi.Generator/TypeDefinitionsGenerator.cs +++ b/src/NodeApi.Generator/TypeDefinitionsGenerator.cs @@ -15,6 +15,7 @@ using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; +using Microsoft.JavaScript.NodeApi.DotNetHost; using Microsoft.JavaScript.NodeApi.Interop; using static Microsoft.JavaScript.NodeApi.DotNetHost.JSMarshaller; using NullabilityInfo = System.Reflection.NullabilityInfo; @@ -2010,7 +2011,7 @@ private string GetExportName(MemberInfo member) } } - return _autoCamelCase && member is not Type ? ToCamelCase(name) : name; + return _autoCamelCase && member is not Type ? JSMarshaller.ToCamelCase(name) : name; } }