Skip to content

Commit c91b04e

Browse files
Merge pull request #167 from JasonXuDeveloper/claude/optimize-serializer-performance-011CUoq3vskRx4B6SWwa2wih
feat(generator): add NinoRefDeserializationAttribute for custom objec…
2 parents 74d9767 + e3d2c2f commit c91b04e

File tree

8 files changed

+1028
-2
lines changed

8 files changed

+1028
-2
lines changed

src/Nino.Core/NinoDeserializer.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ public static void Deserialize<T>(ReadOnlySpan<byte> data, ref T value)
2121
DeserializeRef(ref value, ref reader);
2222
}
2323

24+
/// <summary>
25+
/// Deserialize data into an existing object reference (convenience overload for ReadOnlySpan)
26+
/// </summary>
27+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
28+
public static void DeserializeRef<T>(ref T value, ReadOnlySpan<byte> data)
29+
{
30+
var reader = new Reader(data);
31+
DeserializeRef(ref value, ref reader);
32+
}
33+
2434
// ULTIMATE: Zero-overhead single entry point
2535
[MethodImpl(MethodImplOptions.AggressiveInlining)]
2636
public static void DeserializeRef<T>(ref T value, ref Reader reader)
@@ -106,6 +116,16 @@ public static object DeserializeBoxed(ref Reader reader, Type type = null)
106116
return typeDeserializer.outOverload(ref reader);
107117
}
108118

