diff --git a/src/UtilityVerse.Copy/Analyzers/ValidationAnalyzer.cs b/src/UtilityVerse.Copy/Analyzers/ValidationAnalyzer.cs index 6207a06..d9332ff 100644 --- a/src/UtilityVerse.Copy/Analyzers/ValidationAnalyzer.cs +++ b/src/UtilityVerse.Copy/Analyzers/ValidationAnalyzer.cs @@ -9,125 +9,117 @@ namespace UtilityVerse.Copy.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ValidationAnalyzer : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics => - [Rules.MissingPartialRule, Rules.DeepRule, Rules.ShallowRule]; - - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterCompilationStartAction(compilationContext => - { - compilationContext.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType); - }); - } - - private static void AnalyzeNamedType(SymbolAnalysisContext context) - { - if (context.Symbol is not INamedTypeSymbol namedType) - return; - - if (namedType.TypeKind is not (TypeKind.Class or TypeKind.Struct)) - return; - - if (namedType.GetAttributes().Any(attr => - attr.AttributeClass?.ToDisplayString() is - "System.CodeDom.Compiler.GeneratedCodeAttribute" or - "System.Runtime.CompilerServices.CompilerGeneratedAttribute")) - return; - - var syntaxRef = namedType.DeclaringSyntaxReferences.FirstOrDefault(); - if (syntaxRef?.GetSyntax() is not TypeDeclarationSyntax typeSyntax) - return; - - var hasDeepCopy = Helper.HasDeepCopyOptIn(namedType); - var hasShallowCopy = Helper.HasShallowCopyOptIn(namedType); - - if ((hasDeepCopy || hasShallowCopy) && !Helper.IsPartial(namedType)) - { - var diagnostic = Diagnostic.Create(Rules.MissingPartialRule, typeSyntax.Identifier.GetLocation(), namedType.Name); - context.ReportDiagnostic(diagnostic); - } - - if (!hasDeepCopy && !hasShallowCopy) - return; - - foreach (var prop in Helper.GetAllProperties(namedType).Where(p => !p.IsStatic)) - { - AnalyzeTypeRecursive( - context, - prop.Type, - prop.Locations.FirstOrDefault() ?? typeSyntax.Identifier.GetLocation(), - prop.Name, - namedType, - hasDeepCopy, - hasShallowCopy - ); - } - } - - private static void AnalyzeTypeRecursive( - SymbolAnalysisContext context, - ITypeSymbol type, - Location diagnosticLocation, - string propertyName, - INamedTypeSymbol parentType, - bool requireDeepCopy, - bool requireShallowCopy) - { - if (type is not INamedTypeSymbol namedType) - return; - - var unwrapped = UnwrapNullable(namedType); - - if (Helper.IsTrulyPrimitive(unwrapped)) - return; - - if (Helper.IsGenericCollection(unwrapped.OriginalDefinition.ToDisplayString(), out _)) - { - foreach (var typeArg in unwrapped.TypeArguments.OfType()) - { - AnalyzeTypeRecursive(context, typeArg, diagnosticLocation, propertyName, parentType, requireDeepCopy, requireShallowCopy); - } - return; - } - - if (requireDeepCopy && !Helper.HasDeepCopyOptIn(unwrapped)) - { - var diag = Diagnostic.Create(Rules.DeepRule, diagnosticLocation, - propertyName, parentType.Name, unwrapped.Name); - context.ReportDiagnostic(diag); - } - - if (requireShallowCopy && !Helper.HasShallowCopyOptIn(unwrapped)) - { - var diag = Diagnostic.Create(Rules.ShallowRule, diagnosticLocation, - propertyName, parentType.Name, unwrapped.Name); - context.ReportDiagnostic(diag); - } - } - - private static INamedTypeSymbol UnwrapNullable(INamedTypeSymbol type) - { - return type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && - type.TypeArguments.FirstOrDefault() is INamedTypeSymbol underlying - ? underlying - : type; - } - - private static bool IsInsideCopyEnabledPartialParent(INamedTypeSymbol? symbol) - { - while (symbol is not null) - { - if (!Helper.IsPartial(symbol)) - return false; - - if (Helper.HasDeepCopyOptIn(symbol) || Helper.HasShallowCopyOptIn(symbol)) - return true; - - symbol = symbol.ContainingType; - } - return false; - } + public override ImmutableArray SupportedDiagnostics => + [Rules.MissingPartialRule, Rules.DeepRule, Rules.ShallowRule]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationContext => + { + compilationContext.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType); + }); + } + + private static void AnalyzeNamedType(SymbolAnalysisContext context) + { + if (context.Symbol is not INamedTypeSymbol namedType) + return; + + if (namedType.TypeKind is not (TypeKind.Class or TypeKind.Struct)) + return; + + if (namedType.GetAttributes().Any(attr => + attr.AttributeClass?.ToDisplayString() is + "System.CodeDom.Compiler.GeneratedCodeAttribute" or + "System.Runtime.CompilerServices.CompilerGeneratedAttribute")) + return; + + var syntaxRef = namedType.DeclaringSyntaxReferences.FirstOrDefault(); + if (syntaxRef?.GetSyntax() is not TypeDeclarationSyntax typeSyntax) + return; + + var hasDeepCopy = Helper.HasDeepCopyOptIn(namedType); + var hasShallowCopy = Helper.HasShallowCopyOptIn(namedType); + + if ((hasDeepCopy || hasShallowCopy) && !Helper.IsPartial(namedType)) + { + var diagnostic = Diagnostic.Create(Rules.MissingPartialRule, typeSyntax.Identifier.GetLocation(), namedType.Name); + context.ReportDiagnostic(diagnostic); + } + + if (!hasDeepCopy && !hasShallowCopy) + return; + + foreach (var prop in Helper.GetAllProperties(namedType).Where(p => !p.IsStatic)) + { + AnalyzeTypeRecursive( + context, + prop.Type, + prop.Locations.FirstOrDefault() ?? typeSyntax.Identifier.GetLocation(), + prop.Name, + namedType, + hasDeepCopy, + hasShallowCopy + ); + } + } + + private static void AnalyzeTypeRecursive( + SymbolAnalysisContext context, + ITypeSymbol type, + Location diagnosticLocation, + string propertyName, + INamedTypeSymbol parentType, + bool requireDeepCopy, + bool requireShallowCopy) + { + if (type is not INamedTypeSymbol namedType) + return; + + var unwrapped = Helper.UnwrapNullable(namedType); + + if (Helper.IsTrulyPrimitive(unwrapped) || Helper.IsBuiltinType(unwrapped)) + return; + + if (Helper.IsGenericCollection(unwrapped.OriginalDefinition.ToDisplayString(), out _)) + { + foreach (var typeArg in unwrapped.TypeArguments.OfType()) + { + AnalyzeTypeRecursive(context, typeArg, diagnosticLocation, propertyName, parentType, requireDeepCopy, requireShallowCopy); + } + return; + } + + if (requireDeepCopy && !Helper.HasDeepCopyOptIn(unwrapped)) + { + var diag = Diagnostic.Create(Rules.DeepRule, diagnosticLocation, + propertyName, parentType.Name, unwrapped.Name); + context.ReportDiagnostic(diag); + } + + if (requireShallowCopy && !Helper.HasShallowCopyOptIn(unwrapped)) + { + var diag = Diagnostic.Create(Rules.ShallowRule, diagnosticLocation, + propertyName, parentType.Name, unwrapped.Name); + context.ReportDiagnostic(diag); + } + } + + private static bool IsInsideCopyEnabledPartialParent(INamedTypeSymbol? symbol) + { + while (symbol is not null) + { + if (!Helper.IsPartial(symbol)) + return false; + + if (Helper.HasDeepCopyOptIn(symbol) || Helper.HasShallowCopyOptIn(symbol)) + return true; + + symbol = symbol.ContainingType; + } + return false; + } } diff --git a/src/UtilityVerse.Copy/Generators/DeepCopyGenerator.cs b/src/UtilityVerse.Copy/Generators/DeepCopyGenerator.cs index 0677cc8..fd5b564 100644 --- a/src/UtilityVerse.Copy/Generators/DeepCopyGenerator.cs +++ b/src/UtilityVerse.Copy/Generators/DeepCopyGenerator.cs @@ -12,30 +12,30 @@ namespace UtilityVerse.Copy.Generators; internal static class DeepCopyGenerator { - internal static string? Generate(INamedTypeSymbol typeSymbol) - { - if (typeSymbol.IsAbstract) return string.Empty; - if (!Helper.IsPartial(typeSymbol)) return string.Empty; + internal static string? Generate(INamedTypeSymbol typeSymbol) + { + if (typeSymbol.IsAbstract) return string.Empty; + if (!Helper.IsPartial(typeSymbol)) return string.Empty; - var isGlobalNamespace = typeSymbol.ContainingNamespace.IsGlobalNamespace; - var namespaceName = isGlobalNamespace ? null : typeSymbol.ContainingNamespace.ToDisplayString(); - var typeName = typeSymbol.Name; + var isGlobalNamespace = typeSymbol.ContainingNamespace.IsGlobalNamespace; + var namespaceName = isGlobalNamespace ? null : typeSymbol.ContainingNamespace.ToDisplayString(); + var typeName = typeSymbol.Name; - var typeKind = typeSymbol switch - { - { TypeKind: TypeKind.Class, IsRecord: true } => "record", - { TypeKind: TypeKind.Struct, IsRecord: true } => "record struct", - { TypeKind: TypeKind.Class } => "class", - { TypeKind: TypeKind.Struct } => "struct", - _ => null - }; + var typeKind = typeSymbol switch + { + { TypeKind: TypeKind.Class, IsRecord: true } => "record", + { TypeKind: TypeKind.Struct, IsRecord: true } => "record struct", + { TypeKind: TypeKind.Class } => "class", + { TypeKind: TypeKind.Struct } => "struct", + _ => null + }; - if (typeKind == null) - return string.Empty; + if (typeKind == null) + return string.Empty; - var sb = new StringBuilder(); + var sb = new StringBuilder(); - sb.AppendLine(""" + sb.AppendLine(""" // // This code was generated by Copy. @@ -54,156 +54,156 @@ internal static class DeepCopyGenerator """); - if (!string.IsNullOrEmpty(namespaceName)) - { - sb.AppendLine($"namespace {namespaceName}"); - sb.AppendLine("{"); - } - - sb.AppendLine($" public partial {typeKind} {typeName}"); - sb.AppendLine(" {"); - sb.AppendLine(" [System.CodeDom.Compiler.GeneratedCode(\"DeepCopyGenerator\", \"1.0\")]"); - sb.AppendLine($" public {typeName} DeepCopy()"); - sb.AppendLine(" {"); - - var hasParameterlessCtor = typeSymbol.InstanceConstructors.Any(c => c.Parameters.Length == 0); - - if (!hasParameterlessCtor) - { - var ctor = typeSymbol.InstanceConstructors - .OrderByDescending(c => c.Parameters.Length) - .FirstOrDefault(); - - if (ctor != null) - { - var args = ctor.Parameters.Select(p => - { - var prop = Helper.GetAllProperties(typeSymbol) - .FirstOrDefault(m => m.Name.Equals(p.Name, System.StringComparison.OrdinalIgnoreCase)); - - if (prop == null) - return $"default({p.Type.ToDisplayString()})"; - - return Helper.IsTrulyPrimitive(prop.Type) - ? $"this.{prop.Name}" - : $"this.{prop.Name}?.DeepCopy()"; - }); - - sb.AppendLine($" return new {typeName}({string.Join(", ", args)});"); - } - else - { - // No constructor found - return empty string (no code generated) - if (!string.IsNullOrEmpty(namespaceName)) - sb.AppendLine(" }"); - sb.AppendLine(" }"); - if (!string.IsNullOrEmpty(namespaceName)) - sb.AppendLine("}"); - return string.Empty; - } - } - else - { - sb.AppendLine($@" return new {typeName} + if (!string.IsNullOrEmpty(namespaceName)) + { + sb.AppendLine($"namespace {namespaceName}"); + sb.AppendLine("{"); + } + + sb.AppendLine($" public partial {typeKind} {typeName}"); + sb.AppendLine(" {"); + sb.AppendLine(" [System.CodeDom.Compiler.GeneratedCode(\"DeepCopyGenerator\", \"1.0\")]"); + sb.AppendLine($" public {typeName} DeepCopy()"); + sb.AppendLine(" {"); + + var hasParameterlessCtor = typeSymbol.InstanceConstructors.Any(c => c.Parameters.Length == 0); + + if (!hasParameterlessCtor) + { + var ctor = typeSymbol.InstanceConstructors + .OrderByDescending(c => c.Parameters.Length) + .FirstOrDefault(); + + if (ctor != null) + { + var args = ctor.Parameters.Select(p => + { + var prop = Helper.GetAllProperties(typeSymbol) + .FirstOrDefault(m => m.Name.Equals(p.Name, System.StringComparison.OrdinalIgnoreCase)); + + if (prop == null) + return $"default({p.Type.ToDisplayString()})"; + + return (Helper.IsTrulyPrimitive(prop.Type) || Helper.IsBuiltinType(prop.Type)) + ? $"this.{prop.Name}" + : $"this.{prop.Name}?.DeepCopy()"; + }); + + sb.AppendLine($" return new {typeName}({string.Join(", ", args)});"); + } + else + { + // No constructor found - return empty string (no code generated) + if (!string.IsNullOrEmpty(namespaceName)) + sb.AppendLine(" }"); + sb.AppendLine(" }"); + if (!string.IsNullOrEmpty(namespaceName)) + sb.AppendLine("}"); + return string.Empty; + } + } + else + { + sb.AppendLine($@" return new {typeName} {{"); - foreach (var member in Helper.GetAllProperties(typeSymbol)) - { - if (member.IsReadOnly || member.SetMethod is null) - continue; - - var propType = member.Type; - string assignment; - - if (Helper.IsTrulyPrimitive(propType)) - { - assignment = $"this.{member.Name}"; - } - else switch (propType) - { - case IArrayTypeSymbol arrayType: - { - var elementType = arrayType.ElementType; - assignment = Helper.IsTrulyPrimitive(elementType) - ? $"this.{member.Name}?.ToArray()" - : $"this.{member.Name}?.Select(x => x?.DeepCopy()).ToArray()"; - break; - } - case INamedTypeSymbol { IsGenericType: true } named: - { - var original = named.OriginalDefinition.ToDisplayString(); - var typeArgs = named.TypeArguments; - - if (Helper.IsGenericCollection(original, out var collectionKind)) - { - var elementType = typeArgs[0]; - var cloneExpr = Helper.IsTrulyPrimitive(elementType) - ? "x" - : "x?.DeepCopy()"; - - if (collectionKind == "HashSet") - { - assignment = - $"this.{member.Name} != null ? new HashSet<{elementType.ToDisplayString()}>(this.{member.Name}.Select(x => {cloneExpr}).ToList()) : null"; - } - else if (collectionKind == "Dictionary") - { - var keyType = typeArgs[0]; - var valueType = typeArgs[1]; - - var keyCopy = Helper.IsTrulyPrimitive(keyType) ? "x.Key" : "x.Key?.DeepCopy()"; - var valueCopy = Helper.IsTrulyPrimitive(valueType) ? "x.Value" : "x.Value?.DeepCopy()"; - - assignment = $"this.{member.Name}?.ToDictionary(x => {keyCopy}, x => {valueCopy})"; - } - else - { - // For List, IEnumerable, Collection, ReadOnlyCollection, etc. - assignment = $"this.{member.Name}?.Select(x => {cloneExpr}).ToList()"; - } - } - else if (original == "System.Collections.Generic.Dictionary") - { - var keyType = typeArgs[0]; - var valueType = typeArgs[1]; - - assignment = $"this.{member.Name}?.ToDictionary(" + - $"x => {(Helper.IsTrulyPrimitive(keyType) ? "x.Key" : "x.Key?.DeepCopy()")}, " + - $"x => {(Helper.IsTrulyPrimitive(valueType) ? "x.Value" : "x.Value?.DeepCopy()")})"; - } - else if (original.StartsWith("System.Tuple") || original.StartsWith("System.ValueTuple")) - { - var tupleArgs = typeArgs.Select((arg, i) => - Helper.IsTrulyPrimitive(arg) ? $"x.Item{i + 1}" : $"x.Item{i + 1}?.DeepCopy()"); - - assignment = $"this.{member.Name} is {{ }} x ? new {propType.ToDisplayString()}({string.Join(", ", tupleArgs)}) : default"; - } - else - { - assignment = $"this.{member.Name}?.DeepCopy()"; - } - - break; - } - default: - assignment = $"this.{member.Name}?.DeepCopy()"; - break; - } - - sb.AppendLine($" {member.Name} = {assignment},"); - } - - sb.AppendLine(@" };"); - } - - sb.AppendLine(" }"); - sb.AppendLine(" }"); - - if (!string.IsNullOrEmpty(namespaceName)) - { - sb.AppendLine("}"); - } - - return sb.ToString(); - } + foreach (var member in Helper.GetAllProperties(typeSymbol)) + { + if (member.IsReadOnly || member.SetMethod is null) + continue; + + var propType = member.Type; + string assignment; + + if (Helper.IsTrulyPrimitive(propType) || Helper.IsBuiltinType(propType)) + { + assignment = $"this.{member.Name}"; + } + else switch (propType) + { + case IArrayTypeSymbol arrayType: + { + var elementType = arrayType.ElementType; + assignment = (Helper.IsTrulyPrimitive(elementType) || Helper.IsBuiltinType(elementType)) + ? $"this.{member.Name}?.ToArray()" + : $"this.{member.Name}?.Select(x => x?.DeepCopy()).ToArray()"; + break; + } + case INamedTypeSymbol { IsGenericType: true } named: + { + var original = named.OriginalDefinition.ToDisplayString(); + var typeArgs = named.TypeArguments; + + if (Helper.IsGenericCollection(original, out var collectionKind)) + { + var elementType = typeArgs[0]; + var cloneExpr = (Helper.IsTrulyPrimitive(elementType) || Helper.IsBuiltinType(elementType)) + ? "x" + : "x?.DeepCopy()"; + + if (collectionKind == "HashSet") + { + assignment = + $"this.{member.Name} != null ? new HashSet<{elementType.ToDisplayString()}>(this.{member.Name}.Select(x => {cloneExpr}).ToList()) : null"; + } + else if (collectionKind == "Dictionary") + { + var keyType = typeArgs[0]; + var valueType = typeArgs[1]; + + var keyCopy = (Helper.IsTrulyPrimitive(keyType) || Helper.IsBuiltinType(keyType)) ? "x.Key" : "x.Key?.DeepCopy()"; + var valueCopy = (Helper.IsTrulyPrimitive(valueType) || Helper.IsBuiltinType(valueType)) ? "x.Value" : "x.Value?.DeepCopy()"; + + assignment = $"this.{member.Name}?.ToDictionary(x => {keyCopy}, x => {valueCopy})"; + } + else + { + // For List, IEnumerable, Collection, ReadOnlyCollection, etc. + assignment = $"this.{member.Name}?.Select(x => {cloneExpr}).ToList()"; + } + } + else if (original == "System.Collections.Generic.Dictionary") + { + var keyType = typeArgs[0]; + var valueType = typeArgs[1]; + + assignment = $"this.{member.Name}?.ToDictionary(" + + $"x => {((Helper.IsTrulyPrimitive(keyType) || Helper.IsBuiltinType(keyType)) ? "x.Key" : "x.Key?.DeepCopy()")}, " + + $"x => {((Helper.IsTrulyPrimitive(valueType) || Helper.IsBuiltinType(valueType)) ? "x.Value" : "x.Value?.DeepCopy()")})"; + } + else if (original.StartsWith("System.Tuple") || original.StartsWith("System.ValueTuple")) + { + var tupleArgs = typeArgs.Select((arg, i) => + (Helper.IsTrulyPrimitive(arg) || Helper.IsBuiltinType(arg)) ? $"x.Item{i + 1}" : $"x.Item{i + 1}?.DeepCopy()"); + + assignment = $"this.{member.Name} is {{ }} x ? new {propType.ToDisplayString()}({string.Join(", ", tupleArgs)}) : default"; + } + else + { + assignment = $"this.{member.Name}?.DeepCopy()"; + } + + break; + } + default: + assignment = $"this.{member.Name}?.DeepCopy()"; + break; + } + + sb.AppendLine($" {member.Name} = {assignment},"); + } + + sb.AppendLine(@" };"); + } + + sb.AppendLine(" }"); + sb.AppendLine(" }"); + + if (!string.IsNullOrEmpty(namespaceName)) + { + sb.AppendLine("}"); + } + + return sb.ToString(); + } } \ No newline at end of file diff --git a/src/UtilityVerse.Copy/Generators/ShallowCopyGenerator.cs b/src/UtilityVerse.Copy/Generators/ShallowCopyGenerator.cs index f3c1da9..fa4886d 100644 --- a/src/UtilityVerse.Copy/Generators/ShallowCopyGenerator.cs +++ b/src/UtilityVerse.Copy/Generators/ShallowCopyGenerator.cs @@ -12,30 +12,30 @@ namespace UtilityVerse.Copy.Generators; internal static class ShallowCopyGenerator { - internal static string? Generate(INamedTypeSymbol typeSymbol) - { - if (typeSymbol.IsAbstract) return string.Empty; - if (!Helper.IsPartial(typeSymbol)) return string.Empty; + internal static string? Generate(INamedTypeSymbol typeSymbol) + { + if (typeSymbol.IsAbstract) return string.Empty; + if (!Helper.IsPartial(typeSymbol)) return string.Empty; - var isGlobalNamespace = typeSymbol.ContainingNamespace.IsGlobalNamespace; - var namespaceName = isGlobalNamespace ? null : typeSymbol.ContainingNamespace.ToDisplayString(); - var typeName = typeSymbol.Name; + var isGlobalNamespace = typeSymbol.ContainingNamespace.IsGlobalNamespace; + var namespaceName = isGlobalNamespace ? null : typeSymbol.ContainingNamespace.ToDisplayString(); + var typeName = typeSymbol.Name; - var typeKind = typeSymbol switch - { - { TypeKind: TypeKind.Class, IsRecord: true } => "record", - { TypeKind: TypeKind.Struct, IsRecord: true } => "record struct", - { TypeKind: TypeKind.Class } => "class", - { TypeKind: TypeKind.Struct } => "struct", - _ => null - }; + var typeKind = typeSymbol switch + { + { TypeKind: TypeKind.Class, IsRecord: true } => "record", + { TypeKind: TypeKind.Struct, IsRecord: true } => "record struct", + { TypeKind: TypeKind.Class } => "class", + { TypeKind: TypeKind.Struct } => "struct", + _ => null + }; - if (typeKind == null) - return null; + if (typeKind == null) + return null; - var sb = new StringBuilder(); + var sb = new StringBuilder(); - sb.AppendLine(""" + sb.AppendLine(""" // // This code was generated by Copy. @@ -54,93 +54,94 @@ internal static class ShallowCopyGenerator """); - if (!string.IsNullOrEmpty(namespaceName)) - { - sb.AppendLine($"namespace {namespaceName}"); - sb.AppendLine("{"); - } - - sb.AppendLine($" public partial {typeKind} {typeName}"); - sb.AppendLine(" {"); - sb.AppendLine(" [System.CodeDom.Compiler.GeneratedCode(\"ShallowCopyGenerator\", \"1.0\")]"); - sb.AppendLine($" public {typeName} ShallowCopy()"); - sb.AppendLine(" {"); - - if (typeSymbol.TypeKind == TypeKind.Class || typeSymbol.IsRecord) - { - sb.AppendLine($" var copy = ({typeName})this.MemberwiseClone();"); - - foreach (var member in Helper.GetAllProperties(typeSymbol)) - { - if (member.IsReadOnly || member.SetMethod is null) - continue; - - var propType = member.Type; - if (Helper.IsPrimitive(propType)) continue; - - string? assignment = null; - - switch (propType) - { - case IArrayTypeSymbol: - assignment = $"this.{member.Name} != null ? ({member.Type.ToDisplayString()})this.{member.Name}.Clone() : null"; - break; - - case INamedTypeSymbol named when named.IsGenericType: - { - var original = named.OriginalDefinition.ConstructUnboundGenericType().ToDisplayString(); - - if (Helper.IsGenericCollection(original, out var kind)) - { - var itemType = named.TypeArguments.FirstOrDefault()?.ToDisplayString() ?? "var"; - assignment = kind switch - { - "List" or "Enumerable" => - $"this.{member.Name}?.ToList()", - - "HashSet" => - $"this.{member.Name} != null ? new HashSet<{itemType}>(this.{member.Name}) : null", - - "Dictionary" => - $"this.{member.Name}?.ToDictionary(x => x.Key, x => x.Value)", - - _ => null - }; - } - else if (original.StartsWith("System.Tuple")) - { - assignment = $"this.{member.Name}"; - } - - break; - } - - case INamedTypeSymbol named when named.ToDisplayString().StartsWith("System.ValueTuple"): - assignment = $"this.{member.Name}"; - break; - } - - if (assignment != null) - { - sb.AppendLine($" copy.{member.Name} = {assignment};"); - } - } - - sb.AppendLine(" return copy;"); - } - else - { - sb.AppendLine(" return this;"); - } - - sb.AppendLine(" }"); - sb.AppendLine(" }"); - - if (!string.IsNullOrEmpty(namespaceName)) - { - sb.AppendLine("}"); - } - - return sb.ToString(); - } + if (!string.IsNullOrEmpty(namespaceName)) + { + sb.AppendLine($"namespace {namespaceName}"); + sb.AppendLine("{"); + } + + sb.AppendLine($" public partial {typeKind} {typeName}"); + sb.AppendLine(" {"); + sb.AppendLine(" [System.CodeDom.Compiler.GeneratedCode(\"ShallowCopyGenerator\", \"1.0\")]"); + sb.AppendLine($" public {typeName} ShallowCopy()"); + sb.AppendLine(" {"); + + if (typeSymbol.TypeKind == TypeKind.Class || typeSymbol.IsRecord) + { + sb.AppendLine($" var copy = ({typeName})this.MemberwiseClone();"); + + foreach (var member in Helper.GetAllProperties(typeSymbol)) + { + if (member.IsReadOnly || member.SetMethod is null) + continue; + + var propType = member.Type; + + if (Helper.IsTrulyPrimitive(propType) || Helper.IsBuiltinType(propType)) continue; + + string? assignment = null; + + switch (propType) + { + case IArrayTypeSymbol: + assignment = $"this.{member.Name} != null ? ({member.Type.ToDisplayString()})this.{member.Name}.Clone() : null"; + break; + + case INamedTypeSymbol named when named.IsGenericType: + { + var original = named.OriginalDefinition.ConstructUnboundGenericType().ToDisplayString(); + + if (Helper.IsGenericCollection(original, out var kind)) + { + var itemType = named.TypeArguments.FirstOrDefault()?.ToDisplayString() ?? "var"; + assignment = kind switch + { + "List" or "Enumerable" => + $"this.{member.Name}?.ToList()", + + "HashSet" => + $"this.{member.Name} != null ? new HashSet<{itemType}>(this.{member.Name}) : null", + + "Dictionary" => + $"this.{member.Name}?.ToDictionary(x => x.Key, x => x.Value)", + + _ => null + }; + } + else if (original.StartsWith("System.Tuple")) + { + assignment = $"this.{member.Name}"; + } + + break; + } + + case INamedTypeSymbol named when named.ToDisplayString().StartsWith("System.ValueTuple"): + assignment = $"this.{member.Name}"; + break; + } + + if (assignment != null) + { + sb.AppendLine($" copy.{member.Name} = {assignment};"); + } + } + + sb.AppendLine(" return copy;"); + } + else + { + sb.AppendLine(" return this;"); + } + + sb.AppendLine(" }"); + sb.AppendLine(" }"); + + if (!string.IsNullOrEmpty(namespaceName)) + { + sb.AppendLine("}"); + } + + return sb.ToString(); + } } \ No newline at end of file diff --git a/src/UtilityVerse.Copy/Helper.cs b/src/UtilityVerse.Copy/Helper.cs index 83ce90d..76591da 100644 --- a/src/UtilityVerse.Copy/Helper.cs +++ b/src/UtilityVerse.Copy/Helper.cs @@ -8,109 +8,145 @@ namespace UtilityVerse.Copy; internal static class Helper { - internal static bool IsPrimitive(ITypeSymbol symbol) => - symbol.IsValueType || symbol.SpecialType == SpecialType.System_String; - - internal static bool IsTrulyPrimitive(ITypeSymbol symbol) - { - return symbol.SpecialType switch - { - SpecialType.System_Boolean or - SpecialType.System_Byte or - SpecialType.System_SByte or - SpecialType.System_Int16 or - SpecialType.System_UInt16 or - SpecialType.System_Int32 or - SpecialType.System_UInt32 or - SpecialType.System_Int64 or - SpecialType.System_UInt64 or - SpecialType.System_Single or - SpecialType.System_Double or - SpecialType.System_Char or - SpecialType.System_Enum or - SpecialType.System_String => true, - _ => false - }; - } - - internal static bool IsGenericCollection(string? originalDef, out string? kind) - { - kind = originalDef switch - { - "System.Collections.Generic.List" => "List", - "System.Collections.Generic.IList" => "List", - "System.Collections.ObjectModel.Collection" => "List", - "System.Collections.ObjectModel.ReadOnlyCollection" => "List", - "System.Collections.ObjectModel.ObservableCollection" => "List", - "System.ComponentModel.BindingList" => "List", - "System.Collections.Immutable.ImmutableList" => "List", - "System.Collections.Generic.ReadOnlyList" => "List", - - "System.Collections.Generic.IEnumerable" => "Enumerable", - "System.Collections.Generic.ICollection" => "Enumerable", - "System.Collections.Generic.IReadOnlyCollection" => "Enumerable", - "System.Collections.Generic.IReadOnlyList" => "Enumerable", - - "System.Collections.Generic.HashSet" => "HashSet", - - "System.Collections.Generic.Dictionary" => "Dictionary", - "System.Collections.Frozen.FrozenDictionary" => "Dictionary", - "System.Collections.Concurrent.ConcurrentDictionary" => "Dictionary", - - "System.Collections.Immutable.ImmutableArray" => "List", - "System.Collections.Immutable.ImmutableSortedSet" => "HashSet", - "System.Collections.Immutable.ImmutableHashSet" => "HashSet", - "System.Collections.Immutable.ImmutableDictionary" => "Dictionary", - "System.Collections.Immutable.ImmutableSortedDictionary" => "Dictionary", - - "System.Collections.Generic.Queue" => "Queue", - "System.Collections.Generic.Stack" => "Stack", - - _ => null - }; - - return kind != null; - } - - internal static IEnumerable GetAllProperties(INamedTypeSymbol? type) - { - while (type != null && type.SpecialType != SpecialType.System_Object) - { - foreach (var member in type.GetMembers().OfType()) - { - if (!member.IsReadOnly && member.SetMethod != null) - yield return member; - } - - type = type.BaseType; - } - } - - internal static bool IsPartial(INamedTypeSymbol typeSymbol) - { - return typeSymbol.DeclaringSyntaxReferences.Any(syntaxRef => - syntaxRef.GetSyntax() is TypeDeclarationSyntax syntax && - syntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))); - } - - internal static bool HasDeepCopyOptIn(INamedTypeSymbol typeSymbol) - { - return typeSymbol.GetAttributes().Any(a => - SymbolEquals(a.AttributeClass, "UtilityVerse.Copy.DeepCopy")) || - typeSymbol.AllInterfaces.Any(i => - SymbolEquals(i, "UtilityVerse.Copy.IDeepCopy")); - } - - internal static bool HasShallowCopyOptIn(INamedTypeSymbol typeSymbol) - { - return typeSymbol.GetAttributes().Any(a => - SymbolEquals(a.AttributeClass, "UtilityVerse.Copy.ShallowCopy")) || - typeSymbol.AllInterfaces.Any(i => - SymbolEquals(i, "UtilityVerse.Copy.IShallowCopy")); - } - - private static bool SymbolEquals(ISymbol? symbol, string fullyQualifiedMetadataName) - { - return symbol?.ToDisplayString() == fullyQualifiedMetadataName; - } + internal static bool IsTrulyPrimitive(ITypeSymbol symbol) + { + return symbol.IsValueType || symbol.TypeKind is TypeKind.Enum || symbol.SpecialType switch + { + SpecialType.System_Boolean or + SpecialType.System_Byte or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_UInt16 or + SpecialType.System_Int32 or + SpecialType.System_UInt32 or + SpecialType.System_Int64 or + SpecialType.System_UInt64 or + SpecialType.System_Single or + SpecialType.System_Double or + SpecialType.System_Char or + SpecialType.System_Enum or + SpecialType.System_String => true, + _ => false + }; + } + + internal static bool IsBuiltinType(ITypeSymbol symbol) + { + return symbol.ToDisplayString() switch + { + "System.DateTime" or + "System.DateOnly" or + "System.DateTimeOffset" or + "System.Guid" or + "System.TimeSpan" or + "System.Decimal" or + "System.Uri" or + "System.Version" or + "System.Numerics.BigInteger" or + "System.Numerics.Complex" or + "System.Index" or + "System.Range" or + "System.Half" or + "System.Int128" or + "System.UInt128" => true, + _ => false + }; + } + internal static bool IsCollectionType(ITypeSymbol symbol) + { + return symbol.AllInterfaces.Any(i => + i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IEnumerable"); + } + + internal static bool IsGenericCollection(string? originalDef, out string? kind) + { + kind = originalDef switch + { + "System.Collections.Generic.List" => "List", + "System.Collections.Generic.IList" => "List", + "System.Collections.ObjectModel.Collection" => "List", + "System.Collections.ObjectModel.ReadOnlyCollection" => "List", + "System.Collections.ObjectModel.ObservableCollection" => "List", + "System.ComponentModel.BindingList" => "List", + "System.Collections.Immutable.ImmutableList" => "List", + "System.Collections.Generic.ReadOnlyList" => "List", + + "System.Collections.Generic.IEnumerable" => "Enumerable", + "System.Collections.Generic.ICollection" => "Enumerable", + "System.Collections.Generic.IReadOnlyCollection" => "Enumerable", + "System.Collections.Generic.IReadOnlyList" => "Enumerable", + + "System.Collections.Generic.HashSet" => "HashSet", + + "System.Collections.Generic.Dictionary" => "Dictionary", + "System.Collections.Frozen.FrozenDictionary" => "Dictionary", + "System.Collections.Concurrent.ConcurrentDictionary" => "Dictionary", + + "System.Collections.Immutable.ImmutableArray" => "List", + "System.Collections.Immutable.ImmutableSortedSet" => "HashSet", + "System.Collections.Immutable.ImmutableHashSet" => "HashSet", + "System.Collections.Immutable.ImmutableDictionary" => "Dictionary", + "System.Collections.Immutable.ImmutableSortedDictionary" => "Dictionary", + + "System.Collections.Generic.Queue" => "Queue", + "System.Collections.Generic.Stack" => "Stack", + + _ => null + }; + + return kind != null; + } + + internal static IEnumerable GetAllProperties(INamedTypeSymbol? type) + { + while (type != null && type.SpecialType != SpecialType.System_Object) + { + foreach (var member in type.GetMembers().OfType()) + { + if (!member.IsReadOnly && member.SetMethod != null) + yield return member; + } + + type = type.BaseType; + } + } + + internal static bool IsPartial(INamedTypeSymbol typeSymbol) + { + return typeSymbol.DeclaringSyntaxReferences.Any(syntaxRef => + syntaxRef.GetSyntax() is TypeDeclarationSyntax syntax && + syntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))); + } + + internal static bool HasDeepCopyOptIn(INamedTypeSymbol typeSymbol) + { + return typeSymbol.GetAttributes().Any(a => + SymbolEquals(a.AttributeClass, "UtilityVerse.Copy.DeepCopy")) || + typeSymbol.AllInterfaces.Any(i => + SymbolEquals(i, "UtilityVerse.Copy.IDeepCopy")); + } + + internal static bool HasShallowCopyOptIn(INamedTypeSymbol typeSymbol) + { + return typeSymbol.GetAttributes().Any(a => + SymbolEquals(a.AttributeClass, "UtilityVerse.Copy.ShallowCopy")) || + typeSymbol.AllInterfaces.Any(i => + SymbolEquals(i, "UtilityVerse.Copy.IShallowCopy")); + } + + internal static bool SymbolEquals(ISymbol? symbol, string fullyQualifiedMetadataName) + { + return symbol?.ToDisplayString() == fullyQualifiedMetadataName; + } + internal static INamedTypeSymbol UnwrapNullable(ITypeSymbol type) + { + if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && + type is INamedTypeSymbol namedType && + namedType.TypeArguments.FirstOrDefault() is INamedTypeSymbol underlying) + { + return underlying; + } + + return type as INamedTypeSymbol ?? throw new System.InvalidOperationException("Expected named type."); + } } diff --git a/src/UtilityVerse.Copy/UtilityVerse.Copy.csproj b/src/UtilityVerse.Copy/UtilityVerse.Copy.csproj index ad62653..511e9dd 100644 --- a/src/UtilityVerse.Copy/UtilityVerse.Copy.csproj +++ b/src/UtilityVerse.Copy/UtilityVerse.Copy.csproj @@ -15,7 +15,7 @@ Copy UtilityVerse.Copy - 0.4.0 + 0.5.0 Pritom Purkayasta A Roslyn source generator for generating shallow copy and deep copy for class, records and structs diff --git a/src/UtilityVerse.Copy/nuget.md b/src/UtilityVerse.Copy/nuget.md index e84e0bb..f3c0245 100644 --- a/src/UtilityVerse.Copy/nuget.md +++ b/src/UtilityVerse.Copy/nuget.md @@ -1,4 +1,5 @@ --- +--- # 📦 UtilityVerse.Copy — Source Generator for DeepCopy and ShallowCopy in C\# diff --git a/src/UtilityVerse/Helpers/Utils.cs b/src/UtilityVerse/Helpers/Utils.cs index fed81ab..221d943 100644 --- a/src/UtilityVerse/Helpers/Utils.cs +++ b/src/UtilityVerse/Helpers/Utils.cs @@ -1,4 +1,4 @@ -namespace UtilityVerse.Helpers; +namespace UtilityVerse.Helpers; internal static class Utils { diff --git a/test/UtilityVerse.Copy.Test/HelloWorld.cs b/test/UtilityVerse.Copy.Test/HelloWorld.cs index 9caaae1..3256989 100644 --- a/test/UtilityVerse.Copy.Test/HelloWorld.cs +++ b/test/UtilityVerse.Copy.Test/HelloWorld.cs @@ -3,31 +3,53 @@ namespace UtilityVerse.Copy.Test; [DeepCopy] public partial class HelloWorld { - public int Id { get; set; } - public string? Name { get; set; } - public IEnumerable
Addresses { get; set; } = []; - - public HelloWorld Clone() - { - return new HelloWorld - { - Id = Id, - Name = Name, - Addresses = Addresses.Select(x => x.Clone()).ToList() - }; - } + public int Id { get; set; } + public string? Name { get; set; } + public DateTime CreatedAt { get; set; } + public IEnumerable
Addresses { get; set; } = []; + + public HelloWorld Clone() + { + return new HelloWorld + { + Id = Id, + Name = Name, + Addresses = Addresses.Select(x => x.Clone()).ToList() + }; + } } [DeepCopy] public partial class Address { - public string? StreetNumber { get; set; } - - public Address Clone() - { - return new Address - { - StreetNumber = StreetNumber - }; - } + public string? StreetNumber { get; set; } + + public Address Clone() + { + return new Address + { + StreetNumber = StreetNumber + }; + } +} + + +[DeepCopy] +public partial class Hello +{ + public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } + public Status Status { get; set; } + public DateOnly CreatedDateOnly { get; set; } + + public Hello2[]? Hello2s { get; set; } +} + +[DeepCopy] +public partial record Hello2(Guid Id); + +public enum Status +{ + Ready, + Finished } \ No newline at end of file diff --git a/test/UtilityVerse.Copy.Test/Program.cs b/test/UtilityVerse.Copy.Test/Program.cs index 54af99a..3e9c547 100644 --- a/test/UtilityVerse.Copy.Test/Program.cs +++ b/test/UtilityVerse.Copy.Test/Program.cs @@ -1,4 +1,4 @@ -using UtilityVerse.Copy.Test; +using UtilityVerse.Copy.Test; using UtilityVerse.Copy; Console.WriteLine("Starting...."); @@ -44,6 +44,15 @@ static void AutomatedTesting() static void ManualTesting() { + var h = new Hello() + { + + }; + + h.DeepCopy(); + + + var originalObject = new HelloWorld() { Id = 1, @@ -51,6 +60,8 @@ static void ManualTesting() Addresses = new[] { new Address() { StreetNumber = "12" } } }; var clonedObject = originalObject.Clone(); + + var c = originalObject.DeepCopy(); Console.WriteLine("Before Cloning..."); Console.WriteLine( $"OriginalObject Id: {originalObject.Id} Name: {originalObject.Name} Addresses: {originalObject.Addresses.First().StreetNumber}"); diff --git a/test/UtilityVerse.UnitTest/Usings.cs b/test/UtilityVerse.UnitTest/Usings.cs index 1b3f01b..09c1f23 100644 --- a/test/UtilityVerse.UnitTest/Usings.cs +++ b/test/UtilityVerse.UnitTest/Usings.cs @@ -1,3 +1,2 @@ global using Xunit; -global using UtilityVerse; -global using UtilityVerse.Extensions; \ No newline at end of file +global using UtilityVerse.Extensions;