From 06d4d2e26c4095967e02ea83aef4a86db6aa807a Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Sat, 12 Sep 2026 15:08:35 -0400 Subject: [PATCH 1/4] Take the cloning layer from the Zomp.MethodCloning package The Cloning folder, EquatableArray and ReportedDiagnostic move to the Zomp.MethodCloning source package, maintained in its own repository. They come back as internal types compiled into the generator, with this repository's ROSLYN_* constants, so every Roslyn variant still gets its own build of them. Generated output is unchanged. nuget.config maps the package to a local folder feed until it is published, so this branch cannot pass CI yet. Generated with Claude Code --- AGENTS.md | 34 +- Directory.Packages.props | 1 + nuget.config | 4 + .../Cloning/CloneTarget.cs | 61 --- .../Cloning/ClonedMethod.cs | 53 --- .../Cloning/ClonedMethodOutput.cs | 138 ------ .../Cloning/ClonedMethodSource.cs | 146 ------ .../Cloning/CloningRewriter.cs | 432 ------------------ .../Cloning/MethodLocation.cs | 183 -------- .../Cloning/MethodParent.cs | 9 - .../Cloning/MethodParentDeclaration.cs | 30 -- .../Cloning/MethodSignature.cs | 89 ---- .../Helpers/EquatableArray.cs | 154 ------- .../Models/ReportedDiagnostic.cs | 35 -- .../Zomp.SyncMethodGenerator.csproj | 4 +- .../Generator.Tests/IdentityCloneGenerator.cs | 2 +- tests/Generator.Tests/IdentityRewriter.cs | 2 +- 17 files changed, 28 insertions(+), 1349 deletions(-) delete mode 100644 src/Zomp.SyncMethodGenerator/Cloning/CloneTarget.cs delete mode 100644 src/Zomp.SyncMethodGenerator/Cloning/ClonedMethod.cs delete mode 100644 src/Zomp.SyncMethodGenerator/Cloning/ClonedMethodOutput.cs delete mode 100644 src/Zomp.SyncMethodGenerator/Cloning/ClonedMethodSource.cs delete mode 100644 src/Zomp.SyncMethodGenerator/Cloning/CloningRewriter.cs delete mode 100644 src/Zomp.SyncMethodGenerator/Cloning/MethodLocation.cs delete mode 100644 src/Zomp.SyncMethodGenerator/Cloning/MethodParent.cs delete mode 100644 src/Zomp.SyncMethodGenerator/Cloning/MethodParentDeclaration.cs delete mode 100644 src/Zomp.SyncMethodGenerator/Cloning/MethodSignature.cs delete mode 100644 src/Zomp.SyncMethodGenerator/Helpers/EquatableArray.cs delete mode 100644 src/Zomp.SyncMethodGenerator/Models/ReportedDiagnostic.cs diff --git a/AGENTS.md b/AGENTS.md index 9962475..5cf3f2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,9 +112,7 @@ src/Zomp.SyncMethodGenerator/ Generator (netstandard2.0) SourceGenerationHelper.cs Attribute definitions Extensions.cs Type-checking extensions on INamedTypeSymbol DiagnosticMessages.cs ZSMGEN001-006 diagnostic descriptors - Cloning/ Copying a method into a file of its own - Models/ Data records for the pipeline - Helpers/ EquatableArray, DirectiveStack, etc. + Helpers/ CompilationExtensions, LoopExitWalker Properties/ Assembly attributes tools/ MSBuild props/targets shipped in the NuGet package src/Zomp.SyncMethodGenerator.Pack/ Packing-only project (no code) @@ -124,18 +122,24 @@ tests/GenerationSandbox.Tests/ Integration tests (real-world patterns) ## Cloning layer -`Cloning/` holds everything about emitting a copy of a method that has nothing -to do with async: finding the marked methods (`CloneTarget`), collecting the -namespaces, usings and containing types around them (`MethodLocation`), fully -qualifying the names a method uses (`CloningRewriter`), detecting colliding -signatures and naming the files (`ClonedMethodOutput`), and writing the file -(`ClonedMethodSource`). - -Keep async knowledge out of it. A transformation plugs in by deriving from -`CloningRewriter`, overriding the visitors it needs, and using the `MapSymbol` -and `MapTypeName` hooks to substitute types. Nothing under `Cloning/` should -know about tasks or `SYNC_ONLY`; a grep for `Task|Async|SYNC_ONLY` there should -come back empty. +Everything about emitting a copy of a method that has nothing to do with async +comes from the `Zomp.MethodCloning` source package, maintained in the +`zompinc/method-cloning` repository: finding the marked methods +(`CloneTarget`), collecting the namespaces, usings and containing types around +them (`MethodLocation`), fully qualifying the names a method uses +(`CloningRewriter`), detecting colliding signatures and naming the files +(`ClonedMethodOutput`), and writing the file (`ClonedMethodSource`). + +The package ships source, not a DLL. Its files compile into the generator as +internal types in the `Zomp.MethodCloning` namespace, with the `ROSLYN_*` +constants this repository defines, so each Roslyn variant gets its own build of +them. A change to them belongs in that repository: fix it there, pack it, and +bump the version in `Directory.Packages.props`. `AsyncToSyncRewriter` plugs in +by deriving from `CloningRewriter` and using its `MapSymbol` and `MapTypeName` +hooks. + +Until the package is on a public feed, `nuget.config` maps `Zomp.MethodCloning` +to a local folder feed, `~/.nuget/local-feed`, which CI cannot restore from. ## Transformation Pipeline diff --git a/Directory.Packages.props b/Directory.Packages.props index f3875a6..91b543f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,5 +17,6 @@ + diff --git a/nuget.config b/nuget.config index 006dc55..4d7bd39 100644 --- a/nuget.config +++ b/nuget.config @@ -5,6 +5,7 @@ + @@ -16,5 +17,8 @@ --> + + + diff --git a/src/Zomp.SyncMethodGenerator/Cloning/CloneTarget.cs b/src/Zomp.SyncMethodGenerator/Cloning/CloneTarget.cs deleted file mode 100644 index c57662e..0000000 --- a/src/Zomp.SyncMethodGenerator/Cloning/CloneTarget.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace Zomp.SyncMethodGenerator.Cloning; - -/// -/// A method marked for cloning, either by an attribute of its own or by one on its containing type. -/// -/// Context of the attribute which marked the method. -/// Declaration of the method. -internal sealed record CloneTarget(GeneratorAttributeSyntaxContext Context, MethodDeclarationSyntax Syntax) -{ - /// - /// Finds the methods an attribute marks. One on a type marks every method the type declares. - /// - /// Syntax provider of the generator. - /// Name of the attribute. - /// A method for each method marked. - public static IncrementalValuesProvider ForAttribute(SyntaxValueProvider syntaxProvider, string fullyQualifiedMetadataName) - => syntaxProvider - .ForAttributeWithMetadataName( - fullyQualifiedMetadataName, - predicate: static (s, _) => IsCandidate(s), - transform: static (ctx, ct) => Expand(ctx, ct)) - .SelectMany((list, ct) => list); - - private static bool IsCandidate(SyntaxNode node) => node switch - { - MethodDeclarationSyntax { AttributeLists.Count: > 0 } => true, - ClassDeclarationSyntax { AttributeLists.Count: > 0 } => true, - StructDeclarationSyntax { AttributeLists.Count: > 0 } => true, - InterfaceDeclarationSyntax { AttributeLists.Count: > 0 } => true, - RecordDeclarationSyntax { AttributeLists.Count: > 0 } => true, - _ => false, - }; - - private static ImmutableArray Expand(GeneratorAttributeSyntaxContext ctx, CancellationToken ct) - { - ct.ThrowIfCancellationRequested(); - - if (ctx.TargetNode is TypeDeclarationSyntax typeDecl) - { -#if ROSLYN_4_12_OR_GREATER - return [.. typeDecl.Members.OfType().Select(s => new CloneTarget(ctx, s))]; -#else - return ImmutableArray.CreateRange(typeDecl.Members.OfType().Select(s => new CloneTarget(ctx, s))); -#endif - } - else if (ctx.TargetNode is MethodDeclarationSyntax methodDecl) - { -#if ROSLYN_4_12_OR_GREATER - return [new CloneTarget(ctx, methodDecl)]; -#else - return ImmutableArray.Create(new CloneTarget(ctx, methodDecl)); -#endif - } - -#if ROSLYN_4_12_OR_GREATER - return []; -#else - return ImmutableArray.Empty; -#endif - } -} diff --git a/src/Zomp.SyncMethodGenerator/Cloning/ClonedMethod.cs b/src/Zomp.SyncMethodGenerator/Cloning/ClonedMethod.cs deleted file mode 100644 index ccf899f..0000000 --- a/src/Zomp.SyncMethodGenerator/Cloning/ClonedMethod.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Zomp.SyncMethodGenerator.Cloning; - -/// -/// A method rewritten by a transformation, ready to be emitted into a file of its own. -/// -/// Where the original method sits. -/// Name of the original method. -/// The rewritten method. -/// True to leave the #nullable enable directive out of the file. -/// Diagnostics the transformation reported. -/// True if there are errors in , in which case nothing is emitted. -/// Signature the rewritten method will be emitted with, used to detect colliding overloads. -internal sealed record ClonedMethod( - MethodLocation Location, - string MethodName, - string Implementation, - bool DisableNullable, - EquatableArray Diagnostics, - bool HasErrors, - MethodSignature? Signature) -{ - /// - /// Creates a cloned method from the result of rewriting it. - /// - /// Where the original method sits. - /// Declaration of the original method. - /// Result of rewriting the method, or the extension block which holds it. - /// True to leave the #nullable enable directive out of the file. - /// Diagnostics the transformation reported. - /// A new . - public static ClonedMethod Create( - MethodLocation location, - MethodDeclarationSyntax original, - SyntaxNode rewritten, - bool disableNullable, - ImmutableArray diagnostics) - { - var hasErrors = false; - foreach (var diagnostic in diagnostics) - { - hasErrors |= diagnostic.Descriptor.DefaultSeverity == DiagnosticSeverity.Error; - } - - return new( - location, - original.Identifier.ValueText, - rewritten.ToFullString(), - disableNullable, - diagnostics, - hasErrors, - MethodSignature.Create(rewritten, location, original)); - } -} diff --git a/src/Zomp.SyncMethodGenerator/Cloning/ClonedMethodOutput.cs b/src/Zomp.SyncMethodGenerator/Cloning/ClonedMethodOutput.cs deleted file mode 100644 index 5486ce0..0000000 --- a/src/Zomp.SyncMethodGenerator/Cloning/ClonedMethodOutput.cs +++ /dev/null @@ -1,138 +0,0 @@ -namespace Zomp.SyncMethodGenerator.Cloning; - -/// -/// Emits cloned methods, each into a file of its own, and reports those which cannot be emitted. -/// -internal static class ClonedMethodOutput -{ - /// - /// Longest generated file name before the scope is shortened. Emitted files sit under - /// {project}\obj\{configuration}\{framework}\generated\{generator assembly}\{generator}, - /// which alone is around a hundred characters before the project path is counted. - /// - private const int MaxFileNameLength = 100; - - /// - /// Adds the output of the generator: a file for each method, and the diagnostics of each. - /// - /// Context of the generator. - /// Methods to emit. - /// Reported against each method whose signature another method also produces. - public static void Register( - IncrementalGeneratorInitializationContext context, - IncrementalValuesProvider methods, - DiagnosticDescriptor collision) - { - var sourceTexts = methods - .Select(static (m, _) => GenerateSource(m)) - .WithTrackingName("GenerateSource"); - - // Signatures produced more than once. Emitting them all would declare the same member - // twice, so the methods behind them are reported instead of generated. - var collidingSignatures = methods - .Select(static (m, _) => m.Signature?.Key) - .Collect() - .Select(static (keys, _) => MethodSignature.FindCollisions(keys)); - - context.RegisterSourceOutput( - sourceTexts.Combine(collidingSignatures), - (spc, pair) => - { - var (source, colliding) = pair; - - foreach (var diagnostic in source.Method.Diagnostics) - { - spc.ReportDiagnostic(diagnostic); - } - - if (source.Method.Signature is { } signature - && colliding.AsImmutableArray().Contains(signature.Key, StringComparer.Ordinal)) - { - spc.ReportDiagnostic(signature.ToDiagnostic(collision)); - return; - } - - if (!source.Method.HasErrors) - { - spc.AddSource(source.Path, SourceText.From(source.Content, Encoding.UTF8)); - } - }); - } - - private static (ClonedMethod Method, string Path, string Content) GenerateSource(ClonedMethod m) - { - var location = m.Location; - - var scope = $"{string.Join(".", location.Namespaces)}" + - $".{string.Join(".", location.Parents.Select(static p => p.ScopeName))}" + - (location.IsCSharp14Extension ? ".ext" : string.Empty); - - var method = m.MethodName + (location.Index == 1 ? string.Empty : "_" + location.Index); - - var sourcePath = BuildFileName(scope, method); - - var source = ClonedMethodSource.Generate(m); - - return (m, sourcePath, source); - } - - /// - /// Builds the file name a generated method is emitted under, shortening it when the - /// namespace and containing type chain make it long enough to be a problem. Emitted files - /// live several directories deep inside the intermediate output path, so a long name here - /// can carry the full path past the limit the file system accepts. - /// - /// - /// The scope is what gets shortened, since the method name is what someone reads the file - /// name for. A hash of the untruncated name keeps distinct methods in distinct files. - /// - private static string BuildFileName(string scope, string method) - { - const string extension = ".g.cs"; - - var fileName = $"{scope}.{method}{extension}"; - - if (fileName.Length <= MaxFileNameLength) - { - return fileName; - } - - var hash = Hash(fileName); - - // separator, hash, dot, extension - var fixedLength = 1 + hash.Length + 1 + extension.Length; - - var forScope = MaxFileNameLength - fixedLength - method.Length; - - if (forScope > 0) - { - return $"{scope[..Math.Min(scope.Length, forScope)]}_{hash}.{method}{extension}"; - } - - // The method name alone fills the budget, so it has to be shortened as well. - return $"_{hash}.{method[..(MaxFileNameLength - fixedLength)]}{extension}"; - } - - /// - /// FNV-1a. Short, dependency free, and stable across processes and runtimes, which - /// is not. - /// - private static string Hash(string value) - { - const uint offsetBasis = 2166136261; - const uint prime = 16777619; - - var hash = offsetBasis; - - unchecked - { - foreach (var c in value) - { - hash ^= c; - hash *= prime; - } - } - - return hash.ToString("x8", System.Globalization.CultureInfo.InvariantCulture); - } -} diff --git a/src/Zomp.SyncMethodGenerator/Cloning/ClonedMethodSource.cs b/src/Zomp.SyncMethodGenerator/Cloning/ClonedMethodSource.cs deleted file mode 100644 index 870b0a8..0000000 --- a/src/Zomp.SyncMethodGenerator/Cloning/ClonedMethodSource.cs +++ /dev/null @@ -1,146 +0,0 @@ -namespace Zomp.SyncMethodGenerator.Cloning; - -/// -/// Builds the source file a cloned method is emitted in, declaring again, as partial, each -/// namespace and type the original method was nested in. -/// -internal static class ClonedMethodSource -{ - /// - /// Builds the source file for a method. - /// - /// The method. - /// Text of the file. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0072:Add missing cases", Justification = "Only deal with selected SyntaxKinds")] - public static string Generate(ClonedMethod method) - { - static string GetKeyword(SyntaxKind sk) => sk switch - { - SyntaxKind.PublicKeyword => "public", - SyntaxKind.InternalKeyword => "internal", - SyntaxKind.PrivateKeyword => "private", - SyntaxKind.SealedKeyword => "sealed", - SyntaxKind.ProtectedKeyword => "protected", - SyntaxKind.StaticKeyword => "static", - SyntaxKind.AbstractKeyword => "abstract", - _ => throw new InvalidOperationException($"{sk} is not supported"), - }; - - var location = method.Location; - - // Handle namespaces - var sbBegin = new StringBuilder(); - var sbEnd = new StringBuilder(); - var i = 0; - - if (!location.IsNamespaceFileScoped) - { - foreach (var @namespace in location.Namespaces) - { - var indent = new string(' ', 4 * i); - _ = sbBegin.Append($$""" -{{indent}}namespace {{@namespace}} -{{indent}}{ - -"""); - _ = sbEnd.Insert(0, $$""" -{{indent}}} - -"""); - ++i; - } - } - - // Directives which were written inside the namespace are emitted inside it, at the - // innermost level. One which names a namespace relatively still resolves from there. - foreach (var @using in location.InnerUsings) - { - _ = sbBegin.Append($$""" -{{new string(' ', 4 * i)}}{{@using}} - -"""); - } - - if (!location.InnerUsings.IsEmpty) - { - _ = sbBegin.Append(""" - - -"""); - } - - // Handle classes - foreach (var parent in location.Parents) - { - var indent = new string(' ', 4 * i); - - var modifiers = string.Join(string.Empty, parent.Modifiers.Select(z => GetKeyword((SyntaxKind)z) + " ")); - var parentType = parent.MethodParent switch - { - MethodParent.Class => "class", - MethodParent.Interface => "interface", - MethodParent.Struct => "struct", - MethodParent.Record => "record" - + parent.ClassOrStructKeyword.Kind() switch - { - SyntaxKind.StructKeyword => " struct", - SyntaxKind.ClassKeyword => " class", - _ => string.Empty, - }, - _ => throw new NotImplementedException("Cannot handle the parent of the method"), - }; - - var classDeclarationLine = $"{modifiers}partial {parentType} {parent.ParentName}{(parent.TypeParameterListSyntax.IsEmpty ? string.Empty - : "<" + string.Join(", ", parent.TypeParameterListSyntax) + ">")}"; - - _ = sbBegin.Append($$""" -{{indent}}{{classDeclarationLine}} -{{indent}}{ - -"""); - _ = sbEnd.Insert(0, $$""" -{{indent}}} - -"""); - ++i; - } - - var sbOuterUsings = new StringBuilder(); - foreach (var @using in location.OuterUsings) - { - if (sbOuterUsings.Length == 0) - { - // Separates the directives from the header above them. - _ = sbOuterUsings.Append(""" - - - -"""); - } - - _ = sbOuterUsings.Append($$""" -{{@using}} - -"""); - } - - var beforeNamespace = $""" -// {(method.DisableNullable ? string.Empty : """ - -#nullable enable -""")}{sbOuterUsings} -"""; - - return location.IsNamespaceFileScoped ? $$""" -{{beforeNamespace}} -namespace {{location.Namespaces.First()}}; -{{sbBegin}}{{new string(' ', 4 * i)}}{{method.Implementation.Trim()}} -{{sbEnd}} -""" - : $$""" -{{beforeNamespace}} -{{sbBegin}}{{new string(' ', 4 * i)}}{{method.Implementation.Trim()}} -{{sbEnd}} -"""; - } -} diff --git a/src/Zomp.SyncMethodGenerator/Cloning/CloningRewriter.cs b/src/Zomp.SyncMethodGenerator/Cloning/CloningRewriter.cs deleted file mode 100644 index 05e74cf..0000000 --- a/src/Zomp.SyncMethodGenerator/Cloning/CloningRewriter.cs +++ /dev/null @@ -1,432 +0,0 @@ -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace Zomp.SyncMethodGenerator.Cloning; - -/// -/// Rewrites a method so that a copy of it compiles in a file of its own. Every type and static -/// member the method names is fully qualified, so that it resolves to the same symbol in the file -/// the copy is emitted to as in the file the method was written in. A transformation derives from -/// this class to change what the copy does. -/// -/// The semantic model. -/// The method declaration to rewrite; other method declarations are dropped. -internal abstract class CloningRewriter(SemanticModel semanticModel, MethodDeclarationSyntax targetMethod) : CSharpSyntaxRewriter -{ - private static readonly SymbolDisplayFormat GlobalDisplayFormat = new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - genericsOptions: SymbolDisplayGenericsOptions.None, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - private static readonly SymbolDisplayFormat GlobalDisplayFormatWithTypeParameters = new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - /// - /// Gets the semantic model of the tree the method is in. - /// - protected SemanticModel SemanticModel { get; } = semanticModel; - - /// - public override SyntaxNode? VisitArgument(ArgumentSyntax node) - { - var @base = (ArgumentSyntax)base.VisitArgument(node)!; - - // Handles nameof(Type) - return GetSymbol(node.Expression) is ITypeSymbol typeSymbol && !TypeAlreadyQualified(typeSymbol) - ? @base.WithExpression(ProcessSymbol(typeSymbol)).WithTriviaFrom(@base) - : @base; - } - - /// - public override SyntaxNode? VisitArrayType(ArrayTypeSyntax node) - { - var @base = (ArrayTypeSyntax)base.VisitArrayType(node)!; - var elementType = TypeAlreadyQualified(node.ElementType) - ? @base.ElementType - : ProcessType(@base.ElementType); - return @base.WithElementType(elementType).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitAttribute(AttributeSyntax node) - { - var @base = (AttributeSyntax)base.VisitAttribute(node)!; - - if (GetSymbol(node.Name) is not IMethodSymbol ms) - { - return @base; - } - - var retval = @base.WithName(ProcessSymbol(ms.ContainingType)); - return retval; - } - - /// - public override SyntaxNode? VisitBinaryExpression(BinaryExpressionSyntax node) - { - var @base = (BinaryExpressionSyntax)base.VisitBinaryExpression(node)!; - - if (@base.OperatorToken.IsKind(SyntaxKind.IsKeyword) || @base.OperatorToken.IsKind(SyntaxKind.AsKeyword)) - { - if (GetSymbol(node.Left) is IFieldSymbol leftSymbol) - { - @base = @base.WithLeft(ProcessSymbol(leftSymbol).WithTriviaFrom(node.Left)); - } - - if (GetSymbol(node.Right) is ISymbol symbol) - { - @base = @base.WithRight(ProcessSymbol(symbol).WithTriviaFrom(node.Right)); - } - - return @base.WithTriviaFrom(@base); - } - - return @base; - } - - /// - public override SyntaxNode? VisitCastExpression(CastExpressionSyntax node) - { - var @base = (CastExpressionSyntax)base.VisitCastExpression(node)!; - return TypeAlreadyQualified(node.Type) ? @base : @base.WithType(ProcessType(node.Type)).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitCatchDeclaration(CatchDeclarationSyntax node) - { - var @base = (CatchDeclarationSyntax)base.VisitCatchDeclaration(node)!; - return @base.WithType(ProcessType(node.Type)).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitConstantPattern(ConstantPatternSyntax node) - { - var @base = (ConstantPatternSyntax)base.VisitConstantPattern(node)!; - return node.Expression switch - { - LiteralExpressionSyntax or MemberAccessExpressionSyntax => @base, - _ => SemanticModel.GetTypeInfo(node.Expression).Type is { } type - ? @base.WithExpression(ProcessSymbol(type).WithTriviaFrom(@base)) - : @base, - }; - } - - /// - public override SyntaxNode? VisitDeclarationExpression(DeclarationExpressionSyntax node) - { - var @base = (DeclarationExpressionSyntax)base.VisitDeclarationExpression(node)!; - return TypeAlreadyQualified(node.Type) ? @base : @base.WithType(ProcessType(node.Type)).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitDeclarationPattern(DeclarationPatternSyntax node) - { - var @base = (DeclarationPatternSyntax)base.VisitDeclarationPattern(node)!; - return TypeAlreadyQualified(node.Type) ? @base : @base.WithType(ProcessType(node.Type)).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitForEachStatement(ForEachStatementSyntax node) - { - var @base = (ForEachStatementSyntax)base.VisitForEachStatement(node)!; - return TypeAlreadyQualified(node.Type) ? @base : @base.WithType(ProcessType(node.Type)).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitGenericName(GenericNameSyntax node) - { - var @base = (GenericNameSyntax)base.VisitGenericName(node)!; - - if (GetSymbol(node) is not INamedTypeSymbol symbol) - { - return @base; - } - - var identifier = symbol switch - { - { ContainingSymbol: INamedTypeSymbol { IsGenericType: true } parentSymbol } - => parentSymbol.ToDisplayString(GlobalDisplayFormatWithTypeParameters) + "." + symbol.Name, - _ => symbol.ToDisplayString(GlobalDisplayFormat), - }; - - return @base.WithIdentifier(Identifier(identifier)).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) - { - var @base = (IdentifierNameSyntax)base.VisitIdentifierName(node)!; - - if (node.Parent is not MemberAccessExpressionSyntax) - { - var symbol = GetSymbol(node); - if (symbol is { IsStatic: true, ContainingType: { } containingType } memberSymbol) - { - if (symbol is IFieldSymbol or IMethodSymbol { MethodKind: not MethodKind.LocalFunction }) - { - var typeString = containingType.ToDisplayString(GlobalDisplayFormatWithTypeParameters); - return @base.WithIdentifier(Identifier($"{typeString}.{memberSymbol.Name}")).WithTriviaFrom(node); - } - } - } - - if (node.Parent is TypeArgumentListSyntax) - { - return ProcessType(node); - } - - return @base; - } - - /// - /// Parenthesizes every interpolated expression. A fully qualified name contains ::, - /// whose first colon would otherwise start a format string. - /// - /// The interpolation. - /// The interpolation with its expression parenthesized. - public override SyntaxNode? VisitInterpolation(InterpolationSyntax node) - { - var @base = (InterpolationSyntax)base.VisitInterpolation(node)!; - if (@base.Expression is not ParenthesizedExpressionSyntax) - { - var newExpression = ParenthesizedExpression(@base.Expression); - @base = @base.WithExpression(newExpression); - } - - return @base; - } - - /// - public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node) - { - var @base = (MemberAccessExpressionSyntax)base.VisitMemberAccessExpression(node)!; - - // Rewrite static invocation (eg. File.ReadAllText) - if (GetSymbol(node.Expression) is ITypeSymbol && node.Expression is TypeSyntax type) - { - var newType = ProcessType(type); - if (newType != type) - { - @base = @base.WithExpression(newType); - } - } - - return @base; - } - - /// - public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) - => node == targetMethod ? base.VisitMethodDeclaration(node) : null; - - /// - public override SyntaxNode? VisitNullableType(NullableTypeSyntax node) - { - var @base = (NullableTypeSyntax)base.VisitNullableType(node)!; - - return TypeAlreadyQualified(node.ElementType) ? @base : @base.WithElementType(ProcessType(@base.ElementType)).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) - { - var @base = (ObjectCreationExpressionSyntax)base.VisitObjectCreationExpression(node)!; - - if (SemanticModel.GetTypeInfo(node).Type is not { } t - || t is INamedTypeSymbol { IsGenericType: true }) - { - return @base; - } - - var newType = ProcessSymbol(t); - return newType == node.Type ? @base : @base.WithType(newType); - } - - /// - public override SyntaxNode? VisitParameter(ParameterSyntax node) - { - var @base = (ParameterSyntax)base.VisitParameter(node)!; - - return node.Type is null || TypeAlreadyQualified(node.Type) ? @base - : @base.WithType(ProcessType(node.Type)).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitQualifiedName(QualifiedNameSyntax node) - { - var @base = (QualifiedNameSyntax)base.VisitQualifiedName(node)!; - - return @base.Right is GenericNameSyntax ? @base.Right : (SyntaxNode)ProcessType(node); - } - - /// - public override SyntaxNode? VisitTupleType(TupleTypeSyntax node) - { - var @base = (TupleTypeSyntax)base.VisitTupleType(node)!; - - var newTuples = new List(); - foreach (var t in node.Elements.Zip(@base.Elements, (original, visited) => (original, visited))) - { - var newType = TypeAlreadyQualified(t.original.Type) ? t.visited.Type : ProcessType(t.original.Type); - newTuples.Add(TupleElement(newType, t.original.Identifier)); - } - - return @base.WithElements(SeparatedList(newTuples, node.Elements.GetSeparators())); - } - - /// - public override SyntaxNode? VisitTypeConstraint(TypeConstraintSyntax node) - { - var @base = (TypeConstraintSyntax)base.VisitTypeConstraint(node)!; - var newType = ProcessType(@base.Type); - return newType == @base.Type ? @base : @base.WithType(newType).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitTypeOfExpression(TypeOfExpressionSyntax node) - { - var @base = (TypeOfExpressionSyntax)base.VisitTypeOfExpression(node)!; - return TypeAlreadyQualified(node.Type) ? @base : @base.WithType(ProcessType(node.Type)).WithTriviaFrom(@base); - } - - /// - public override SyntaxNode? VisitVariableDeclaration(VariableDeclarationSyntax node) - { - var @base = (VariableDeclarationSyntax)base.VisitVariableDeclaration(node)!; - - var type = node.Type; - var newType = @base.Type; - - if (newType == type || - (newType is IdentifierNameSyntax { Identifier.ValueText: { } newTypeString } - && type is IdentifierNameSyntax { Identifier.ValueText: { } typeString } - && newTypeString == typeString)) - { - // not replaced - newType = ProcessType(type); - - if (newType == type) - { - return @base; - } - } - - return @base.WithType(newType).WithTriviaFrom(@base); - } - - /// - /// Prefixes a fully qualified name with the global alias. - /// - /// Fully qualified name. - /// The name, prefixed with global::. - protected static string Global(string type) => $"global::{type}"; - - /// - /// Removes the preprocessor directives, and the text they disable, from the trivia which - /// leads a method. The copy is emitted without whatever surrounded the original. - /// - /// Leading trivia of the method. - /// The trivia without preprocessor directives. - protected static SyntaxTriviaList RemovePreprocessorDirectives(SyntaxTriviaList trivia) - { - static bool Preprocessors(SyntaxTrivia st) - => st.IsKind(SyntaxKind.IfDirectiveTrivia) - || st.IsKind(SyntaxKind.ElifDirectiveTrivia) - || st.IsKind(SyntaxKind.ElseDirectiveTrivia) - || st.IsKind(SyntaxKind.EndIfDirectiveTrivia) - || st.IsKind(SyntaxKind.RegionDirectiveTrivia) - || st.IsKind(SyntaxKind.EndRegionDirectiveTrivia) - || st.IsKind(SyntaxKind.DisabledTextTrivia); - - while (trivia.FirstOrDefault(Preprocessors) is { } preprocessor - && preprocessor != default) - { - trivia = trivia.Remove(preprocessor); - } - - return trivia; - } - - /// - /// Checks whether a type is written in a form which its own visitor qualifies. - /// - /// The type. - /// True if the type needs nothing more. - protected static bool TypeAlreadyQualified(TypeSyntax type) - => type is NullableTypeSyntax or GenericNameSyntax or TupleTypeSyntax or ArrayTypeSyntax or QualifiedNameSyntax; - - /// - /// Gets the symbol a node refers to. - /// - /// The node. - /// The symbol, or null if the node does not bind to one. - protected ISymbol? GetSymbol(SyntaxNode node) => SemanticModel.GetSymbolInfo(node).Symbol; - - /// - /// Gets the fully qualified name of a type. - /// - /// The type, or any other symbol, which is named as it is. - /// The name. - protected string MakeType(ISymbol symbol) - => symbol switch - { - INamedTypeSymbol nts => MapTypeName(nts) ?? symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - _ => symbol.Name, - }; - - /// - /// Names a symbol so that it resolves wherever the copy is emitted. - /// - /// The symbol. - /// The name. - protected SimpleNameSyntax ProcessSymbol(ISymbol typeSymbol) => MapSymbol(typeSymbol) ?? typeSymbol switch - { - INamedTypeSymbol nts => IdentifierName(MakeType(nts)), - IArrayTypeSymbol ats => IdentifierName(MakeType(ats.ElementType) + $"[{new string(',', ats.Rank - 1)}]"), - IFieldSymbol fs => IdentifierName(MakeType(fs.Type) + '.' + fs.Name), - _ => IdentifierName(typeSymbol.Name), - }; - - /// - /// Qualifies a type written as a simple or qualified name. - /// - /// The type. - /// The qualified type, or the type itself if it needs nothing. - protected TypeSyntax ProcessType(TypeSyntax typeSyntax) => typeSyntax switch - { - IdentifierNameSyntax { Identifier.ValueText: "var" } => typeSyntax, - IdentifierNameSyntax or QualifiedNameSyntax => ProcessSyntaxUsingSymbol(typeSyntax), - _ => typeSyntax, - }; - - /// - /// Gives a transformation the chance to name a symbol differently in the copy. - /// - /// The symbol. - /// The name to use, or null to use the fully qualified name. - protected virtual SimpleNameSyntax? MapSymbol(ISymbol symbol) => null; - - /// - /// Gives a transformation the chance to substitute one type for another wherever a type is - /// named by its fully qualified name. - /// - /// The type. - /// The name to use, or null to use the fully qualified name. - protected virtual string? MapTypeName(INamedTypeSymbol symbol) => null; - - private static bool TypeAlreadyQualified(ITypeSymbol type) - => type is INamedTypeSymbol namedType - && namedType is { IsGenericType: true }; - - private TypeSyntax ProcessSyntaxUsingSymbol(TypeSyntax typeSyntax) - { - var typeSymbol = SemanticModel.GetTypeInfo(typeSyntax).Type; - return typeSymbol is null ? typeSyntax : ProcessSymbol(typeSymbol).WithTriviaFrom(typeSyntax); - } -} diff --git a/src/Zomp.SyncMethodGenerator/Cloning/MethodLocation.cs b/src/Zomp.SyncMethodGenerator/Cloning/MethodLocation.cs deleted file mode 100644 index 158cc23..0000000 --- a/src/Zomp.SyncMethodGenerator/Cloning/MethodLocation.cs +++ /dev/null @@ -1,183 +0,0 @@ -namespace Zomp.SyncMethodGenerator.Cloning; - -/// -/// Everything around a method which a copy of it needs in order to compile in a file of its own: -/// the namespaces and types it is nested in, and the using directives in scope where it was written. -/// -/// Position of the method among the members of its type which share its name, starting at 1. -/// Namespaces the method is under, outermost first. -/// Using directives the source file declares outside its namespace. -/// Using directives the source file declares inside its namespace. -/// True if the namespace is file scoped. -/// True if the method is a member of a C# 14 extension block. -/// Types the method is nested in, outermost first. -internal sealed record MethodLocation( - int Index, - EquatableArray Namespaces, - EquatableArray OuterUsings, - EquatableArray InnerUsings, - bool IsNamespaceFileScoped, - bool IsCSharp14Extension, - EquatableArray Parents) -{ - /// - /// Gets the namespaces and types the method is nested in, joined with dots. - /// - public string Scope => string.Join(".", Namespaces.Concat(Parents.Select(static p => p.ScopeName))); - - /// - /// Locates a method. - /// - /// Declaration of the method. - /// Symbol of the method. - /// Where the method sits. - /// Node to rewrite: the method, or the extension block which holds it. - /// False when the method is not nested in a type which can be declared again as partial. - public static bool TryCreate( - MethodDeclarationSyntax method, - IMethodSymbol symbol, - [NotNullWhen(true)] out MethodLocation? location, - [NotNullWhen(true)] out SyntaxNode? root) - { - location = null; - root = null; - - var parents = ImmutableArray.CreateBuilder(); - SyntaxNode? node = method; -#if ROSLYN_5_0_OR_GREATER - ExtensionBlockDeclarationSyntax? extensionParent = null; -#endif - while (node.Parent is not null) - { - node = node.Parent; -#if ROSLYN_5_0_OR_GREATER - if (node is ExtensionBlockDeclarationSyntax eds) - { - extensionParent = eds; - continue; - } -#endif - - MethodParentDeclaration? mpd = node switch - { - ClassDeclarationSyntax o => new(MethodParent.Class, o.Identifier, o.Modifiers, o.TypeParameterList), - StructDeclarationSyntax o => new(MethodParent.Struct, o.Identifier, o.Modifiers, o.TypeParameterList), - RecordDeclarationSyntax o => new(MethodParent.Record, o.Identifier, o.Modifiers, o.TypeParameterList, o.ClassOrStructKeyword), - InterfaceDeclarationSyntax o => new(MethodParent.Interface, o.Identifier, o.Modifiers, o.TypeParameterList), - _ => null, - }; - - if (mpd is null) - { - break; - } - - parents.Insert(0, mpd); - } - - if (parents.Count == 0) - { - return false; - } - - var isNamespaceFileScoped = false; - var namespaces = ImmutableArray.CreateBuilder(); - - // Documentation comments are copied across verbatim, and a cref in one is resolved - // against the file it lands in rather than the file it was written in. Without the - // using directives which were in scope where it was written, every cref which relied on - // one goes unresolved, which a project building with warnings as errors reads as a - // build failure. The directives are kept on the side of the namespace they were - // declared on, since one declared inside a namespace may name it only relatively. - var outerUsings = ImmutableArray.CreateBuilder(); - var innerUsings = ImmutableArray.CreateBuilder(); - - while (node is not null and not CompilationUnitSyntax) - { - switch (node) - { - case NamespaceDeclarationSyntax nds: - namespaces.Insert(0, nds.Name.ToString()); - InsertUsings(innerUsings, nds.Usings); - break; - case FileScopedNamespaceDeclarationSyntax file: - namespaces.Add(file.Name.ToString()); - InsertUsings(innerUsings, file.Usings); - isNamespaceFileScoped = true; - break; - default: - throw new InvalidOperationException($"Cannot handle {node}"); - } - - node = node.Parent; - } - - if (node is CompilationUnitSyntax compilationUnit) - { - InsertUsings(outerUsings, compilationUnit.Usings); - } - -#if ROSLYN_5_0_OR_GREATER - var isCSharp14Extension = extensionParent is not null; - root = extensionParent ?? (SyntaxNode)method; -#else - var isCSharp14Extension = false; - root = method; -#endif - - location = new( - GetIndex(symbol), - namespaces.ToImmutable(), - outerUsings.ToImmutable(), - innerUsings.ToImmutable(), - isNamespaceFileScoped, - isCSharp14Extension, - parents.ToImmutable()); - - return true; - } - - /// - /// Counts the members of the containing type which share the method's name and come before - /// it, so that overloads are told apart in the names of the files they are emitted to. - /// - /// Symbol of the method. - /// Position of the method among its namesakes, starting at 1. - private static int GetIndex(IMethodSymbol symbol) - { - var index = 1; - - if (symbol.ContainingType is { } containingType) - { - foreach (var member in containingType.GetMembers()) - { - if (member.Equals(symbol, SymbolEqualityComparer.Default)) - { - break; - } - - if (member.Name.Equals(symbol.Name, StringComparison.Ordinal)) - { - ++index; - } - } - } - - return index; - } - - /// - /// Records the directives, innermost first, so that walking outwards from the method builds - /// them up in the order they were written. - /// - /// Collected directives. - /// Directives declared at one level. - private static void InsertUsings(ImmutableArray.Builder destination, SyntaxList usings) - { - var index = 0; - foreach (var @using in usings) - { - destination.Insert(index++, @using.ToString()); - } - } -} diff --git a/src/Zomp.SyncMethodGenerator/Cloning/MethodParent.cs b/src/Zomp.SyncMethodGenerator/Cloning/MethodParent.cs deleted file mode 100644 index f7e78ce..0000000 --- a/src/Zomp.SyncMethodGenerator/Cloning/MethodParent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Zomp.SyncMethodGenerator.Cloning; - -internal enum MethodParent -{ - Class, - Struct, - Record, - Interface, -} diff --git a/src/Zomp.SyncMethodGenerator/Cloning/MethodParentDeclaration.cs b/src/Zomp.SyncMethodGenerator/Cloning/MethodParentDeclaration.cs deleted file mode 100644 index 6b5d538..0000000 --- a/src/Zomp.SyncMethodGenerator/Cloning/MethodParentDeclaration.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Zomp.SyncMethodGenerator.Cloning; - -/// -/// Represents a type a is nested in. -/// -/// Type of container. -/// Class name. -/// A list of modifiers. -/// A list of type parameters. -/// Indicates whether struct or class are explicitly specified for a record. -internal sealed record MethodParentDeclaration(MethodParent MethodParent, string ParentName, EquatableArray Modifiers, EquatableArray TypeParameterListSyntax, SyntaxToken ClassOrStructKeyword) -{ - public MethodParentDeclaration(MethodParent methodParent, SyntaxToken parentName, SyntaxTokenList modifiers, TypeParameterListSyntax? typeParameterList, SyntaxToken classOrStructKeyword = default) - : this( - methodParent, - parentName.ValueText, - modifiers.Select(z => (ushort)z.RawKind).Where(z => z != (ushort)SyntaxKind.PartialKeyword).ToImmutableArray(), - (typeParameterList is null ? [] : typeParameterList.Parameters.Select(z => z.Identifier.ValueText)).ToImmutableArray(), - classOrStructKeyword) - { - } - - /// - /// Gets the name of the type with its type parameters, so that Class, Class{T} - /// and Class{T,T2} stay distinct. - /// - public string ScopeName => TypeParameterListSyntax.IsEmpty - ? ParentName - : ParentName + "{" + string.Join(",", TypeParameterListSyntax) + "}"; -} diff --git a/src/Zomp.SyncMethodGenerator/Cloning/MethodSignature.cs b/src/Zomp.SyncMethodGenerator/Cloning/MethodSignature.cs deleted file mode 100644 index 881b916..0000000 --- a/src/Zomp.SyncMethodGenerator/Cloning/MethodSignature.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace Zomp.SyncMethodGenerator.Cloning; - -/// -/// The signature a cloned method will be emitted with, and where the method it came from was -/// written. Two methods sharing a cannot both be emitted: the compiler would -/// see the same member declared twice. -/// -/// Fully qualified signature of the method which will be emitted. -/// File path of the method being cloned. -/// Text span of the method being cloned. -/// Line span of the method being cloned. -internal sealed record MethodSignature(string Key, string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan) -{ - /// - /// Compares by alone. The spans move whenever anything above the method is - /// edited, and letting that reach the incremental pipeline would defeat caching for a method - /// whose generated output has not changed at all. - /// - /// Signature to compare against. - /// True if both describe the same emitted member. - public bool Equals(MethodSignature? other) - => other is not null && string.Equals(Key, other.Key, StringComparison.Ordinal); - - /// - public override int GetHashCode() => Key.GetHashCode(); - - /// - /// Describes the method which is about to be emitted, precisely enough to tell whether two - /// of them would declare the same member. The rewritten declaration is used rather than the - /// original symbol, so the comparison sees exactly what the compiler will see - parameters - /// already dropped, types already substituted. - /// - /// Result of rewriting the method. - /// Where the original method sits. - /// Method being cloned, for the location to report against. - /// The signature, or null when the rewritten method could not be located. - public static MethodSignature? Create(SyntaxNode rewritten, MethodLocation location, MethodDeclarationSyntax original) - { - var method = rewritten as MethodDeclarationSyntax - ?? rewritten.DescendantNodes().OfType().FirstOrDefault(); - - if (method is null) - { - return null; - } - - // Arity by count rather than by name, since overloads which differ only in what they - // call their type parameters still declare the same member. - var arity = method.TypeParameterList?.Parameters.Count ?? 0; - var arityMarker = arity > 0 ? "`" + arity.ToString(System.Globalization.CultureInfo.InvariantCulture) : string.Empty; - - var parameters = string.Join(",", method.ParameterList.Parameters.Select(static p => p.Type?.ToString() ?? string.Empty)); - - var key = $"{location.Scope}.{method.Identifier.ValueText}{arityMarker}({parameters})"; - - var sourceLocation = original.GetLocation(); - - return new(key, sourceLocation.SourceTree?.FilePath ?? string.Empty, sourceLocation.SourceSpan, sourceLocation.GetLineSpan().Span); - } - - /// - /// Picks out the signatures produced by more than one method. Emitting all of them would - /// declare the same member twice. - /// - /// Signature of every method being generated. - /// The signatures which appear more than once. - public static EquatableArray FindCollisions(ImmutableArray keys) - { - var colliding = keys - .Where(static key => key is not null) - .GroupBy(static key => key!, StringComparer.Ordinal) - .Where(static group => group.Count() > 1) - .Select(static group => group.Key); - -#if ROSLYN_4_12_OR_GREATER - return new([.. colliding]); -#else - return new(ImmutableArray.CreateRange(colliding)); -#endif - } - - /// - /// Builds the diagnostic reported when this signature is produced more than once. - /// - /// Descriptor of the diagnostic. - /// A new . - public ReportedDiagnostic ToDiagnostic(DiagnosticDescriptor descriptor) - => new(descriptor, FilePath, TextSpan, LineSpan, Key); -} diff --git a/src/Zomp.SyncMethodGenerator/Helpers/EquatableArray.cs b/src/Zomp.SyncMethodGenerator/Helpers/EquatableArray.cs deleted file mode 100644 index 9d30b5b..0000000 --- a/src/Zomp.SyncMethodGenerator/Helpers/EquatableArray.cs +++ /dev/null @@ -1,154 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; - -namespace Zomp.SyncMethodGenerator.Helpers; - -/// -/// An immutable, equatable array. This is equivalent to but with value equality support. -/// -/// The type of values in the array. -/// -/// Modified from: https://github.com/dotnet/runtime/issues/77183#issuecomment-1284577055. -/// Remove this struct when the issue above is resolved. -/// -/// -/// Initializes a new instance of the struct. -/// -/// The input to wrap. -[ExcludeFromCodeCoverage] -internal readonly struct EquatableArray(ImmutableArray array) : IEquatable>, IEnumerable - where T : IEquatable -{ - /// - /// The underlying array. - /// - private readonly T[]? array = Unsafe.As, T[]?>(ref array); - - /// - /// Gets a value indicating whether the current array is empty. - /// - public bool IsEmpty - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => AsImmutableArray().IsEmpty; - } - - /// - /// Gets a reference to an item at a specified position within the array. - /// - /// The index of the item to retrieve a reference to. - /// A reference to an item at a specified position within the array. - public ref readonly T this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref AsImmutableArray().ItemRef(index); - } - - /// - /// Implicitly converts an to . - /// - /// An instance from a given . - public static implicit operator EquatableArray(ImmutableArray array) => FromImmutableArray(array); - - /// - /// Implicitly converts an to . - /// - /// An instance from a given . - public static implicit operator ImmutableArray(EquatableArray array) => array.AsImmutableArray(); - - /// - /// Checks whether two values are the same. - /// - /// The first value. - /// The second value. - /// Whether and are equal. - public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right); - - /// - /// Checks whether two values are not the same. - /// - /// The first value. - /// The second value. - /// Whether and are not equal. - public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right); - - /// - /// Creates an instance from a given . - /// - /// The input instance. - /// An instance from a given . - public static EquatableArray FromImmutableArray(ImmutableArray array) => new(array); - - /// - public bool Equals(EquatableArray array) => AsSpan().SequenceEqual(array.AsSpan()); - - /// - public override bool Equals([NotNullWhen(true)] object? obj) => obj is EquatableArray array && Equals(this, array); - - /// - public override int GetHashCode() - { - if (this.array is not T[] array) - { - return 0; - } - - HashCode hashCode = default; - - foreach (var item in array) - { - hashCode.Add(item); - } - - return hashCode.ToHashCode(); - } - - /// - /// Gets an instance from the current . - /// - /// The from the current . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ImmutableArray AsImmutableArray() => Unsafe.As>(ref Unsafe.AsRef(in array)); - - /// - /// Returns a wrapping the current items. - /// - /// A wrapping the current items. - public ReadOnlySpan AsSpan() => AsImmutableArray().AsSpan(); - - /// - /// Copies the contents of this instance to a mutable array. - /// - /// The newly instantiated array. - public T[] ToArray() => [.. AsImmutableArray()]; - - /// - /// Gets an value to traverse items in the current array. - /// - /// An value to traverse items in the current array. - public ImmutableArray.Enumerator GetEnumerator() => AsImmutableArray().GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)AsImmutableArray()).GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)AsImmutableArray()).GetEnumerator(); -} - -/// -/// Extensions for . -/// -[ExcludeFromCodeCoverage] -internal static class EquatableArray -{ - /// - /// Creates an instance from a given . - /// - /// The type of items in the input array. - /// The input instance. - /// An instance from a given . - public static EquatableArray AsEquatableArray(this ImmutableArray array) - where T : IEquatable => new(array); -} diff --git a/src/Zomp.SyncMethodGenerator/Models/ReportedDiagnostic.cs b/src/Zomp.SyncMethodGenerator/Models/ReportedDiagnostic.cs deleted file mode 100644 index 694aa50..0000000 --- a/src/Zomp.SyncMethodGenerator/Models/ReportedDiagnostic.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Zomp.SyncMethodGenerator.Models; - -/// -/// Basic diagnostic description for reporting diagnostic inside the incremental pipeline. -/// -/// Diagnostic descriptor. -/// File path. -/// Text span. -/// Line span. -/// Trivia. -/// -internal sealed record ReportedDiagnostic(DiagnosticDescriptor Descriptor, string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan, string Trivia) -{ - /// - /// Implicitly converts to . - /// - /// Diagnostic to convert. - public static implicit operator Diagnostic(ReportedDiagnostic diagnostic) - => Diagnostic.Create( - descriptor: diagnostic.Descriptor, - location: Location.Create(diagnostic.FilePath, diagnostic.TextSpan, diagnostic.LineSpan), - messageArgs: [diagnostic.Trivia]); - - /// - /// Creates a new from and . - /// - /// Descriptor. - /// Location. - /// Trivia. - /// A new . - public static ReportedDiagnostic Create(DiagnosticDescriptor descriptor, Location location, string trivia) - { - return new(descriptor, location.SourceTree?.FilePath ?? string.Empty, location.SourceSpan, location.GetLineSpan().Span, trivia); - } -} diff --git a/src/Zomp.SyncMethodGenerator/Zomp.SyncMethodGenerator.csproj b/src/Zomp.SyncMethodGenerator/Zomp.SyncMethodGenerator.csproj index a8c2e83..68316c6 100644 --- a/src/Zomp.SyncMethodGenerator/Zomp.SyncMethodGenerator.csproj +++ b/src/Zomp.SyncMethodGenerator/Zomp.SyncMethodGenerator.csproj @@ -29,6 +29,7 @@ + @@ -45,9 +46,8 @@ - + - diff --git a/tests/Generator.Tests/IdentityCloneGenerator.cs b/tests/Generator.Tests/IdentityCloneGenerator.cs index c9f1039..7544b4a 100644 --- a/tests/Generator.Tests/IdentityCloneGenerator.cs +++ b/tests/Generator.Tests/IdentityCloneGenerator.cs @@ -1,5 +1,5 @@ +using Zomp.MethodCloning; using Zomp.SyncMethodGenerator; -using Zomp.SyncMethodGenerator.Cloning; namespace Generator.Tests; diff --git a/tests/Generator.Tests/IdentityRewriter.cs b/tests/Generator.Tests/IdentityRewriter.cs index 459bab3..5d54240 100644 --- a/tests/Generator.Tests/IdentityRewriter.cs +++ b/tests/Generator.Tests/IdentityRewriter.cs @@ -1,5 +1,5 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; -using Zomp.SyncMethodGenerator.Cloning; +using Zomp.MethodCloning; namespace Generator.Tests; From 132d6e6d7d2320c4692915bc30cc27a6f2c11cc1 Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Sun, 13 Sep 2026 12:36:59 -0400 Subject: [PATCH 2/4] Take Zomp.MethodCloning 1.0.20-alpha from nuget.org The package is published, so the local feed it came from is gone from nuget.config. The version brings the core's fixes; the one snapshot they change is a method in the global namespace, whose file is now named Class.ReturnTrueAsync.g.cs rather than .Class.ReturnTrueAsync.g.cs. Generated with Claude Code --- Directory.Packages.props | 2 +- nuget.config | 4 ---- ...Tests.EnumPatternName#Class.ReturnTrueAsync.g.verified.cs} | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) rename tests/Generator.Tests/Snapshots/{TypeTests.EnumPatternName#.Class.ReturnTrueAsync.g.verified.cs => TypeTests.EnumPatternName#Class.ReturnTrueAsync.g.verified.cs} (78%) diff --git a/Directory.Packages.props b/Directory.Packages.props index 91b543f..3d2f63a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,6 +17,6 @@ - + diff --git a/nuget.config b/nuget.config index 4d7bd39..006dc55 100644 --- a/nuget.config +++ b/nuget.config @@ -5,7 +5,6 @@ - @@ -17,8 +16,5 @@ --> - - - diff --git a/tests/Generator.Tests/Snapshots/TypeTests.EnumPatternName#.Class.ReturnTrueAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.EnumPatternName#Class.ReturnTrueAsync.g.verified.cs similarity index 78% rename from tests/Generator.Tests/Snapshots/TypeTests.EnumPatternName#.Class.ReturnTrueAsync.g.verified.cs rename to tests/Generator.Tests/Snapshots/TypeTests.EnumPatternName#Class.ReturnTrueAsync.g.verified.cs index d64773f..b3fd43e 100644 --- a/tests/Generator.Tests/Snapshots/TypeTests.EnumPatternName#.Class.ReturnTrueAsync.g.verified.cs +++ b/tests/Generator.Tests/Snapshots/TypeTests.EnumPatternName#Class.ReturnTrueAsync.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: .Class.ReturnTrueAsync.g.cs +//HintName: Class.ReturnTrueAsync.g.cs // #nullable enable partial class Class From 2eda902c489017abf60abf08bead587754c5c7cd Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Sun, 13 Sep 2026 12:40:21 -0400 Subject: [PATCH 3/4] Remove the tests the cloning layer now has in its own repository The cloning layer lives in Zomp.MethodCloning, which tests it with an identity clone. These 22 tests only exercised that layer: their output here differs from the identity clone by nothing but the method's name and Task becoming void. File naming, containing types, name qualification and namespaces are tested there instead. Tests where this generator's output does more than rename the method stay, such as Task.Delay becoming Thread.Sleep, a Func returning Task becoming an Action, and the attribute on a type. Generated with Claude Code --- tests/Generator.Tests/FileNameTests.cs | 83 -------------- ...Test.Class{T,T2}.MethodAsync.g.verified.cs | 10 -- ...es#Test.Class{T}.MethodAsync.g.verified.cs | 10 -- ...Tests.DoNotCollideClassNames#g.verified.cs | 10 -- ....Is.Rea_89a6d38e.MethodAsync.g.verified.cs | 13 --- ....Is.R_79dbcd19.MethodAsync_2.g.verified.cs | 13 --- ....Is.Rea_89a6d38e.MethodAsync.g.verified.cs | 13 --- ...oomForTheNamespaceOrTheConta.g.verified.cs | 10 -- .../TypeTests.ArrayParameter#g.verified.cs | 4 - .../TypeTests.BinaryPattern#g.verified.cs | 2 - ...eTests.DeclarationExpression#g.verified.cs | 2 - ...ypeTests.FullyQualifiedArray#g.verified.cs | 2 - ...Class#Test.Class{T}.FooAsync.g.verified.cs | 8 -- .../TypeTests.NestedGenerics#g.verified.cs | 2 - .../TypeTests.NotPattern#g.verified.cs | 3 - ...ullableDeclarationExpression#g.verified.cs | 2 - .../TypeTests.NullableForeach#g.verified.cs | 4 - ...ypeTests.PatternIsEnumMember#g.verified.cs | 2 - ...ypeTests.PatternIsNotLiteral#g.verified.cs | 2 - ....PatternMatchingWithConstant#g.verified.cs | 2 - ...peTests.QualifiedGenericName#g.verified.cs | 2 - ...ame#System.Class.MethodAsync.g.verified.cs | 11 -- ...ypeTests.VariableDeclaration#g.verified.cs | 2 - ...VariableDeclarationRedundant#g.verified.cs | 2 - ...ultipleNamespaces.EmptyAsync.g.verified.cs | 18 --- ...aticUsings#N1.C1.MethodAsync.g.verified.cs | 16 --- tests/Generator.Tests/TypeTests.cs | 108 ------------------ tests/Generator.Tests/UnitTests.cs | 46 -------- 28 files changed, 402 deletions(-) delete mode 100644 tests/Generator.Tests/FileNameTests.cs delete mode 100644 tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#Test.Class{T,T2}.MethodAsync.g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#Test.Class{T}.MethodAsync.g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileName#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Rea_89a6d38e.MethodAsync.g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameKeepsOverloadsApart#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.R_79dbcd19.MethodAsync_2.g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameKeepsOverloadsApart#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Rea_89a6d38e.MethodAsync.g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameWhenTheMethodNameFillsIt#_b60220f2.ThisMethodHasAnUnreasonablyLongNameWhichOnItsOwnLeavesNoRoomForTheNamespaceOrTheConta.g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.ArrayParameter#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.BinaryPattern#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.DeclarationExpression#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.FullyQualifiedArray#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.GenericClassWithGenericInnerClass#Test.Class{T}.FooAsync.g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.NestedGenerics#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.NotPattern#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.NullableDeclarationExpression#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.NullableForeach#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.PatternIsEnumMember#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.PatternIsNotLiteral#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.PatternMatchingWithConstant#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.QualifiedGenericName#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.QualifiedNonGenericName#System.Class.MethodAsync.g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.VariableDeclaration#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/TypeTests.VariableDeclarationRedundant#g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/UnitTests.MultipleNamespaces#NsOne.NsTwo.NsThree.NsFour.MultipleNamespaces.EmptyAsync.g.verified.cs delete mode 100644 tests/Generator.Tests/Snapshots/UnitTests.StaticUsings#N1.C1.MethodAsync.g.verified.cs diff --git a/tests/Generator.Tests/FileNameTests.cs b/tests/Generator.Tests/FileNameTests.cs deleted file mode 100644 index 38309d7..0000000 --- a/tests/Generator.Tests/FileNameTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -namespace Generator.Tests; - -public class FileNameTests -{ - [Fact] - public Task DoNotCollideClassNames() => """ -namespace Test; - -public partial class Class -{ - [CreateSyncVersion] - public async Task MethodAsync() - { - } -} - -public partial class Class -{ - [CreateSyncVersion] - public async Task MethodAsync() - { - } -} - -public partial class Class -{ - [CreateSyncVersion] - public async Task MethodAsync() - { - } -} -""".Verify(sourceType: SourceType.Full); - - [Fact] - public Task ShortenLongFileName() => """ -namespace A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Reasonable; - -public partial class OuterClass -{ - public partial class InnerClass - { - [CreateSyncVersion] - public async Task MethodAsync() - { - } - } -} -""".Verify(sourceType: SourceType.Full); - - [Fact] - public Task ShortenLongFileNameWhenTheMethodNameFillsIt() => """ -namespace A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Reasonable; - -public partial class OuterClass -{ - [CreateSyncVersion] - public async Task ThisMethodHasAnUnreasonablyLongNameWhichOnItsOwnLeavesNoRoomForTheNamespaceOrTheContainingTypeAsync() - { - } -} -""".Verify(sourceType: SourceType.Full); - - [Fact] - public Task ShortenLongFileNameKeepsOverloadsApart() => """ -namespace A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Reasonable; - -public partial class OuterClass -{ - public partial class InnerClass - { - [CreateSyncVersion] - public async Task MethodAsync() - { - } - - [CreateSyncVersion] - public async Task MethodAsync(int i) - { - } - } -} -""".Verify(sourceType: SourceType.Full); -} diff --git a/tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#Test.Class{T,T2}.MethodAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#Test.Class{T,T2}.MethodAsync.g.verified.cs deleted file mode 100644 index ec9b3af..0000000 --- a/tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#Test.Class{T,T2}.MethodAsync.g.verified.cs +++ /dev/null @@ -1,10 +0,0 @@ -//HintName: Test.Class{T,T2}.MethodAsync.g.cs -// -#nullable enable -namespace Test; -public partial class Class -{ - public void Method() - { - } -} diff --git a/tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#Test.Class{T}.MethodAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#Test.Class{T}.MethodAsync.g.verified.cs deleted file mode 100644 index a29686b..0000000 --- a/tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#Test.Class{T}.MethodAsync.g.verified.cs +++ /dev/null @@ -1,10 +0,0 @@ -//HintName: Test.Class{T}.MethodAsync.g.cs -// -#nullable enable -namespace Test; -public partial class Class -{ - public void Method() - { - } -} diff --git a/tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#g.verified.cs b/tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#g.verified.cs deleted file mode 100644 index 654d66b..0000000 --- a/tests/Generator.Tests/Snapshots/FileNameTests.DoNotCollideClassNames#g.verified.cs +++ /dev/null @@ -1,10 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -// -#nullable enable -namespace Test; -public partial class Class -{ - public void Method() - { - } -} diff --git a/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileName#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Rea_89a6d38e.MethodAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileName#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Rea_89a6d38e.MethodAsync.g.verified.cs deleted file mode 100644 index 071af7c..0000000 --- a/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileName#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Rea_89a6d38e.MethodAsync.g.verified.cs +++ /dev/null @@ -1,13 +0,0 @@ -//HintName: A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Rea_89a6d38e.MethodAsync.g.cs -// -#nullable enable -namespace A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Reasonable; -public partial class OuterClass -{ - public partial class InnerClass - { - public void Method() - { - } - } -} diff --git a/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameKeepsOverloadsApart#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.R_79dbcd19.MethodAsync_2.g.verified.cs b/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameKeepsOverloadsApart#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.R_79dbcd19.MethodAsync_2.g.verified.cs deleted file mode 100644 index b81dba6..0000000 --- a/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameKeepsOverloadsApart#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.R_79dbcd19.MethodAsync_2.g.verified.cs +++ /dev/null @@ -1,13 +0,0 @@ -//HintName: A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.R_79dbcd19.MethodAsync_2.g.cs -// -#nullable enable -namespace A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Reasonable; -public partial class OuterClass -{ - public partial class InnerClass - { - public void Method(int i) - { - } - } -} diff --git a/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameKeepsOverloadsApart#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Rea_89a6d38e.MethodAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameKeepsOverloadsApart#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Rea_89a6d38e.MethodAsync.g.verified.cs deleted file mode 100644 index 071af7c..0000000 --- a/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameKeepsOverloadsApart#A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Rea_89a6d38e.MethodAsync.g.verified.cs +++ /dev/null @@ -1,13 +0,0 @@ -//HintName: A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Rea_89a6d38e.MethodAsync.g.cs -// -#nullable enable -namespace A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Reasonable; -public partial class OuterClass -{ - public partial class InnerClass - { - public void Method() - { - } - } -} diff --git a/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameWhenTheMethodNameFillsIt#_b60220f2.ThisMethodHasAnUnreasonablyLongNameWhichOnItsOwnLeavesNoRoomForTheNamespaceOrTheConta.g.verified.cs b/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameWhenTheMethodNameFillsIt#_b60220f2.ThisMethodHasAnUnreasonablyLongNameWhichOnItsOwnLeavesNoRoomForTheNamespaceOrTheConta.g.verified.cs deleted file mode 100644 index 5f00229..0000000 --- a/tests/Generator.Tests/Snapshots/FileNameTests.ShortenLongFileNameWhenTheMethodNameFillsIt#_b60220f2.ThisMethodHasAnUnreasonablyLongNameWhichOnItsOwnLeavesNoRoomForTheNamespaceOrTheConta.g.verified.cs +++ /dev/null @@ -1,10 +0,0 @@ -//HintName: _b60220f2.ThisMethodHasAnUnreasonablyLongNameWhichOnItsOwnLeavesNoRoomForTheNamespaceOrTheConta.g.cs -// -#nullable enable -namespace A.Long.Enough.Namespace.To.Make.The.Generated.File.Name.Exceed.What.Is.Reasonable; -public partial class OuterClass -{ - public void ThisMethodHasAnUnreasonablyLongNameWhichOnItsOwnLeavesNoRoomForTheNamespaceOrTheContainingType() - { - } -} diff --git a/tests/Generator.Tests/Snapshots/TypeTests.ArrayParameter#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.ArrayParameter#g.verified.cs deleted file mode 100644 index 380f8e6..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.ArrayParameter#g.verified.cs +++ /dev/null @@ -1,4 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -public void Method(int[] o) -{ -} diff --git a/tests/Generator.Tests/Snapshots/TypeTests.BinaryPattern#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.BinaryPattern#g.verified.cs deleted file mode 100644 index 8fa63d4..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.BinaryPattern#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -_ = new object() is global::System.DBNull or global::System.IO.Stream; diff --git a/tests/Generator.Tests/Snapshots/TypeTests.DeclarationExpression#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.DeclarationExpression#g.verified.cs deleted file mode 100644 index c13d1ed..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.DeclarationExpression#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -new global::System.Collections.Generic.Dictionary().TryGetValue(0, out global::System.IO.Stream a); diff --git a/tests/Generator.Tests/Snapshots/TypeTests.FullyQualifiedArray#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.FullyQualifiedArray#g.verified.cs deleted file mode 100644 index 846696c..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.FullyQualifiedArray#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -global::System.Text.RegularExpressions.Regex[] variable = null!; diff --git a/tests/Generator.Tests/Snapshots/TypeTests.GenericClassWithGenericInnerClass#Test.Class{T}.FooAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.GenericClassWithGenericInnerClass#Test.Class{T}.FooAsync.g.verified.cs deleted file mode 100644 index cc511f8..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.GenericClassWithGenericInnerClass#Test.Class{T}.FooAsync.g.verified.cs +++ /dev/null @@ -1,8 +0,0 @@ -//HintName: Test.Class{T}.FooAsync.g.cs -// -#nullable enable -namespace Test; -internal partial class Class -{ - public void Foo(global::Test.Class.Int i) { } -} diff --git a/tests/Generator.Tests/Snapshots/TypeTests.NestedGenerics#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.NestedGenerics#g.verified.cs deleted file mode 100644 index f9df188..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.NestedGenerics#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -var dict = new global::System.Collections.Generic.Dictionary>(); diff --git a/tests/Generator.Tests/Snapshots/TypeTests.NotPattern#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.NotPattern#g.verified.cs deleted file mode 100644 index fe1035f..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.NotPattern#g.verified.cs +++ /dev/null @@ -1,3 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -_ = new object() is not global::System.DBNull; - diff --git a/tests/Generator.Tests/Snapshots/TypeTests.NullableDeclarationExpression#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.NullableDeclarationExpression#g.verified.cs deleted file mode 100644 index c7ab4f3..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.NullableDeclarationExpression#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -new global::System.Collections.Generic.Dictionary().TryGetValue(0, out global::System.IO.Stream? a); diff --git a/tests/Generator.Tests/Snapshots/TypeTests.NullableForeach#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.NullableForeach#g.verified.cs deleted file mode 100644 index c2a3105..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.NullableForeach#g.verified.cs +++ /dev/null @@ -1,4 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -foreach (global::System.IO.Stream? i in global::System.Array.Empty()) -{ -} diff --git a/tests/Generator.Tests/Snapshots/TypeTests.PatternIsEnumMember#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.PatternIsEnumMember#g.verified.cs deleted file mode 100644 index 31e8cad..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.PatternIsEnumMember#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -_ = global::System.StringComparison.CurrentCulture is global::System.StringComparison.CurrentCulture; diff --git a/tests/Generator.Tests/Snapshots/TypeTests.PatternIsNotLiteral#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.PatternIsNotLiteral#g.verified.cs deleted file mode 100644 index 5cfd56a..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.PatternIsNotLiteral#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -_ = global::System.StringComparison.CurrentCulture is not global::System.StringComparison.CurrentCulture; diff --git a/tests/Generator.Tests/Snapshots/TypeTests.PatternMatchingWithConstant#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.PatternMatchingWithConstant#g.verified.cs deleted file mode 100644 index e010dff..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.PatternMatchingWithConstant#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -_ = 1 is 2; diff --git a/tests/Generator.Tests/Snapshots/TypeTests.QualifiedGenericName#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.QualifiedGenericName#g.verified.cs deleted file mode 100644 index dbd8c8b..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.QualifiedGenericName#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -global::System.Collections.Generic.HashSet z = null!; diff --git a/tests/Generator.Tests/Snapshots/TypeTests.QualifiedNonGenericName#System.Class.MethodAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.QualifiedNonGenericName#System.Class.MethodAsync.g.verified.cs deleted file mode 100644 index 8aff483..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.QualifiedNonGenericName#System.Class.MethodAsync.g.verified.cs +++ /dev/null @@ -1,11 +0,0 @@ -//HintName: System.Class.MethodAsync.g.cs -// -#nullable enable -namespace System; -public partial class Class -{ - public void Method() - { - global::System.Security.Cryptography.CryptographicException z = null!; - } -} diff --git a/tests/Generator.Tests/Snapshots/TypeTests.VariableDeclaration#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.VariableDeclaration#g.verified.cs deleted file mode 100644 index d39a3c5..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.VariableDeclaration#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -global::System.IO.MemoryStream ms = new(); diff --git a/tests/Generator.Tests/Snapshots/TypeTests.VariableDeclarationRedundant#g.verified.cs b/tests/Generator.Tests/Snapshots/TypeTests.VariableDeclarationRedundant#g.verified.cs deleted file mode 100644 index 06b808a..0000000 --- a/tests/Generator.Tests/Snapshots/TypeTests.VariableDeclarationRedundant#g.verified.cs +++ /dev/null @@ -1,2 +0,0 @@ -//HintName: Test.Class.MethodAsync.g.cs -global::System.IO.MemoryStream ms = new global::System.IO.MemoryStream(); diff --git a/tests/Generator.Tests/Snapshots/UnitTests.MultipleNamespaces#NsOne.NsTwo.NsThree.NsFour.MultipleNamespaces.EmptyAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/UnitTests.MultipleNamespaces#NsOne.NsTwo.NsThree.NsFour.MultipleNamespaces.EmptyAsync.g.verified.cs deleted file mode 100644 index 8176c1a..0000000 --- a/tests/Generator.Tests/Snapshots/UnitTests.MultipleNamespaces#NsOne.NsTwo.NsThree.NsFour.MultipleNamespaces.EmptyAsync.g.verified.cs +++ /dev/null @@ -1,18 +0,0 @@ -//HintName: NsOne.NsTwo.NsThree.NsFour.MultipleNamespaces.EmptyAsync.g.cs -// -#nullable enable -namespace NsOne -{ - namespace NsTwo.NsThree - { - namespace NsFour - { - public partial class MultipleNamespaces - { - void Empty() - { - } - } - } - } -} diff --git a/tests/Generator.Tests/Snapshots/UnitTests.StaticUsings#N1.C1.MethodAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/UnitTests.StaticUsings#N1.C1.MethodAsync.g.verified.cs deleted file mode 100644 index a1f40f5..0000000 --- a/tests/Generator.Tests/Snapshots/UnitTests.StaticUsings#N1.C1.MethodAsync.g.verified.cs +++ /dev/null @@ -1,16 +0,0 @@ -//HintName: N1.C1.MethodAsync.g.cs -// -#nullable enable - -using static N2.C2; - -namespace N1 -{ - public partial class C1 - { - public void Method() - { - _ = global::N2.C2.OtherConst; - } - } -} diff --git a/tests/Generator.Tests/TypeTests.cs b/tests/Generator.Tests/TypeTests.cs index d4268e8..de27532 100644 --- a/tests/Generator.Tests/TypeTests.cs +++ b/tests/Generator.Tests/TypeTests.cs @@ -13,41 +13,6 @@ public Task ConvertExceptionType() => $$""" } """.Verify(false, true, sourceType: SourceType.MethodBody); - [Fact] - public Task NotPattern() => -"_ = new object() is not DBNull;" -.Verify(false, true, sourceType: SourceType.MethodBody); - - [Fact] - public Task PatternMatchingWithConstant() => -"_ = 1 is 2;" -.Verify(sourceType: SourceType.MethodBody); - - [Fact] - public Task PatternIsNotLiteral() => -"_ = StringComparison.CurrentCulture is not StringComparison.CurrentCulture;" -.Verify(sourceType: SourceType.MethodBody); - - [Fact] - public Task PatternIsEnumMember() => -"_ = StringComparison.CurrentCulture is StringComparison.CurrentCulture;" -.Verify(sourceType: SourceType.MethodBody); - - [Fact] - public Task VariableDeclarationRedundant() => -"MemoryStream ms = new MemoryStream();" -.Verify(false, true, sourceType: SourceType.MethodBody); - - [Fact] - public Task VariableDeclaration() => -"MemoryStream ms = new();" -.Verify(false, true, sourceType: SourceType.MethodBody); - - [Fact] - public Task BinaryPattern() => -"_ = new object() is DBNull or Stream;" -.Verify(false, true, sourceType: SourceType.MethodBody); - [Theory] [InlineData(true)] [InlineData(false)] @@ -84,16 +49,6 @@ public Task ReturnTrueAsync() """ .Verify(false, true, sourceType: SourceType.Full); - [Fact] - public Task DeclarationExpression() => -"new Dictionary().TryGetValue(0, out Stream a);" -.Verify(false, true, sourceType: SourceType.MethodBody); - - [Fact] - public Task NullableDeclarationExpression() => -"new Dictionary().TryGetValue(0, out Stream? a);" -.Verify(false, true, sourceType: SourceType.MethodBody); - [Fact] public Task ConvertForeachType() => $$""" foreach (Int32 i in new Int32[] { 1 }) @@ -103,20 +58,6 @@ public Task ConvertForeachType() => $$""" await Task.CompletedTask; """.Verify(false, true, sourceType: SourceType.MethodBody); - [Fact] - public Task NullableForeach() - => """ -foreach (Stream? i in Array.Empty()) -{ -} -""" - .Verify(sourceType: SourceType.MethodBody); - - [Fact] - public Task NestedGenerics() - => "var dict = new Dictionary>();" - .Verify(sourceType: SourceType.MethodBody); - [Fact] public Task TestTypes() => $$""" String myStr; @@ -129,14 +70,6 @@ public Task TestTypes() => $$""" await Task.CompletedTask; """.Verify(false, true, sourceType: SourceType.MethodBody); - [Fact] - public Task ArrayParameter() => """ -[CreateSyncVersion] -public async Task MethodAsync(Int32[] o) -{ -} -""".Verify(); - [Fact] public Task TwoDArrayParameter() => """ [CreateSyncVersion] @@ -156,11 +89,6 @@ public async Task GetCustomObjectAsync(object o) } """.Verify(); - [Fact] - public Task FullyQualifiedArray() - => "System.Text.RegularExpressions.Regex[] variable = null!;" - .Verify(sourceType: SourceType.MethodBody); - [Fact] public Task CastFullyQualifiedTypeTwice() => """ class CustomClass { } @@ -203,29 +131,6 @@ public Task HandleNameOf() => "_ = nameof(Stream);" .Verify(sourceType: SourceType.MethodBody); - [Fact] - public Task QualifiedGenericName() - => "System.Collections.Generic.HashSet z = null!;" - .Verify(sourceType: SourceType.MethodBody); - - [Theory] - [InlineData("")] - [InlineData("System.")] - [InlineData("global::System.")] - public Task QualifiedNonGenericName(string prefix) - => $$""" -namespace System; - -public partial class Class -{ - [CreateSyncVersion] - public async Task MethodAsync() - { - {{prefix}}Security.Cryptography.CryptographicException z = null!; - } -} -""".Verify(disableUnique: true, sourceType: SourceType.Full); - [Fact] public Task HandleAsCast() => "_ = new object() as Stream;" @@ -249,19 +154,6 @@ public async Task SwitchAsync(Stream stream) } """.Verify(); - [Fact] - public Task GenericClassWithGenericInnerClass() => """ -namespace Test; - -internal partial class Class -{ - [CreateSyncVersion] - public async Task FooAsync(Int i) { } - - internal class Int { } -} -""".Verify(sourceType: SourceType.Full); - [Fact] public Task EventHandlerType() => "_ = new DataReceivedEventHandler((s, e) => { });" diff --git a/tests/Generator.Tests/UnitTests.cs b/tests/Generator.Tests/UnitTests.cs index 80e110f..a355337 100644 --- a/tests/Generator.Tests/UnitTests.cs +++ b/tests/Generator.Tests/UnitTests.cs @@ -74,52 +74,6 @@ public async Task FooAsync() public T Bar(Func innerLogic) => innerLogic(); """.Verify(); - [Fact] - public Task MultipleNamespaces() => """ -namespace NsOne -{ - namespace NsTwo.NsThree - { - namespace NsFour - { - public partial class MultipleNamespaces - { - [CreateSyncVersion] - async void EmptyAsync() - { - } - } - } - } -} -""".Verify(sourceType: SourceType.Full); - - [Fact] - public Task StaticUsings() => """ -using static N2.C2; - -namespace N1 -{ - public partial class C1 - { - [CreateSyncVersion] - public async Task MethodAsync() - { - _ = OtherConst; - } - } -} - -namespace N2 -{ - public class C2 - { - public const int OtherConst = 1; - } -} - -""".Verify(sourceType: SourceType.Full); - [Fact] public Task GenericClass() => """ namespace Test; From 154c742bfe61a8b94a0521dfb5d48fa2de1a39d7 Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Sun, 13 Sep 2026 12:41:06 -0400 Subject: [PATCH 4/4] Point AGENTS.md at the published cloning package The package comes from nuget.org now, so the note about the local feed is gone. Cloning bugs are tested and fixed in method-cloning, and so is an identity clone which fails; EquatableArray comes from the package too. Generated with Claude Code --- AGENTS.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5cf3f2e..4ba88a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -133,13 +133,16 @@ them (`MethodLocation`), fully qualifying the names a method uses The package ships source, not a DLL. Its files compile into the generator as internal types in the `Zomp.MethodCloning` namespace, with the `ROSLYN_*` constants this repository defines, so each Roslyn variant gets its own build of -them. A change to them belongs in that repository: fix it there, pack it, and +them. A change to them belongs in that repository: fix it there, publish it, and bump the version in `Directory.Packages.props`. `AsyncToSyncRewriter` plugs in by deriving from `CloningRewriter` and using its `MapSymbol` and `MapTypeName` hooks. -Until the package is on a public feed, `nuget.config` maps `Zomp.MethodCloning` -to a local folder feed, `~/.nuget/local-feed`, which CI cannot restore from. +A cloning bug gets its failing test and its fix in that repository, as a test of +its identity clone, whichever generator it was found through. This repository +adds a test of its own only when the bug shows up through the async to sync +rules. Tests which exercise nothing but cloning - file names, containing types, +name qualification, namespaces - live there, not here. ## Transformation Pipeline @@ -167,7 +170,8 @@ to a local folder feed, `~/.nuget/local-feed`, which CI cannot restore from. - `TestHelper` also clones every synchronized method through `IdentityCloneGenerator`, which changes nothing but the method's name, and fails the test with "Identity clone does not compile" when a clone does not. - That failure is in the `Cloning/` layer, not in the async to sync rules + That failure is in the `Zomp.MethodCloning` package, not in the async to sync + rules, and is fixed there ### Choosing how the source is wrapped @@ -224,7 +228,7 @@ it: check out the test commit alone and watch it fail. - File-scoped namespaces required - Nerdbank.GitVersioning for version management (from git tags/height) - The generator targets `netstandard2.0` for maximum host compatibility -- `EquatableArray` wraps `ImmutableArray` for value equality in the incremental pipeline +- `EquatableArray`, from `Zomp.MethodCloning`, wraps `ImmutableArray` for value equality in the incremental pipeline ## Diagnostics