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/src/InternalExtensions.cs b/src/InternalExtensions.cs new file mode 100644 index 0000000..2f75764 --- /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. + /// + /// The source span from which the characters are removed. + /// The trimmed character span. + 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 + } +} diff --git a/src/SourceGenerators/CopyGenerator.cs b/src/SourceGenerators/CopyGenerator.cs new file mode 100644 index 0000000..d20e9d7 --- /dev/null +++ b/src/SourceGenerators/CopyGenerator.cs @@ -0,0 +1,460 @@ +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.CodeDom.Compiler; +using System.Globalization; + +using System.IO; + +#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 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 = $"{generatedNamespace}.{generateCopyAttributeName}"; + static readonly string generateCopyAttributeFileName = $"{generateCopyAttributeName}.generated.cs"; + static readonly string generateCopyAttributeSource = $$""" + // + #nullable enable + + 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 = true)] + 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(); + + /// + /// The tag to add to the generated file name. + /// + /// + /// Used when generating several copies of the same partial type. + /// + public string GeneratedFileTag { get; set; } = ""; + } + } + """; + + const string indentation = " "; + + 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 Capture? SyntaxProviderTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { +#if DEBUGGENERATORS + if (!Debugger.IsAttached) Debugger.Launch(); +#endif + + var syntaxNode = (TypeDeclarationSyntax)context.TargetNode; + AttributeSyntax attributeSyntax = syntaxNode.GetAttributeSyntax(generateCopyAttributeName); + + string[] usings = syntaxNode.GetUsings().Skip("using SpanExtensions.SourceGenerators;").ToArray(); + + 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 = []; + string generatedFileTag = ""; + 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.", attributeSyntax.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.", attributeSyntax.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; + case "GeneratedFileTag": + generatedFileTag = value.Value?.ToString() ?? ""; + break; + default: + 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.", attributeSyntax.GetLocation(), generateCopyAttributeName); + } + + cancellationToken.ThrowIfCancellationRequested(); + + const bool replaceNamespace = false; + string @namespace = targetAttributeTypeSymbol.GetNamespace(); + string namespaceReplaced = replaceNamespace ? @namespace.Replace(findAndReplaces, regexReplaces) : @namespace; + + 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(); + + if(@namespace == namespaceReplaced) + { + for(int i = 0; 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( + new SyntaxList( + syntaxNode.AttributeLists.Select(al => al.RemoveIfContains(attributeSyntax)).Where(al => al.Attributes.Count != 0) + ) + ); + syntaxToCopy = syntaxToCopy.WithLeadingTrivia(syntaxNode.GetLeadingTrivia()); + string sourceCode = syntaxToCopy.NormalizeWhitespace(indentation: indentation, eol: "\n").ToFullString(); + + 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, + nestedTypeDeclarations: nestedDeclarationsReplaced, + sourceCode: sourceCode, + findAndReplaces: findAndReplaces, + regexReplaces: regexReplaces, + generatedFileTag: generatedFileTag + ); + } + + 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; + } + + 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.Replace(capture.FindAndReplaces, capture.RegexReplaces); + foreach(string line in sourceCode.Split('\n')) + { + sourceWriter.WriteLine(line.TrimEnd('\r')); + } + + context.CancellationToken.ThrowIfCancellationRequested(); + + for(int i = 0; i < capture.NestedTypeDeclarations.Length - 1; i++) + { + sourceWriter.Indent--; // parent declarations + sourceWriter.WriteLine("}"); + } + + sourceWriter.Indent--; // namespace + sourceWriter.WriteLine("}"); + +#if DEBUGGENERATORS + Debug.Assert(sourceWriter.Indent == 0); +#endif + + context.CancellationToken.ThrowIfCancellationRequested(); + + sourceCode = sourceBuilder.ToString(); + + 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, string generatedFileTag) : 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 GeneratedFileTag { get; } = generatedFileTag; + + 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 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); + + 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) && + GeneratedFileTag == other.GeneratedFileTag && + DiagnosticMessage == other.DiagnosticMessage; + + public override int GetHashCode() + { + 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 + { + 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 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 TypeDeclaration WithName(string name) + { + return new(Modifiers, Keyword, name, TypeParameters, 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(), + typeParameters: parentSyntax.TypeParameterList?.ToString() ?? "", + 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 && + TypeParameters == other.TypeParameters && + Constraints == other.Constraints; + + public override int GetHashCode() + { + static int StringHash(string s) => EqualityComparer.Default.GetHashCode(s); + + 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() + { + 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 new file mode 100644 index 0000000..f4b06b7 --- /dev/null +++ b/src/SourceGenerators/InternalExtensions.cs @@ -0,0 +1,108 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis; +using System.Linq; +using System.Collections.Generic; +using System; +using System.Text.RegularExpressions; + +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 IEnumerable GetUsings(this SyntaxNode? syntaxNode) + { + while(syntaxNode != null && syntaxNode is not CompilationUnitSyntax) + { + syntaxNode = syntaxNode.Parent; + } + + return syntaxNode is not CompilationUnitSyntax compilationSyntax + ? [] + : compilationSyntax.Usings.Select(@using => @using.ToString()); + } + + 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); + + namespaceParts.Reverse(); + return string.Join(".", namespaceParts); + } + + public static AttributeSyntax GetAttributeSyntax(this TypeDeclarationSyntax syntaxNode, string attributeName) + { + 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 => attributeNames.Contains(a.Name.ToString()))).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)!; + } + + public static IEnumerable And(this IEnumerable source, T after) + { + foreach(T element in source) + { + yield return element; + } + + 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; + } + } + } + + 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; + } + } +} 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..bf509af 100644 --- a/src/SpanExtensions.csproj +++ b/src/SpanExtensions.csproj @@ -26,16 +26,27 @@ SpanExtensions.Net README.md icon.png + Debug;Release;DebugGenerators portable + + net8.0 + portable + + portable + + net8.0 + portable + + portable @@ -44,10 +55,16 @@ portable + + + + + + - True - \ + True + \ True @@ -57,10 +74,14 @@ - + True \ - + + + + +