119+
/// <summary>
120+
/// Deserialize data into an existing boxed object reference (convenience overload for ReadOnlySpan)
121+
/// </summary>
122+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
123+
public static void DeserializeRefBoxed(ref object val, ReadOnlySpan<byte> data, Type type = null)
124+
{
125+
var reader = new Reader(data);
126+
DeserializeRefBoxed(ref val, ref reader, type);
127+
}
128+
109129
[MethodImpl(MethodImplOptions.AggressiveInlining)]
110130
public static void DeserializeRefBoxed(ref object val, ref Reader reader, Type type = null)
111131
{
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
3+
namespace Nino.Core
4+
{
5+
/// <summary>
6+
/// Mark a public static parameterless method that returns an instance of the type.
7+
/// This method will be called to instantiate objects during deserialization,
8+
/// allowing custom object pooling or initialization logic.
9+
/// <para>
10+
/// The method must be:
11+
/// - Public
12+
/// - Static
13+
/// - Parameterless
14+
/// - Return the same type as the declaring class
15+
/// </para>
16+
/// <para>
17+
/// When this attribute is present:
18+
/// - In Deserialize: calls this method, then uses DeserializeRef to populate the instance
19+
/// - In DeserializeRef: calls this method only if the ref parameter is null
20+
/// </para>
21+
/// </summary>
22+
[AttributeUsage(AttributeTargets.Method)]
23+
public class NinoRefDeserializationAttribute : Attribute
24+
{
25+
}
26+
}

src/Nino.Generator/Common/DeserializerGenerator.Trivial.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,8 +1039,20 @@ private void WriteMembersWithCustomConstructor(SourceProductionContext spc,
10391039
string valName,
10401040
string[] constructorMember,
10411041
IMethodSymbol? constructor,
1042-
string indent = "")
1042+
string indent = "",
1043+
bool checkRefDeserializationNull = true)
10431044
{
1045+
// RefDeserializationMethod: only check null when needed (ref path)
1046+
// Skip for non-ref path where instance is already created
1047+
if (checkRefDeserializationNull && !string.IsNullOrEmpty(nt.RefDeserializationMethod))
1048+
{
1049+
sb.AppendLine($"{indent} if ({valName} == null)");
1050+
sb.AppendLine($"{indent} {{");
1051+
sb.AppendLine($"{indent} // use NinoRefDeserializationAttribute method: {nt.RefDeserializationMethod}");
1052+
sb.AppendLine($"{indent} {valName} = {nt.TypeSymbol.GetDisplayString()}.{nt.RefDeserializationMethod}();");
1053+
sb.AppendLine($"{indent} }}");
1054+
}
1055+
10441056
var (vars, privateVars, args, tupleMap) =
10451057
WriteMembers(valName, sb, nt, constructorMember, constructor == null, spc, indent);
10461058

@@ -1208,6 +1220,16 @@ private void CreateInstance(SourceProductionContext spc, StringBuilder sb,
12081220
return;
12091221
}
12101222

1223+
// Check for NinoRefDeserializationAttribute
1224+
if (!string.IsNullOrEmpty(nt.RefDeserializationMethod))
1225+
{
1226+
sb.AppendLine($"{indent} // use NinoRefDeserializationAttribute method: {nt.RefDeserializationMethod}");
1227+
sb.AppendLine($"{indent} {valName} = {nt.TypeSymbol.GetDisplayString()}.{nt.RefDeserializationMethod}();");
1228+
// Instance already created, skip null check
1229+
WriteMembersWithCustomConstructor(spc, sb, nt, valName, [], null, indent, checkRefDeserializationNull: false);
1230+
return;
1231+
}
1232+
12111233
//if this subtype contains a custom constructor, use it
12121234
//go through all constructors and find the one with the NinoConstructor attribute
12131235
//get constructors of the symbol

src/Nino.Generator/Metadata/NinoType.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class NinoType
1212
public ImmutableList<NinoType> Parents { get; set; }
1313
public string CustomSerializer { get; set; }
1414
public string CustomDeserializer { get; set; }
15+
public string RefDeserializationMethod { get; set; }
1516

1617
public NinoType(Compilation compilation, ITypeSymbol typeSymbol, ImmutableList<NinoMember>? members,
1718
ImmutableList<NinoType>? parents)
@@ -32,6 +33,7 @@ public NinoType(Compilation compilation, ITypeSymbol typeSymbol, ImmutableList<N
3233

3334
CustomSerializer = "";
3435
CustomDeserializer = "";
36+
RefDeserializationMethod = "";
3537

3638
var declaredTypeAssembly = typeSymbol.ContainingAssembly;
3739
bool isSameAssembly = declaredTypeAssembly.Equals(compilation.Assembly,
@@ -140,6 +142,11 @@ public override string ToString()
140142
sb.AppendLine($"CustomDeserializer: {CustomDeserializer}");
141143
}
142144

145+
if (!string.IsNullOrEmpty(RefDeserializationMethod))
146+
{
147+
sb.AppendLine($"RefDeserializationMethod: {RefDeserializationMethod}");
148+
}
149+
143150
sb.AppendLine("Parents:");
144151
foreach (var parent in Parents)
145152
{

src/Nino.Generator/NinoAnalyzer.cs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,56 @@ public override void Initialize(AnalysisContext context)
222222
SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration,
223223
SyntaxKind.InterfaceDeclaration, SyntaxKind.RecordDeclaration,
224224
SyntaxKind.RecordStructDeclaration);
225+
226+
// Validate NinoRefDeserializationAttribute usage - NINO012
227+
context.RegisterSymbolAction(
228+
symbolContext =>
229+
{
230+
var symbol = symbolContext.Symbol;
231+
if (symbol is not IMethodSymbol methodSymbol) return;
232+
233+
var hasAttribute = methodSymbol.GetAttributesCache().Any(a =>
234+
a.AttributeClass != null &&
235+
a.AttributeClass.ToDisplayString().EndsWith("NinoRefDeserializationAttribute"));
236+
237+
if (!hasAttribute) return;
238+
239+
var errors = new List<string>();
240+
241+
// Check if method is public
242+
if (methodSymbol.DeclaredAccessibility != Accessibility.Public)
243+
{
244+
errors.Add("must be public");
245+
}
246+
247+
// Check if method is static
248+
if (!methodSymbol.IsStatic)
249+
{
250+
errors.Add("must be static");
251+
}
252+
253+
// Check if method is parameterless
254+
if (methodSymbol.Parameters.Length > 0)
255+
{
256+
errors.Add("must be parameterless");
257+
}
258+
259+
// Check if method returns the containing type
260+
if (!SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, methodSymbol.ContainingType))
261+
{
262+
errors.Add($"must return {methodSymbol.ContainingType.ToDisplayString()}");
263+
}
264+
265+
if (errors.Count > 0)
266+
{
267+
symbolContext.ReportDiagnostic(Diagnostic.Create(
268+
SupportedDiagnostics[11], // NINO012
269+
methodSymbol.Locations.First(),
270+
methodSymbol.Name,
271+
string.Join(", ", errors)));
272+
}
273+
},
274+
SymbolKind.Method);
225275
}
226276

227277
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
@@ -279,7 +329,12 @@ public override void Initialize(AnalysisContext context)
279329
"Member type cannot be serialized",
280330
"Member '{0}' of type '{1}' in NinoType '{2}' has an unrecognizable type and will be skipped during serialization/deserialization",
281331
"Nino",
282-
DiagnosticSeverity.Warning, true)
332+
DiagnosticSeverity.Warning, true),
333+
new DiagnosticDescriptor("NINO012",
334+
"Invalid NinoRefDeserializationAttribute usage",
335+
"Method '{0}' with [NinoRefDeserialization] {1}",
336+
"Nino",
337+
DiagnosticSeverity.Error, true)
283338
);
284339

285340
private static bool IsValidNinoFormatter(INamedTypeSymbol formatterType, ITypeSymbol memberType)

src/Nino.Generator/Parser/CSharpParser.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,22 @@ void AddMembers(NinoType type)
354354
}
355355
}
356356

