diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 4245ab0..a5ca465 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -5,9 +5,9 @@ name: .NET on: push: - branches: [ "master", "development" ] + branches: [ "development" ] pull_request: - branches: [ "master", "development" ] + branches: [ "master" ] jobs: build: diff --git a/src/UtilityVerse.Copy/AnalyzerReleases.Shipped.md b/src/UtilityVerse.Copy/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..9c59d23 --- /dev/null +++ b/src/UtilityVerse.Copy/AnalyzerReleases.Shipped.md @@ -0,0 +1,9 @@ +## Release 0.3.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|------------------|----------|-------------------- +UV001 | UtilityVerse.Copy | Error | MissingPartialErrorAnalyzer +UV002 | UtilityVerse.Copy | Error | NestedTypeErrorAnalyzer (DeepCopy) +UV003 | UtilityVerse.Copy | Error | NestedTypeErrorAnalyzer (ShallowCopy) diff --git a/src/UtilityVerse.Copy/AnalyzerReleases.Unshipped.md b/src/UtilityVerse.Copy/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..e69de29 diff --git a/src/UtilityVerse.Copy/Analyzers/MissingPartialErrorAnalyzer.cs b/src/UtilityVerse.Copy/Analyzers/MissingPartialErrorAnalyzer.cs new file mode 100644 index 0000000..3161723 --- /dev/null +++ b/src/UtilityVerse.Copy/Analyzers/MissingPartialErrorAnalyzer.cs @@ -0,0 +1,56 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace UtilityVerse.Copy.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class MissingPartialErrorAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor MissingPartialModifierRule = new( + id: "UV001", + title: "Type must be marked as partial for DeepCopy/ShallowCopy generation", + messageFormat: "Type '{0}' must be marked as 'partial' to support DeepCopy/ShallowCopy generation", + category: "UtilityVerse.Copy", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "UtilityVerse.Copy requires all target types to be marked with 'partial' so the DeepCopy() or ShallowCopy() method can be injected." + ); + + public override ImmutableArray SupportedDiagnostics => [MissingPartialModifierRule]; + + public override void Initialize(AnalysisContext context) + { + // Configure analysis for syntax trees + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeTypeDeclaration, SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.RecordDeclaration, SyntaxKind.RecordStructDeclaration); + } + + private static void AnalyzeTypeDeclaration(SyntaxNodeAnalysisContext context) + { + var typeDecl = (TypeDeclarationSyntax)context.Node; + + // Skip if already marked partial + if (typeDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + return; + + // Skip if not user-defined (e.g., external/metadata references) + var symbol = context.SemanticModel.GetDeclaredSymbol(typeDecl); + if (symbol is null || symbol.DeclaringSyntaxReferences.Length == 0) + return; + + // Optionally: check if it's used for DeepCopy somehow + // Example: skip types with [GeneratedCode] or [CompilerGenerated] + if (symbol.GetAttributes().Any(attr => + attr.AttributeClass?.ToDisplayString() is "System.CodeDom.Compiler.GeneratedCodeAttribute" or "System.Runtime.CompilerServices.CompilerGeneratedAttribute")) + return; + + var diagnostic = Diagnostic.Create(MissingPartialModifierRule, typeDecl.Identifier.GetLocation(), symbol.Name); + context.ReportDiagnostic(diagnostic); + } +} \ No newline at end of file diff --git a/src/UtilityVerse.Copy/Analyzers/NestedTypeErrorAnalyzer.cs b/src/UtilityVerse.Copy/Analyzers/NestedTypeErrorAnalyzer.cs new file mode 100644 index 0000000..1c3a407 --- /dev/null +++ b/src/UtilityVerse.Copy/Analyzers/NestedTypeErrorAnalyzer.cs @@ -0,0 +1,122 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace UtilityVerse.Copy.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class NestedTypeErrorAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor DeepRule = new( + id: "UV002", + title: "Nested type missing DeepCopy attribute", + messageFormat: "Property '{0}' of type '{1}' requires nested type '{2}' to have DeepCopy attribute or implement IDeepCopy", + category: "UtilityVerse.Copy", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor ShallowRule = new DiagnosticDescriptor( + id: "UV003", + title: "Nested type missing ShallowCopy attribute", + messageFormat: "Property '{0}' of type '{1}' requires nested type '{2}' to have ShallowCopy attribute or implement IShallowCopy", + category: "UtilityVerse.Copy", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => + [DeepRule, ShallowRule]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType); + } + + private void AnalyzeNamedType(SymbolAnalysisContext context) + { + var namedType = (INamedTypeSymbol)context.Symbol; + + // Only analyze public partial types (assuming generator requires partial) + if (namedType.DeclaredAccessibility != Accessibility.Public) + return; + + if (!namedType.DeclaringSyntaxReferences.Any(syntaxRef => + syntaxRef.GetSyntax() is TypeDeclarationSyntax tds && + tds.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))) + return; + + // Check if type opts into DeepCopy or ShallowCopy + bool hasDeepCopy = Helper.HasDeepCopyOptIn(namedType); + bool hasShallowCopy = Helper.HasShallowCopyOptIn(namedType); + + if (!hasDeepCopy && !hasShallowCopy) + return; // type itself not opted in - no need to check nested types + + // Get all properties including inherited + var properties = namedType.GetMembers().OfType().Where(p => !p.IsStatic); + + foreach (var prop in properties) + { + if (prop.Type is not INamedTypeSymbol propType) + { + continue; + } + // Skip primitives and enums + if (Helper.IsTrulyPrimitive(propType)) + continue; + + if (propType.IsGenericType) + { + foreach (var typeArg in propType.TypeArguments.OfType()) + { + if (Helper.IsTrulyPrimitive(typeArg)) + continue; + + if (hasDeepCopy && !Helper.HasDeepCopyOptIn(typeArg)) + { + var diagnostic = Diagnostic.Create( + DeepRule, + prop.Locations.FirstOrDefault(), + prop.Name, namedType.Name, typeArg.Name); + context.ReportDiagnostic(diagnostic); + } + + if (hasShallowCopy && !Helper.HasShallowCopyOptIn(typeArg)) + { + var diagnostic = Diagnostic.Create( + ShallowRule, + prop.Locations.FirstOrDefault(), + prop.Name, namedType.Name, typeArg.Name); + context.ReportDiagnostic(diagnostic); + } + } + } + else + { + // Handle non-generic nested types + if (hasDeepCopy && !Helper.HasDeepCopyOptIn(propType)) + { + var diagnostic = Diagnostic.Create( + DeepRule, + prop.Locations.FirstOrDefault(), + prop.Name, namedType.Name, propType.Name); + context.ReportDiagnostic(diagnostic); + } + + if (hasShallowCopy && !Helper.HasShallowCopyOptIn(propType)) + { + var diagnostic = Diagnostic.Create( + ShallowRule, + prop.Locations.FirstOrDefault(), + prop.Name, namedType.Name, propType.Name); + context.ReportDiagnostic(diagnostic); + } + } + } + } +} \ No newline at end of file diff --git a/src/UtilityVerse.Copy/CopyGenerator.cs b/src/UtilityVerse.Copy/CopyGenerator.cs index 70ca80a..50121f6 100644 --- a/src/UtilityVerse.Copy/CopyGenerator.cs +++ b/src/UtilityVerse.Copy/CopyGenerator.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using UtilityVerse.Copy.Generators; @@ -52,8 +51,8 @@ private static (INamedTypeSymbol Symbol, CopyMode Mode)? GetSemanticTarget(Gener if (context.Node is not TypeDeclarationSyntax typeDecl) return null; - if (!typeDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) - return null; + // if (!typeDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + // return null; var symbol = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, typeDecl) as INamedTypeSymbol; if (symbol is not { DeclaredAccessibility: Accessibility.Public }) @@ -71,7 +70,7 @@ private static (INamedTypeSymbol Symbol, CopyMode Mode)? GetSemanticTarget(Gener return null; } - private static string GenerateDeepCopy(INamedTypeSymbol typeSymbol) => DeepCopyGenerator.Generate(typeSymbol); + private static string? GenerateDeepCopy(INamedTypeSymbol typeSymbol) => DeepCopyGenerator.Generate(typeSymbol); private static string? GenerateShallowCopy(INamedTypeSymbol typeSymbol) => ShallowCopyGenerator.Generate(typeSymbol); diff --git a/src/UtilityVerse.Copy/Generators/DeepCopyGenerator.cs b/src/UtilityVerse.Copy/Generators/DeepCopyGenerator.cs index 7b08e8d..0677cc8 100644 --- a/src/UtilityVerse.Copy/Generators/DeepCopyGenerator.cs +++ b/src/UtilityVerse.Copy/Generators/DeepCopyGenerator.cs @@ -12,9 +12,10 @@ namespace UtilityVerse.Copy.Generators; internal static class DeepCopyGenerator { - internal static string Generate(INamedTypeSymbol typeSymbol) + 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(); @@ -36,16 +37,20 @@ internal static string Generate(INamedTypeSymbol typeSymbol) sb.AppendLine(""" - // - // This code was generated by Copy. - // Author: Pritom Purkayasta - // DO NOT modify this file manually. Changes may be overwritten. - // This file contains auto-generated DeepCopy() implementations. - // - - using System; - using System.Linq; - using System.Collections.Generic; + // + // This code was generated by Copy. + // Author: Pritom Purkayasta + // DO NOT modify this file manually. Changes may be overwritten. + // This file contains auto-generated DeepCopy() implementations. + // + + using System; + using System.Linq; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Collections.Immutable; + using System.Collections.Concurrent; + using System.Collections.Frozen; """); diff --git a/src/UtilityVerse.Copy/Generators/ShallowCopyGenerator.cs b/src/UtilityVerse.Copy/Generators/ShallowCopyGenerator.cs index 3d764b3..f3c1da9 100644 --- a/src/UtilityVerse.Copy/Generators/ShallowCopyGenerator.cs +++ b/src/UtilityVerse.Copy/Generators/ShallowCopyGenerator.cs @@ -10,12 +10,12 @@ namespace UtilityVerse.Copy.Generators; -public static class ShallowCopyGenerator +internal static class ShallowCopyGenerator { - public static string? Generate(INamedTypeSymbol typeSymbol) + internal static string? Generate(INamedTypeSymbol typeSymbol) { - if (typeSymbol.IsAbstract) - return null; + 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(); @@ -87,34 +87,34 @@ public static class ShallowCopyGenerator 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()", + var original = named.OriginalDefinition.ConstructUnboundGenericType().ToDisplayString(); - "HashSet" => - $"this.{member.Name} != null ? new HashSet<{itemType}>(this.{member.Name}) : null", - - "Dictionary" => - $"this.{member.Name}?.ToDictionary(x => x.Key, x => x.Value)", + 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}"; + } - _ => null - }; - } - else if (original.StartsWith("System.Tuple")) - { - assignment = $"this.{member.Name}"; + break; } - break; - } - case INamedTypeSymbol named when named.ToDisplayString().StartsWith("System.ValueTuple"): assignment = $"this.{member.Name}"; break; diff --git a/src/UtilityVerse.Copy/Helper.cs b/src/UtilityVerse.Copy/Helper.cs index 0209aba..7a0a838 100644 --- a/src/UtilityVerse.Copy/Helper.cs +++ b/src/UtilityVerse.Copy/Helper.cs @@ -5,7 +5,10 @@ /// using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace UtilityVerse.Copy; @@ -30,6 +33,7 @@ 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 }; @@ -94,4 +98,22 @@ internal static IEnumerable GetAllProperties(INamedTypeSymbol? } } + 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 => a.AttributeClass?.ToDisplayString() == "UtilityVerse.Copy.DeepCopy") || + typeSymbol.AllInterfaces.Any(i => i.ToDisplayString() == "UtilityVerse.Copy.IDeepCopy"); + } + + internal static bool HasShallowCopyOptIn(INamedTypeSymbol typeSymbol) + { + return typeSymbol.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "UtilityVerse.Copy.ShallowCopy") || + typeSymbol.AllInterfaces.Any(i => i.ToDisplayString() == "UtilityVerse.Copy.IShallowCopy"); + } } \ No newline at end of file diff --git a/src/UtilityVerse.Copy/UtilityVerse.Copy.csproj b/src/UtilityVerse.Copy/UtilityVerse.Copy.csproj index 12c128f..3e5aee4 100644 --- a/src/UtilityVerse.Copy/UtilityVerse.Copy.csproj +++ b/src/UtilityVerse.Copy/UtilityVerse.Copy.csproj @@ -1,5 +1,4 @@  - netstandard2.0 latest @@ -10,13 +9,13 @@ Analyzer true true - true + true Copy UtilityVerse.Copy - 0.2.1 + 0.3.0 Pritom Purkayasta A Roslyn source generator for generating shallow copy and deep copy for class, records and structs @@ -45,12 +44,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + - - + @@ -63,6 +62,4 @@ \ - - \ No newline at end of file diff --git a/src/UtilityVerse/Extensions/CollectionExtension.cs b/src/UtilityVerse/Extensions/CollectionExtension.cs index 0b70496..c399393 100644 --- a/src/UtilityVerse/Extensions/CollectionExtension.cs +++ b/src/UtilityVerse/Extensions/CollectionExtension.cs @@ -5,6 +5,8 @@ using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; namespace UtilityVerse.Extensions; diff --git a/src/UtilityVerse/Extensions/DateTimeExtension.cs b/src/UtilityVerse/Extensions/DateTimeExtension.cs index 1768be5..7e4f686 100644 --- a/src/UtilityVerse/Extensions/DateTimeExtension.cs +++ b/src/UtilityVerse/Extensions/DateTimeExtension.cs @@ -4,6 +4,7 @@ // --------------------------------------------------------------- +using System; using UtilityVerse.Contracts; using UtilityVerse.Helpers; using UtilityVerse.Shared; diff --git a/src/UtilityVerse/Extensions/StringExtension.cs b/src/UtilityVerse/Extensions/StringExtension.cs index 07de8f1..2f95dc4 100644 --- a/src/UtilityVerse/Extensions/StringExtension.cs +++ b/src/UtilityVerse/Extensions/StringExtension.cs @@ -4,6 +4,7 @@ // --------------------------------------------------------------- +using System; using System.Text; using System.Text.RegularExpressions; using UtilityVerse.Contracts; diff --git a/src/UtilityVerse/UtilityVerse.csproj b/src/UtilityVerse/UtilityVerse.csproj index 74adfb1..4ab4649 100644 --- a/src/UtilityVerse/UtilityVerse.csproj +++ b/src/UtilityVerse/UtilityVerse.csproj @@ -10,7 +10,32 @@ UtilityVerse 1.0 utility, utils, C#, dotnet, c# utility, dotnet core - a curated list of utility methods that will help you, do your work at your best. + a curated list of utility methods that will help you, do your work at your + best. + Pritom Purkayasta + https://github.com/purkayasta/TheUtilityVerse + https://www.nuget.org/packages/UtilityVerse/ + git + + Pritom Purkayasta + Copyright (c) Pritom Purkayasta All rights reserved. + FREE TO USE TO CONNECT THE WORLD + + true + TheUtilityVerse + utility-verse.png + nuget.md + True + snupkg + + + MIT + true + UtilityVerse + 1.0 + utility, utils, C#, dotnet, c# utility, dotnet core + a curated list of utility methods that will help you, do your work at your + best. Pritom Purkayasta https://github.com/purkayasta/TheUtilityVerse https://www.nuget.org/packages/UtilityVerse/ @@ -50,4 +75,4 @@ - + \ No newline at end of file diff --git a/test/UtilityVerse.Copy.Test/HelloWorld.cs b/test/UtilityVerse.Copy.Test/HelloWorld.cs index 8f1f2f5..9caaae1 100644 --- a/test/UtilityVerse.Copy.Test/HelloWorld.cs +++ b/test/UtilityVerse.Copy.Test/HelloWorld.cs @@ -18,7 +18,6 @@ public HelloWorld Clone() } } - [DeepCopy] public partial class Address { diff --git a/test/UtilityVerse.Copy.Test/Program.cs b/test/UtilityVerse.Copy.Test/Program.cs index 7ae0607..54af99a 100644 --- a/test/UtilityVerse.Copy.Test/Program.cs +++ b/test/UtilityVerse.Copy.Test/Program.cs @@ -1,4 +1,5 @@ using UtilityVerse.Copy.Test; +using UtilityVerse.Copy; Console.WriteLine("Starting....");