From a67a86a740bb25e93f05d71eea1304a9fa7b6809 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 18:36:30 +0400 Subject: [PATCH 01/27] added the first draft for source generators --- SpanExtensions.sln | 11 + TestProject1/GlobalUsings.cs | 1 + src/SourceGenerators/CopyGenerator.cs | 332 +++++++++++++++++++ src/SourceGenerators/InternalExtensions.cs | 65 ++++ src/SourceGenerators/SourceGenerators.csproj | 32 ++ src/SpanExtensions.csproj | 19 ++ 6 files changed, 460 insertions(+) create mode 100644 TestProject1/GlobalUsings.cs create mode 100644 src/SourceGenerators/CopyGenerator.cs create mode 100644 src/SourceGenerators/InternalExtensions.cs create mode 100644 src/SourceGenerators/SourceGenerators.csproj diff --git a/SpanExtensions.sln b/SpanExtensions.sln index f44a04a..c86baca 100644 --- a/SpanExtensions.sln +++ b/SpanExtensions.sln @@ -5,16 +5,27 @@ VisualStudioVersion = 17.5.33627.172 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpanExtensions", "src\SpanExtensions.csproj", "{75DE5AFD-663E-415D-9B95-6BC513BD4A07}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGenerators", "src\SourceGenerators\SourceGenerators.csproj", "{F21A6902-D205-4127-8DF4-49B5548BF28F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + DebugGenerators|Any CPU = DebugGenerators|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {75DE5AFD-663E-415D-9B95-6BC513BD4A07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {75DE5AFD-663E-415D-9B95-6BC513BD4A07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75DE5AFD-663E-415D-9B95-6BC513BD4A07}.DebugGenerators|Any CPU.ActiveCfg = DebugGenerators|Any CPU + {75DE5AFD-663E-415D-9B95-6BC513BD4A07}.DebugGenerators|Any CPU.Build.0 = DebugGenerators|Any CPU {75DE5AFD-663E-415D-9B95-6BC513BD4A07}.Release|Any CPU.ActiveCfg = Release|Any CPU {75DE5AFD-663E-415D-9B95-6BC513BD4A07}.Release|Any CPU.Build.0 = Release|Any CPU + {F21A6902-D205-4127-8DF4-49B5548BF28F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F21A6902-D205-4127-8DF4-49B5548BF28F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F21A6902-D205-4127-8DF4-49B5548BF28F}.DebugGenerators|Any CPU.ActiveCfg = DebugGenerators|Any CPU + {F21A6902-D205-4127-8DF4-49B5548BF28F}.DebugGenerators|Any CPU.Build.0 = DebugGenerators|Any CPU + {F21A6902-D205-4127-8DF4-49B5548BF28F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F21A6902-D205-4127-8DF4-49B5548BF28F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TestProject1/GlobalUsings.cs b/TestProject1/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/TestProject1/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs new file mode 100644 index 0000000..b8d2192 --- /dev/null +++ b/src/SourceGenerators/CopyGenerator.cs @@ -0,0 +1,332 @@ +using Microsoft.CodeAnalysis; +using System.Reflection; +using System.Text; +using System.Linq; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp; +using System.Collections.Generic; + +using System; +using System.Text.RegularExpressions; + + +#if DEBUGGENERATORS +using System.Diagnostics; +#endif + +namespace SpanExtensions.SourceGenerators +{ + /// + /// Generates a copy of the marked class/interface/struct/record and runs a set of FindAndReplace and RegexReplace operations on the copied source. + /// + /// + /// Based on . + /// + [Generator(LanguageNames.CSharp)] + sealed class CopyGenerator : IIncrementalGenerator + { + static readonly string @namespace = typeof(CopyGenerator).Namespace; + static readonly AssemblyName assemblyName = typeof(CopyGenerator).Assembly.GetName(); + static readonly string generatedCodeAttribute = $@"global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{assemblyName.Name}"", ""{assemblyName.Version}"")"; + + const string generateCopyAttributeName = "GenerateCopyAttribute"; + static readonly string generateCopyAttributeFullName = $"{@namespace}.{generateCopyAttributeName}"; + static readonly string generateCopyAttributeFileName = $"{generateCopyAttributeName}.generated.cs"; + static readonly string generateCopyAttributeSource = $$""" + // + #nullable enable + + namespace {{@namespace}} + { + /// + /// Marks the class/interface/struct/record to be copied, and the FindAndReplace and RegexReplace operations to be performed on the copied source. + /// + [{{generatedCodeAttribute}}] + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Interface | global::System.AttributeTargets.Struct, AllowMultiple = false)] + public sealed class {{generateCopyAttributeName}} : global::System.Attribute + { + /// + /// The FindAndReplace operations to be performed on the copied source. + /// + public string[] FindAndReplaces { get; set; } = global::System.Array.Empty(); + + /// + /// The RegexReplace operations to be performed on the copied source. + /// + public string[] RegexReplaces { get; set; } = global::System.Array.Empty(); + } + } + """; + + const string indentation = " "; + static readonly char[] endLineChars = ['\r', '\n']; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(static context => context.AddSource(generateCopyAttributeFileName, SourceText.From(generateCopyAttributeSource, Encoding.UTF8))); + + IncrementalValuesProvider provider = context.SyntaxProvider + .ForAttributeWithMetadataName(generateCopyAttributeFullName, SyntaxProviderPredicate, SyntaxProviderTransform) + .Where(static capture => capture != null) + .WithComparer(Capture.EqualityComparer)!; + + IncrementalValuesProvider failed = provider.Where(static capture => capture!.DiagnosticMessage != null); + context.RegisterImplementationSourceOutput(failed, static (context, capture) => ReportDiagnostic(context, capture.DiagnosticMessage!, capture.DiagnosticMessageLocation!, capture.DiagnosticMessageArgs!)); + + IncrementalValuesProvider successful = provider.Where(static capture => capture!.DiagnosticMessage == null); + context.RegisterSourceOutput(successful, static (context, capture) => GenerateSource(context, capture)); + } + + static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + return syntaxNode is TypeDeclarationSyntax; + } + + static void ReportDiagnostic(SourceProductionContext context, string diagnosticMessage, Location location, params object?[] messageArgs) + { + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor("GEN1", "SourceGenerators", diagnosticMessage, "Generation", DiagnosticSeverity.Error, true), + location, + messageArgs + )); + } + + static void GenerateSource(SourceProductionContext context, Capture capture) + { + if(capture.DiagnosticMessage != null) + { + ReportDiagnostic(context, capture.DiagnosticMessage, capture.DiagnosticMessageLocation!, capture.DiagnosticMessageArgs!); + return; + } + + string sourceCode = capture.SourceCode; + + foreach((string find, string replace) in capture.FindAndReplaces) + { + sourceCode = sourceCode.Replace(find, replace); + } + + foreach((string pattern, string replacement) in capture.RegexReplaces) + { + sourceCode = Regex.Replace(sourceCode, pattern, replacement); + } + + // todo: generate source including parent declarations + + return; + } + + static Capture? SyntaxProviderTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { +#if DEBUGGENERATORS + if (!Debugger.IsAttached) Debugger.Launch(); +#endif + + var syntaxNode = (TypeDeclarationSyntax)context.TargetNode; + + string[] usings = syntaxNode.GetUsings(); + + cancellationToken.ThrowIfCancellationRequested(); + + // We need the symbol to get the namespace, using parent syntax could not work with FileScopedNamespaceDeclaration + if(context.TargetSymbol is not INamedTypeSymbol targetAttributeTypeSymbol) + { + return new("SyntaxNode {0} can't get INamedTypeSymbol.", syntaxNode.GetLocation(), syntaxNode); + } + + (string find, string replace)[] findAndReplaces = []; + (string pattern, string replacement)[] regexReplaces = []; + AttributeData attribute = context.Attributes.First(a => a.AttributeClass!.Name == generateCopyAttributeName); + foreach((string parameter, TypedConstant value) in attribute.NamedArguments) + { + switch(parameter) + { + case "FindAndReplaces": + string[] strings = value.Values.Select(v => (string)v.Value!).ToArray(); + + if(strings.Length % 2 != 0) + { + return new("Attribute {0} requres the parameter {1} have even number of values, but {2} were defined.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), generateCopyAttributeName, parameter, strings.Length); + } + + findAndReplaces = new (string find, string replace)[strings.Length / 2]; + for(int i = 0, j = 0; j < strings.Length; i++, j += 2) + { + findAndReplaces[i] = (strings[j], strings[j + 1]); + } + break; + case "RegexReplaces": + strings = value.Values.Select(v => (string)v.Value!).ToArray(); + + if(strings.Length % 2 != 0) + { + return new("Attribute {0} requres the parameter {1} have even number of values, but {2} were defined.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), generateCopyAttributeName, parameter, strings.Length); + } + + regexReplaces = new (string find, string replace)[strings.Length / 2]; + for(int i = 0, j = 0; j < strings.Length; i++, j += 2) + { + regexReplaces[i] = (strings[j], strings[j + 1]); + } + break; + default: + return new("Unrecognized parameter {0} for attribute {1}.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), parameter, generateCopyAttributeName); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + + TypeDeclaration[] nestedDeclarations = TypeDeclaration.GetNestedDeclarations(syntaxNode); + + cancellationToken.ThrowIfCancellationRequested(); + + // todo: remove generateCopyAttributeName attribute from the targets attributes + // todo: include preceding documentation comments in the generated source code + string sourceCode = syntaxNode.NormalizeWhitespace(indentation: indentation, eol: "\n").ToString(); + + cancellationToken.ThrowIfCancellationRequested(); + + return new( + usings: usings, + @namespace: targetAttributeTypeSymbol.GetNamespace(), + nestedTypeDeclarations: nestedDeclarations, + sourceCode: sourceCode, + findAndReplaces: findAndReplaces, + regexReplaces: regexReplaces + ); + } + + sealed class Capture(string[] usings, string @namespace, TypeDeclaration[] nestedTypeDeclarations, string sourceCode, (string find, string replace)[] findAndReplaces, (string pattern, string replacement)[] regexReplaces) : IEquatable + { + public string[] Usings { get; } = usings; + public string Namespace { get; } = @namespace; + public TypeDeclaration[] NestedTypeDeclarations { get; } = nestedTypeDeclarations; + public string SourceCode { get; } = sourceCode; + public (string find, string replace)[] FindAndReplaces { get; } = findAndReplaces; + public (string pattern, string replacement)[] RegexReplaces { get; } = regexReplaces; + + public string? DiagnosticMessage { get; } + public object?[]? DiagnosticMessageArgs { get; } + public Location? DiagnosticMessageLocation { get; } + + Capture() : this([], "", [], "", [], []) { } + + public Capture(string diagnosticMessage, params object?[]? diagnosticMessageArgs) : this() + { + DiagnosticMessage = diagnosticMessage; + DiagnosticMessageArgs = diagnosticMessageArgs; + DiagnosticMessageLocation = Location.None; + } + + public Capture(string diagnosticMessage, Location diagnosticMessageLocation, params object?[]? diagnosticMessageArgs) : this() + { + DiagnosticMessage = diagnosticMessage; + DiagnosticMessageArgs = diagnosticMessageArgs; + DiagnosticMessageLocation = diagnosticMessageLocation; + } + + public override bool Equals(object? obj) => + obj is Capture other && Equals(other); + + public bool Equals(Capture other) => + Usings.AsSpan().SequenceEqual(other.Usings) && + Namespace == other.Namespace && + NestedTypeDeclarations.AsSpan().SequenceEqual(other.NestedTypeDeclarations) && + SourceCode == other.SourceCode && + FindAndReplaces.AsSpan().SequenceEqual(other.FindAndReplaces) && + RegexReplaces.AsSpan().SequenceEqual(other.RegexReplaces) && + DiagnosticMessage == other.DiagnosticMessage; + + public override int GetHashCode() + { + const int multiplier = -1521134295; + int hashCode = -573548719; + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(Usings); + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(Namespace); + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(NestedTypeDeclarations); + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(SourceCode); + hashCode = (hashCode * multiplier) + EqualityComparer<(string, string)[]>.Default.GetHashCode(FindAndReplaces); + hashCode = (hashCode * multiplier) + EqualityComparer<(string, string)[]>.Default.GetHashCode(RegexReplaces); + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(DiagnosticMessage ?? ""); + return hashCode; + } + + public sealed class Comparer : IEqualityComparer + { + bool IEqualityComparer.Equals(Capture? x, Capture? y) => + x != null && y != null && x.Equals(y); + + int IEqualityComparer.GetHashCode(Capture? generator) => + generator?.GetHashCode() ?? 0; + } + + public static readonly Comparer EqualityComparer = new(); + } + + internal sealed class TypeDeclaration(string modifiers, string keyword, string name, string constraints) : IEquatable + { + public string Modifiers { get; } = modifiers; + public string Keyword { get; } = keyword; + public string Name { get; } = name; + public string Constraints { get; } = constraints; + + public static TypeDeclaration[] GetNestedDeclarations(TypeDeclarationSyntax typeSyntax) + { + var declarations = new List(); + + TypeDeclarationSyntax? parentSyntax = typeSyntax; + do + { + declarations.Add(new( + modifiers: parentSyntax.Modifiers.ToString(), + keyword: parentSyntax.Keyword.ValueText, + name: parentSyntax.Identifier.ToString() + parentSyntax.TypeParameterList, + constraints: parentSyntax.ConstraintClauses.ToString() + )); + + parentSyntax = parentSyntax.Parent as TypeDeclarationSyntax; + } + while(parentSyntax != null && IsAllowedKind(parentSyntax.Kind())); + + return [..declarations]; + } + + static bool IsAllowedKind(SyntaxKind kind) => + kind is SyntaxKind.ClassDeclaration or + SyntaxKind.InterfaceDeclaration or + SyntaxKind.StructDeclaration or + SyntaxKind.RecordDeclaration; + + public override bool Equals(object? obj) => + obj is TypeDeclaration other && Equals(other); + + public bool Equals(TypeDeclaration other) => + Modifiers == other.Modifiers && + Keyword == other.Keyword && + Name == other.Name && + Constraints == other.Constraints; + + public override int GetHashCode() + { + static int StringHash(string s) => EqualityComparer.Default.GetHashCode(s); + + const int multiplier = -1521134295; + int hashCode = -754136522; + hashCode = (hashCode * multiplier) + StringHash(Modifiers); + hashCode = (hashCode * multiplier) + StringHash(Keyword); + hashCode = (hashCode * multiplier) + StringHash(Name); + hashCode = (hashCode * multiplier) + StringHash(Constraints); + return hashCode; + } + + public override string ToString() + { + string type = $"{Modifiers} {Keyword} {Name}"; + + return Constraints.Length != 0 ? $"{type} {string.Join(" ", Constraints)}" : type; + } + } + } +} diff --git a/src/SourceGenerators/InternalExtensions.cs b/src/SourceGenerators/InternalExtensions.cs new file mode 100644 index 0000000..6709733 --- /dev/null +++ b/src/SourceGenerators/InternalExtensions.cs @@ -0,0 +1,65 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis; +using System.Linq; +using System.Collections.Generic; + +namespace SpanExtensions.SourceGenerators +{ + static class InternalExtensions + { + public static void Deconstruct(this KeyValuePair source, out TKey Key, out TValue Value) + { + Key = source.Key; + Value = source.Value; + } + + public static string[] GetUsings(this SyntaxNode? syntaxNode) + { + while(syntaxNode != null && syntaxNode is not CompilationUnitSyntax) + { + syntaxNode = syntaxNode.Parent; + } + + return syntaxNode is not CompilationUnitSyntax compilationSyntax + ? (string[])([]) + : compilationSyntax.Usings.Select(@using => @using.ToString()).ToArray(); + } + + public static string GetNamespace(this INamedTypeSymbol namedTypeSymbol) + { + INamespaceSymbol? currentNameSpace = namedTypeSymbol.ContainingNamespace; + + if(currentNameSpace?.IsGlobalNamespace != false) + { + return ""; + } + + List namespaceParts = []; + do + { + if(!currentNameSpace.IsGlobalNamespace) + { + namespaceParts.Add(currentNameSpace.Name); + } + } + while((currentNameSpace = currentNameSpace!.ContainingNamespace) is not null); + return string.Join(".", namespaceParts); + } + + public static AttributeSyntax GetAttributeSyntax(this TypeDeclarationSyntax syntaxNode, string attributeName) + { + string attributeFullName = ""; + if(attributeName.EndsWith("Attribute")) + { + attributeFullName = attributeName; + attributeName = attributeName.Substring(0, attributeName.Length - "Attribute".Length); + } + else + { + attributeFullName = attributeName + "Attribute"; + } + + return syntaxNode.AttributeLists.Select(al => al.Attributes.FirstOrDefault(a => a.Name.ToString() == attributeName || a.Name.ToString() == attributeFullName)).First(a => a != null); + } + } +} diff --git a/src/SourceGenerators/SourceGenerators.csproj b/src/SourceGenerators/SourceGenerators.csproj new file mode 100644 index 0000000..cb06937 --- /dev/null +++ b/src/SourceGenerators/SourceGenerators.csproj @@ -0,0 +1,32 @@ + + + + netstandard2.0 + 12 + enable + true + false + Generated + true + Debug;Release;DebugGenerators + SpanExtensions.SourceGenerators + + + + 9999 + + + + 9999 + + + + 9999 + + + + + + + + diff --git a/src/SpanExtensions.csproj b/src/SpanExtensions.csproj index 15337b4..331cc01 100644 --- a/src/SpanExtensions.csproj +++ b/src/SpanExtensions.csproj @@ -26,16 +26,25 @@ SpanExtensions.Net README.md icon.png + Debug;Release;DebugGenerators portable + + portable + + portable + + portable + + portable @@ -44,6 +53,12 @@ portable + + + + + + True @@ -63,4 +78,8 @@ + + + + From 066bd870d31b6d7b24d7df9f56743cb72078b818 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 18:52:04 +0400 Subject: [PATCH 02/27] error out if neither FindAndReplaces nor RegexReplaces is set --- src/SourceGenerators/CopyGenerator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index b8d2192..11761cc 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -175,6 +175,10 @@ static void GenerateSource(SourceProductionContext context, Capture capture) return new("Unrecognized parameter {0} for attribute {1}.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), parameter, generateCopyAttributeName); } } + if(findAndReplaces.Length == 0 && regexReplaces.Length == 0) + { + return new("Attribute {0} requres either FindAndReplace or RegexReplaces be specified.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), generateCopyAttributeName); + } cancellationToken.ThrowIfCancellationRequested(); From c1e60fc2c9285866b81bb0ca347e06ee5a365706 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 18:54:47 +0400 Subject: [PATCH 03/27] fixed typo (missed plural s) --- src/SourceGenerators/CopyGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index 11761cc..1d21fad 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -177,7 +177,7 @@ static void GenerateSource(SourceProductionContext context, Capture capture) } if(findAndReplaces.Length == 0 && regexReplaces.Length == 0) { - return new("Attribute {0} requres either FindAndReplace or RegexReplaces be specified.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), generateCopyAttributeName); + return new("Attribute {0} requres either FindAndReplaces or RegexReplaces be specified.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), generateCopyAttributeName); } cancellationToken.ThrowIfCancellationRequested(); From 4ce158e6c6db687ecc016cd0cf4bc7fbe627eae0 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 20:31:50 +0400 Subject: [PATCH 04/27] remove GenerateCopy attribute in the copied source --- src/SourceGenerators/CopyGenerator.cs | 17 +++++++++++------ src/SourceGenerators/InternalExtensions.cs | 10 ++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index 1d21fad..f100459 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -125,6 +125,7 @@ static void GenerateSource(SourceProductionContext context, Capture capture) #endif var syntaxNode = (TypeDeclarationSyntax)context.TargetNode; + AttributeSyntax attributeSyntax = syntaxNode.GetAttributeSyntax(generateCopyAttributeName); string[] usings = syntaxNode.GetUsings(); @@ -148,7 +149,7 @@ static void GenerateSource(SourceProductionContext context, Capture capture) if(strings.Length % 2 != 0) { - return new("Attribute {0} requres the parameter {1} have even number of values, but {2} were defined.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), generateCopyAttributeName, parameter, strings.Length); + return new("Attribute {0} requres the parameter {1} have even number of values, but {2} were defined.", attributeSyntax.GetLocation(), generateCopyAttributeName, parameter, strings.Length); } findAndReplaces = new (string find, string replace)[strings.Length / 2]; @@ -162,7 +163,7 @@ static void GenerateSource(SourceProductionContext context, Capture capture) if(strings.Length % 2 != 0) { - return new("Attribute {0} requres the parameter {1} have even number of values, but {2} were defined.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), generateCopyAttributeName, parameter, strings.Length); + return new("Attribute {0} requres the parameter {1} have even number of values, but {2} were defined.", attributeSyntax.GetLocation(), generateCopyAttributeName, parameter, strings.Length); } regexReplaces = new (string find, string replace)[strings.Length / 2]; @@ -172,12 +173,12 @@ static void GenerateSource(SourceProductionContext context, Capture capture) } break; default: - return new("Unrecognized parameter {0} for attribute {1}.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), parameter, generateCopyAttributeName); + return new("Unrecognized parameter {0} for attribute {1}.", attributeSyntax.GetLocation(), parameter, generateCopyAttributeName); } } if(findAndReplaces.Length == 0 && regexReplaces.Length == 0) { - return new("Attribute {0} requres either FindAndReplaces or RegexReplaces be specified.", syntaxNode.GetAttributeSyntax(generateCopyAttributeName).GetLocation(), generateCopyAttributeName); + return new("Attribute {0} requres either FindAndReplaces or RegexReplaces be specified.", attributeSyntax.GetLocation(), generateCopyAttributeName); } cancellationToken.ThrowIfCancellationRequested(); @@ -186,9 +187,13 @@ static void GenerateSource(SourceProductionContext context, Capture capture) cancellationToken.ThrowIfCancellationRequested(); - // todo: remove generateCopyAttributeName attribute from the targets attributes // todo: include preceding documentation comments in the generated source code - string sourceCode = syntaxNode.NormalizeWhitespace(indentation: indentation, eol: "\n").ToString(); + TypeDeclarationSyntax syntaxToCopy = syntaxNode.WithAttributeLists( + new SyntaxList( + syntaxNode.AttributeLists.Select(al => al.RemoveIfContains(attributeSyntax)).Where(al => al.Attributes.Count != 0) + ) + ); + string sourceCode = syntaxToCopy.NormalizeWhitespace(indentation: indentation, eol: "\n").ToString(); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/SourceGenerators/InternalExtensions.cs b/src/SourceGenerators/InternalExtensions.cs index 6709733..f8b646a 100644 --- a/src/SourceGenerators/InternalExtensions.cs +++ b/src/SourceGenerators/InternalExtensions.cs @@ -61,5 +61,15 @@ public static AttributeSyntax GetAttributeSyntax(this TypeDeclarationSyntax synt return syntaxNode.AttributeLists.Select(al => al.Attributes.FirstOrDefault(a => a.Name.ToString() == attributeName || a.Name.ToString() == attributeFullName)).First(a => a != null); } + + public static AttributeListSyntax RemoveIfContains(this AttributeListSyntax list, AttributeSyntax attribute) + { + if (!list.Contains(attribute)) + { + return list; + } + + return list.RemoveNode(attribute, SyntaxRemoveOptions.KeepNoTrivia)!; + } } } From a11aa101749ba1050043cb05ef1719f7bb5c8db9 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 20:39:32 +0400 Subject: [PATCH 05/27] include leading trivia (doc comments) --- src/SourceGenerators/CopyGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index f100459..4d922b8 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -187,13 +187,13 @@ static void GenerateSource(SourceProductionContext context, Capture capture) cancellationToken.ThrowIfCancellationRequested(); - // todo: include preceding documentation comments in the generated source code TypeDeclarationSyntax syntaxToCopy = syntaxNode.WithAttributeLists( new SyntaxList( syntaxNode.AttributeLists.Select(al => al.RemoveIfContains(attributeSyntax)).Where(al => al.Attributes.Count != 0) ) ); - string sourceCode = syntaxToCopy.NormalizeWhitespace(indentation: indentation, eol: "\n").ToString(); + syntaxToCopy = syntaxToCopy.WithLeadingTrivia(syntaxNode.GetLeadingTrivia()); + string sourceCode = syntaxToCopy.NormalizeWhitespace(indentation: indentation, eol: "\n").ToFullString(); cancellationToken.ThrowIfCancellationRequested(); From d515de694448b83fedaca7f2accc69b709acb414 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 22:27:11 +0400 Subject: [PATCH 06/27] moved GenerateSource method below SyntaxProviderTransform --- src/SourceGenerators/CopyGenerator.cs | 50 +++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index 4d922b8..8cd124d 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -93,31 +93,6 @@ static void ReportDiagnostic(SourceProductionContext context, string diagnosticM )); } - static void GenerateSource(SourceProductionContext context, Capture capture) - { - if(capture.DiagnosticMessage != null) - { - ReportDiagnostic(context, capture.DiagnosticMessage, capture.DiagnosticMessageLocation!, capture.DiagnosticMessageArgs!); - return; - } - - string sourceCode = capture.SourceCode; - - foreach((string find, string replace) in capture.FindAndReplaces) - { - sourceCode = sourceCode.Replace(find, replace); - } - - foreach((string pattern, string replacement) in capture.RegexReplaces) - { - sourceCode = Regex.Replace(sourceCode, pattern, replacement); - } - - // todo: generate source including parent declarations - - return; - } - static Capture? SyntaxProviderTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) { #if DEBUGGENERATORS @@ -207,6 +182,31 @@ static void GenerateSource(SourceProductionContext context, Capture capture) ); } + static void GenerateSource(SourceProductionContext context, Capture capture) + { + if(capture.DiagnosticMessage != null) + { + ReportDiagnostic(context, capture.DiagnosticMessage, capture.DiagnosticMessageLocation!, capture.DiagnosticMessageArgs!); + return; + } + + string sourceCode = capture.SourceCode; + + foreach((string find, string replace) in capture.FindAndReplaces) + { + sourceCode = sourceCode.Replace(find, replace); + } + + foreach((string pattern, string replacement) in capture.RegexReplaces) + { + sourceCode = Regex.Replace(sourceCode, pattern, replacement); + } + + // todo: generate source including parent declarations + + return; + } + sealed class Capture(string[] usings, string @namespace, TypeDeclaration[] nestedTypeDeclarations, string sourceCode, (string find, string replace)[] findAndReplaces, (string pattern, string replacement)[] regexReplaces) : IEquatable { public string[] Usings { get; } = usings; From 4fd0d77c64955a7de3f9c2ff19b382df3b5ddd06 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 22:28:45 +0400 Subject: [PATCH 07/27] moved ReportDiagnostic method below SyntaxProviderTransform --- src/SourceGenerators/CopyGenerator.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index 8cd124d..26a7501 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -84,15 +84,6 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can return syntaxNode is TypeDeclarationSyntax; } - static void ReportDiagnostic(SourceProductionContext context, string diagnosticMessage, Location location, params object?[] messageArgs) - { - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor("GEN1", "SourceGenerators", diagnosticMessage, "Generation", DiagnosticSeverity.Error, true), - location, - messageArgs - )); - } - static Capture? SyntaxProviderTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) { #if DEBUGGENERATORS @@ -182,6 +173,15 @@ static void ReportDiagnostic(SourceProductionContext context, string diagnosticM ); } + static void ReportDiagnostic(SourceProductionContext context, string diagnosticMessage, Location location, params object?[] messageArgs) + { + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor("GEN1", "SourceGenerators", diagnosticMessage, "Generation", DiagnosticSeverity.Error, true), + location, + messageArgs + )); + } + static void GenerateSource(SourceProductionContext context, Capture capture) { if(capture.DiagnosticMessage != null) From 996bc90d77d63a7df0046fe1ffbdc7ee4c8b612e Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 22:40:33 +0400 Subject: [PATCH 08/27] fixed namespace reversed --- src/SourceGenerators/InternalExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/SourceGenerators/InternalExtensions.cs b/src/SourceGenerators/InternalExtensions.cs index f8b646a..776cb07 100644 --- a/src/SourceGenerators/InternalExtensions.cs +++ b/src/SourceGenerators/InternalExtensions.cs @@ -43,6 +43,8 @@ public static string GetNamespace(this INamedTypeSymbol namedTypeSymbol) } } while((currentNameSpace = currentNameSpace!.ContainingNamespace) is not null); + + namespaceParts.Reverse(); return string.Join(".", namespaceParts); } From 16771408cbf498312ab77a7e0de1f726358b3a98 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 22:52:52 +0400 Subject: [PATCH 09/27] generate the copy source file --- src/SourceGenerators/CopyGenerator.cs | 72 +++++++++++++++++++--- src/SourceGenerators/InternalExtensions.cs | 10 +++ 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index 26a7501..eb305a5 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -10,7 +10,10 @@ using System; using System.Text.RegularExpressions; +using System.CodeDom.Compiler; +using System.Globalization; +using System.IO; #if DEBUGGENERATORS using System.Diagnostics; @@ -61,7 +64,6 @@ public sealed class {{generateCopyAttributeName}} : global::System.Attribute """; const string indentation = " "; - static readonly char[] endLineChars = ['\r', '\n']; public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -190,21 +192,63 @@ static void GenerateSource(SourceProductionContext context, Capture capture) return; } - string sourceCode = capture.SourceCode; + StringBuilder sourceBuilder = new(); + using StringWriter _sourceWriter = new(sourceBuilder, CultureInfo.InvariantCulture); + using IndentedTextWriter sourceWriter = new(_sourceWriter, indentation); + + sourceWriter.WriteLine("// "); + sourceWriter.WriteLine("#nullable enable"); + sourceWriter.WriteLine(); + foreach(string @using in capture.Usings) + { + sourceWriter.WriteLine(@using); + } + sourceWriter.WriteLine(); + sourceWriter.WriteLine($"namespace {capture.Namespace}"); + sourceWriter.WriteLine('{'); + sourceWriter.Indent++; // namespace + sourceWriter.WriteLine(); + + foreach(TypeDeclaration? type in capture.NestedTypeDeclarations.Skip(1).Reverse()) + { + sourceWriter.WriteLine(type.ToString()); + sourceWriter.WriteLine('{'); + sourceWriter.Indent++; // parent declarations + } + + context.CancellationToken.ThrowIfCancellationRequested(); + string sourceCode = capture.SourceCode; foreach((string find, string replace) in capture.FindAndReplaces) { sourceCode = sourceCode.Replace(find, replace); } - foreach((string pattern, string replacement) in capture.RegexReplaces) { sourceCode = Regex.Replace(sourceCode, pattern, replacement); } + foreach(string line in sourceCode.Split('\n')) + { + sourceWriter.WriteLine(line); + } - // todo: generate source including parent declarations + context.CancellationToken.ThrowIfCancellationRequested(); - return; + for(int i = 0; i < capture.NestedTypeDeclarations.Length - 1; i++) + { + sourceWriter.Indent--; // parent declarations + sourceWriter.WriteLine("}"); + } + + sourceWriter.Indent--; // namespace + sourceWriter.WriteLine("}"); + + Debug.Assert(sourceWriter.Indent == 0); + + context.CancellationToken.ThrowIfCancellationRequested(); + + sourceCode = sourceBuilder.ToString(); + context.AddSource($"{capture.UniqueName}.{generateCopyAttributeName}.generated.cs", sourceCode); } sealed class Capture(string[] usings, string @namespace, TypeDeclaration[] nestedTypeDeclarations, string sourceCode, (string find, string replace)[] findAndReplaces, (string pattern, string replacement)[] regexReplaces) : IEquatable @@ -236,6 +280,14 @@ public Capture(string diagnosticMessage, Location diagnosticMessageLocation, par DiagnosticMessageLocation = diagnosticMessageLocation; } + public string UniqueName + { + get + { + return string.Join(".", NestedTypeDeclarations.Select(d => d.Name).And(Namespace).Reverse()); + } + } + public override bool Equals(object? obj) => obj is Capture other && Equals(other); @@ -274,11 +326,12 @@ public sealed class Comparer : IEqualityComparer public static readonly Comparer EqualityComparer = new(); } - internal sealed class TypeDeclaration(string modifiers, string keyword, string name, string constraints) : IEquatable + internal sealed class TypeDeclaration(string modifiers, string keyword, string name, string typeParameters, string constraints) : IEquatable { public string Modifiers { get; } = modifiers; public string Keyword { get; } = keyword; public string Name { get; } = name; + public string TypeParameters { get; } = typeParameters; public string Constraints { get; } = constraints; public static TypeDeclaration[] GetNestedDeclarations(TypeDeclarationSyntax typeSyntax) @@ -291,7 +344,8 @@ public static TypeDeclaration[] GetNestedDeclarations(TypeDeclarationSyntax type declarations.Add(new( modifiers: parentSyntax.Modifiers.ToString(), keyword: parentSyntax.Keyword.ValueText, - name: parentSyntax.Identifier.ToString() + parentSyntax.TypeParameterList, + name: parentSyntax.Identifier.ToString(), + typeParameters: parentSyntax.TypeParameterList?.ToString() ?? "", constraints: parentSyntax.ConstraintClauses.ToString() )); @@ -315,6 +369,7 @@ public bool Equals(TypeDeclaration other) => Modifiers == other.Modifiers && Keyword == other.Keyword && Name == other.Name && + TypeParameters == other.TypeParameters && Constraints == other.Constraints; public override int GetHashCode() @@ -326,13 +381,14 @@ public override int GetHashCode() hashCode = (hashCode * multiplier) + StringHash(Modifiers); hashCode = (hashCode * multiplier) + StringHash(Keyword); hashCode = (hashCode * multiplier) + StringHash(Name); + hashCode = (hashCode * multiplier) + StringHash(TypeParameters); hashCode = (hashCode * multiplier) + StringHash(Constraints); return hashCode; } public override string ToString() { - string type = $"{Modifiers} {Keyword} {Name}"; + string type = $"{Modifiers} {Keyword} {Name}{TypeParameters}"; return Constraints.Length != 0 ? $"{type} {string.Join(" ", Constraints)}" : type; } diff --git a/src/SourceGenerators/InternalExtensions.cs b/src/SourceGenerators/InternalExtensions.cs index 776cb07..2ff7723 100644 --- a/src/SourceGenerators/InternalExtensions.cs +++ b/src/SourceGenerators/InternalExtensions.cs @@ -73,5 +73,15 @@ public static AttributeListSyntax RemoveIfContains(this AttributeListSyntax list return list.RemoveNode(attribute, SyntaxRemoveOptions.KeepNoTrivia)!; } + + public static IEnumerable And(this IEnumerable source, T after) + { + foreach(T element in source) + { + yield return element; + } + + yield return after; + } } } From f69cafadd5a6ab1076b65e59e37eabddb665c966 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 22:54:58 +0400 Subject: [PATCH 10/27] only target .NET 8 when building DebugGenerators --- src/SpanExtensions.csproj | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/SpanExtensions.csproj b/src/SpanExtensions.csproj index 331cc01..bf509af 100644 --- a/src/SpanExtensions.csproj +++ b/src/SpanExtensions.csproj @@ -34,7 +34,8 @@ - portable + net8.0 + portable @@ -42,7 +43,8 @@ - portable + net8.0 + portable @@ -54,15 +56,15 @@ - - - + + + - True - \ + True + \ True @@ -72,10 +74,10 @@ - + True \ - + From f912cb4d41dd482c7a8ce6062aa5ccfadbfb3800 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 23:01:15 +0400 Subject: [PATCH 11/27] put assertion inside #if DEBUGGENERATORS --- src/SourceGenerators/CopyGenerator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index eb305a5..67c5aa6 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -243,7 +243,9 @@ static void GenerateSource(SourceProductionContext context, Capture capture) sourceWriter.Indent--; // namespace sourceWriter.WriteLine("}"); +#if DEBUGGENERATORS Debug.Assert(sourceWriter.Indent == 0); +#endif context.CancellationToken.ThrowIfCancellationRequested(); From 63a57b80f26e5fb877e197ebb6bb35536d429613 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 23:29:51 +0400 Subject: [PATCH 12/27] remove SourceGenerators namespace from the copy --- src/SourceGenerators/CopyGenerator.cs | 2 +- src/SourceGenerators/InternalExtensions.cs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index 67c5aa6..1e54ac1 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -95,7 +95,7 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can var syntaxNode = (TypeDeclarationSyntax)context.TargetNode; AttributeSyntax attributeSyntax = syntaxNode.GetAttributeSyntax(generateCopyAttributeName); - string[] usings = syntaxNode.GetUsings(); + string[] usings = syntaxNode.GetUsings().Skip("using SpanExtensions.SourceGenerators;").ToArray(); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/SourceGenerators/InternalExtensions.cs b/src/SourceGenerators/InternalExtensions.cs index 2ff7723..6bb4bb2 100644 --- a/src/SourceGenerators/InternalExtensions.cs +++ b/src/SourceGenerators/InternalExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis; using System.Linq; using System.Collections.Generic; +using System; namespace SpanExtensions.SourceGenerators { @@ -13,7 +14,7 @@ public static void Deconstruct(this KeyValuePair sou Value = source.Value; } - public static string[] GetUsings(this SyntaxNode? syntaxNode) + public static IEnumerable GetUsings(this SyntaxNode? syntaxNode) { while(syntaxNode != null && syntaxNode is not CompilationUnitSyntax) { @@ -21,8 +22,8 @@ public static string[] GetUsings(this SyntaxNode? syntaxNode) } return syntaxNode is not CompilationUnitSyntax compilationSyntax - ? (string[])([]) - : compilationSyntax.Usings.Select(@using => @using.ToString()).ToArray(); + ? [] + : compilationSyntax.Usings.Select(@using => @using.ToString()); } public static string GetNamespace(this INamedTypeSymbol namedTypeSymbol) @@ -83,5 +84,16 @@ public static IEnumerable And(this IEnumerable source, T after) yield return after; } + + public static IEnumerable Skip(this IEnumerable source, T skip) where T : IEquatable + { + foreach(T element in source) + { + if(!element.Equals(skip)) + { + yield return element; + } + } + } } } From 4b661ae19551ea7b2628bd679bff2c5198b39e4c Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Sun, 25 Feb 2024 23:30:50 +0400 Subject: [PATCH 13/27] removed Carriage Return (\r) character at the end of comment lines --- src/SourceGenerators/CopyGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index 1e54ac1..7891f66 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -229,7 +229,7 @@ static void GenerateSource(SourceProductionContext context, Capture capture) } foreach(string line in sourceCode.Split('\n')) { - sourceWriter.WriteLine(line); + sourceWriter.WriteLine(line.TrimEnd('\r')); } context.CancellationToken.ThrowIfCancellationRequested(); From b988032fb9b6aeb2ffebb7f1bdda6206be572174 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Mon, 26 Feb 2024 12:49:51 +0400 Subject: [PATCH 14/27] perform replacements on namespace and declarations --- src/SourceGenerators/CopyGenerator.cs | 25 +++++++++++----------- src/SourceGenerators/InternalExtensions.cs | 16 ++++++++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index 7891f66..7070bf5 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System; -using System.Text.RegularExpressions; using System.CodeDom.Compiler; using System.Globalization; @@ -152,6 +151,11 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can cancellationToken.ThrowIfCancellationRequested(); TypeDeclaration[] nestedDeclarations = TypeDeclaration.GetNestedDeclarations(syntaxNode); + TypeDeclaration[] nestedDeclarationsReplaced = nestedDeclarations.Select(d => + { + string newName = d.Name.Replace(findAndReplaces, regexReplaces); + return newName == d.Name ? d : d.WithName(newName); + }).ToArray(); cancellationToken.ThrowIfCancellationRequested(); @@ -167,8 +171,8 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can return new( usings: usings, - @namespace: targetAttributeTypeSymbol.GetNamespace(), - nestedTypeDeclarations: nestedDeclarations, + @namespace: targetAttributeTypeSymbol.GetNamespace().Replace(findAndReplaces, regexReplaces), + nestedTypeDeclarations: nestedDeclarationsReplaced, sourceCode: sourceCode, findAndReplaces: findAndReplaces, regexReplaces: regexReplaces @@ -218,15 +222,7 @@ static void GenerateSource(SourceProductionContext context, Capture capture) context.CancellationToken.ThrowIfCancellationRequested(); - string sourceCode = capture.SourceCode; - foreach((string find, string replace) in capture.FindAndReplaces) - { - sourceCode = sourceCode.Replace(find, replace); - } - foreach((string pattern, string replacement) in capture.RegexReplaces) - { - sourceCode = Regex.Replace(sourceCode, pattern, replacement); - } + string sourceCode = capture.SourceCode.Replace(capture.FindAndReplaces, capture.RegexReplaces); foreach(string line in sourceCode.Split('\n')) { sourceWriter.WriteLine(line.TrimEnd('\r')); @@ -336,6 +332,11 @@ internal sealed class TypeDeclaration(string modifiers, string keyword, string n public string TypeParameters { get; } = typeParameters; public string Constraints { get; } = constraints; + public TypeDeclaration WithName(string name) + { + return new(Modifiers, Keyword, name, TypeParameters, Constraints); + } + public static TypeDeclaration[] GetNestedDeclarations(TypeDeclarationSyntax typeSyntax) { var declarations = new List(); diff --git a/src/SourceGenerators/InternalExtensions.cs b/src/SourceGenerators/InternalExtensions.cs index 6bb4bb2..576fe55 100644 --- a/src/SourceGenerators/InternalExtensions.cs +++ b/src/SourceGenerators/InternalExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Collections.Generic; using System; +using System.Text.RegularExpressions; namespace SpanExtensions.SourceGenerators { @@ -95,5 +96,20 @@ public static IEnumerable Skip(this IEnumerable source, T skip) where T } } } + + public static string Replace(this string source, (string find, string replace)[] findAndReplaces, (string pattern, string replacement)[] regexReplaces) + { + foreach((string find, string replace) in findAndReplaces) + { + source = source.Replace(find, replace); + } + + foreach((string pattern, string replacement) in regexReplaces) + { + source = Regex.Replace(source, pattern, replacement); + } + + return source; + } } } From 82f7e70368a094a8efa106c867b423aa15df2a2a Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Mon, 26 Feb 2024 13:49:21 +0400 Subject: [PATCH 15/27] error out if parent is not declared as partial --- src/SourceGenerators/CopyGenerator.cs | 41 +++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index 7070bf5..f3a033c 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -29,18 +29,18 @@ namespace SpanExtensions.SourceGenerators [Generator(LanguageNames.CSharp)] sealed class CopyGenerator : IIncrementalGenerator { - static readonly string @namespace = typeof(CopyGenerator).Namespace; - static readonly AssemblyName assemblyName = typeof(CopyGenerator).Assembly.GetName(); - static readonly string generatedCodeAttribute = $@"global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{assemblyName.Name}"", ""{assemblyName.Version}"")"; + static readonly string generatedNamespace = typeof(CopyGenerator).Namespace; + static readonly AssemblyName generatedAssemblyName = typeof(CopyGenerator).Assembly.GetName(); + static readonly string generatedCodeAttribute = $@"global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{generatedAssemblyName.Name}"", ""{generatedAssemblyName.Version}"")"; const string generateCopyAttributeName = "GenerateCopyAttribute"; - static readonly string generateCopyAttributeFullName = $"{@namespace}.{generateCopyAttributeName}"; + static readonly string generateCopyAttributeFullName = $"{generatedNamespace}.{generateCopyAttributeName}"; static readonly string generateCopyAttributeFileName = $"{generateCopyAttributeName}.generated.cs"; static readonly string generateCopyAttributeSource = $$""" // #nullable enable - namespace {{@namespace}} + namespace {{generatedNamespace}} { /// /// Marks the class/interface/struct/record to be copied, and the FindAndReplace and RegexReplace operations to be performed on the copied source. @@ -150,6 +150,9 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can cancellationToken.ThrowIfCancellationRequested(); + string @namespace = targetAttributeTypeSymbol.GetNamespace(); + string namespaceReplaced = @namespace.Replace(findAndReplaces, regexReplaces); + TypeDeclaration[] nestedDeclarations = TypeDeclaration.GetNestedDeclarations(syntaxNode); TypeDeclaration[] nestedDeclarationsReplaced = nestedDeclarations.Select(d => { @@ -157,6 +160,32 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can return newName == d.Name ? d : d.WithName(newName); }).ToArray(); + if(@namespace != namespaceReplaced) + { + for(int i = 1; i < nestedDeclarations.Length; i++) + { + if(nestedDeclarations[i] != nestedDeclarationsReplaced[i]) + { + break; + } + + if(!nestedDeclarations[i].Modifiers.Contains("partial")) + { + SyntaxNode parent = syntaxNode; + while(i-- != 0) + { + parent = parent.Parent!; + } + + Location location = parent is TypeDeclarationSyntax declarationWithoutPartial + ? declarationWithoutPartial.Identifier.GetLocation() + : syntaxNode.Identifier.GetLocation(); + + return new("Type must be declared as \"partial\"", location); + } + } + } + cancellationToken.ThrowIfCancellationRequested(); TypeDeclarationSyntax syntaxToCopy = syntaxNode.WithAttributeLists( @@ -171,7 +200,7 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can return new( usings: usings, - @namespace: targetAttributeTypeSymbol.GetNamespace().Replace(findAndReplaces, regexReplaces), + @namespace: namespaceReplaced, nestedTypeDeclarations: nestedDeclarationsReplaced, sourceCode: sourceCode, findAndReplaces: findAndReplaces, From 82c6826f7a41faad46dae214a38825c82c564da8 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Mon, 26 Feb 2024 19:08:07 +0400 Subject: [PATCH 16/27] fixed wrong condition --- src/SourceGenerators/CopyGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index f3a033c..bcbb4a2 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -160,7 +160,7 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can return newName == d.Name ? d : d.WithName(newName); }).ToArray(); - if(@namespace != namespaceReplaced) + if(@namespace == namespaceReplaced) { for(int i = 1; i < nestedDeclarations.Length; i++) { From 931b9f6f4ba9001bdac2c76f32f4331060a9c33e Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Mon, 26 Feb 2024 19:08:25 +0400 Subject: [PATCH 17/27] disable namespace replacement for now --- src/SourceGenerators/CopyGenerator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index bcbb4a2..f4b9eb6 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -150,8 +150,9 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can cancellationToken.ThrowIfCancellationRequested(); + const bool replaceNamespace = false; string @namespace = targetAttributeTypeSymbol.GetNamespace(); - string namespaceReplaced = @namespace.Replace(findAndReplaces, regexReplaces); + string namespaceReplaced = replaceNamespace ? @namespace.Replace(findAndReplaces, regexReplaces) : @namespace; TypeDeclaration[] nestedDeclarations = TypeDeclaration.GetNestedDeclarations(syntaxNode); TypeDeclaration[] nestedDeclarationsReplaced = nestedDeclarations.Select(d => From 82cc4dffcc4d1a3814660bee42aec71d6aa1a591 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Mon, 26 Feb 2024 22:59:47 +0400 Subject: [PATCH 18/27] added Span Trim and IsWhiteSpace extensions --- src/InternalExtensions.cs | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/InternalExtensions.cs diff --git a/src/InternalExtensions.cs b/src/InternalExtensions.cs new file mode 100644 index 0000000..528dddd --- /dev/null +++ b/src/InternalExtensions.cs @@ -0,0 +1,41 @@ +using System; + + +namespace SpanExtensions +{ + static class InternalExtensions + { + /// + /// Indicates whether the specified span contains only white-space characters. + /// + /// The source span. + /// if the span contains only whitespace characters, otherwise. + public static bool IsWhiteSpace(this Span span) + { + return ((ReadOnlySpan)span).IsWhiteSpace(); + } + +#if !NETCOREAPP3_0_OR_GREATER + /// + /// Removes all leading and trailing white-space characters from the span. + /// Constructs a from a span and a delimiter. Only consume this class through . + /// + /// The source span from which the characters are removed. + public static Span Trim(this Span span) + { + ReadOnlySpan rospan = span; + ReadOnlySpan leftTrimmed = rospan.TrimStart(); + ReadOnlySpan trimmed = rospan.TrimEnd(); + + if(span.Length == trimmed.Length) + { + return span; + } + else + { + return span.Slice(rospan.Length - leftTrimmed.Length, trimmed.Length); + } + } +#endif + } +} From c46848328aacb344993ef28ba4e9517ea514d22c Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Mon, 26 Feb 2024 23:26:34 +0400 Subject: [PATCH 19/27] allow multiple uses of the attribute --- src/SourceGenerators/CopyGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index f4b9eb6..28b4f34 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -46,7 +46,7 @@ namespace {{generatedNamespace}} /// Marks the class/interface/struct/record to be copied, and the FindAndReplace and RegexReplace operations to be performed on the copied source. /// [{{generatedCodeAttribute}}] - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Interface | global::System.AttributeTargets.Struct, AllowMultiple = false)] + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Interface | global::System.AttributeTargets.Struct, AllowMultiple = true)] public sealed class {{generateCopyAttributeName}} : global::System.Attribute { /// From 4b718bf782d40c5c7ac26eccd6888a95a0f37b55 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Mon, 26 Feb 2024 23:26:44 +0400 Subject: [PATCH 20/27] added GeneratedFileTag to the attribute --- src/SourceGenerators/CopyGenerator.cs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index 28b4f34..b07f2ae 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -58,6 +58,14 @@ public sealed class {{generateCopyAttributeName}} : global::System.Attribute /// The RegexReplace operations to be performed on the copied source. /// public string[] RegexReplaces { get; set; } = global::System.Array.Empty(); + + /// + /// The tag to add to the generated file name. + /// + /// + /// Used when generating several copies of the same partial type. + /// + public string GeneratedFileTag { get; set; } = ""; } } """; @@ -106,6 +114,7 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can (string find, string replace)[] findAndReplaces = []; (string pattern, string replacement)[] regexReplaces = []; + string generatedFileTag = ""; AttributeData attribute = context.Attributes.First(a => a.AttributeClass!.Name == generateCopyAttributeName); foreach((string parameter, TypedConstant value) in attribute.NamedArguments) { @@ -139,6 +148,9 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can regexReplaces[i] = (strings[j], strings[j + 1]); } break; + case "GeneratedFileTag": + generatedFileTag = value.Value.ToString(); + break; default: return new("Unrecognized parameter {0} for attribute {1}.", attributeSyntax.GetLocation(), parameter, generateCopyAttributeName); } @@ -205,7 +217,8 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can nestedTypeDeclarations: nestedDeclarationsReplaced, sourceCode: sourceCode, findAndReplaces: findAndReplaces, - regexReplaces: regexReplaces + regexReplaces: regexReplaces, + generatedFileTag: generatedFileTag ); } @@ -276,10 +289,12 @@ static void GenerateSource(SourceProductionContext context, Capture capture) context.CancellationToken.ThrowIfCancellationRequested(); sourceCode = sourceBuilder.ToString(); - context.AddSource($"{capture.UniqueName}.{generateCopyAttributeName}.generated.cs", sourceCode); + + string fileTag = string.IsNullOrEmpty(capture.GeneratedFileTag) ? "0" : capture.GeneratedFileTag; + context.AddSource($"{capture.UniqueName}.{generateCopyAttributeName}.{fileTag}.generated.cs", sourceCode); } - sealed class Capture(string[] usings, string @namespace, TypeDeclaration[] nestedTypeDeclarations, string sourceCode, (string find, string replace)[] findAndReplaces, (string pattern, string replacement)[] regexReplaces) : IEquatable + sealed class Capture(string[] usings, string @namespace, TypeDeclaration[] nestedTypeDeclarations, string sourceCode, (string find, string replace)[] findAndReplaces, (string pattern, string replacement)[] regexReplaces, string generatedFileTag) : IEquatable { public string[] Usings { get; } = usings; public string Namespace { get; } = @namespace; @@ -287,12 +302,13 @@ sealed class Capture(string[] usings, string @namespace, TypeDeclaration[] neste public string SourceCode { get; } = sourceCode; public (string find, string replace)[] FindAndReplaces { get; } = findAndReplaces; public (string pattern, string replacement)[] RegexReplaces { get; } = regexReplaces; + public string GeneratedFileTag { get; } = generatedFileTag; public string? DiagnosticMessage { get; } public object?[]? DiagnosticMessageArgs { get; } public Location? DiagnosticMessageLocation { get; } - Capture() : this([], "", [], "", [], []) { } + Capture() : this([], "", [], "", [], [], "") { } public Capture(string diagnosticMessage, params object?[]? diagnosticMessageArgs) : this() { @@ -326,6 +342,7 @@ public bool Equals(Capture other) => SourceCode == other.SourceCode && FindAndReplaces.AsSpan().SequenceEqual(other.FindAndReplaces) && RegexReplaces.AsSpan().SequenceEqual(other.RegexReplaces) && + GeneratedFileTag == other.GeneratedFileTag && DiagnosticMessage == other.DiagnosticMessage; public override int GetHashCode() @@ -338,6 +355,7 @@ public override int GetHashCode() hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(SourceCode); hashCode = (hashCode * multiplier) + EqualityComparer<(string, string)[]>.Default.GetHashCode(FindAndReplaces); hashCode = (hashCode * multiplier) + EqualityComparer<(string, string)[]>.Default.GetHashCode(RegexReplaces); + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(GeneratedFileTag); hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(DiagnosticMessage ?? ""); return hashCode; } From b57120c56fb22dc6b3f3ccb1c2fd4dc34b9e5f3b Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Mon, 26 Feb 2024 23:37:51 +0400 Subject: [PATCH 21/27] use file name as GeneratedFileTag by default if target is partial --- src/SourceGenerators/CopyGenerator.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index b07f2ae..b72a7d7 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -149,7 +149,7 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can } break; case "GeneratedFileTag": - generatedFileTag = value.Value.ToString(); + generatedFileTag = value.Value?.ToString() ?? ""; break; default: return new("Unrecognized parameter {0} for attribute {1}.", attributeSyntax.GetLocation(), parameter, generateCopyAttributeName); @@ -211,6 +211,12 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can cancellationToken.ThrowIfCancellationRequested(); + if(string.IsNullOrEmpty(generatedFileTag) && nestedDeclarations[0].Modifiers.Contains("partial")) + { + string filePath = syntaxNode.SyntaxTree.FilePath; + generatedFileTag = Path.GetFileNameWithoutExtension(filePath); + } + return new( usings: usings, @namespace: namespaceReplaced, From 6e270a59e964f9920b139489d016f90a03723501 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Tue, 27 Feb 2024 00:06:46 +0400 Subject: [PATCH 22/27] make the GenerateCopy attribute internal --- src/SourceGenerators/CopyGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index b72a7d7..b8b83a3 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -47,7 +47,7 @@ namespace {{generatedNamespace}} /// [{{generatedCodeAttribute}}] [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Interface | global::System.AttributeTargets.Struct, AllowMultiple = true)] - public sealed class {{generateCopyAttributeName}} : global::System.Attribute + sealed class {{generateCopyAttributeName}} : global::System.Attribute { /// /// The FindAndReplace operations to be performed on the copied source. From cecf12251f793e5c3e2b57384b2b98d73ed5b342 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Tue, 27 Feb 2024 12:27:41 +0400 Subject: [PATCH 23/27] fixed doc comments for Span.Trim --- src/InternalExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InternalExtensions.cs b/src/InternalExtensions.cs index 528dddd..2f75764 100644 --- a/src/InternalExtensions.cs +++ b/src/InternalExtensions.cs @@ -18,9 +18,9 @@ public static bool IsWhiteSpace(this Span span) #if !NETCOREAPP3_0_OR_GREATER /// /// Removes all leading and trailing white-space characters from the span. - /// Constructs a from a span and a delimiter. Only consume this class through . /// /// The source span from which the characters are removed. + /// The trimmed character span. public static Span Trim(this Span span) { ReadOnlySpan rospan = span; From aa7ff10f270121f517b2cd49657283c7bac8d91e Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Tue, 27 Feb 2024 12:29:15 +0400 Subject: [PATCH 24/27] also check target class for partial --- src/SourceGenerators/CopyGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index b8b83a3..a73a049 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -175,7 +175,7 @@ static bool SyntaxProviderPredicate(SyntaxNode syntaxNode, CancellationToken can if(@namespace == namespaceReplaced) { - for(int i = 1; i < nestedDeclarations.Length; i++) + for(int i = 0; i < nestedDeclarations.Length; i++) { if(nestedDeclarations[i] != nestedDeclarationsReplaced[i]) { From 38f638365b3bb607e138499988f251519047dcd0 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Tue, 27 Feb 2024 21:37:56 +0400 Subject: [PATCH 25/27] removed incorrectly pushed temporary file --- TestProject1/GlobalUsings.cs | 1 - 1 file changed, 1 deletion(-) delete mode 100644 TestProject1/GlobalUsings.cs diff --git a/TestProject1/GlobalUsings.cs b/TestProject1/GlobalUsings.cs deleted file mode 100644 index 8c927eb..0000000 --- a/TestProject1/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file From b2f0151f372de15d9ed6ada015c3f0532fa271f2 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Mon, 18 Mar 2024 18:32:27 +0400 Subject: [PATCH 26/27] reformatted the GetAttributeSyntax method --- src/SourceGenerators/InternalExtensions.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/SourceGenerators/InternalExtensions.cs b/src/SourceGenerators/InternalExtensions.cs index 576fe55..f4b06b7 100644 --- a/src/SourceGenerators/InternalExtensions.cs +++ b/src/SourceGenerators/InternalExtensions.cs @@ -52,18 +52,11 @@ public static string GetNamespace(this INamedTypeSymbol namedTypeSymbol) public static AttributeSyntax GetAttributeSyntax(this TypeDeclarationSyntax syntaxNode, string attributeName) { - string attributeFullName = ""; - if(attributeName.EndsWith("Attribute")) - { - attributeFullName = attributeName; - attributeName = attributeName.Substring(0, attributeName.Length - "Attribute".Length); - } - else - { - attributeFullName = attributeName + "Attribute"; - } + string[] attributeNames = attributeName.EndsWith("Attribute") + ? [attributeName.Substring(0, attributeName.Length - "Attribute".Length), attributeName] + : [attributeName, attributeName + "Attribute"]; - return syntaxNode.AttributeLists.Select(al => al.Attributes.FirstOrDefault(a => a.Name.ToString() == attributeName || a.Name.ToString() == attributeFullName)).First(a => a != null); + return syntaxNode.AttributeLists.Select(al => al.Attributes.FirstOrDefault(a => attributeNames.Contains(a.Name.ToString()))).First(a => a != null); } public static AttributeListSyntax RemoveIfContains(this AttributeListSyntax list, AttributeSyntax attribute) From 87bb0085323555ee5884e0118f8e5e84e28c6e66 Mon Sep 17 00:00:00 2001 From: Guiorgy Date: Mon, 18 Mar 2024 19:17:53 +0400 Subject: [PATCH 27/27] calculate hash codes inside unchecked blocks --- src/SourceGenerators/CopyGenerator.cs | 44 +++++++++++++++------------ 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs index a73a049..d20e9d7 100644 --- a/src/SourceGenerators/CopyGenerator.cs +++ b/src/SourceGenerators/CopyGenerator.cs @@ -353,17 +353,20 @@ public bool Equals(Capture other) => public override int GetHashCode() { - const int multiplier = -1521134295; - int hashCode = -573548719; - hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(Usings); - hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(Namespace); - hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(NestedTypeDeclarations); - hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(SourceCode); - hashCode = (hashCode * multiplier) + EqualityComparer<(string, string)[]>.Default.GetHashCode(FindAndReplaces); - hashCode = (hashCode * multiplier) + EqualityComparer<(string, string)[]>.Default.GetHashCode(RegexReplaces); - hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(GeneratedFileTag); - hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(DiagnosticMessage ?? ""); - return hashCode; + unchecked + { + const int multiplier = -1521134295; + int hashCode = -573548719; + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(Usings); + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(Namespace); + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(NestedTypeDeclarations); + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(SourceCode); + hashCode = (hashCode * multiplier) + EqualityComparer<(string, string)[]>.Default.GetHashCode(FindAndReplaces); + hashCode = (hashCode * multiplier) + EqualityComparer<(string, string)[]>.Default.GetHashCode(RegexReplaces); + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(GeneratedFileTag); + hashCode = (hashCode * multiplier) + EqualityComparer.Default.GetHashCode(DiagnosticMessage ?? ""); + return hashCode; + } } public sealed class Comparer : IEqualityComparer @@ -433,14 +436,17 @@ public override int GetHashCode() { static int StringHash(string s) => EqualityComparer.Default.GetHashCode(s); - const int multiplier = -1521134295; - int hashCode = -754136522; - hashCode = (hashCode * multiplier) + StringHash(Modifiers); - hashCode = (hashCode * multiplier) + StringHash(Keyword); - hashCode = (hashCode * multiplier) + StringHash(Name); - hashCode = (hashCode * multiplier) + StringHash(TypeParameters); - hashCode = (hashCode * multiplier) + StringHash(Constraints); - return hashCode; + unchecked + { + const int multiplier = -1521134295; + int hashCode = -754136522; + hashCode = (hashCode * multiplier) + StringHash(Modifiers); + hashCode = (hashCode * multiplier) + StringHash(Keyword); + hashCode = (hashCode * multiplier) + StringHash(Name); + hashCode = (hashCode * multiplier) + StringHash(TypeParameters); + hashCode = (hashCode * multiplier) + StringHash(Constraints); + return hashCode; + } } public override string ToString()