From 5a1b2e9265ca8fcd4d72b5feb22333f6610c3c05 Mon Sep 17 00:00:00 2001 From: neuecc Date: Thu, 6 Jun 2024 20:41:32 +0900 Subject: [PATCH 01/14] writer writer --- sandbox/GeneratorSandbox/Program.cs | 4 + .../ConsoleAppGenerator.cs | 122 +++++++++++++++--- .../CSharpGeneratorRunner.cs | 33 +++++ .../IncrementalGeneratorTest.cs | 39 ++++++ 4 files changed, 178 insertions(+), 20 deletions(-) create mode 100644 tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index defda724..d25a770f 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -10,6 +10,10 @@ app.Add(); app.Run(args); + + + + public class Test { public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index d84f923b..8c62eea4 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -1,12 +1,88 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Reflection; +using System.Text; using static ConsoleAppFramework.Emitter; namespace ConsoleAppFramework; + +public static class StringWriterPool +{ + static readonly ConcurrentQueue pool = new(); + + public static StringWriter Rent() + { + if (!pool.TryDequeue(out var writer)) + { + writer = new StringWriter(); + } + return writer; + } + + public static void Return(StringWriter writer) + { + writer.GetStringBuilder().Clear(); + pool.Enqueue(writer); + } +} + + +public class SyntaxNodeTextEqualityComparer : IEqualityComparer +{ + + + public bool Equals(SyntaxNode x, SyntaxNode y) + { + + + + throw new NotImplementedException(); + } + + public int GetHashCode(SyntaxNode obj) + { + throw new NotImplementedException(); + } +} + + +public class RunNode(InvocationExpressionSyntax node, SemanticModel model) : IEquatable +{ + public InvocationExpressionSyntax Node => node; + public SemanticModel SemanticModel => model; + + public bool Equals(RunNode other) + { + //var eq = this.Node.Equals(other.Node); + + var left = StringWriterPool.Rent(); + + var txt = other.Node.GetText(); + + // other.Node.WriteTo + + // new StringBuilder().Equals(); + // StringWriter sw = new StringWriter(); + + other.Node.WriteTo(sw); + var txt = other.Node.Expression.GetText(); + + + return true; + // return eq; + } + + public override int GetHashCode() + { + var hash = node.GetHashCode(); + return hash; + } +} + [Generator(LanguageNames.CSharp)] public partial class ConsoleAppGenerator : IIncrementalGenerator { @@ -15,28 +91,30 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterPostInitializationOutput(EmitConsoleAppTemplateSource); // ConsoleApp.Run - var runSource = context.SyntaxProvider.CreateSyntaxProvider((node, ct) => - { - if (node.IsKind(SyntaxKind.InvocationExpression)) + var runSource = context.SyntaxProvider + .CreateSyntaxProvider((node, ct) => { - var invocationExpression = (node as InvocationExpressionSyntax); - if (invocationExpression == null) return false; - - var expr = invocationExpression.Expression as MemberAccessExpressionSyntax; - if ((expr?.Expression as IdentifierNameSyntax)?.Identifier.Text == "ConsoleApp") + if (node.IsKind(SyntaxKind.InvocationExpression)) { - var methodName = expr?.Name.Identifier.Text; - if (methodName is "Run" or "RunAsync") + var invocationExpression = (node as InvocationExpressionSyntax); + if (invocationExpression == null) return false; + + var expr = invocationExpression.Expression as MemberAccessExpressionSyntax; + if ((expr?.Expression as IdentifierNameSyntax)?.Identifier.Text == "ConsoleApp") { - return true; + var methodName = expr?.Name.Identifier.Text; + if (methodName is "Run" or "RunAsync") + { + return true; + } } + + return false; } return false; - } - - return false; - }, (context, ct) => ((InvocationExpressionSyntax)context.Node, context.SemanticModel)); + }, (context, ct) => new RunNode((InvocationExpressionSyntax)context.Node, context.SemanticModel)) + .WithTrackingName("ConsoleApp.Run.CreateSyntaxProvider"); context.RegisterSourceOutput(runSource, EmitConsoleAppRun); @@ -64,13 +142,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) (InvocationExpressionSyntax)context.Node, ((context.Node as InvocationExpressionSyntax)!.Expression as MemberAccessExpressionSyntax)!.Name.Identifier.Text, context.SemanticModel)) + .WithTrackingName("ConsoleApp.Builder.CreateSyntaxProvider") .Where(x => { var model = x.SemanticModel.GetTypeInfo((x.Item1.Expression as MemberAccessExpressionSyntax)!.Expression); return model.Type?.Name == "ConsoleAppBuilder"; - }); + }) + .WithTrackingName("ConsoleApp.Builder.Where") + .Collect() + .WithTrackingName("ConsoleApp.Builder.Collect"); - context.RegisterSourceOutput(builderSource.Collect(), EmitConsoleAppBuilder); + context.RegisterSourceOutput(builderSource, EmitConsoleAppBuilder); } public const string ConsoleAppBaseCode = """ @@ -553,10 +635,10 @@ namespace ConsoleAppFramework; """; - static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, (InvocationExpressionSyntax, SemanticModel) generatorSyntaxContext) + static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, RunNode runNode) { - var node = generatorSyntaxContext.Item1; - var model = generatorSyntaxContext.Item2; + var node = runNode.Node; + var model = runNode.SemanticModel; var wellKnownTypes = new WellKnownTypes(model.Compilation); diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 57a7b890..d074c40d 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -87,6 +87,39 @@ public static (Compilation, ImmutableArray, string) CompileAndExecut { Console.SetOut(originalOut); } + } + + public static void CheckIncrementalGenerator(params string[] sources) + { + var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp12); // 12 + var driver = CSharpGeneratorDriver.Create( + [new ConsoleAppGenerator().AsSourceGenerator()], + driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)) + .WithUpdatedParseOptions(parseOptions); + + var generatorResults = sources + .Select(source => + { + var compilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source, parseOptions)); + driver = driver.RunGenerators(compilation); + return driver.GetRunResult().Results[0]; + }) + .ToArray(); + + var reasons = generatorResults + .Select(x => x.TrackedSteps + .Where(x => x.Key.StartsWith("ConsoleApp")) + .Select(x => new + { + x.Key, + Reasons = string.Join(", ", x.Value.SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray()) + }) + .ToArray()) + .ToArray(); + + + + } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs new file mode 100644 index 00000000..ffb6f5ec --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs @@ -0,0 +1,39 @@ +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleAppFramework.GeneratorTests; + +public class IncrementalGeneratorTest +{ + [Fact] + public void CheckIncrementalStep() + { + var step1 = """ +using ConsoleAppFramework; + +ConsoleApp.Run(args, () => { }); +"""; + + var step2 = """ +using ConsoleAppFramework; + +ConsoleApp.Run(args, () => { }); + +Console.WriteLine("foo"); // unrelated line +"""; + + var step3 = """ +using ConsoleAppFramework; + +ConsoleApp.Run(args, (int x, int y) => { }); // change signature + +Console.WriteLine("foo"); // unrelated line +"""; + + CSharpGeneratorRunner.CheckIncrementalGenerator(step1, step2, step3); + } +} From e2f5db2f718683f8c0626eb7e783b98bd8989dfe Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 7 Jun 2024 14:19:34 +0900 Subject: [PATCH 02/14] Run incremental --- .../ConsoleAppGenerator.cs | 176 ++++++++++-------- src/ConsoleAppFramework/FunctionSyntax.cs | 42 +++++ src/ConsoleAppFramework/Parser.cs | 2 +- src/ConsoleAppFramework/PooledStringWriter.cs | 159 ++++++++++++++++ .../SyntaxNodeTextEqualityComparer.cs | 72 +++++++ .../CSharpGeneratorRunner.cs | 16 +- .../IncrementalGeneratorTest.cs | 132 ++++++++++++- .../PooledStringWriterTest.cs | 43 +++++ 8 files changed, 550 insertions(+), 92 deletions(-) create mode 100644 src/ConsoleAppFramework/FunctionSyntax.cs create mode 100644 src/ConsoleAppFramework/PooledStringWriter.cs create mode 100644 src/ConsoleAppFramework/SyntaxNodeTextEqualityComparer.cs create mode 100644 tests/ConsoleAppFramework.GeneratorTests/PooledStringWriterTest.cs diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index 8c62eea4..3604b3f4 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -1,87 +1,12 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Collections.Concurrent; using System.Collections.Immutable; using System.Reflection; -using System.Text; -using static ConsoleAppFramework.Emitter; namespace ConsoleAppFramework; -public static class StringWriterPool -{ - static readonly ConcurrentQueue pool = new(); - - public static StringWriter Rent() - { - if (!pool.TryDequeue(out var writer)) - { - writer = new StringWriter(); - } - return writer; - } - - public static void Return(StringWriter writer) - { - writer.GetStringBuilder().Clear(); - pool.Enqueue(writer); - } -} - - -public class SyntaxNodeTextEqualityComparer : IEqualityComparer -{ - - - public bool Equals(SyntaxNode x, SyntaxNode y) - { - - - - throw new NotImplementedException(); - } - - public int GetHashCode(SyntaxNode obj) - { - throw new NotImplementedException(); - } -} - - -public class RunNode(InvocationExpressionSyntax node, SemanticModel model) : IEquatable -{ - public InvocationExpressionSyntax Node => node; - public SemanticModel SemanticModel => model; - - public bool Equals(RunNode other) - { - //var eq = this.Node.Equals(other.Node); - - var left = StringWriterPool.Rent(); - - var txt = other.Node.GetText(); - - // other.Node.WriteTo - - // new StringBuilder().Equals(); - // StringWriter sw = new StringWriter(); - - other.Node.WriteTo(sw); - var txt = other.Node.Expression.GetText(); - - - return true; - // return eq; - } - - public override int GetHashCode() - { - var hash = node.GetHashCode(); - return hash; - } -} [Generator(LanguageNames.CSharp)] public partial class ConsoleAppGenerator : IIncrementalGenerator @@ -643,7 +568,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, R var wellKnownTypes = new WellKnownTypes(model.Compilation); var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes, DelegateBuildType.MakeDelegateWhenHasDefaultValue, []); - var command = parser.ParseAndValidate(); + var command = parser.ParseAndValidateForRun(); if (command == null) { return; @@ -794,7 +719,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var commandIds = commands .Select((x, i) => { - return new CommandWithId( + return new Emitter.CommandWithId( FieldType: x!.BuildDelegateSignature(out _), // for builder, always generate Action/Func so ok to ignore out var. Command: x!, Id: i @@ -821,4 +746,101 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex } sourceProductionContext.AddSource("ConsoleApp.Builder.Help.g.cs", help.ToString()); } + + class RunNode(InvocationExpressionSyntax node, SemanticModel model) : IEquatable + { + public InvocationExpressionSyntax Node => node; + public SemanticModel SemanticModel => model; + + public bool Equals(RunNode other) + { + if (!SyntaxNodeTextEqualityComparer.Default.Equals(node.Expression, other.Node.Expression)) return false; + + var args1 = node.ArgumentList.Arguments; + var args2 = other.Node.ArgumentList.Arguments; + + if (args1.Count != args2.Count) return false; + if (args1.Count != 2) return false; + + if (args1[1].Kind() != args2[1].Kind()) + { + return false; + } + + var expression = args1[1].Expression; + var lambda1 = expression as ParenthesizedLambdaExpressionSyntax; + if (lambda1 != null) + { + // check async, returntype, parameters + + var lambda2 = (ParenthesizedLambdaExpressionSyntax)args2[1].Expression; + + if (!lambda1.AsyncKeyword.IsKind(lambda2.AsyncKeyword.Kind())) return false; + + if (!SyntaxNodeTextEqualityComparer.Default.Equals(lambda1.ReturnType!, lambda2.ReturnType!)) + { + return false; + } + + return SyntaxNodeTextEqualityComparer.Default.Equals(lambda1.ParameterList, lambda2.ParameterList); + } + else + { + ImmutableArray methodSymbols1; + ImmutableArray methodSymbols2; + if (expression.IsKind(SyntaxKind.AddressOfExpression)) + { + var operand1 = (expression as PrefixUnaryExpressionSyntax)!.Operand; + var operand2 = (args2[1].Expression as PrefixUnaryExpressionSyntax)!.Operand; + if (operand1 == null || operand2 == null) return false; + + methodSymbols1 = model.GetMemberGroup(operand1); + methodSymbols2 = other.SemanticModel.GetMemberGroup(operand2); + } + else + { + methodSymbols1 = model.GetMemberGroup(expression); + methodSymbols2 = other.SemanticModel.GetMemberGroup(args2[1].Expression); + } + + if (methodSymbols1.Length > 0 && methodSymbols1[0] is IMethodSymbol methodSymbol1) + { + if (methodSymbols2.Length > 0 && methodSymbols2[0] is IMethodSymbol methodSymbol2) + { + var syntax1 = FunctionSyntax.From(methodSymbol1.DeclaringSyntaxReferences[0].GetSyntax()); + var syntax2 = FunctionSyntax.From(methodSymbol2.DeclaringSyntaxReferences[0].GetSyntax()); + if (syntax1 == null || syntax2 == null) return false; + + // document comment + if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.GetDocumentationCommentTriviaSyntax()!, syntax2.GetDocumentationCommentTriviaSyntax()!)) + { + return false; + } + + // return type + if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.ReturnType, syntax2.ReturnType)) + { + return false; + } + + // attributes + if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.AttributeLists, syntax2.AttributeLists)) + { + return false; + } + + // parameters + return SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.ParameterList, syntax2.ParameterList); + } + } + } + + return false; + } + + public override int GetHashCode() + { + return SyntaxNodeTextEqualityComparer.Default.GetHashCode(node); + } + } } diff --git a/src/ConsoleAppFramework/FunctionSyntax.cs b/src/ConsoleAppFramework/FunctionSyntax.cs new file mode 100644 index 00000000..c72610ca --- /dev/null +++ b/src/ConsoleAppFramework/FunctionSyntax.cs @@ -0,0 +1,42 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace ConsoleAppFramework; + +public interface IFunctionSyntax +{ + DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(); + TypeSyntax ReturnType { get; } + ParameterListSyntax ParameterList { get; } + SyntaxList AttributeLists { get; } +} + +public static class FunctionSyntax +{ + public static IFunctionSyntax? From(SyntaxNode node) + { + var syntax = node switch + { + MethodDeclarationSyntax x => new FromMethodDeclaration(x), + LocalFunctionStatementSyntax x => new FromLocalFunctionStatement(x), + _ => (IFunctionSyntax?)null + }; + return syntax; + } + + class FromMethodDeclaration(MethodDeclarationSyntax syntax) : IFunctionSyntax + { + public DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax() => syntax.GetDocumentationCommentTriviaSyntax(); + public TypeSyntax ReturnType => syntax.ReturnType; + public ParameterListSyntax ParameterList => syntax.ParameterList; + public SyntaxList AttributeLists => syntax.AttributeLists; + } + + class FromLocalFunctionStatement(LocalFunctionStatementSyntax syntax) : IFunctionSyntax + { + public DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax() => syntax.GetDocumentationCommentTriviaSyntax(); + public TypeSyntax ReturnType => syntax.ReturnType; + public ParameterListSyntax ParameterList => syntax.ParameterList; + public SyntaxList AttributeLists => syntax.AttributeLists; + } +} diff --git a/src/ConsoleAppFramework/Parser.cs b/src/ConsoleAppFramework/Parser.cs index 746c5655..507aa01a 100644 --- a/src/ConsoleAppFramework/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -6,7 +6,7 @@ namespace ConsoleAppFramework; internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes, DelegateBuildType delegateBuildType, FilterInfo[] globalFilters) { - public Command? ParseAndValidate() // for ConsoleApp.Run + public Command? ParseAndValidateForRun() // for ConsoleApp.Run, lambda or method or &method { var args = node.ArgumentList.Arguments; if (args.Count == 2) // 0 = args, 1 = lambda diff --git a/src/ConsoleAppFramework/PooledStringWriter.cs b/src/ConsoleAppFramework/PooledStringWriter.cs new file mode 100644 index 00000000..2e3d5b5b --- /dev/null +++ b/src/ConsoleAppFramework/PooledStringWriter.cs @@ -0,0 +1,159 @@ +using System.Collections.Concurrent; +using System.Text; + +namespace ConsoleAppFramework; + +// based on StringWriter to calc span equality + +public sealed class PooledStringWriter : TextWriter +{ + // Pool + + static readonly ConcurrentQueue pool = new(); + + public static PooledStringWriter Rent() + { + if (!pool.TryDequeue(out var writer)) + { + writer = new PooledStringWriter(); + } + return writer; + } + + public static void PoolClear() + { + while (pool.TryDequeue(out _)) { } + } + + // Return + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.Clear(); + pool.Enqueue(this); + } + } + + // Impl + + static UnicodeEncoding? encoding; + + char[] chars; + int position; + + PooledStringWriter() + { + chars = new char[256]; + } + + public override void Close() + { + Dispose(true); + } + + public void Clear() + { + position = 0; + } + + public ReadOnlySpan AsSpan() => chars.AsSpan(0, position); + + public override Encoding Encoding => encoding ??= new UnicodeEncoding(false, false); + + // Writes a character to the underlying string buffer. + // + public override void Write(char value) + { + if (chars.Length < position + 1) + { + EnsureCapacity(1); + } + + chars[position++] = value; + } + + void EnsureCapacity(int length) + { + var finalLength = position + length; + var next = chars.Length * 2; + var newSize = Math.Max(next, finalLength); + Array.Resize(ref chars, newSize); + } + + public override void Write(char[] buffer, int index, int count) + { + Write(buffer.AsSpan(index, count)); + } + + public void Write(ReadOnlySpan buffer) + { + var dest = chars.AsSpan(position); + if (dest.Length < buffer.Length) + { + EnsureCapacity(buffer.Length); + dest = chars.AsSpan(position); + } + + buffer.CopyTo(dest); + position += buffer.Length; + } + + public override void Write(string? value) + { + if (value != null) + { + Write(value.AsSpan()); + } + } + + #region Task based Async APIs + + public override Task WriteAsync(char value) + { + Write(value); + return Task.CompletedTask; + } + + public override Task WriteAsync(string? value) + { + Write(value); + return Task.CompletedTask; + } + + public override Task WriteAsync(char[] buffer, int index, int count) + { + Write(buffer, index, count); + return Task.CompletedTask; + } + + public override Task WriteLineAsync(char value) + { + WriteLine(value); + return Task.CompletedTask; + } + + public override Task WriteLineAsync(string? value) + { + WriteLine(value); + return Task.CompletedTask; + } + + public override Task WriteLineAsync(char[] buffer, int index, int count) + { + WriteLine(buffer, index, count); + return Task.CompletedTask; + } + + public override Task FlushAsync() + { + return Task.CompletedTask; + } + + #endregion + + public override string ToString() + { + return new string(chars, 0, position); + } +} diff --git a/src/ConsoleAppFramework/SyntaxNodeTextEqualityComparer.cs b/src/ConsoleAppFramework/SyntaxNodeTextEqualityComparer.cs new file mode 100644 index 00000000..8560dd9c --- /dev/null +++ b/src/ConsoleAppFramework/SyntaxNodeTextEqualityComparer.cs @@ -0,0 +1,72 @@ +using Microsoft.CodeAnalysis; + +namespace ConsoleAppFramework; + +public class SyntaxNodeTextEqualityComparer : IEqualityComparer +{ + public static readonly SyntaxNodeTextEqualityComparer Default = new SyntaxNodeTextEqualityComparer(); + + SyntaxNodeTextEqualityComparer() + { + } + + public bool Equals(SyntaxNode x, SyntaxNode y) + { + if (x == null & y == null) return true; + if (x == null || y == null) return false; + + using var xWriter = PooledStringWriter.Rent(); + using var yWriter = PooledStringWriter.Rent(); + + x.WriteTo(xWriter); + y.WriteTo(yWriter); + + return xWriter.AsSpan().SequenceEqual(yWriter.AsSpan()); + } + + public bool Equals(SyntaxList xs, SyntaxList ys) + { + if (xs.Count != ys.Count) return false; + + using var xWriter = PooledStringWriter.Rent(); + using var yWriter = PooledStringWriter.Rent(); + + for (int i = 0; i < xs.Count; i++) + { + var x = xs[i]; + var y = ys[i]; + + x.WriteTo(xWriter); + y.WriteTo(yWriter); + + if (!xWriter.AsSpan().SequenceEqual(yWriter.AsSpan())) + { + return false; + } + + xWriter.Clear(); + yWriter.Clear(); + } + + return true; + } + + public int GetHashCode(SyntaxNode obj) + { + if (obj == null) return 0; + + using var writer = PooledStringWriter.Rent(); + obj.WriteTo(writer); + + var span = writer.AsSpan(); + + // simple hashing + int hash = 0; + for (int i = 0; i < span.Length; i++) + { + hash = (hash * 37) ^ span[i]; + } + + return hash; + } +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index d074c40d..3c13afac 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -89,7 +89,7 @@ public static (Compilation, ImmutableArray, string) CompileAndExecut } } - public static void CheckIncrementalGenerator(params string[] sources) + public static (string Key, string Reasons)[][] GetIncrementalGeneratorTrackedStepsReasons(string keyPrefixFilter, params string[] sources) { var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp12); // 12 var driver = CSharpGeneratorDriver.Create( @@ -108,18 +108,16 @@ [new ConsoleAppGenerator().AsSourceGenerator()], var reasons = generatorResults .Select(x => x.TrackedSteps - .Where(x => x.Key.StartsWith("ConsoleApp")) - .Select(x => new - { + .Where(x => x.Key.StartsWith(keyPrefixFilter)) + .Select(x => + ( x.Key, - Reasons = string.Join(", ", x.Value.SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray()) - }) + Reasons: string.Join(", ", x.Value.SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray()) + )) .ToArray()) .ToArray(); - - - + return reasons; } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs index ffb6f5ec..d4d2b3ff 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs @@ -10,18 +10,18 @@ namespace ConsoleAppFramework.GeneratorTests; public class IncrementalGeneratorTest { [Fact] - public void CheckIncrementalStep() + public void RunLambda() { var step1 = """ using ConsoleAppFramework; -ConsoleApp.Run(args, () => { }); +ConsoleApp.Run(args, int () => 0); """; var step2 = """ using ConsoleAppFramework; -ConsoleApp.Run(args, () => { }); +ConsoleApp.Run(args, int () => 100); // body change Console.WriteLine("foo"); // unrelated line """; @@ -29,11 +29,133 @@ public void CheckIncrementalStep() var step3 = """ using ConsoleAppFramework; -ConsoleApp.Run(args, (int x, int y) => { }); // change signature +ConsoleApp.Run(args, int (int x, int y) => 99); // change signature Console.WriteLine("foo"); // unrelated line """; - CSharpGeneratorRunner.CheckIncrementalGenerator(step1, step2, step3); + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Run", step1, step2, step3); + + reasons[0][0].Reasons.Should().Be("New"); + reasons[1][0].Reasons.Should().Be("Unchanged"); + reasons[2][0].Reasons.Should().Be("Modified"); + } + + [Fact] + public void RunMethod() + { + var step1 = """ +using ConsoleAppFramework; + +var tako = new Tako(); +ConsoleApp.Run(args, tako.DoHello); + +public class Tako +{ + /// + /// AAAAA + /// + public void DoHello() + { + } +} +"""; + + var step2 = """ +using ConsoleAppFramework; + +var tako = new Tako(); +ConsoleApp.Run(args, tako.DoHello); + +Console.WriteLine("foo"); // unrelated line + +public class Tako +{ + /// + /// AAAAA + /// + public void DoHello() + { + Console.WriteLine("foo"); // body change + } +} +"""; + + var step3 = """ +using ConsoleAppFramework; + +var tako = new Tako(); +ConsoleApp.Run(args, tako.DoHello); + +public class Tako +{ + /// + /// AAAAA + /// + public void DoHello(int x, int y) // signature change + { + } +} +"""; + + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Run", step1, step2, step3); + + reasons[0][0].Reasons.Should().Be("New"); + reasons[1][0].Reasons.Should().Be("Unchanged"); + reasons[2][0].Reasons.Should().Be("Modified"); } + + [Fact] + public void RunMethodRef() + { + var step1 = """ +using ConsoleAppFramework; + +unsafe +{ + ConsoleApp.Run(args, &DoHello); } + +static void DoHello() +{ +} +"""; + + var step2 = """ +using ConsoleAppFramework; + +unsafe +{ + ConsoleApp.Run(args, &DoHello); + Console.WriteLine("bar"); // unrelated line +} + +static void DoHello() +{ + Console.WriteLine("foo"); // body +} +"""; + + var step3 = """ +using ConsoleAppFramework; + +unsafe +{ + ConsoleApp.Run(args, &DoHello); + Console.WriteLine("bar"); +} + +static void DoHello(int x, int y) // change signature +{ + Console.WriteLine("foo"); +} +"""; + + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Run", step1, step2, step3); + + reasons[0][0].Reasons.Should().Be("New"); + reasons[1][0].Reasons.Should().Be("Unchanged"); + reasons[2][0].Reasons.Should().Be("Modified"); + } +} + diff --git a/tests/ConsoleAppFramework.GeneratorTests/PooledStringWriterTest.cs b/tests/ConsoleAppFramework.GeneratorTests/PooledStringWriterTest.cs new file mode 100644 index 00000000..a8812bf8 --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/PooledStringWriterTest.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleAppFramework.GeneratorTests; + +public class PooledStringWriterTest +{ + [Fact] + public void AppendChars() + { + PooledStringWriter.PoolClear(); + + using var writer = PooledStringWriter.Rent(); + var reference = new StringWriter(); + for (int i = 0; i < 300; i++) + { + writer.Write((char)i); + reference.Write((char)i); + } + + writer.ToString().Equals(reference.ToString()); + } + + [Fact] + public void AppendStrings() + { + PooledStringWriter.PoolClear(); + + var str = Guid.NewGuid().ToString(); + using var writer = PooledStringWriter.Rent(); + var reference = new StringWriter(); + for (int i = 0; i < 300; i++) + { + writer.Write(str); + reference.Write(str); + } + + writer.ToString().Equals(reference.ToString()); + } +} From 7d7a84fd6edfb801e51468fca4de102fdfd8984d Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 7 Jun 2024 19:07:38 +0900 Subject: [PATCH 03/14] doing --- .../ConsoleAppGenerator.cs | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index 3604b3f4..f606a400 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -3,11 +3,10 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Immutable; using System.Reflection; +using System.Xml.Linq; namespace ConsoleAppFramework; - - [Generator(LanguageNames.CSharp)] public partial class ConsoleAppGenerator : IIncrementalGenerator { @@ -38,7 +37,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } return false; - }, (context, ct) => new RunNode((InvocationExpressionSyntax)context.Node, context.SemanticModel)) + }, (context, ct) => new RunContext((InvocationExpressionSyntax)context.Node, context.SemanticModel)) .WithTrackingName("ConsoleApp.Run.CreateSyntaxProvider"); context.RegisterSourceOutput(runSource, EmitConsoleAppRun); @@ -63,14 +62,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } return false; - }, (context, ct) => ( + }, (context, ct) => new BuilderContext( (InvocationExpressionSyntax)context.Node, ((context.Node as InvocationExpressionSyntax)!.Expression as MemberAccessExpressionSyntax)!.Name.Identifier.Text, context.SemanticModel)) .WithTrackingName("ConsoleApp.Builder.CreateSyntaxProvider") .Where(x => { - var model = x.SemanticModel.GetTypeInfo((x.Item1.Expression as MemberAccessExpressionSyntax)!.Expression); + var model = x.Model.GetTypeInfo((x.Node.Expression as MemberAccessExpressionSyntax)!.Expression); return model.Type?.Name == "ConsoleAppBuilder"; }) .WithTrackingName("ConsoleApp.Builder.Where") @@ -560,7 +559,7 @@ namespace ConsoleAppFramework; """; - static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, RunNode runNode) + static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, RunContext runNode) { var node = runNode.Node; var model = runNode.SemanticModel; @@ -601,7 +600,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, R sourceProductionContext.AddSource("ConsoleApp.Run.Help.g.cs", help.ToString()); } - static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContext, ImmutableArray<(InvocationExpressionSyntax Node, string Name, SemanticModel Model)> generatorSyntaxContexts) + static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContext, ImmutableArray generatorSyntaxContexts) { if (generatorSyntaxContexts.Length == 0) return; @@ -747,12 +746,12 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex sourceProductionContext.AddSource("ConsoleApp.Builder.Help.g.cs", help.ToString()); } - class RunNode(InvocationExpressionSyntax node, SemanticModel model) : IEquatable + readonly struct RunContext(InvocationExpressionSyntax node, SemanticModel model) : IEquatable { public InvocationExpressionSyntax Node => node; public SemanticModel SemanticModel => model; - public bool Equals(RunNode other) + public bool Equals(RunContext other) { if (!SyntaxNodeTextEqualityComparer.Default.Equals(node.Expression, other.Node.Expression)) return false; @@ -843,4 +842,24 @@ public override int GetHashCode() return SyntaxNodeTextEqualityComparer.Default.GetHashCode(node); } } + + readonly struct BuilderContext(InvocationExpressionSyntax node, string name, SemanticModel model) : IEquatable + { + public InvocationExpressionSyntax Node => node; + public string Name => name; + public SemanticModel Model => model; + + public bool Equals(BuilderContext other) + { + // TODO: + return node == other.Node; + } + + public override int GetHashCode() + { + // TODO: + return base.GetHashCode(); + } + } + } From cd7c130dc0e2a1db51b7eb4c9eccc2187e9d91a9 Mon Sep 17 00:00:00 2001 From: neuecc Date: Sun, 9 Jun 2024 04:09:07 +0900 Subject: [PATCH 04/14] more --- sandbox/GeneratorSandbox/Program.cs | 38 +++- src/ConsoleAppFramework/Command.cs | 13 +- .../ConsoleAppGenerator.cs | 91 ++++++-- src/ConsoleAppFramework/Emitter.cs | 8 +- src/ConsoleAppFramework/Parser.cs | 2 + src/ConsoleAppFramework/RoslynExtensions.cs | 18 ++ .../IncrementalGeneratorTest.cs | 196 ++++++++++++++++++ 7 files changed, 328 insertions(+), 38 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index d25a770f..8ac1072a 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,21 +1,39 @@ -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; -using ConsoleAppFramework; - - - -args = ["show", "--aaa", "a", "value", "10.2"]; +using ConsoleAppFramework; +using Foo.Bar; +using System.ComponentModel.DataAnnotations; var app = ConsoleApp.Create(); app.Add(); +app.Add("", (int x, int y) => // different +{ + Console.WriteLine("body"); // body +}); +app.Add("foo", () => { }); // newline +app.UseFilter(); app.Run(args); - - - +Console.WriteLine(""); // unrelated line public class Test { public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); } +namespace Foo.Bar +{ + public class MyFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) + { + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + public class MyFilter2(ConsoleAppFilter next) : ConsoleAppFilter(next) + { + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/ConsoleAppFramework/Command.cs b/src/ConsoleAppFramework/Command.cs index 269c28e4..4d66862f 100644 --- a/src/ConsoleAppFramework/Command.cs +++ b/src/ConsoleAppFramework/Command.cs @@ -24,7 +24,7 @@ public record class Command { public required bool IsAsync { get; init; } // Task or Task public required bool IsVoid { get; init; } // void or int - + public bool IsRootCommand => Name == ""; public required string Name { get; init; } @@ -150,6 +150,7 @@ public record class CommandParameter { public required ITypeSymbol Type { get; init; } public required Location Location { get; init; } + public required WellKnownTypes WellKnownTypes { get; init; } public required bool IsNullableReference { get; init; } public required bool IsParams { get; init; } public required string Name { get; init; } @@ -170,7 +171,7 @@ public record class CommandParameter public bool RequireCheckArgumentParsed => !(HasDefaultValue || IsParams || IsFlag); // increment = false when passed from [Argument] - public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes wellKnownTypes, bool increment) + public string BuildParseMethod(int argCount, string argumentName, bool increment) { var incrementIndex = increment ? "!TryIncrementIndex(ref i, args.Length) || " : ""; return Core(Type, false); @@ -207,7 +208,7 @@ string Core(ITypeSymbol type, bool nullable) { return $"arg{argCount} = args[i];"; } - + case SpecialType.System_Boolean: return $"arg{argCount} = true;"; // bool is true flag case SpecialType.System_Char: @@ -242,7 +243,7 @@ string Core(ITypeSymbol type, bool nullable) if (type.TypeKind == TypeKind.Array) { var elementType = (type as IArrayTypeSymbol)!.ElementType; - var parsable = wellKnownTypes.ISpanParsable; + var parsable = WellKnownTypes.ISpanParsable; if (parsable != null) // has parsable { if (elementType.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable))) @@ -254,12 +255,12 @@ string Core(ITypeSymbol type, bool nullable) } // System.DateTimeOffset, System.Guid, System.Version - tryParseKnownPrimitive = wellKnownTypes.HasTryParse(type); + tryParseKnownPrimitive = WellKnownTypes.HasTryParse(type); if (!tryParseKnownPrimitive) { // ISpanParsable (BigInteger, Complex, Half, Int128, etc...) - var parsable = wellKnownTypes.ISpanParsable; + var parsable = WellKnownTypes.ISpanParsable; if (parsable != null) // has parsable { tryParseIParsable = type.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable)); diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index f606a400..e0e35de9 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -38,7 +38,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return false; }, (context, ct) => new RunContext((InvocationExpressionSyntax)context.Node, context.SemanticModel)) - .WithTrackingName("ConsoleApp.Run.CreateSyntaxProvider"); + .WithTrackingName("ConsoleApp.Run.CreateSyntaxProvider"); // annotate for IncrementalGeneratorTest context.RegisterSourceOutput(runSource, EmitConsoleAppRun); @@ -584,7 +584,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, R sb.AppendLine(GeneratedCodeHeader); using (sb.BeginBlock("internal static partial class ConsoleApp")) { - var emitter = new Emitter(wellKnownTypes); + var emitter = new Emitter(); var withId = new Emitter.CommandWithId(null, command, -1); emitter.EmitRun(sb, withId, isRunAsync); } @@ -594,7 +594,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, R help.AppendLine(GeneratedCodeHeader); using (help.BeginBlock("internal static partial class ConsoleApp")) { - var emitter = new Emitter(wellKnownTypes); + var emitter = new Emitter(); emitter.EmitHelp(help, command); } sourceProductionContext.AddSource("ConsoleApp.Run.Help.g.cs", help.ToString()); @@ -604,9 +604,9 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex { if (generatorSyntaxContexts.Length == 0) return; - var model = generatorSyntaxContexts[0].Model; + // var model = generatorSyntaxContexts[0].Model; - var wellKnownTypes = new WellKnownTypes(model.Compilation); + // var wellKnownTypes = new WellKnownTypes(model.Compilation); // validation, invoke in loop is not allowed. foreach (var item in generatorSyntaxContexts) @@ -638,7 +638,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex { var genericName = (x.Node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; var genericType = genericName!.TypeArgumentList.Arguments[0]; - var type = model.GetTypeInfo(genericType).Type; + var type = x.Model.GetTypeInfo(genericType).Type; if (type == null) return null!; var filter = FilterInfo.Create(type); @@ -663,6 +663,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var commands1 = methodGroup["Add"] .Select(x => { + var wellKnownTypes = new WellKnownTypes(x.Model.Compilation); var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.OnlyActionFunc, globalFilters); var command = parser.ParseAndValidateForBuilderDelegateRegistration(); @@ -680,6 +681,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var commands2 = methodGroup["Add"] .SelectMany(x => { + var wellKnownTypes = new WellKnownTypes(x.Model.Compilation); var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.None, globalFilters); var commands = parser.ParseAndValidateForBuilderClassRegistration(); @@ -728,7 +730,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex using (sb.BeginBlock("internal static partial class ConsoleApp")) { - var emitter = new Emitter(wellKnownTypes); + var emitter = new Emitter(); emitter.EmitBuilder(sb, commandIds, hasRun, hasRunAsync); } sourceProductionContext.AddSource("ConsoleApp.Builder.g.cs", sb.ToString()); @@ -740,7 +742,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex using (help.BeginBlock("internal static partial class ConsoleApp")) using (help.BeginBlock("internal partial struct ConsoleAppBuilder")) { - var emitter = new Emitter(wellKnownTypes); + var emitter = new Emitter(); emitter.EmitHelp(help, commandIds!); } sourceProductionContext.AddSource("ConsoleApp.Builder.Help.g.cs", help.ToString()); @@ -755,6 +757,18 @@ public bool Equals(RunContext other) { if (!SyntaxNodeTextEqualityComparer.Default.Equals(node.Expression, other.Node.Expression)) return false; + return DelegateEquals(node, model, (other.Node, other.SemanticModel)); + } + + public override int GetHashCode() + { + // maybe this does not called so don't care impl. + return SyntaxNodeTextEqualityComparer.Default.GetHashCode(node); + } + + // use for both Run and Builder.Add + public static bool DelegateEquals(InvocationExpressionSyntax node, SemanticModel model, (InvocationExpressionSyntax Node, SemanticModel SemanticModel) other) + { var args1 = node.ArgumentList.Arguments; var args2 = other.Node.ArgumentList.Arguments; @@ -836,11 +850,6 @@ public bool Equals(RunContext other) return false; } - - public override int GetHashCode() - { - return SyntaxNodeTextEqualityComparer.Default.GetHashCode(node); - } } readonly struct BuilderContext(InvocationExpressionSyntax node, string name, SemanticModel model) : IEquatable @@ -851,15 +860,61 @@ readonly struct BuilderContext(InvocationExpressionSyntax node, string name, Sem public bool Equals(BuilderContext other) { - // TODO: - return node == other.Node; + if (this.Name != other.Name) return false; + + var typeInfo = Model.GetTypeInfo((Node.Expression as MemberAccessExpressionSyntax)!.Expression); + if (typeInfo.Type?.Name != "ConsoleAppBuilder") + { + return false; + } + + switch (Name) + { + case "Add": // Add or Add + if ((Node.Expression as MemberAccessExpressionSyntax)?.Name.IsKind(SyntaxKind.GenericName) ?? false) + { + return EqualsAddClass(other); + } + else + { + return RunContext.DelegateEquals(node, model, (other.Node, other.Model)); + } + case "UseFilter": + return EqualsUseFilter(other); + case "Run": + case "RunAsync": + return true; // only check name + default: + break; + } + + return false; + } + + bool EqualsAddClass(BuilderContext other) + { + return true; // TODO:final + } + + bool EqualsUseFilter(BuilderContext other) + { + var l = GetType(Node, model); + var r = GetType(other.Node, other.Model); + + return l.EqualsNamespaceAndName(r); + + static ITypeSymbol? GetType(InvocationExpressionSyntax expression, SemanticModel model) + { + var genericName = (expression.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; + var genericType = genericName!.TypeArgumentList.Arguments[0]; + return model.GetTypeInfo(genericType).Type; + } } public override int GetHashCode() { - // TODO: - return base.GetHashCode(); + // maybe this does not called so don't care impl. + return SyntaxNodeTextEqualityComparer.Default.GetHashCode(node); } } - } diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index 0ac3e8e8..440a1e37 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -3,7 +3,7 @@ namespace ConsoleAppFramework; -internal class Emitter(WellKnownTypes wellKnownTypes) +internal class Emitter { public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsync, string? methodName = null) { @@ -124,7 +124,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy sb.AppendLine($"if (i == {parameter.ArgumentIndex})"); using (sb.BeginBlock()) { - sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: false)}"); + sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: false)}"); if (parameter.RequireCheckArgumentParsed) { sb.AppendLine($"arg{i}Parsed = true;"); @@ -154,7 +154,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy } using (sb.BeginBlock()) { - sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); + sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: true)}"); if (parameter.RequireCheckArgumentParsed) { sb.AppendLine($"arg{i}Parsed = true;"); @@ -180,7 +180,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy } using (sb.BeginBlock()) { - sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); + sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: true)}"); if (parameter.RequireCheckArgumentParsed) { sb.AppendLine($"arg{i}Parsed = true;"); diff --git a/src/ConsoleAppFramework/Parser.cs b/src/ConsoleAppFramework/Parser.cs index 507aa01a..fe6d0f03 100644 --- a/src/ConsoleAppFramework/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -343,6 +343,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return new CommandParameter { Name = NameConverter.ToKebabCase(x.Identifier.Text), + WellKnownTypes = wellKnownTypes, OriginalParameterName = x.Identifier.Text, IsNullableReference = isNullableReference, IsConsoleAppContext = isConsoleAppContext, @@ -498,6 +499,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return new CommandParameter { Name = NameConverter.ToKebabCase(x.Name), + WellKnownTypes = wellKnownTypes, OriginalParameterName = x.Name, IsNullableReference = isNullableReference, IsConsoleAppContext = isConsoleAppContext, diff --git a/src/ConsoleAppFramework/RoslynExtensions.cs b/src/ConsoleAppFramework/RoslynExtensions.cs index 682fc136..776ca22f 100644 --- a/src/ConsoleAppFramework/RoslynExtensions.cs +++ b/src/ConsoleAppFramework/RoslynExtensions.cs @@ -29,6 +29,24 @@ public static IEnumerable GetBaseTypes(this INamedTypeSymbol t } } + public static bool EqualsNamespaceAndName(this ITypeSymbol? left, ITypeSymbol? right) + { + if (left == null && right == null) return true; + if (left == null || right == null) return false; + + var l = left.ContainingNamespace; + var r = right.ContainingNamespace; + while (l != null && r != null) + { + if (l.Name != r.Name) return false; + + l = l.ContainingNamespace; + r = r.ContainingNamespace; + } + + return (left.Name == right.Name); + } + public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) { // Hack note: diff --git a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs index d4d2b3ff..b9888c86 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs @@ -157,5 +157,201 @@ static void DoHello(int x, int y) // change signature reasons[1][0].Reasons.Should().Be("Unchanged"); reasons[2][0].Reasons.Should().Be("Modified"); } + + [Fact] + public void Builder() + { + var step1 = """ +using Foo.Bar; + +var app = ConsoleApp.Create(); +app.Add(); +app.Add("", () => { }); +app.UseFilter(); +app.Run(args); + +var l = new List(); +l.Add(10); + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); +} + +namespace Foo.Bar +{ + public class MyFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) + { + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + public class MyFilter2(ConsoleAppFilter next) : ConsoleAppFilter(next) + { + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} +"""; + + var step2 = """ +using Foo.Bar; + +var app = ConsoleApp.Create(); +app.Add(); +app.Add("", () => +{ + Console.WriteLine("body"); // body +}); +app.UseFilter(); +app.Run(args); + +var l = new List(); +l.Add(10); + +Console.WriteLine(""); // unrelated line + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); +} + +namespace Foo.Bar +{ + public class MyFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) + { + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + public class MyFilter2(ConsoleAppFilter next) : ConsoleAppFilter(next) + { + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} +"""; + + var step3 = """ +using Foo.Bar; + +var app = ConsoleApp.Create(); +app.Add(); +app.Add("", (int x, int y) => // different +{ + Console.WriteLine("body"); // body +}); +app.UseFilter(); +app.Run(args); + +var l = new List(); +l.Add(10); + +Console.WriteLine(""); // unrelated line + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); +} + +namespace Foo.Bar +{ + public class MyFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) + { + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + public class MyFilter2(ConsoleAppFilter next) : ConsoleAppFilter(next) + { + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} +"""; + + var step4 = """ +using Foo.Bar; + +var app = ConsoleApp.Create(); +app.Add(); +app.Add("", (int x, int y) => // different +{ + Console.WriteLine("body"); // body +}); +app.Add("foo", () => {}); // newline +app.UseFilter(); +app.Run(args); + +var l = new List(); +l.Add(10); + +Console.WriteLine(""); // unrelated line + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); +} + +namespace Foo.Bar +{ + public class MyFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) + { + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + public class MyFilter2(ConsoleAppFilter next) : ConsoleAppFilter(next) + { + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} +"""; + + var step5 = """ +var l = new List(); +l.Add(10); +"""; + + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder", step1, step2, step3, step4); + } + + [Fact] + public void BuilderOtherType() + { + var step1 = """ +var app = ConsoleApp.Create(); +app.Add("", () => { }); +app.Run(args); +"""; + + var step2 = """ +var l = new List(); +l.Add(10); + +var app = ConsoleApp.Create(); +app.Add("", () => { }); +app.Run(args); +"""; + + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder", step1, step2); + } } From 7b78cc9397b2d9c89699770448220c8cd0eca112 Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 10 Jun 2024 08:50:04 +0900 Subject: [PATCH 05/14] ge --- .../ConsoleAppGenerator.cs | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index e0e35de9..d9255344 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Immutable; using System.Reflection; +using System.Security.Cryptography.X509Certificates; using System.Xml.Linq; namespace ConsoleAppFramework; @@ -604,10 +605,6 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex { if (generatorSyntaxContexts.Length == 0) return; - // var model = generatorSyntaxContexts[0].Model; - - // var wellKnownTypes = new WellKnownTypes(model.Compilation); - // validation, invoke in loop is not allowed. foreach (var item in generatorSyntaxContexts) { @@ -893,6 +890,54 @@ public bool Equals(BuilderContext other) bool EqualsAddClass(BuilderContext other) { + // Add + var genericName = (node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; + var genericType = genericName!.TypeArgumentList.Arguments[0]; + + // Add(string commandPath) + string? commandPath = null; + var args = node.ArgumentList.Arguments; + if (node.ArgumentList.Arguments.Count == 1) + { + var commandName = args[0]; + if (!commandName.Expression.IsKind(SyntaxKind.StringLiteralExpression)) + { + //context.ReportDiagnostic(DiagnosticDescriptors.AddCommandMustBeStringLiteral, commandName.GetLocation()); + //return []; + return false; + } + + commandPath = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; + } + + // T + var type = model.GetTypeInfo(genericType).Type!; + + + // Type:Attributes + // Type:Interface + + // Public Constructor + + // Public Methods + + var publicMethods = type.GetMembers() + .Where(x => x.DeclaredAccessibility == Accessibility.Public) + .OfType() + .Where(x => x.DeclaredAccessibility == Accessibility.Public && !x.IsStatic) + .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Ordinary) + .Where(x => !(x.Name is "Dispose" or "DisposeAsync" or "GetHashCode" or "Equals" or "ToString")) + .ToArray(); + + + + + + + + + + return true; // TODO:final } From 0c34c68ce074169eadddf0bc91277709f42babf9 Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 10 Jun 2024 12:13:55 +0900 Subject: [PATCH 06/14] s --- .../ConsoleAppGenerator.cs | 124 ++++++++++-------- src/ConsoleAppFramework/RoslynExtensions.cs | 20 +++ 2 files changed, 88 insertions(+), 56 deletions(-) diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index d9255344..6a3c16a3 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -817,35 +817,40 @@ public static bool DelegateEquals(InvocationExpressionSyntax node, SemanticModel { if (methodSymbols2.Length > 0 && methodSymbols2[0] is IMethodSymbol methodSymbol2) { - var syntax1 = FunctionSyntax.From(methodSymbol1.DeclaringSyntaxReferences[0].GetSyntax()); - var syntax2 = FunctionSyntax.From(methodSymbol2.DeclaringSyntaxReferences[0].GetSyntax()); - if (syntax1 == null || syntax2 == null) return false; + return MethodSymbolEquals(methodSymbol1, methodSymbol2); + } + } + } - // document comment - if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.GetDocumentationCommentTriviaSyntax()!, syntax2.GetDocumentationCommentTriviaSyntax()!)) - { - return false; - } + return false; + } - // return type - if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.ReturnType, syntax2.ReturnType)) - { - return false; - } + public static bool MethodSymbolEquals(IMethodSymbol methodSymbol1, IMethodSymbol methodSymbol2) + { + var syntax1 = FunctionSyntax.From(methodSymbol1.DeclaringSyntaxReferences[0].GetSyntax()); + var syntax2 = FunctionSyntax.From(methodSymbol2.DeclaringSyntaxReferences[0].GetSyntax()); + if (syntax1 == null || syntax2 == null) return false; - // attributes - if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.AttributeLists, syntax2.AttributeLists)) - { - return false; - } + // document comment + if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.GetDocumentationCommentTriviaSyntax()!, syntax2.GetDocumentationCommentTriviaSyntax()!)) + { + return false; + } - // parameters - return SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.ParameterList, syntax2.ParameterList); - } - } + // return type + if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.ReturnType, syntax2.ReturnType)) + { + return false; } - return false; + // attributes + if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.AttributeLists, syntax2.AttributeLists)) + { + return false; + } + + // parameters + return SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.ParameterList, syntax2.ParameterList); } } @@ -890,29 +895,17 @@ public bool Equals(BuilderContext other) bool EqualsAddClass(BuilderContext other) { - // Add - var genericName = (node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; - var genericType = genericName!.TypeArgumentList.Arguments[0]; + var typeAndPath1 = GetTypeSymbolAndPath(node, model); + var typeAndPath2 = GetTypeSymbolAndPath(other.Node, other.Model); - // Add(string commandPath) - string? commandPath = null; - var args = node.ArgumentList.Arguments; - if (node.ArgumentList.Arguments.Count == 1) - { - var commandName = args[0]; - if (!commandName.Expression.IsKind(SyntaxKind.StringLiteralExpression)) - { - //context.ReportDiagnostic(DiagnosticDescriptors.AddCommandMustBeStringLiteral, commandName.GetLocation()); - //return []; - return false; - } + if (typeAndPath1 == null || typeAndPath2 == null) return false; - commandPath = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; - } + var (type1, path1) = typeAndPath1.Value; + var (type2, path2) = typeAndPath2.Value; - // T - var type = model.GetTypeInfo(genericType).Type!; + if (path1 != path2) return false; + // TODO: // Type:Attributes // Type:Interface @@ -920,25 +913,44 @@ bool EqualsAddClass(BuilderContext other) // Public Constructor // Public Methods + var methods1 = GetMethodSymbols(type1); + var methods2 = GetMethodSymbols(type2); + return methods1.ZipEquals(methods2, RunContext.MethodSymbolEquals); - var publicMethods = type.GetMembers() - .Where(x => x.DeclaredAccessibility == Accessibility.Public) - .OfType() - .Where(x => x.DeclaredAccessibility == Accessibility.Public && !x.IsStatic) - .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Ordinary) - .Where(x => !(x.Name is "Dispose" or "DisposeAsync" or "GetHashCode" or "Equals" or "ToString")) - .ToArray(); - - - - - - + static (ITypeSymbol, string?)? GetTypeSymbolAndPath(InvocationExpressionSyntax node, SemanticModel model) + { + // Add + var genericName = (node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; + var genericType = genericName!.TypeArgumentList.Arguments[0]; + // Add(string commandPath) + string? commandPath = null; + var args = node.ArgumentList.Arguments; + if (node.ArgumentList.Arguments.Count == 1) + { + var commandName = args[0]; + if (!commandName.Expression.IsKind(SyntaxKind.StringLiteralExpression)) + { + return null; + } + commandPath = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; + } + // T + var type = model.GetTypeInfo(genericType).Type!; + return (type, commandPath); + } - return true; // TODO:final + static IEnumerable GetMethodSymbols(ITypeSymbol type) + { + return type.GetMembers() + .Where(x => x.DeclaredAccessibility == Accessibility.Public) + .OfType() + .Where(x => x.DeclaredAccessibility == Accessibility.Public && !x.IsStatic) + .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Ordinary) + .Where(x => !(x.Name is "Dispose" or "DisposeAsync" or "GetHashCode" or "Equals" or "ToString")); + } } bool EqualsUseFilter(BuilderContext other) diff --git a/src/ConsoleAppFramework/RoslynExtensions.cs b/src/ConsoleAppFramework/RoslynExtensions.cs index 776ca22f..481b0898 100644 --- a/src/ConsoleAppFramework/RoslynExtensions.cs +++ b/src/ConsoleAppFramework/RoslynExtensions.cs @@ -47,6 +47,26 @@ public static bool EqualsNamespaceAndName(this ITypeSymbol? left, ITypeSymbol? r return (left.Name == right.Name); } + public static bool ZipEquals(this IEnumerable left, IEnumerable right, Func predicate) + where T : IMethodSymbol + { + using var e1 = left.GetEnumerator(); + using var e2 = right.GetEnumerator(); + while (true) + { + var b1 = e1.MoveNext(); + var b2 = e2.MoveNext(); + + if (b1 != b2) return false; // different sequence length, ng + if (b1 == false) return true; // both false, ok + + if (!predicate(e1.Current, e2.Current)) + { + return false; + } + } + } + public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) { // Hack note: From fb40df44b965ba9a4d0e9babfd462acd4c667079 Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 10 Jun 2024 21:50:03 +0900 Subject: [PATCH 07/14] ??? --- sandbox/GeneratorSandbox/Program.cs | 46 +-- .../ConsoleAppFramework.csproj | 5 +- .../ConsoleAppGenerator.cs | 164 ++++++--- src/ConsoleAppFramework/Emitter.cs | 2 +- src/ConsoleAppFramework/RoslynExtensions.cs | 16 + .../SyntaxNodeTextEqualityComparer.cs | 2 +- .../CSharpGeneratorRunner.cs | 24 +- .../IncrementalGeneratorTest.cs | 317 +++++++++++++++++- 8 files changed, 482 insertions(+), 94 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 8ac1072a..86fc6643 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,39 +1,15 @@ using ConsoleAppFramework; -using Foo.Bar; -using System.ComponentModel.DataAnnotations; var app = ConsoleApp.Create(); -app.Add(); -app.Add("", (int x, int y) => // different -{ - Console.WriteLine("body"); // body -}); -app.Add("foo", () => { }); // newline -app.UseFilter(); -app.Run(args); -Console.WriteLine(""); // unrelated line - -public class Test -{ - public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); -} - -namespace Foo.Bar -{ - public class MyFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) - { - public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - } - - public class MyFilter2(ConsoleAppFilter next) : ConsoleAppFilter(next) - { - public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file +app.Add("foo", () => { }); +app.Add("fooa", () => { }); + +app.Add("choofooaiueo", (int z) => { }); + +app.Add("Y", Task (int x, int y, int z) => { }); + + + + +app.Run(args); diff --git a/src/ConsoleAppFramework/ConsoleAppFramework.csproj b/src/ConsoleAppFramework/ConsoleAppFramework.csproj index f5d50dcd..69c55cab 100644 --- a/src/ConsoleAppFramework/ConsoleAppFramework.csproj +++ b/src/ConsoleAppFramework/ConsoleAppFramework.csproj @@ -14,6 +14,7 @@ true false true + true ConsoleAppFramework @@ -21,7 +22,9 @@ - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index 6a3c16a3..a5467f90 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -3,8 +3,6 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Immutable; using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using System.Xml.Linq; namespace ConsoleAppFramework; @@ -39,7 +37,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return false; }, (context, ct) => new RunContext((InvocationExpressionSyntax)context.Node, context.SemanticModel)) - .WithTrackingName("ConsoleApp.Run.CreateSyntaxProvider"); // annotate for IncrementalGeneratorTest + .WithTrackingName("ConsoleApp.Run.0_CreateSyntaxProvider"); // annotate for IncrementalGeneratorTest context.RegisterSourceOutput(runSource, EmitConsoleAppRun); @@ -47,6 +45,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var builderSource = context.SyntaxProvider .CreateSyntaxProvider((node, ct) => { + ct.ThrowIfCancellationRequested(); if (node.IsKind(SyntaxKind.InvocationExpression)) { var invocationExpression = (node as InvocationExpressionSyntax); @@ -66,16 +65,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }, (context, ct) => new BuilderContext( (InvocationExpressionSyntax)context.Node, ((context.Node as InvocationExpressionSyntax)!.Expression as MemberAccessExpressionSyntax)!.Name.Identifier.Text, - context.SemanticModel)) - .WithTrackingName("ConsoleApp.Builder.CreateSyntaxProvider") + context.SemanticModel, + ct)) + .WithTrackingName("ConsoleApp.Builder.0_CreateSyntaxProvider") .Where(x => { - var model = x.Model.GetTypeInfo((x.Node.Expression as MemberAccessExpressionSyntax)!.Expression); + var model = x.Model.GetTypeInfo((x.Node.Expression as MemberAccessExpressionSyntax)!.Expression, x.CancellationToken); return model.Type?.Name == "ConsoleAppBuilder"; }) - .WithTrackingName("ConsoleApp.Builder.Where") + .WithTrackingName("ConsoleApp.Builder.1_Where") .Collect() - .WithTrackingName("ConsoleApp.Builder.Collect"); + .WithComparer(CollectBuilderContextComparer.Default) + .WithTrackingName("ConsoleApp.Builder.2_Collect"); context.RegisterSourceOutput(builderSource, EmitConsoleAppBuilder); } @@ -783,7 +784,8 @@ public static bool DelegateEquals(InvocationExpressionSyntax node, SemanticModel { // check async, returntype, parameters - var lambda2 = (ParenthesizedLambdaExpressionSyntax)args2[1].Expression; + var lambda2 = args2[1].Expression as ParenthesizedLambdaExpressionSyntax; + if (lambda2 == null) return false; if (!lambda1.AsyncKeyword.IsKind(lambda2.AsyncKeyword.Kind())) return false; @@ -800,8 +802,8 @@ public static bool DelegateEquals(InvocationExpressionSyntax node, SemanticModel ImmutableArray methodSymbols2; if (expression.IsKind(SyntaxKind.AddressOfExpression)) { - var operand1 = (expression as PrefixUnaryExpressionSyntax)!.Operand; - var operand2 = (args2[1].Expression as PrefixUnaryExpressionSyntax)!.Operand; + var operand1 = (expression as PrefixUnaryExpressionSyntax)?.Operand; + var operand2 = (args2[1].Expression as PrefixUnaryExpressionSyntax)?.Operand; if (operand1 == null || operand2 == null) return false; methodSymbols1 = model.GetMemberGroup(operand1); @@ -854,35 +856,62 @@ public static bool MethodSymbolEquals(IMethodSymbol methodSymbol1, IMethodSymbol } } - readonly struct BuilderContext(InvocationExpressionSyntax node, string name, SemanticModel model) : IEquatable + public class CollectBuilderContextComparer : IEqualityComparer> { - public InvocationExpressionSyntax Node => node; - public string Name => name; - public SemanticModel Model => model; + public static CollectBuilderContextComparer Default = new CollectBuilderContextComparer(); - public bool Equals(BuilderContext other) + bool IEqualityComparer>.Equals(ImmutableArray x, ImmutableArray y) + { + if (x.Length != y.Length) return false; + + for (int i = 0; i < x.Length; i++) + { + if (!Equals(x[i], y[i])) return false; + } + + return true; + } + + int IEqualityComparer>.GetHashCode(ImmutableArray obj) + { + return 0; + } + + + static bool Equals(BuilderContext self, BuilderContext other) { - if (this.Name != other.Name) return false; + if (self.Name != other.Name) return false; - var typeInfo = Model.GetTypeInfo((Node.Expression as MemberAccessExpressionSyntax)!.Expression); + var typeInfo = self.Model.GetTypeInfo((self.Node.Expression as MemberAccessExpressionSyntax)!.Expression, self.CancellationToken); if (typeInfo.Type?.Name != "ConsoleAppBuilder") { return false; } - switch (Name) + var typeInfo2 = other.Model.GetTypeInfo((other.Node.Expression as MemberAccessExpressionSyntax)!.Expression, other.CancellationToken); + if (typeInfo2.Type?.Name != "ConsoleAppBuilder") + { + return false; + } + + switch (self.Name) { case "Add": // Add or Add - if ((Node.Expression as MemberAccessExpressionSyntax)?.Name.IsKind(SyntaxKind.GenericName) ?? false) + if ((self.Node.Expression as MemberAccessExpressionSyntax)?.Name.IsKind(SyntaxKind.GenericName) ?? false) { - return EqualsAddClass(other); + return EqualsAddClass(self, other); } else { - return RunContext.DelegateEquals(node, model, (other.Node, other.Model)); + var first = GetFirstStringConstant(self.Node); + var second = GetFirstStringConstant(other.Node); + if (first == null || second == null) return false; + if (first != second) return false; + + return RunContext.DelegateEquals(self.Node, self.Model, (other.Node, other.Model)); } case "UseFilter": - return EqualsUseFilter(other); + return EqualsUseFilter(self, other); case "Run": case "RunAsync": return true; // only check name @@ -893,10 +922,27 @@ public bool Equals(BuilderContext other) return false; } - bool EqualsAddClass(BuilderContext other) + static string? GetFirstStringConstant(InvocationExpressionSyntax invocationExpression) + { + if (invocationExpression.ArgumentList.Arguments.Count != 2) return null; + var commandName = invocationExpression.ArgumentList.Arguments[0]; + + if (!commandName.Expression.IsKind(SyntaxKind.StringLiteralExpression)) + { + return null; + } + + var name = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; + return name; + } + + static bool EqualsAddClass(BuilderContext self, BuilderContext other) { - var typeAndPath1 = GetTypeSymbolAndPath(node, model); - var typeAndPath2 = GetTypeSymbolAndPath(other.Node, other.Model); + var node = self.Node; + var model = self.Model; + + var typeAndPath1 = GetTypeSymbolAndPath(node, model, self.CancellationToken); + var typeAndPath2 = GetTypeSymbolAndPath(other.Node, other.Model, other.CancellationToken); if (typeAndPath1 == null || typeAndPath2 == null) return false; @@ -905,19 +951,44 @@ bool EqualsAddClass(BuilderContext other) if (path1 != path2) return false; - // TODO: + if (type1.DeclaringSyntaxReferences.Length == 0) return false; + if (type2.DeclaringSyntaxReferences.Length == 0) return false; + + var syntax1 = type1.DeclaringSyntaxReferences[0].GetSyntax() as TypeDeclarationSyntax; + var syntax2 = type2.DeclaringSyntaxReferences[0].GetSyntax() as TypeDeclarationSyntax; + + if (syntax1 == null || syntax2 == null) return false; - // Type:Attributes - // Type:Interface + // interface + if (!type1.AllInterfaces.Select(x => x.Name).SequenceEqual(type2.AllInterfaces.Select(x => x.Name))) + { + return false; + } // Public Constructor + var ctor1 = type1.GetMembers().FirstOrDefault(x => (x as IMethodSymbol)?.MethodKind == Microsoft.CodeAnalysis.MethodKind.Constructor); + var ctor2 = type2.GetMembers().FirstOrDefault(x => (x as IMethodSymbol)?.MethodKind == Microsoft.CodeAnalysis.MethodKind.Constructor); + var ctorParameter1 = ctor1?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetParameterListOfConstructor(); + var ctorParameter2 = ctor2?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetParameterListOfConstructor(); + + if (!SyntaxNodeTextEqualityComparer.Default.Equals(ctorParameter1, ctorParameter2)) + { + return false; + } + + // attributes + if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.AttributeLists!, syntax2.AttributeLists!)) + { + return false; + } // Public Methods var methods1 = GetMethodSymbols(type1); var methods2 = GetMethodSymbols(type2); - return methods1.ZipEquals(methods2, RunContext.MethodSymbolEquals); + var methodEquals = methods1.ZipEquals(methods2, RunContext.MethodSymbolEquals); + return methodEquals; - static (ITypeSymbol, string?)? GetTypeSymbolAndPath(InvocationExpressionSyntax node, SemanticModel model) + static (ITypeSymbol, string?)? GetTypeSymbolAndPath(InvocationExpressionSyntax node, SemanticModel model, CancellationToken cancellationToken) { // Add var genericName = (node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; @@ -938,14 +1009,13 @@ bool EqualsAddClass(BuilderContext other) } // T - var type = model.GetTypeInfo(genericType).Type!; + var type = model.GetTypeInfo(genericType, cancellationToken).Type!; return (type, commandPath); } static IEnumerable GetMethodSymbols(ITypeSymbol type) { return type.GetMembers() - .Where(x => x.DeclaredAccessibility == Accessibility.Public) .OfType() .Where(x => x.DeclaredAccessibility == Accessibility.Public && !x.IsStatic) .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Ordinary) @@ -953,25 +1023,35 @@ static IEnumerable GetMethodSymbols(ITypeSymbol type) } } - bool EqualsUseFilter(BuilderContext other) + static bool EqualsUseFilter(BuilderContext self, BuilderContext other) { - var l = GetType(Node, model); - var r = GetType(other.Node, other.Model); + var node = self.Node; + var model = self.Model; + + var l = GetType(node, model, self.CancellationToken); + var r = GetType(other.Node, other.Model, other.CancellationToken); return l.EqualsNamespaceAndName(r); - static ITypeSymbol? GetType(InvocationExpressionSyntax expression, SemanticModel model) + static ITypeSymbol? GetType(InvocationExpressionSyntax expression, SemanticModel model, CancellationToken cancellationToken) { var genericName = (expression.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; var genericType = genericName!.TypeArgumentList.Arguments[0]; - return model.GetTypeInfo(genericType).Type; + return model.GetTypeInfo(genericType, cancellationToken).Type; } } + } - public override int GetHashCode() + readonly struct BuilderContext(InvocationExpressionSyntax node, string name, SemanticModel model, CancellationToken cancellationToken) : IEquatable + { + public InvocationExpressionSyntax Node => node; + public string Name => name; + public SemanticModel Model => model; + public CancellationToken CancellationToken => cancellationToken; + + public bool Equals(BuilderContext other) { - // maybe this does not called so don't care impl. - return SyntaxNodeTextEqualityComparer.Default.GetHashCode(node); + return Node == other.Node; } } -} +} \ No newline at end of file diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index 440a1e37..a4307de8 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -49,7 +49,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy { sb.AppendLine("/// "); var help = CommandHelpBuilder.BuildCommandHelpMessage(commandWithId.Command); - foreach (var line in help.Split([Environment.NewLine], StringSplitOptions.None)) + foreach (var line in help.Split(["\n"], StringSplitOptions.None)) { sb.AppendLine($"/// {line.Replace("<", "<").Replace(">", ">")}
"); } diff --git a/src/ConsoleAppFramework/RoslynExtensions.cs b/src/ConsoleAppFramework/RoslynExtensions.cs index 481b0898..ebc1aebb 100644 --- a/src/ConsoleAppFramework/RoslynExtensions.cs +++ b/src/ConsoleAppFramework/RoslynExtensions.cs @@ -67,6 +67,22 @@ public static bool ZipEquals(this IEnumerable left, IEnumerable right, } } + public static ParameterListSyntax? GetParameterListOfConstructor(this SyntaxNode node) + { + if (node is ConstructorDeclarationSyntax ctor) + { + return ctor.ParameterList; + } + else if (node is ClassDeclarationSyntax primartyCtor) + { + return primartyCtor.ParameterList; + } + else + { + return null; + } + } + public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) { // Hack note: diff --git a/src/ConsoleAppFramework/SyntaxNodeTextEqualityComparer.cs b/src/ConsoleAppFramework/SyntaxNodeTextEqualityComparer.cs index 8560dd9c..d3ffd196 100644 --- a/src/ConsoleAppFramework/SyntaxNodeTextEqualityComparer.cs +++ b/src/ConsoleAppFramework/SyntaxNodeTextEqualityComparer.cs @@ -10,7 +10,7 @@ public class SyntaxNodeTextEqualityComparer : IEqualityComparer { } - public bool Equals(SyntaxNode x, SyntaxNode y) + public bool Equals(SyntaxNode? x, SyntaxNode? y) { if (x == null & y == null) return true; if (x == null || y == null) return false; diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 3c13afac..2c29d0c5 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -108,12 +108,26 @@ [new ConsoleAppGenerator().AsSourceGenerator()], var reasons = generatorResults .Select(x => x.TrackedSteps - .Where(x => x.Key.StartsWith(keyPrefixFilter)) + .Where(x => x.Key.StartsWith(keyPrefixFilter) || x.Key == "SourceOutput") .Select(x => - ( - x.Key, - Reasons: string.Join(", ", x.Value.SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray()) - )) + { + if (x.Key == "SourceOutput") + { + var values = x.Value.Where(x => x.Inputs[0].Source.Name?.StartsWith(keyPrefixFilter) ?? false); + return ( + x.Key, + Reasons: string.Join(", ", values.SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray()) + ); + } + else + { + return ( + Key: x.Key.Substring(keyPrefixFilter.Length), + Reasons: string.Join(", ", x.Value.SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray()) + ); + } + }) + .OrderBy(x => x.Key) .ToArray()) .ToArray(); diff --git a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs index b9888c86..6177aa9a 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Intrinsics.X86; using System.Text; using System.Threading.Tasks; @@ -9,6 +10,18 @@ namespace ConsoleAppFramework.GeneratorTests; public class IncrementalGeneratorTest { + void VerifySourceOutputReasonIsCached((string Key, string Reasons)[] reasons) + { + var reason = reasons.FirstOrDefault(x => x.Key == "SourceOutput").Reasons; + reason.Should().Be("Cached"); + } + + void VerifySourceOutputReasonIsNotCached((string Key, string Reasons)[] reasons) + { + var reason = reasons.FirstOrDefault(x => x.Key == "SourceOutput").Reasons; + reason.Should().NotBe("Cached"); + } + [Fact] public void RunLambda() { @@ -34,11 +47,13 @@ public void RunLambda() Console.WriteLine("foo"); // unrelated line """; - var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Run", step1, step2, step3); + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Run.", step1, step2, step3); reasons[0][0].Reasons.Should().Be("New"); reasons[1][0].Reasons.Should().Be("Unchanged"); reasons[2][0].Reasons.Should().Be("Modified"); + + VerifySourceOutputReasonIsCached(reasons[1]); } [Fact] @@ -98,11 +113,13 @@ public void DoHello(int x, int y) // signature change } """; - var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Run", step1, step2, step3); + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Run.", step1, step2, step3); reasons[0][0].Reasons.Should().Be("New"); reasons[1][0].Reasons.Should().Be("Unchanged"); reasons[2][0].Reasons.Should().Be("Modified"); + + VerifySourceOutputReasonIsCached(reasons[1]); } [Fact] @@ -151,11 +168,13 @@ static void DoHello(int x, int y) // change signature } """; - var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Run", step1, step2, step3); + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Run.", step1, step2, step3); reasons[0][0].Reasons.Should().Be("New"); reasons[1][0].Reasons.Should().Be("Unchanged"); reasons[2][0].Reasons.Should().Be("Modified"); + + VerifySourceOutputReasonIsCached(reasons[1]); } [Fact] @@ -325,12 +344,12 @@ public override Task InvokeAsync(ConsoleAppContext context, CancellationToken ca } """; - var step5 = """ -var l = new List(); -l.Add(10); -"""; + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder.", step1, step2, step3, step4); - var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder", step1, step2, step3, step4); + VerifySourceOutputReasonIsNotCached(reasons[0]); + VerifySourceOutputReasonIsCached(reasons[1]); + VerifySourceOutputReasonIsNotCached(reasons[2]); + VerifySourceOutputReasonIsNotCached(reasons[3]); } [Fact] @@ -351,7 +370,287 @@ public void BuilderOtherType() app.Run(args); """; - var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder", step1, step2); + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder.", step1, step2); + + VerifySourceOutputReasonIsNotCached(reasons[0]); + VerifySourceOutputReasonIsCached(reasons[1]); + } + + [Fact] + public void BuilderClassChange() + { + var step1 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); +} +"""; + + var step2 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value}"); // new parameter +} +"""; + + var step3 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value} aiueo"); // body change + + void PrivateMethod() + { + } +} +"""; + + var step4 = """ +var app = ConsoleApp.Create(); + +var l = new List(); // noise +l.Add(10); + +app.Add(); +app.Run(args); + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value} aiueo"); + + void PrivateMethod() + { + } +} +"""; + + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder.", step1, step2, step3, step4); + + VerifySourceOutputReasonIsNotCached(reasons[0]); + VerifySourceOutputReasonIsNotCached(reasons[1]); + VerifySourceOutputReasonIsCached(reasons[2]); + VerifySourceOutputReasonIsCached(reasons[3]); + } + + [Fact] + public void BuilderClassInterface() + { + var step1 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); +} +"""; + + var step2 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test : IDisposable +{ + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value}"); + + public void Dispose() { } +} +"""; + + var step3 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test : IDisposable, IAsyncDisposable +{ + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value}"); + + public void Dispose() { } + public ValueTask DisposeAsync() { return default; } +} +"""; + + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder.", step1, step2, step3); + + VerifySourceOutputReasonIsNotCached(reasons[0]); + VerifySourceOutputReasonIsNotCached(reasons[1]); + VerifySourceOutputReasonIsNotCached(reasons[2]); + } + + [Fact] + public void ConstructorChange() + { + var step1 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); +} +"""; + + var step2 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test +{ + public Test(IDisposable d) + { + } + + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value}"); +} +"""; + + var step3 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test +{ + public Test(IAsyncDisposable d) + { + } + + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value}"); +} +"""; + + // same as step3 + var step4 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test +{ + public Test(IAsyncDisposable d) + { + } + + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value}"); +} +"""; + + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder.", step1, step2, step3, step4); + + VerifySourceOutputReasonIsNotCached(reasons[0]); + VerifySourceOutputReasonIsNotCached(reasons[1]); + VerifySourceOutputReasonIsNotCached(reasons[2]); + VerifySourceOutputReasonIsCached(reasons[3]); } + + [Fact] + public void PrimaryConstructorChange() + { + var step1 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test +{ + public void Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); } +"""; + var step2 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test(IDisposable d) +{ + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value}"); +} +"""; + + var step3 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test(IAsyncDisposable d) +{ + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value}"); +} +"""; + var step4 = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Test(IAsyncDisposable d) +{ + public void Show(string aaa, [Range(0, 1)] double value, int x2) => ConsoleApp.Log($"{value}"); +} +"""; + + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder.", step1, step2, step3, step4); + + VerifySourceOutputReasonIsNotCached(reasons[0]); + VerifySourceOutputReasonIsNotCached(reasons[1]); + VerifySourceOutputReasonIsNotCached(reasons[2]); + VerifySourceOutputReasonIsCached(reasons[3]); + } + + [Fact] + public void InvalidDefinition() + { + var step1 = """ +using ConsoleAppFramework; + +var app = ConsoleApp.Create(); + +app.Add("foo", () => { }); +app.Add("fooa", () => { }); + + + +app.Add("Y", () => { }); + +app.Run(args); + +"""; + + // add foo before Y + var step2 = """ +using ConsoleAppFramework; + +var app = ConsoleApp.Create(); + +app.Add("foo", () => { }); +app.Add("fooa", () => { }); + + +app.Add("foo", () => { }); + +app.Add("Y", () => { }); + +app.Run(args); + +"""; + + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder.", step1, step2); + + } +} \ No newline at end of file From 77090f9153dbb34cf39b75b0970cc3a708caa0bc Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 11 Jun 2024 02:15:19 +0900 Subject: [PATCH 08/14] done --- ConsoleAppFramework.sln | 19 +++++++++++++++++-- .../CliFrameworkBenchmark.csproj | 1 + .../FilterShareProject.csproj | 1 + .../GeneratorSandbox/GeneratorSandbox.csproj | 2 ++ sandbox/GeneratorSandbox/Program.cs | 6 ------ sandbox/NativeAot/NativeAot.csproj | 1 + .../ConsoleAppFramework.Abstractions.csproj | 1 + .../ConsoleAppFramework.csproj | 5 +++++ .../ConsoleAppGenerator.cs | 3 ++- .../DiagnosticDescriptors.cs | 4 ++++ src/ConsoleAppFramework/Emitter.cs | 4 +++- src/ConsoleAppFramework/RoslynExtensions.cs | 6 ++++++ .../ConsoleAppFramework.GeneratorTests.csproj | 2 ++ .../DiagnosticsTest.cs | 6 +++++- 14 files changed, 50 insertions(+), 11 deletions(-) diff --git a/ConsoleAppFramework.sln b/ConsoleAppFramework.sln index 1c10aef0..7e75f1e7 100644 --- a/ConsoleAppFramework.sln +++ b/ConsoleAppFramework.sln @@ -28,44 +28,59 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework.Generat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeAot", "sandbox\NativeAot\NativeAot.csproj", "{EC1A3299-6597-4AD2-92DE-EDF309875A97}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework.Abstractions", "src\ConsoleAppFramework.Abstractions\ConsoleAppFramework.Abstractions.csproj", "{855B0D28-DC69-470B-B3D9-481EE52737AA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework.Abstractions", "src\ConsoleAppFramework.Abstractions\ConsoleAppFramework.Abstractions.csproj", "{855B0D28-DC69-470B-B3D9-481EE52737AA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FilterShareProject", "sandbox\FilterShareProject\FilterShareProject.csproj", "{2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilterShareProject", "sandbox\FilterShareProject\FilterShareProject.csproj", "{2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + Test|Any CPU = Test|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Debug|Any CPU.Build.0 = Debug|Any CPU {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Release|Any CPU.ActiveCfg = Release|Any CPU {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Release|Any CPU.Build.0 = Release|Any CPU + {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Test|Any CPU.ActiveCfg = Test|Any CPU + {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Test|Any CPU.Build.0 = Test|Any CPU {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Debug|Any CPU.Build.0 = Debug|Any CPU {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Release|Any CPU.Build.0 = Release|Any CPU + {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Test|Any CPU.ActiveCfg = Test|Any CPU + {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Test|Any CPU.Build.0 = Test|Any CPU {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Release|Any CPU.Build.0 = Release|Any CPU + {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Test|Any CPU.ActiveCfg = Test|Any CPU + {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Test|Any CPU.Build.0 = Test|Any CPU {C54F7FE8-650A-4DC7-877F-0DE929351800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C54F7FE8-650A-4DC7-877F-0DE929351800}.Debug|Any CPU.Build.0 = Debug|Any CPU {C54F7FE8-650A-4DC7-877F-0DE929351800}.Release|Any CPU.ActiveCfg = Release|Any CPU {C54F7FE8-650A-4DC7-877F-0DE929351800}.Release|Any CPU.Build.0 = Release|Any CPU + {C54F7FE8-650A-4DC7-877F-0DE929351800}.Test|Any CPU.ActiveCfg = Test|Any CPU + {C54F7FE8-650A-4DC7-877F-0DE929351800}.Test|Any CPU.Build.0 = Test|Any CPU {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Release|Any CPU.Build.0 = Release|Any CPU + {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Test|Any CPU.ActiveCfg = Test|Any CPU + {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Test|Any CPU.Build.0 = Test|Any CPU {855B0D28-DC69-470B-B3D9-481EE52737AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {855B0D28-DC69-470B-B3D9-481EE52737AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {855B0D28-DC69-470B-B3D9-481EE52737AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {855B0D28-DC69-470B-B3D9-481EE52737AA}.Release|Any CPU.Build.0 = Release|Any CPU + {855B0D28-DC69-470B-B3D9-481EE52737AA}.Test|Any CPU.ActiveCfg = Test|Any CPU + {855B0D28-DC69-470B-B3D9-481EE52737AA}.Test|Any CPU.Build.0 = Test|Any CPU {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Release|Any CPU.Build.0 = Release|Any CPU + {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Test|Any CPU.ActiveCfg = Test|Any CPU + {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Test|Any CPU.Build.0 = Test|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj index 5b0c95f7..68e56796 100644 --- a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj +++ b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj @@ -7,6 +7,7 @@ annotations true false + Debug;Release;Test diff --git a/sandbox/FilterShareProject/FilterShareProject.csproj b/sandbox/FilterShareProject/FilterShareProject.csproj index b3a0603b..5a40f424 100644 --- a/sandbox/FilterShareProject/FilterShareProject.csproj +++ b/sandbox/FilterShareProject/FilterShareProject.csproj @@ -5,6 +5,7 @@ enable false enable + Debug;Release;Test diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index d59bb11d..ac65ef5c 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -10,6 +10,8 @@ false USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS + + Debug;Release;Test diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 86fc6643..713d8a8e 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -2,12 +2,6 @@ var app = ConsoleApp.Create(); -app.Add("foo", () => { }); -app.Add("fooa", () => { }); - -app.Add("choofooaiueo", (int z) => { }); - -app.Add("Y", Task (int x, int y, int z) => { }); diff --git a/sandbox/NativeAot/NativeAot.csproj b/sandbox/NativeAot/NativeAot.csproj index d6775be7..979f0cfc 100644 --- a/sandbox/NativeAot/NativeAot.csproj +++ b/sandbox/NativeAot/NativeAot.csproj @@ -9,6 +9,7 @@ true true + Debug;Release;Test diff --git a/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj b/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj index c957e55b..a29046c0 100644 --- a/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj +++ b/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj @@ -8,6 +8,7 @@ ConsoleAppFramework.Abstractions ConsoleAppFramework external abstractions library. + Debug;Release;Test diff --git a/src/ConsoleAppFramework/ConsoleAppFramework.csproj b/src/ConsoleAppFramework/ConsoleAppFramework.csproj index 69c55cab..0f4196bb 100644 --- a/src/ConsoleAppFramework/ConsoleAppFramework.csproj +++ b/src/ConsoleAppFramework/ConsoleAppFramework.csproj @@ -19,8 +19,13 @@ ConsoleAppFramework Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator. + Debug;Release;Test + + $(DefineConstants);TEST + + diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index a5467f90..219386a9 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -668,7 +668,8 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex // validation command name duplicate if (command != null && !names.Add(command.Name)) { - sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.ArgumentList.Arguments[0].GetLocation(), command!.Name); + var location = x.Node.ArgumentList.Arguments[0].GetLocation(); + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, location, command!.Name); return null; } diff --git a/src/ConsoleAppFramework/DiagnosticDescriptors.cs b/src/ConsoleAppFramework/DiagnosticDescriptors.cs index 632b8212..3415074a 100644 --- a/src/ConsoleAppFramework/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework/DiagnosticDescriptors.cs @@ -8,6 +8,10 @@ internal static class DiagnosticDescriptors public static void ReportDiagnostic(this SourceProductionContext context, DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs) { +#if !TEST + // must use location.Clone(), incremental cached code + diagnostic craches visual studio however use Clone() can avoid it. + location = location.Clone(); +#endif var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs); context.ReportDiagnostic(diagnostic); } diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index a4307de8..1e814232 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -49,10 +49,12 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy { sb.AppendLine("/// "); var help = CommandHelpBuilder.BuildCommandHelpMessage(commandWithId.Command); - foreach (var line in help.Split(["\n"], StringSplitOptions.None)) +#pragma warning disable RS1035 + foreach (var line in help.Split([Environment.NewLine], StringSplitOptions.None)) { sb.AppendLine($"/// {line.Replace("<", "<").Replace(">", ">")}
"); } +#pragma warning restore RS1035 sb.AppendLine("///
"); } diff --git a/src/ConsoleAppFramework/RoslynExtensions.cs b/src/ConsoleAppFramework/RoslynExtensions.cs index ebc1aebb..e8722a54 100644 --- a/src/ConsoleAppFramework/RoslynExtensions.cs +++ b/src/ConsoleAppFramework/RoslynExtensions.cs @@ -83,6 +83,12 @@ public static bool ZipEquals(this IEnumerable left, IEnumerable right, } } + public static Location Clone(this Location location) + { + // without inner SyntaxTree + return Location.Create(location.SourceTree?.FilePath ?? "", location.SourceSpan, location.GetLineSpan().Span); + } + public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) { // Hack note: diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj index 6d6be799..71f4591b 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj @@ -7,8 +7,10 @@ false true + Debug;Release;Test + diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index 7d59683a..8a99a497 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -1,4 +1,6 @@ -using Xunit.Abstractions; +#if TEST + +using Xunit.Abstractions; namespace ConsoleAppFramework.GeneratorTests; @@ -435,3 +437,5 @@ public interface IFoo """, "app.Add()"); } } + +#endif \ No newline at end of file From 4e4c9c2d80ac57306df0b77c1ffeb399ba635168 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 11 Jun 2024 13:50:02 +0900 Subject: [PATCH 09/14] work but not work --- ConsoleAppFramework.sln | 15 ------------ .../CliFrameworkBenchmark.csproj | 2 +- .../FilterShareProject.csproj | 2 +- .../GeneratorSandbox/GeneratorSandbox.csproj | 2 +- sandbox/GeneratorSandbox/Program.cs | 12 +++++++--- sandbox/NativeAot/NativeAot.csproj | 2 +- sandbox/NativeAot/Program.cs | 7 +++++- .../ConsoleAppFramework.Abstractions.csproj | 2 +- .../ConsoleAppFramework.csproj | 6 +---- .../DiagnosticDescriptors.cs | 2 -- .../CSharpGeneratorRunner.cs | 16 +++++++++---- .../ConsoleAppFramework.GeneratorTests.csproj | 2 +- .../DiagnosticsTest.cs | 5 +--- .../IncrementalGeneratorTest.cs | 23 +++++++++++++++++++ 14 files changed, 57 insertions(+), 41 deletions(-) diff --git a/ConsoleAppFramework.sln b/ConsoleAppFramework.sln index 7e75f1e7..124388f5 100644 --- a/ConsoleAppFramework.sln +++ b/ConsoleAppFramework.sln @@ -36,51 +36,36 @@ Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU - Test|Any CPU = Test|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Debug|Any CPU.Build.0 = Debug|Any CPU {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Release|Any CPU.ActiveCfg = Release|Any CPU {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Release|Any CPU.Build.0 = Release|Any CPU - {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Test|Any CPU.ActiveCfg = Test|Any CPU - {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Test|Any CPU.Build.0 = Test|Any CPU {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Debug|Any CPU.Build.0 = Debug|Any CPU {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Release|Any CPU.Build.0 = Release|Any CPU - {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Test|Any CPU.ActiveCfg = Test|Any CPU - {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Test|Any CPU.Build.0 = Test|Any CPU {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Release|Any CPU.Build.0 = Release|Any CPU - {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Test|Any CPU.ActiveCfg = Test|Any CPU - {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Test|Any CPU.Build.0 = Test|Any CPU {C54F7FE8-650A-4DC7-877F-0DE929351800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C54F7FE8-650A-4DC7-877F-0DE929351800}.Debug|Any CPU.Build.0 = Debug|Any CPU {C54F7FE8-650A-4DC7-877F-0DE929351800}.Release|Any CPU.ActiveCfg = Release|Any CPU {C54F7FE8-650A-4DC7-877F-0DE929351800}.Release|Any CPU.Build.0 = Release|Any CPU - {C54F7FE8-650A-4DC7-877F-0DE929351800}.Test|Any CPU.ActiveCfg = Test|Any CPU - {C54F7FE8-650A-4DC7-877F-0DE929351800}.Test|Any CPU.Build.0 = Test|Any CPU {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Release|Any CPU.Build.0 = Release|Any CPU - {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Test|Any CPU.ActiveCfg = Test|Any CPU - {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Test|Any CPU.Build.0 = Test|Any CPU {855B0D28-DC69-470B-B3D9-481EE52737AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {855B0D28-DC69-470B-B3D9-481EE52737AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {855B0D28-DC69-470B-B3D9-481EE52737AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {855B0D28-DC69-470B-B3D9-481EE52737AA}.Release|Any CPU.Build.0 = Release|Any CPU - {855B0D28-DC69-470B-B3D9-481EE52737AA}.Test|Any CPU.ActiveCfg = Test|Any CPU - {855B0D28-DC69-470B-B3D9-481EE52737AA}.Test|Any CPU.Build.0 = Test|Any CPU {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Release|Any CPU.Build.0 = Release|Any CPU - {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Test|Any CPU.ActiveCfg = Test|Any CPU - {2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025}.Test|Any CPU.Build.0 = Test|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj index 68e56796..184a2be9 100644 --- a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj +++ b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj @@ -7,7 +7,7 @@ annotations true false - Debug;Release;Test + Debug;Release diff --git a/sandbox/FilterShareProject/FilterShareProject.csproj b/sandbox/FilterShareProject/FilterShareProject.csproj index 5a40f424..181c71e4 100644 --- a/sandbox/FilterShareProject/FilterShareProject.csproj +++ b/sandbox/FilterShareProject/FilterShareProject.csproj @@ -5,7 +5,7 @@ enable false enable - Debug;Release;Test + Debug;Release diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index ac65ef5c..7a583a97 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -11,7 +11,7 @@ USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS - Debug;Release;Test + Debug;Release diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 713d8a8e..0dc35f83 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -2,8 +2,14 @@ var app = ConsoleApp.Create(); - - - +app.Add("aaa", () => +{ +}); + +app.Add("aaa", async Task () => +{ + await Task.Yield(); + return default!; +}); app.Run(args); diff --git a/sandbox/NativeAot/NativeAot.csproj b/sandbox/NativeAot/NativeAot.csproj index 979f0cfc..ba6a3b0a 100644 --- a/sandbox/NativeAot/NativeAot.csproj +++ b/sandbox/NativeAot/NativeAot.csproj @@ -9,7 +9,7 @@ true true - Debug;Release;Test + Debug;Release diff --git a/sandbox/NativeAot/Program.cs b/sandbox/NativeAot/Program.cs index a37522ff..ed5e48e7 100644 --- a/sandbox/NativeAot/Program.cs +++ b/sandbox/NativeAot/Program.cs @@ -1,3 +1,8 @@ using ConsoleAppFramework; -ConsoleApp.Run(args, (int x, int y) => Console.WriteLine(x + y)); \ No newline at end of file + +var app = ConsoleApp.Create(); + +//ConsoleApp.Run(args, (int x, int y) => Console.WriteLine(x + y)); + +app.Run(args); \ No newline at end of file diff --git a/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj b/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj index a29046c0..80cabfef 100644 --- a/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj +++ b/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj @@ -8,7 +8,7 @@ ConsoleAppFramework.Abstractions ConsoleAppFramework external abstractions library. - Debug;Release;Test + Debug;Release diff --git a/src/ConsoleAppFramework/ConsoleAppFramework.csproj b/src/ConsoleAppFramework/ConsoleAppFramework.csproj index 0f4196bb..1be4fdea 100644 --- a/src/ConsoleAppFramework/ConsoleAppFramework.csproj +++ b/src/ConsoleAppFramework/ConsoleAppFramework.csproj @@ -19,11 +19,7 @@ ConsoleAppFramework Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator. - Debug;Release;Test - - - - $(DefineConstants);TEST + Debug;Release diff --git a/src/ConsoleAppFramework/DiagnosticDescriptors.cs b/src/ConsoleAppFramework/DiagnosticDescriptors.cs index 3415074a..1692e067 100644 --- a/src/ConsoleAppFramework/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework/DiagnosticDescriptors.cs @@ -8,10 +8,8 @@ internal static class DiagnosticDescriptors public static void ReportDiagnostic(this SourceProductionContext context, DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs) { -#if !TEST // must use location.Clone(), incremental cached code + diagnostic craches visual studio however use Clone() can avoid it. location = location.Clone(); -#endif var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs); context.ReportDiagnostic(diagnostic); } diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 2c29d0c5..d035d078 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -28,7 +28,7 @@ public static void InitializeCompilation() var compilation = CSharpCompilation.Create("generatortest", references: references, - syntaxTrees: [CSharpSyntaxTree.ParseText(globalUsings)], + syntaxTrees: [CSharpSyntaxTree.ParseText(globalUsings, path: "GlobalUsings.cs")], options: new CSharpCompilationOptions(OutputKind.ConsoleApplication)); // .exe baseCompilation = compilation; @@ -166,7 +166,8 @@ public void Verify(int id, string code, string diagnosticsCodeSpan, [CallerArgum diagnostics.Length.Should().Be(1); diagnostics[0].Id.Should().Be(idPrefix + id.ToString("000")); - var text = GetLocationText(diagnostics[0]); + + var text = GetLocationText(diagnostics[0], compilation.SyntaxTrees); text.Should().Be(diagnosticsCodeSpan); } @@ -176,7 +177,7 @@ public void Verify(int id, string code, string diagnosticsCodeSpan, [CallerArgum var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); OutputGeneratedCode(compilation); - return diagnostics.Select(x => (x.Id, GetLocationText(x))).ToArray(); + return diagnostics.Select(x => (x.Id, GetLocationText(x, compilation.SyntaxTrees))).ToArray(); } // Execute and check stdout result @@ -209,14 +210,19 @@ public string Error(string code, string args, [CallerArgumentExpression("code")] return stdout; } - string GetLocationText(Diagnostic diagnostic) + string GetLocationText(Diagnostic diagnostic, IEnumerable syntaxTrees) { var location = diagnostic.Location; + var textSpan = location.SourceSpan; var sourceTree = location.SourceTree; if (sourceTree == null) { - return ""; + var lineSpan = location.GetLineSpan(); + if (lineSpan.Path == null) return ""; + + sourceTree = syntaxTrees.FirstOrDefault(x => x.FilePath == lineSpan.Path); + if (sourceTree == null) return ""; } var text = sourceTree.GetText().GetSubText(textSpan).ToString(); diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj index 71f4591b..88f4f81e 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj @@ -7,7 +7,7 @@ false true - Debug;Release;Test + Debug;Release diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index 8a99a497..d3eb0c30 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -1,6 +1,4 @@ -#if TEST - -using Xunit.Abstractions; +using Xunit.Abstractions; namespace ConsoleAppFramework.GeneratorTests; @@ -438,4 +436,3 @@ public interface IFoo } } -#endif \ No newline at end of file diff --git a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs index 6177aa9a..b9e4bdba 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs @@ -653,4 +653,27 @@ public void InvalidDefinition() var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder.", step1, step2); } + + [Fact] + public void IncrDual() + { + var step1 = """ +using ConsoleAppFramework; + +var app = ConsoleApp.Create(); +app.Add("aaa", () =>{ }); +app.Run(args); +"""; + + var step2 = """ +using ConsoleAppFramework; + +var app = ConsoleApp.Create(); +app.Add("aaa", () =>{ }); +app.Add("aaa", () =>{ }); +app.Run(args); +"""; + + var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder.", step1, step2); + } } \ No newline at end of file From 17aa9841dfe69d862c1ee972c6b1405c1caed750 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 11 Jun 2024 14:56:27 +0900 Subject: [PATCH 10/14] ready reporter --- .../ConsoleAppGenerator.cs | 15 ++++++++-- .../DiagnosticDescriptors.cs | 30 +++++++++++++++++-- src/ConsoleAppFramework/Parser.cs | 2 +- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index 219386a9..55db0fed 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -568,10 +568,12 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, R var wellKnownTypes = new WellKnownTypes(model.Compilation); - var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes, DelegateBuildType.MakeDelegateWhenHasDefaultValue, []); + var reporter = new DiagnosticReporter(); + var parser = new Parser(reporter, node, model, wellKnownTypes, DelegateBuildType.MakeDelegateWhenHasDefaultValue, []); var command = parser.ParseAndValidateForRun(); if (command == null) { + reporter.ReportToContext(sourceProductionContext); return; } if (command.HasFilter) @@ -657,12 +659,13 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex return; } + var reporter = new DiagnosticReporter(); var names = new HashSet(); var commands1 = methodGroup["Add"] .Select(x => { var wellKnownTypes = new WellKnownTypes(x.Model.Compilation); - var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.OnlyActionFunc, globalFilters); + var parser = new Parser(reporter, x.Node, x.Model, wellKnownTypes, DelegateBuildType.OnlyActionFunc, globalFilters); var command = parser.ParseAndValidateForBuilderDelegateRegistration(); // validation command name duplicate @@ -681,7 +684,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex .SelectMany(x => { var wellKnownTypes = new WellKnownTypes(x.Model.Compilation); - var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.None, globalFilters); + var parser = new Parser(reporter, x.Node, x.Model, wellKnownTypes, DelegateBuildType.None, globalFilters); var commands = parser.ParseAndValidateForBuilderClassRegistration(); // validation command name duplicate @@ -699,6 +702,12 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var commands = commands1.Concat(commands2).ToArray(); + if (reporter.HasDiagnostics) + { + reporter.ReportToContext(sourceProductionContext); + return; + } + // don't emit if exists failure(already reported error) if (commands.Any(x => x == null)) { diff --git a/src/ConsoleAppFramework/DiagnosticDescriptors.cs b/src/ConsoleAppFramework/DiagnosticDescriptors.cs index 1692e067..2d4c2fe6 100644 --- a/src/ConsoleAppFramework/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework/DiagnosticDescriptors.cs @@ -2,14 +2,40 @@ namespace ConsoleAppFramework; +internal sealed class DiagnosticReporter +{ + List? diagnostics; + + public bool HasDiagnostics => diagnostics != null && diagnostics.Count != 0; + + public void ReportDiagnostic(DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs) + { + var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs); + if (diagnostics == null) + { + diagnostics = new(); + } + diagnostics.Add(diagnostic); + } + + public void ReportToContext(SourceProductionContext context) + { + if (diagnostics != null) + { + foreach (var item in diagnostics) + { + context.ReportDiagnostic(item); + } + } + } +} + internal static class DiagnosticDescriptors { const string Category = "GenerateConsoleAppFramework"; public static void ReportDiagnostic(this SourceProductionContext context, DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs) { - // must use location.Clone(), incremental cached code + diagnostic craches visual studio however use Clone() can avoid it. - location = location.Clone(); var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs); context.ReportDiagnostic(diagnostic); } diff --git a/src/ConsoleAppFramework/Parser.cs b/src/ConsoleAppFramework/Parser.cs index fe6d0f03..c96ed890 100644 --- a/src/ConsoleAppFramework/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -4,7 +4,7 @@ namespace ConsoleAppFramework; -internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes, DelegateBuildType delegateBuildType, FilterInfo[] globalFilters) +internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes, DelegateBuildType delegateBuildType, FilterInfo[] globalFilters) { public Command? ParseAndValidateForRun() // for ConsoleApp.Run, lambda or method or &method { From 34ef3e3f589cbe3205751f5129df0d782309bab0 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 11 Jun 2024 15:52:04 +0900 Subject: [PATCH 11/14] readying equatable command --- src/ConsoleAppFramework/Command.cs | 23 +++----- .../ConsoleAppGenerator.cs | 17 ++++++ src/ConsoleAppFramework/Emitter.cs | 2 +- src/ConsoleAppFramework/EquatableArray.cs | 58 +++++++++++++++++++ .../EquatableTypeSymbol.cs | 24 ++++++++ src/ConsoleAppFramework/Parser.cs | 8 +-- src/ConsoleAppFramework/SourceBuilder.cs | 2 +- 7 files changed, 114 insertions(+), 20 deletions(-) create mode 100644 src/ConsoleAppFramework/EquatableArray.cs create mode 100644 src/ConsoleAppFramework/EquatableTypeSymbol.cs diff --git a/src/ConsoleAppFramework/Command.cs b/src/ConsoleAppFramework/Command.cs index 4d66862f..5d0262af 100644 --- a/src/ConsoleAppFramework/Command.cs +++ b/src/ConsoleAppFramework/Command.cs @@ -1,9 +1,4 @@ using Microsoft.CodeAnalysis; -using System; -using System.Data.Common; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Reflection.Metadata; using System.Text; namespace ConsoleAppFramework; @@ -28,12 +23,12 @@ public record class Command public bool IsRootCommand => Name == ""; public required string Name { get; init; } - public required CommandParameter[] Parameters { get; init; } + public required EquatableArray Parameters { get; init; } public required string Description { get; init; } public required MethodKind MethodKind { get; init; } public required DelegateBuildType DelegateBuildType { get; init; } public CommandMethodInfo? CommandMethodInfo { get; set; } // can set...! - public required FilterInfo[] Filters { get; init; } + public required EquatableArray Filters { get; init; } public bool HasFilter => Filters.Length != 0; public string? BuildDelegateSignature(out string? delegateType) @@ -148,7 +143,7 @@ public string BuildDelegateType(string delegateName) public record class CommandParameter { - public required ITypeSymbol Type { get; init; } + public required EquatableTypeSymbol Type { get; init; } public required Location Location { get; init; } public required WellKnownTypes WellKnownTypes { get; init; } public required bool IsNullableReference { get; init; } @@ -157,7 +152,7 @@ public record class CommandParameter public required string OriginalParameterName { get; init; } public required bool HasDefaultValue { get; init; } public object? DefaultValue { get; init; } - public required ITypeSymbol? CustomParserType { get; init; } + public required EquatableTypeSymbol? CustomParserType { get; init; } public required bool IsFromServices { get; init; } public required bool IsConsoleAppContext { get; init; } public required bool IsCancellationToken { get; init; } @@ -166,7 +161,7 @@ public record class CommandParameter public required bool HasValidation { get; init; } public required int ArgumentIndex { get; init; } // -1 is not Argument, other than marked as [Argument] public bool IsArgument => ArgumentIndex != -1; - public required string[] Aliases { get; init; } + public required EquatableArray Aliases { get; init; } public required string Description { get; init; } public bool RequireCheckArgumentParsed => !(HasDefaultValue || IsParams || IsFlag); @@ -174,7 +169,7 @@ public record class CommandParameter public string BuildParseMethod(int argCount, string argumentName, bool increment) { var incrementIndex = increment ? "!TryIncrementIndex(ref i, args.Length) || " : ""; - return Core(Type, false); + return Core(Type.TypeSymbol, false); string Core(ITypeSymbol type, bool nullable) { @@ -360,7 +355,7 @@ public record class CommandMethodInfo { public required string TypeFullName { get; init; } public required string MethodName { get; init; } - public required ITypeSymbol[] ConstructorParameterTypes { get; init; } + public required EquatableArray ConstructorParameterTypes { get; init; } public required bool IsIDisposable { get; init; } public required bool IsIAsyncDisposable { get; init; } @@ -379,7 +374,7 @@ public string BuildNew() public record class FilterInfo { public required string TypeFullName { get; init; } - public required ITypeSymbol[] ConstructorParameterTypes { get; init; } + public required EquatableArray ConstructorParameterTypes { get; init; } FilterInfo() { @@ -401,7 +396,7 @@ public record class FilterInfo var filter = new FilterInfo { TypeFullName = type.ToFullyQualifiedFormatDisplayString(), - ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => x.Type).ToArray() + ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => new EquatableTypeSymbol(x.Type)).ToArray() }; return filter; diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index 55db0fed..c53eff39 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -756,6 +756,23 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex sourceProductionContext.AddSource("ConsoleApp.Builder.Help.g.cs", help.ToString()); } + class CommandContext(Command command, bool isAsync, DiagnosticReporter diagnosticReporter) : IEquatable + { + public Command Command => command; + public DiagnosticReporter DiagnosticReporter => diagnosticReporter; + public bool IsAsync => isAsync; + + public bool Equals(CommandContext other) + { + // has diagnostics, always go to modified(don't cache) + if (diagnosticReporter.HasDiagnostics || other.DiagnosticReporter.HasDiagnostics) return false; + + if (isAsync != other.IsAsync) return false; + + return command.Equals(other.Command); + } + } + readonly struct RunContext(InvocationExpressionSyntax node, SemanticModel model) : IEquatable { public InvocationExpressionSyntax Node => node; diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index 1e814232..e01294c6 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -109,7 +109,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy sb.AppendLine($"var arg{i} = ({type})ServiceProvider!.GetService(typeof({type}))!;"); } } - sb.AppendLineIfExists(command.Parameters); + sb.AppendLineIfExists(command.Parameters.AsSpan()); using (command.HasFilter ? sb.Nop : sb.BeginBlock("try")) { diff --git a/src/ConsoleAppFramework/EquatableArray.cs b/src/ConsoleAppFramework/EquatableArray.cs new file mode 100644 index 00000000..c6975da9 --- /dev/null +++ b/src/ConsoleAppFramework/EquatableArray.cs @@ -0,0 +1,58 @@ +using System.Collections; +using System.Runtime.CompilerServices; + +namespace ConsoleAppFramework; + +public readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + readonly T[]? array; + + public EquatableArray() // for collection literal [] + { + array = []; + } + + public EquatableArray(T[] array) + { + this.array = array; + } + + public static implicit operator EquatableArray(T[] array) + { + return new EquatableArray(array); + } + + public ref readonly T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref array![index]; + } + + public int Length => array!.Length; + + public ReadOnlySpan AsSpan() + { + return array.AsSpan(); + } + + public ReadOnlySpan.Enumerator GetEnumerator() + { + return AsSpan().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return array.AsEnumerable().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return array.AsEnumerable().GetEnumerator(); + } + + public bool Equals(EquatableArray other) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } +} diff --git a/src/ConsoleAppFramework/EquatableTypeSymbol.cs b/src/ConsoleAppFramework/EquatableTypeSymbol.cs new file mode 100644 index 00000000..db4eadda --- /dev/null +++ b/src/ConsoleAppFramework/EquatableTypeSymbol.cs @@ -0,0 +1,24 @@ +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; + +namespace ConsoleAppFramework; + +public class EquatableTypeSymbol(ITypeSymbol typeSymbol) : IEquatable +{ + // check this two types usage for Equality + public ITypeSymbol TypeSymbol => typeSymbol; + public ImmutableArray GetMembers() => typeSymbol.GetMembers(); + + public TypeKind TypeKind { get; } = typeSymbol.TypeKind; + public SpecialType SpecialType { get; } = typeSymbol.SpecialType; + + + public string ToFullyQualifiedFormatDisplayString() => typeSymbol.ToFullyQualifiedFormatDisplayString(); + public string ToDisplayString(NullableFlowState state, SymbolDisplayFormat format) => typeSymbol.ToDisplayString(state, format); + + public bool Equals(EquatableTypeSymbol other) + { + // TODO: + return false; + } +} diff --git a/src/ConsoleAppFramework/Parser.cs b/src/ConsoleAppFramework/Parser.cs index c96ed890..019bf15f 100644 --- a/src/ConsoleAppFramework/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -133,7 +133,7 @@ internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax nod TypeFullName = type.ToFullyQualifiedFormatDisplayString(), IsIDisposable = hasIDisposable, IsIAsyncDisposable = hasIAsyncDisposable, - ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => x.Type).ToArray(), + ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => new EquatableTypeSymbol(x.Type)).ToArray(), MethodName = "", // without methodname }; @@ -348,11 +348,11 @@ internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax nod IsNullableReference = isNullableReference, IsConsoleAppContext = isConsoleAppContext, IsParams = hasParams, - Type = type.Type!, + Type = new EquatableTypeSymbol(type.Type!), Location = x.GetLocation(), HasDefaultValue = hasDefault, DefaultValue = defaultValue, - CustomParserType = customParserType, + CustomParserType = customParserType == null ? null : new EquatableTypeSymbol(customParserType), HasValidation = hasValidation, IsCancellationToken = isCancellationToken, IsFromServices = isFromServices, @@ -505,7 +505,7 @@ internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax nod IsConsoleAppContext = isConsoleAppContext, IsParams = x.IsParams, Location = x.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), - Type = x.Type, + Type = new EquatableTypeSymbol(x.Type), HasDefaultValue = x.HasExplicitDefaultValue, DefaultValue = x.HasExplicitDefaultValue ? x.ExplicitDefaultValue : null, CustomParserType = null, diff --git a/src/ConsoleAppFramework/SourceBuilder.cs b/src/ConsoleAppFramework/SourceBuilder.cs index d62e3181..b16db5b6 100644 --- a/src/ConsoleAppFramework/SourceBuilder.cs +++ b/src/ConsoleAppFramework/SourceBuilder.cs @@ -54,7 +54,7 @@ public void AppendLine() builder.AppendLine(); } - public void AppendLineIfExists(T[] values) + public void AppendLineIfExists(ReadOnlySpan values) { if (values.Length != 0) { From adca5c6929c20136165b974975df65bf26722102 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 11 Jun 2024 16:21:10 +0900 Subject: [PATCH 12/14] Run to new style --- src/ConsoleAppFramework/Command.cs | 17 +++----- .../ConsoleAppGenerator.cs | 42 ++++++++++--------- .../EquatableTypeSymbol.cs | 12 ++++-- src/ConsoleAppFramework/IgnoreEquality.cs | 22 ++++++++++ .../IncrementalGeneratorTest.cs | 1 + 5 files changed, 59 insertions(+), 35 deletions(-) create mode 100644 src/ConsoleAppFramework/IgnoreEquality.cs diff --git a/src/ConsoleAppFramework/Command.cs b/src/ConsoleAppFramework/Command.cs index 5d0262af..8eaed1da 100644 --- a/src/ConsoleAppFramework/Command.cs +++ b/src/ConsoleAppFramework/Command.cs @@ -144,8 +144,8 @@ public string BuildDelegateType(string delegateName) public record class CommandParameter { public required EquatableTypeSymbol Type { get; init; } - public required Location Location { get; init; } - public required WellKnownTypes WellKnownTypes { get; init; } + public required IgnoreEquality Location { get; init; } + public required IgnoreEquality WellKnownTypes { get; init; } public required bool IsNullableReference { get; init; } public required bool IsParams { get; init; } public required string Name { get; init; } @@ -238,7 +238,7 @@ string Core(ITypeSymbol type, bool nullable) if (type.TypeKind == TypeKind.Array) { var elementType = (type as IArrayTypeSymbol)!.ElementType; - var parsable = WellKnownTypes.ISpanParsable; + var parsable = WellKnownTypes.Value.ISpanParsable; if (parsable != null) // has parsable { if (elementType.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable))) @@ -250,12 +250,12 @@ string Core(ITypeSymbol type, bool nullable) } // System.DateTimeOffset, System.Guid, System.Version - tryParseKnownPrimitive = WellKnownTypes.HasTryParse(type); + tryParseKnownPrimitive = WellKnownTypes.Value.HasTryParse(type); if (!tryParseKnownPrimitive) { // ISpanParsable (BigInteger, Complex, Half, Int128, etc...) - var parsable = WellKnownTypes.ISpanParsable; + var parsable = WellKnownTypes.Value.ISpanParsable; if (parsable != null) // has parsable { tryParseIParsable = type.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable)); @@ -312,13 +312,6 @@ public string DefaultValueToString(bool castValue = true, bool enumIncludeTypeNa return $"({Type.ToFullyQualifiedFormatDisplayString()}){DefaultValue}"; } - public string? GetEnumSymbolName(object value) - { - var symbol = Type.GetMembers().OfType().FirstOrDefault(x => x.ConstantValue == value); - if (symbol == null) return ""; - return symbol.Name; - } - public string ToTypeDisplayString() { var t = Type.ToFullyQualifiedFormatDisplayString(); diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index c53eff39..21e8cadf 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -36,7 +36,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } return false; - }, (context, ct) => new RunContext((InvocationExpressionSyntax)context.Node, context.SemanticModel)) + }, (context, ct) => + { + var reporter = new DiagnosticReporter(); + var node = (InvocationExpressionSyntax)context.Node; + var wellknownTypes = new WellKnownTypes(context.SemanticModel.Compilation); + var parser = new Parser(reporter, node, context.SemanticModel, wellknownTypes, DelegateBuildType.MakeDelegateWhenHasDefaultValue, []); + var isRunAsync = (node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"; + + var command = parser.ParseAndValidateForRun(); + return new CommandContext(command, isRunAsync, reporter, node); + }) .WithTrackingName("ConsoleApp.Run.0_CreateSyntaxProvider"); // annotate for IncrementalGeneratorTest context.RegisterSourceOutput(runSource, EmitConsoleAppRun); @@ -561,36 +571,29 @@ namespace ConsoleAppFramework; """; - static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, RunContext runNode) + static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, CommandContext commandContext) { - var node = runNode.Node; - var model = runNode.SemanticModel; - - var wellKnownTypes = new WellKnownTypes(model.Compilation); - - var reporter = new DiagnosticReporter(); - var parser = new Parser(reporter, node, model, wellKnownTypes, DelegateBuildType.MakeDelegateWhenHasDefaultValue, []); - var command = parser.ParseAndValidateForRun(); - if (command == null) + if (commandContext.DiagnosticReporter.HasDiagnostics) { - reporter.ReportToContext(sourceProductionContext); + commandContext.DiagnosticReporter.ReportToContext(sourceProductionContext); return; } + var command = commandContext.Command; + if (command == null) return; + if (command.HasFilter) { - sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.CommandHasFilter, node.GetLocation()); + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.CommandHasFilter, commandContext.Node.GetLocation()); return; } - var isRunAsync = ((node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"); - var sb = new SourceBuilder(0); sb.AppendLine(GeneratedCodeHeader); using (sb.BeginBlock("internal static partial class ConsoleApp")) { var emitter = new Emitter(); var withId = new Emitter.CommandWithId(null, command, -1); - emitter.EmitRun(sb, withId, isRunAsync); + emitter.EmitRun(sb, withId, command.IsAsync); } sourceProductionContext.AddSource("ConsoleApp.Run.g.cs", sb.ToString()); @@ -756,19 +759,20 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex sourceProductionContext.AddSource("ConsoleApp.Builder.Help.g.cs", help.ToString()); } - class CommandContext(Command command, bool isAsync, DiagnosticReporter diagnosticReporter) : IEquatable + class CommandContext(Command? command, bool isAsync, DiagnosticReporter diagnosticReporter, InvocationExpressionSyntax node) : IEquatable { - public Command Command => command; + public Command? Command => command; public DiagnosticReporter DiagnosticReporter => diagnosticReporter; + public InvocationExpressionSyntax Node => node; public bool IsAsync => isAsync; public bool Equals(CommandContext other) { // has diagnostics, always go to modified(don't cache) if (diagnosticReporter.HasDiagnostics || other.DiagnosticReporter.HasDiagnostics) return false; + if (command == null || other.Command == null) return false; // maybe has diagnostics if (isAsync != other.IsAsync) return false; - return command.Equals(other.Command); } } diff --git a/src/ConsoleAppFramework/EquatableTypeSymbol.cs b/src/ConsoleAppFramework/EquatableTypeSymbol.cs index db4eadda..b9db7ad4 100644 --- a/src/ConsoleAppFramework/EquatableTypeSymbol.cs +++ b/src/ConsoleAppFramework/EquatableTypeSymbol.cs @@ -5,20 +5,24 @@ namespace ConsoleAppFramework; public class EquatableTypeSymbol(ITypeSymbol typeSymbol) : IEquatable { - // check this two types usage for Equality + // Used for build argument parser, maybe ok to equals name. public ITypeSymbol TypeSymbol => typeSymbol; + + // GetMembers is called for Enum and fields is not condition for command equality. public ImmutableArray GetMembers() => typeSymbol.GetMembers(); public TypeKind TypeKind { get; } = typeSymbol.TypeKind; public SpecialType SpecialType { get; } = typeSymbol.SpecialType; - public string ToFullyQualifiedFormatDisplayString() => typeSymbol.ToFullyQualifiedFormatDisplayString(); public string ToDisplayString(NullableFlowState state, SymbolDisplayFormat format) => typeSymbol.ToDisplayString(state, format); public bool Equals(EquatableTypeSymbol other) { - // TODO: - return false; + if (this.TypeKind != other.TypeKind) return false; + if (this.SpecialType != other.SpecialType) return false; + if (this.TypeSymbol.Name != other.TypeSymbol.Name) return false; + + return this.TypeSymbol.EqualsNamespaceAndName(other.TypeSymbol); } } diff --git a/src/ConsoleAppFramework/IgnoreEquality.cs b/src/ConsoleAppFramework/IgnoreEquality.cs new file mode 100644 index 00000000..72942880 --- /dev/null +++ b/src/ConsoleAppFramework/IgnoreEquality.cs @@ -0,0 +1,22 @@ +namespace ConsoleAppFramework; + +public readonly struct IgnoreEquality(T value) : IEquatable> +{ + public readonly T Value => value; + + public static implicit operator IgnoreEquality(T value) + { + return new IgnoreEquality(value); + } + + public static implicit operator T(IgnoreEquality value) + { + return value.Value; + } + + public bool Equals(IgnoreEquality other) + { + // always true to ignore equality check. + return true; + } +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs index b9e4bdba..c9b4a26f 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs @@ -54,6 +54,7 @@ public void RunLambda() reasons[2][0].Reasons.Should().Be("Modified"); VerifySourceOutputReasonIsCached(reasons[1]); + VerifySourceOutputReasonIsNotCached(reasons[2]); } [Fact] From 9d14ac8d567d663e77a34fce35756abe22ef27b5 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 11 Jun 2024 16:23:46 +0900 Subject: [PATCH 13/14] done --- src/ConsoleAppFramework/EquatableArray.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConsoleAppFramework/EquatableArray.cs b/src/ConsoleAppFramework/EquatableArray.cs index c6975da9..c9bd85b8 100644 --- a/src/ConsoleAppFramework/EquatableArray.cs +++ b/src/ConsoleAppFramework/EquatableArray.cs @@ -53,6 +53,6 @@ IEnumerator IEnumerable.GetEnumerator() public bool Equals(EquatableArray other) { - return AsSpan().SequenceEqual(array.AsSpan()); + return AsSpan().SequenceEqual(other.AsSpan()); } } From addfba1f9227917e379c2bbaac1707fa53d0662e Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 12 Jun 2024 09:58:26 +0900 Subject: [PATCH 14/14] done true incremental --- sandbox/GeneratorSandbox/Program.cs | 8 +- .../ConsoleAppGenerator.cs | 468 ++++-------------- 2 files changed, 105 insertions(+), 371 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 0dc35f83..2e43516d 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -6,10 +6,12 @@ { }); -app.Add("aaa", async Task () => + + + +app.Add("aabcdefg", int (int x, string y) => { - await Task.Yield(); - return default!; + return default!; }); app.Run(args); diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index 21e8cadf..3b07d130 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -45,7 +45,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var isRunAsync = (node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"; var command = parser.ParseAndValidateForRun(); - return new CommandContext(command, isRunAsync, reporter, node); + return new CommanContext(command, isRunAsync, reporter, node); }) .WithTrackingName("ConsoleApp.Run.0_CreateSyntaxProvider"); // annotate for IncrementalGeneratorTest @@ -72,7 +72,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } return false; - }, (context, ct) => new BuilderContext( + }, (context, ct) => new BuilderContext( // no equality check (InvocationExpressionSyntax)context.Node, ((context.Node as InvocationExpressionSyntax)!.Expression as MemberAccessExpressionSyntax)!.Name.Identifier.Text, context.SemanticModel, @@ -85,7 +85,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }) .WithTrackingName("ConsoleApp.Builder.1_Where") .Collect() - .WithComparer(CollectBuilderContextComparer.Default) + .Select((x, ct) => new CollectBuilderContext(x, ct)) .WithTrackingName("ConsoleApp.Builder.2_Collect"); context.RegisterSourceOutput(builderSource, EmitConsoleAppBuilder); @@ -571,7 +571,7 @@ namespace ConsoleAppFramework; """; - static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, CommandContext commandContext) + static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, CommanContext commandContext) { if (commandContext.DiagnosticReporter.HasDiagnostics) { @@ -607,103 +607,11 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, C sourceProductionContext.AddSource("ConsoleApp.Run.Help.g.cs", help.ToString()); } - static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContext, ImmutableArray generatorSyntaxContexts) + static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContext, CollectBuilderContext collectBuilderContext) { - if (generatorSyntaxContexts.Length == 0) return; - - // validation, invoke in loop is not allowed. - foreach (var item in generatorSyntaxContexts) - { - if (item.Name is "Run" or "RunAsync") continue; - foreach (var n in item.Node.Ancestors()) - { - if (n.Kind() is SyntaxKind.WhileStatement or SyntaxKind.DoStatement or SyntaxKind.ForStatement or SyntaxKind.ForEachStatement) - { - sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.AddInLoopIsNotAllowed, item.Node.GetLocation()); - return; - } - } - } - - var methodGroup = generatorSyntaxContexts.ToLookup(x => - { - if (x.Name == "Add" && ((x.Node.Expression as MemberAccessExpressionSyntax)?.Name.IsKind(SyntaxKind.GenericName) ?? false)) - { - return "Add"; - } - - return x.Name; - }); - - var globalFilters = methodGroup["UseFilter"] - .OrderBy(x => x.Node.GetLocation().SourceSpan) // sort by line number - .Select(x => - { - var genericName = (x.Node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; - var genericType = genericName!.TypeArgumentList.Arguments[0]; - var type = x.Model.GetTypeInfo(genericType).Type; - if (type == null) return null!; - - var filter = FilterInfo.Create(type); - - if (filter == null) - { - sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, genericType.GetLocation()); - return null!; - } - - return filter!; - }) - .ToArray(); - - // don't emit if exists failure(already reported error) - if (globalFilters.Any(x => x == null)) - { - return; - } - - var reporter = new DiagnosticReporter(); - var names = new HashSet(); - var commands1 = methodGroup["Add"] - .Select(x => - { - var wellKnownTypes = new WellKnownTypes(x.Model.Compilation); - var parser = new Parser(reporter, x.Node, x.Model, wellKnownTypes, DelegateBuildType.OnlyActionFunc, globalFilters); - var command = parser.ParseAndValidateForBuilderDelegateRegistration(); - - // validation command name duplicate - if (command != null && !names.Add(command.Name)) - { - var location = x.Node.ArgumentList.Arguments[0].GetLocation(); - sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, location, command!.Name); - return null; - } - - return command; - }) - .ToArray(); // evaluate first. - - var commands2 = methodGroup["Add"] - .SelectMany(x => - { - var wellKnownTypes = new WellKnownTypes(x.Model.Compilation); - var parser = new Parser(reporter, x.Node, x.Model, wellKnownTypes, DelegateBuildType.None, globalFilters); - var commands = parser.ParseAndValidateForBuilderClassRegistration(); - - // validation command name duplicate - foreach (var command in commands) - { - if (command != null && !names.Add(command.Name)) - { - sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.GetLocation(), command!.Name); - return [null]; - } - } - - return commands; - }); - - var commands = commands1.Concat(commands2).ToArray(); + var reporter = collectBuilderContext.DiagnosticReporter; + var hasRun = collectBuilderContext.HasRun; + var hasRunAsync = collectBuilderContext.HasRunAsync; if (reporter.HasDiagnostics) { @@ -711,24 +619,13 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex return; } - // don't emit if exists failure(already reported error) - if (commands.Any(x => x == null)) - { - return; - } - - if (commands.Length == 0) return; - - var hasRun = methodGroup["Run"].Any(); - var hasRunAsync = methodGroup["RunAsync"].Any(); - if (!hasRun && !hasRunAsync) return; var sb = new SourceBuilder(0); sb.AppendLine(GeneratedCodeHeader); // with id number - var commandIds = commands + var commandIds = collectBuilderContext.Commands .Select((x, i) => { return new Emitter.CommandWithId( @@ -759,14 +656,14 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex sourceProductionContext.AddSource("ConsoleApp.Builder.Help.g.cs", help.ToString()); } - class CommandContext(Command? command, bool isAsync, DiagnosticReporter diagnosticReporter, InvocationExpressionSyntax node) : IEquatable + class CommanContext(Command? command, bool isAsync, DiagnosticReporter diagnosticReporter, InvocationExpressionSyntax node) : IEquatable { public Command? Command => command; public DiagnosticReporter DiagnosticReporter => diagnosticReporter; public InvocationExpressionSyntax Node => node; public bool IsAsync => isAsync; - public bool Equals(CommandContext other) + public bool Equals(CommanContext other) { // has diagnostics, always go to modified(don't cache) if (diagnosticReporter.HasDiagnostics || other.DiagnosticReporter.HasDiagnostics) return false; @@ -777,302 +674,137 @@ public bool Equals(CommandContext other) } } - readonly struct RunContext(InvocationExpressionSyntax node, SemanticModel model) : IEquatable + class CollectBuilderContext : IEquatable { - public InvocationExpressionSyntax Node => node; - public SemanticModel SemanticModel => model; - - public bool Equals(RunContext other) - { - if (!SyntaxNodeTextEqualityComparer.Default.Equals(node.Expression, other.Node.Expression)) return false; - - return DelegateEquals(node, model, (other.Node, other.SemanticModel)); - } + public Command[] Commands { get; } = []; + public DiagnosticReporter DiagnosticReporter { get; } + public CancellationToken CancellationToken { get; } + public bool HasRun { get; } + public bool HasRunAsync { get; } - public override int GetHashCode() + public CollectBuilderContext(ImmutableArray contexts, CancellationToken cancellationToken) { - // maybe this does not called so don't care impl. - return SyntaxNodeTextEqualityComparer.Default.GetHashCode(node); - } - - // use for both Run and Builder.Add - public static bool DelegateEquals(InvocationExpressionSyntax node, SemanticModel model, (InvocationExpressionSyntax Node, SemanticModel SemanticModel) other) - { - var args1 = node.ArgumentList.Arguments; - var args2 = other.Node.ArgumentList.Arguments; + this.DiagnosticReporter = new DiagnosticReporter(); + this.CancellationToken = cancellationToken; - if (args1.Count != args2.Count) return false; - if (args1.Count != 2) return false; - - if (args1[1].Kind() != args2[1].Kind()) + if (contexts.Length == 0) { - return false; + return; } - var expression = args1[1].Expression; - var lambda1 = expression as ParenthesizedLambdaExpressionSyntax; - if (lambda1 != null) + // validation, invoke in loop is not allowed. + foreach (var item in contexts) { - // check async, returntype, parameters - - var lambda2 = args2[1].Expression as ParenthesizedLambdaExpressionSyntax; - if (lambda2 == null) return false; - - if (!lambda1.AsyncKeyword.IsKind(lambda2.AsyncKeyword.Kind())) return false; - - if (!SyntaxNodeTextEqualityComparer.Default.Equals(lambda1.ReturnType!, lambda2.ReturnType!)) + if (item.Name is "Run" or "RunAsync") continue; + foreach (var n in item.Node.Ancestors()) { - return false; - } - - return SyntaxNodeTextEqualityComparer.Default.Equals(lambda1.ParameterList, lambda2.ParameterList); - } - else - { - ImmutableArray methodSymbols1; - ImmutableArray methodSymbols2; - if (expression.IsKind(SyntaxKind.AddressOfExpression)) - { - var operand1 = (expression as PrefixUnaryExpressionSyntax)?.Operand; - var operand2 = (args2[1].Expression as PrefixUnaryExpressionSyntax)?.Operand; - if (operand1 == null || operand2 == null) return false; - - methodSymbols1 = model.GetMemberGroup(operand1); - methodSymbols2 = other.SemanticModel.GetMemberGroup(operand2); - } - else - { - methodSymbols1 = model.GetMemberGroup(expression); - methodSymbols2 = other.SemanticModel.GetMemberGroup(args2[1].Expression); - } - - if (methodSymbols1.Length > 0 && methodSymbols1[0] is IMethodSymbol methodSymbol1) - { - if (methodSymbols2.Length > 0 && methodSymbols2[0] is IMethodSymbol methodSymbol2) + if (n.Kind() is SyntaxKind.WhileStatement or SyntaxKind.DoStatement or SyntaxKind.ForStatement or SyntaxKind.ForEachStatement) { - return MethodSymbolEquals(methodSymbol1, methodSymbol2); + DiagnosticReporter.ReportDiagnostic(DiagnosticDescriptors.AddInLoopIsNotAllowed, item.Node.GetLocation()); + return; } } } - return false; - } - - public static bool MethodSymbolEquals(IMethodSymbol methodSymbol1, IMethodSymbol methodSymbol2) - { - var syntax1 = FunctionSyntax.From(methodSymbol1.DeclaringSyntaxReferences[0].GetSyntax()); - var syntax2 = FunctionSyntax.From(methodSymbol2.DeclaringSyntaxReferences[0].GetSyntax()); - if (syntax1 == null || syntax2 == null) return false; - - // document comment - if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.GetDocumentationCommentTriviaSyntax()!, syntax2.GetDocumentationCommentTriviaSyntax()!)) - { - return false; - } - - // return type - if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.ReturnType, syntax2.ReturnType)) - { - return false; - } - - // attributes - if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.AttributeLists, syntax2.AttributeLists)) - { - return false; - } - - // parameters - return SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.ParameterList, syntax2.ParameterList); - } - } - - public class CollectBuilderContextComparer : IEqualityComparer> - { - public static CollectBuilderContextComparer Default = new CollectBuilderContextComparer(); - - bool IEqualityComparer>.Equals(ImmutableArray x, ImmutableArray y) - { - if (x.Length != y.Length) return false; - - for (int i = 0; i < x.Length; i++) + var methodGroup = contexts.ToLookup(x => { - if (!Equals(x[i], y[i])) return false; - } - - return true; - } - - int IEqualityComparer>.GetHashCode(ImmutableArray obj) - { - return 0; - } - + if (x.Name == "Add" && ((x.Node.Expression as MemberAccessExpressionSyntax)?.Name.IsKind(SyntaxKind.GenericName) ?? false)) + { + return "Add"; + } - static bool Equals(BuilderContext self, BuilderContext other) - { - if (self.Name != other.Name) return false; + return x.Name; + }); - var typeInfo = self.Model.GetTypeInfo((self.Node.Expression as MemberAccessExpressionSyntax)!.Expression, self.CancellationToken); - if (typeInfo.Type?.Name != "ConsoleAppBuilder") - { - return false; - } + var globalFilters = methodGroup["UseFilter"] + .OrderBy(x => x.Node.GetLocation().SourceSpan) // sort by line number + .Select(x => + { + var genericName = (x.Node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; + var genericType = genericName!.TypeArgumentList.Arguments[0]; + var type = x.Model.GetTypeInfo(genericType).Type; + if (type == null) return null!; - var typeInfo2 = other.Model.GetTypeInfo((other.Node.Expression as MemberAccessExpressionSyntax)!.Expression, other.CancellationToken); - if (typeInfo2.Type?.Name != "ConsoleAppBuilder") - { - return false; - } + var filter = FilterInfo.Create(type); - switch (self.Name) - { - case "Add": // Add or Add - if ((self.Node.Expression as MemberAccessExpressionSyntax)?.Name.IsKind(SyntaxKind.GenericName) ?? false) + if (filter == null) { - return EqualsAddClass(self, other); + DiagnosticReporter.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, genericType.GetLocation()); + return null!; } - else - { - var first = GetFirstStringConstant(self.Node); - var second = GetFirstStringConstant(other.Node); - if (first == null || second == null) return false; - if (first != second) return false; - - return RunContext.DelegateEquals(self.Node, self.Model, (other.Node, other.Model)); - } - case "UseFilter": - return EqualsUseFilter(self, other); - case "Run": - case "RunAsync": - return true; // only check name - default: - break; - } - - return false; - } - static string? GetFirstStringConstant(InvocationExpressionSyntax invocationExpression) - { - if (invocationExpression.ArgumentList.Arguments.Count != 2) return null; - var commandName = invocationExpression.ArgumentList.Arguments[0]; - - if (!commandName.Expression.IsKind(SyntaxKind.StringLiteralExpression)) - { - return null; - } - - var name = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; - return name; - } - - static bool EqualsAddClass(BuilderContext self, BuilderContext other) - { - var node = self.Node; - var model = self.Model; - - var typeAndPath1 = GetTypeSymbolAndPath(node, model, self.CancellationToken); - var typeAndPath2 = GetTypeSymbolAndPath(other.Node, other.Model, other.CancellationToken); - - if (typeAndPath1 == null || typeAndPath2 == null) return false; - - var (type1, path1) = typeAndPath1.Value; - var (type2, path2) = typeAndPath2.Value; + return filter!; + }) + .ToArray(); - if (path1 != path2) return false; - - if (type1.DeclaringSyntaxReferences.Length == 0) return false; - if (type2.DeclaringSyntaxReferences.Length == 0) return false; - - var syntax1 = type1.DeclaringSyntaxReferences[0].GetSyntax() as TypeDeclarationSyntax; - var syntax2 = type2.DeclaringSyntaxReferences[0].GetSyntax() as TypeDeclarationSyntax; - - if (syntax1 == null || syntax2 == null) return false; - - // interface - if (!type1.AllInterfaces.Select(x => x.Name).SequenceEqual(type2.AllInterfaces.Select(x => x.Name))) + // don't emit if exists failure + if (DiagnosticReporter.HasDiagnostics) { - return false; + return; } - // Public Constructor - var ctor1 = type1.GetMembers().FirstOrDefault(x => (x as IMethodSymbol)?.MethodKind == Microsoft.CodeAnalysis.MethodKind.Constructor); - var ctor2 = type2.GetMembers().FirstOrDefault(x => (x as IMethodSymbol)?.MethodKind == Microsoft.CodeAnalysis.MethodKind.Constructor); - var ctorParameter1 = ctor1?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetParameterListOfConstructor(); - var ctorParameter2 = ctor2?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetParameterListOfConstructor(); - - if (!SyntaxNodeTextEqualityComparer.Default.Equals(ctorParameter1, ctorParameter2)) - { - return false; - } + var names = new HashSet(); + var commands1 = methodGroup["Add"] + .Select(x => + { + var wellKnownTypes = new WellKnownTypes(x.Model.Compilation); + var parser = new Parser(DiagnosticReporter, x.Node, x.Model, wellKnownTypes, DelegateBuildType.OnlyActionFunc, globalFilters); + var command = parser.ParseAndValidateForBuilderDelegateRegistration(); - // attributes - if (!SyntaxNodeTextEqualityComparer.Default.Equals(syntax1.AttributeLists!, syntax2.AttributeLists!)) - { - return false; - } + // validation command name duplicate + if (command != null && !names.Add(command.Name)) + { + var location = x.Node.ArgumentList.Arguments[0].GetLocation(); + DiagnosticReporter.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, location, command!.Name); + return null; + } - // Public Methods - var methods1 = GetMethodSymbols(type1); - var methods2 = GetMethodSymbols(type2); - var methodEquals = methods1.ZipEquals(methods2, RunContext.MethodSymbolEquals); - return methodEquals; + return command; + }) + .ToArray(); // evaluate first. - static (ITypeSymbol, string?)? GetTypeSymbolAndPath(InvocationExpressionSyntax node, SemanticModel model, CancellationToken cancellationToken) - { - // Add - var genericName = (node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; - var genericType = genericName!.TypeArgumentList.Arguments[0]; - - // Add(string commandPath) - string? commandPath = null; - var args = node.ArgumentList.Arguments; - if (node.ArgumentList.Arguments.Count == 1) + var commands2 = methodGroup["Add"] + .SelectMany(x => { - var commandName = args[0]; - if (!commandName.Expression.IsKind(SyntaxKind.StringLiteralExpression)) + var wellKnownTypes = new WellKnownTypes(x.Model.Compilation); + var parser = new Parser(DiagnosticReporter, x.Node, x.Model, wellKnownTypes, DelegateBuildType.None, globalFilters); + var commands = parser.ParseAndValidateForBuilderClassRegistration(); + + // validation command name duplicate + foreach (var command in commands) { - return null; + if (command != null && !names.Add(command.Name)) + { + DiagnosticReporter.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.GetLocation(), command!.Name); + return [null]; + } } - commandPath = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; - } + return commands; + }); - // T - var type = model.GetTypeInfo(genericType, cancellationToken).Type!; - return (type, commandPath); - } - - static IEnumerable GetMethodSymbols(ITypeSymbol type) + if (DiagnosticReporter.HasDiagnostics) { - return type.GetMembers() - .OfType() - .Where(x => x.DeclaredAccessibility == Accessibility.Public && !x.IsStatic) - .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Ordinary) - .Where(x => !(x.Name is "Dispose" or "DisposeAsync" or "GetHashCode" or "Equals" or "ToString")); + return; } + + // set properties + this.Commands = commands1.Concat(commands2!).ToArray()!; // not null if no diagnostics + this.HasRun = methodGroup["Run"].Any(); + this.HasRunAsync = methodGroup["RunAsync"].Any(); } - static bool EqualsUseFilter(BuilderContext self, BuilderContext other) + public bool Equals(CollectBuilderContext other) { - var node = self.Node; - var model = self.Model; - - var l = GetType(node, model, self.CancellationToken); - var r = GetType(other.Node, other.Model, other.CancellationToken); - - return l.EqualsNamespaceAndName(r); + if (DiagnosticReporter.HasDiagnostics || other.DiagnosticReporter.HasDiagnostics) return false; + if (HasRun != other.HasRun) return false; + if (HasRunAsync != other.HasRunAsync) return false; - static ITypeSymbol? GetType(InvocationExpressionSyntax expression, SemanticModel model, CancellationToken cancellationToken) - { - var genericName = (expression.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; - var genericType = genericName!.TypeArgumentList.Arguments[0]; - return model.GetTypeInfo(genericType, cancellationToken).Type; - } + return Commands.AsSpan().SequenceEqual(other.Commands); } } + // intermediate structure(no equatable) readonly struct BuilderContext(InvocationExpressionSyntax node, string name, SemanticModel model, CancellationToken cancellationToken) : IEquatable { public InvocationExpressionSyntax Node => node; @@ -1082,7 +814,7 @@ readonly struct BuilderContext(InvocationExpressionSyntax node, string name, Sem public bool Equals(BuilderContext other) { - return Node == other.Node; + return Node == other.Node; // no means. } } } \ No newline at end of file