357+
//check for NinoRefDeserializationAttribute on public static parameterless methods
358+
var refDeserMethod = typeSymbol.GetMembers().OfType<IMethodSymbol>()
359+
.FirstOrDefault(m =>
360+
m.DeclaredAccessibility == Accessibility.Public &&
361+
m.IsStatic &&
362+
m.Parameters.Length == 0 &&
363+
SymbolEqualityComparer.Default.Equals(m.ReturnType, typeSymbol) &&
364+
m.GetAttributesCache().Any(a =>
365+
a.AttributeClass != null &&
366+
a.AttributeClass.ToDisplayString().EndsWith("NinoRefDeserializationAttribute")));
367+
368+
if (refDeserMethod != null)
369+
{
370+
ninoType.RefDeserializationMethod = refDeserMethod.Name;
371+
}
372+
357373
//add to map
358374
typeMap[typeSymbol] = ninoType;
359375

src/Nino.UnitTests/AnalyzerTest.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,4 +438,134 @@ await SetUpSourceGeneratorTest(code,
438438
.WithArguments("UnsupportedTask", "System.Threading.Tasks.Task<int>", "TestClassWithTask")
439439
).RunAsync();
440440
}
441+
442+
[TestMethod]
443+
public async Task TestNino012_NotPublic()
444+
{
445+
var code = @"
446+
using Nino.Core;
447+
448+
[NinoType]
449+
public class TestClass
450+
{
451+
[NinoRefDeserialization]
452+
private static TestClass GetInstance() => new TestClass();
453+
454+
public int Value;
455+
}
456+
";
457+
458+
await SetUpAnalyzerTest(code,
459+
Verify.Diagnostic("NINO012")
460+
.WithSpan(8, 30, 8, 41)
461+
.WithArguments("GetInstance", "must be public")).RunAsync();
462+
}
463+
464+
[TestMethod]
465+
public async Task TestNino012_NotStatic()
466+
{
467+
var code = @"
468+
using Nino.Core;
469+
470+
[NinoType]
471+
public class TestClass
472+
{
473+
[NinoRefDeserialization]
474+
public TestClass GetInstance() => new TestClass();
475+
476+
public int Value;
477+
}
478+
";
479+
480+
await SetUpAnalyzerTest(code,
481+
Verify.Diagnostic("NINO012")
482+
.WithSpan(8, 22, 8, 33)
483+
.WithArguments("GetInstance", "must be static")).RunAsync();
484+
}
485+
486+
[TestMethod]
487+
public async Task TestNino012_HasParameters()
488+
{
489+
var code = @"
490+
using Nino.Core;
491+
492+
[NinoType]
493+
public class TestClass
494+
{
495+
[NinoRefDeserialization]
496+
public static TestClass GetInstance(int value) => new TestClass();
497+
498+
public int Value;
499+
}
500+
";
501+
502+
await SetUpAnalyzerTest(code,
503+
Verify.Diagnostic("NINO012")
504+
.WithSpan(8, 29, 8, 40)
505+
.WithArguments("GetInstance", "must be parameterless")).RunAsync();
506+
}
507+
508+
[TestMethod]
509+
public async Task TestNino012_WrongReturnType()
510+
{
511+
var code = @"
512+
using Nino.Core;
513+
514+
[NinoType]
515+
public class TestClass
516+
{
517+
[NinoRefDeserialization]
518+
public static object GetInstance() => new TestClass();
519+
520+
public int Value;
521+
}
522+
";
523+
524+
await SetUpAnalyzerTest(code,
525+
Verify.Diagnostic("NINO012")
526+
.WithSpan(8, 26, 8, 37)
527+
.WithArguments("GetInstance", "must return TestClass")).RunAsync();
528+
}
529+
530+
[TestMethod]
531+
public async Task TestNino012_MultipleErrors()
532+
{
533+
var code = @"
534+
using Nino.Core;
535+
536+
[NinoType]
537+
public class TestClass
538+
{
539+
[NinoRefDeserialization]
540+
private object GetInstance(int value) => new TestClass();
541+
542+
public int Value;
543+
}
544+
";
545+
546+
await SetUpAnalyzerTest(code,
547+
Verify.Diagnostic("NINO012")
548+
.WithSpan(8, 20, 8, 31)
549+
.WithArguments("GetInstance", "must be public, must be static, must be parameterless, must return TestClass")).RunAsync();
550+
}
551+
552+
[TestMethod]
553+
public async Task TestNino012_ValidUsage()
554+
{
555+
var code = @"
556+
using Nino.Core;
557+
558+
[NinoType]
559+
public class TestClass
560+
{
561+
[NinoRefDeserialization]
562+
public static TestClass GetInstance() => new TestClass();
563+
564+
public int Value;
565+
}
566+
";
567+
568+
// No diagnostics expected for valid usage
569+
await SetUpAnalyzerTest(code).RunAsync();
570+
}
441571
}

0 commit comments

Comments
 (0)