diff --git a/AutomaticInterface/DotnetAutomaticInterface/AutomaticInterfaceGenerator.cs b/AutomaticInterface/DotnetAutomaticInterface/AutomaticInterfaceGenerator.cs index b7109e3..e8cefc0 100644 --- a/AutomaticInterface/DotnetAutomaticInterface/AutomaticInterfaceGenerator.cs +++ b/AutomaticInterface/DotnetAutomaticInterface/AutomaticInterfaceGenerator.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Immutable; +using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -19,37 +19,50 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterDefaultAttribute(); context.RegisterIgnoreAttribute(); - var classes = context - .SyntaxProvider.ForAttributeWithMetadataName( - $"DotnetAutomaticInterface.{DefaultAttributeName}Attribute", - (node, _) => node is ClassDeclarationSyntax, - (ctx, _) => (ITypeSymbol)ctx.TargetSymbol - ) - .Collect(); + var classes = context.SyntaxProvider.ForAttributeWithMetadataName( + $"DotnetAutomaticInterface.{DefaultAttributeName}Attribute", + (node, _) => node is ClassDeclarationSyntax, + (ctx, _) => new EquatableModel((ITypeSymbol)ctx.TargetSymbol, ctx.TargetNode) + ); context.RegisterSourceOutput(classes, GenerateCode); } - private static void GenerateCode( - SourceProductionContext context, - ImmutableArray enumerations + private static void GenerateCode(SourceProductionContext context, EquatableModel equatableModel) + { + var type = equatableModel.TypeSymbol; + + Log($"Trigger for: {type.Name}"); + + var typeNamespace = type.ContainingNamespace.IsGlobalNamespace + ? $"${Guid.NewGuid()}" + : $"{type.ContainingNamespace}"; + + var code = Builder.BuildInterfaceFor(equatableModel); + + var hintName = $"{typeNamespace}.I{type.Name}"; + context.AddSource(hintName, code); + } + + /// + /// Couldnt get it to disable when running tests so currently just disabled manually... + /// + /// + /// + /// + /// + public static void Log( + string message, + [CallerMemberName] string member = "", + [CallerFilePath] string file = "", + [CallerLineNumber] int line = 0 ) { - if (enumerations.IsDefaultOrEmpty) - { - return; - } - - foreach (var type in enumerations) - { - var typeNamespace = type.ContainingNamespace.IsGlobalNamespace - ? $"${Guid.NewGuid()}" - : $"{type.ContainingNamespace}"; - - var code = Builder.BuildInterfaceFor(type); - - var hintName = $"{typeNamespace}.I{type.Name}"; - context.AddSource(hintName, code); - } + // #if DEBUG + // var timestamp = DateTime.Now.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture); + // var source = $"{Path.GetFileName(file)}:{line}"; + // + // Debug.WriteLine($"[{timestamp}] [Generator] [{source}] [{member}] {message}"); + // #endif } } diff --git a/AutomaticInterface/DotnetAutomaticInterface/Builder.cs b/AutomaticInterface/DotnetAutomaticInterface/Builder.cs index 8ad7296..f612328 100644 --- a/AutomaticInterface/DotnetAutomaticInterface/Builder.cs +++ b/AutomaticInterface/DotnetAutomaticInterface/Builder.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -41,11 +40,13 @@ private static string InheritDoc(ISymbol source) => miscellaneousOptions: FullyQualifiedDisplayFormat.MiscellaneousOptions ); - public static string BuildInterfaceFor(ITypeSymbol typeSymbol) + public static string BuildInterfaceFor(EquatableModel equatableModel) { + var typeSymbol = equatableModel.TypeSymbol; + var nodeSyntax = equatableModel.ClassSyntax; + if ( - typeSymbol.DeclaringSyntaxReferences.First().GetSyntax() - is not ClassDeclarationSyntax classSyntax + nodeSyntax is not ClassDeclarationSyntax classSyntax || typeSymbol is not INamedTypeSymbol namedTypeSymbol ) { @@ -95,16 +96,19 @@ ClassDeclarationSyntax classSyntax private static void AddMethodsToInterface(List members, InterfaceBuilder codeGenerator) { - members - .Where(x => x.Kind == SymbolKind.Method) - .OfType() - .Where(x => x.MethodKind == MethodKind.Ordinary) - .Where(x => x.ContainingType.Name != nameof(Object)) - .Where(x => !HasIgnoreAttribute(x)) - .GroupBy(x => x.ToDisplayString(FullyQualifiedDisplayFormatForGrouping)) - .Select(g => g.First()) - .ToList() - .ForEach(method => AddMethod(codeGenerator, method)); + foreach ( + var method in members + .Where(x => x.Kind == SymbolKind.Method) + .OfType() + .Where(x => x.MethodKind == MethodKind.Ordinary) + .Where(x => x.ContainingType.Name != nameof(Object)) + .Where(x => !HasIgnoreAttribute(x)) + .GroupBy(x => x.ToDisplayString(FullyQualifiedDisplayFormatForGrouping)) + .Select(g => g.First()) + ) + { + AddMethod(codeGenerator, method); + } } private static void AddMethod(InterfaceBuilder codeGenerator, IMethodSymbol method) @@ -115,10 +119,15 @@ private static void AddMethod(InterfaceBuilder codeGenerator, IMethodSymbol meth ActivateNullableIfNeeded(codeGenerator, method); var paramResult = new HashSet(); - method - .Parameters.Select(p => GetParameterDisplayString(p, codeGenerator.HasNullable)) - .ToList() - .ForEach(x => paramResult.Add(x)); + + foreach ( + var p in method.Parameters.Select(p => + GetParameterDisplayString(p, codeGenerator.HasNullable) + ) + ) + { + paramResult.Add(p); + } var typedArgs = method .TypeParameters.Select(arg => @@ -206,30 +215,31 @@ bool nullableContextEnabled .Visit(param.DeclaringSyntaxReferences.First().GetSyntax()) .ToFullString(); } + return param.ToDisplayString(FullyQualifiedDisplayFormat); } private static void AddEventsToInterface(List members, InterfaceBuilder codeGenerator) { - members - .Where(x => x.Kind == SymbolKind.Event) - .OfType() - .GroupBy(x => x.ToDisplayString(FullyQualifiedDisplayFormatForGrouping)) - .Select(g => g.First()) - .ToList() - .ForEach(evt => - { - var type = evt.Type; - var name = evt.Name; + foreach ( + var evt in members + .Where(x => x.Kind == SymbolKind.Event) + .OfType() + .GroupBy(x => x.ToDisplayString(FullyQualifiedDisplayFormatForGrouping)) + .Select(g => g.First()) + ) + { + var type = evt.Type; + var name = evt.Name; - ActivateNullableIfNeeded(codeGenerator, type); + ActivateNullableIfNeeded(codeGenerator, type); - codeGenerator.AddEventToInterface( - name, - type.ToDisplayString(FullyQualifiedDisplayFormat), - InheritDoc(evt) - ); - }); + codeGenerator.AddEventToInterface( + name, + type.ToDisplayString(FullyQualifiedDisplayFormat), + InheritDoc(evt) + ); + } } private static void AddPropertiesToInterface( @@ -237,33 +247,33 @@ private static void AddPropertiesToInterface( InterfaceBuilder interfaceGenerator ) { - members - .Where(x => x.Kind == SymbolKind.Property) - .OfType() - .Where(x => !x.IsIndexer) - .GroupBy(x => x.Name) - .Select(g => g.First()) - .ToList() - .ForEach(prop => - { - var type = prop.Type; - - var name = prop.Name; - var hasGet = prop.GetMethod?.DeclaredAccessibility == Accessibility.Public; - var hasSet = GetSetKind(prop.SetMethod); - var isRef = prop.ReturnsByRef; - - ActivateNullableIfNeeded(interfaceGenerator, type); - - interfaceGenerator.AddPropertyToInterface( - name, - type.ToDisplayString(FullyQualifiedDisplayFormat), - hasGet, - hasSet, - isRef, - InheritDoc(prop) - ); - }); + foreach ( + var prop in members + .Where(x => x.Kind == SymbolKind.Property) + .OfType() + .Where(x => !x.IsIndexer) + .GroupBy(x => x.Name) + .Select(g => g.First()) + ) + { + var type = prop.Type; + + var name = prop.Name; + var hasGet = prop.GetMethod?.DeclaredAccessibility == Accessibility.Public; + var hasSet = GetSetKind(prop.SetMethod); + var isRef = prop.ReturnsByRef; + + ActivateNullableIfNeeded(interfaceGenerator, type); + + interfaceGenerator.AddPropertyToInterface( + name, + type.ToDisplayString(FullyQualifiedDisplayFormat), + hasGet, + hasSet, + isRef, + InheritDoc(prop) + ); + } } private static PropertySetKind GetSetKind(IMethodSymbol? setMethodSymbol) diff --git a/AutomaticInterface/DotnetAutomaticInterface/EquatableModel.cs b/AutomaticInterface/DotnetAutomaticInterface/EquatableModel.cs new file mode 100644 index 0000000..8d76870 --- /dev/null +++ b/AutomaticInterface/DotnetAutomaticInterface/EquatableModel.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.CodeAnalysis; + +namespace DotnetAutomaticInterface; + +/// +/// This is just a container to enable Roselyn to check whether a Class changed and needs to retrigger src gen. +/// Easiest approach is here to just compare the entire Syntax. This is more than we need, as we e.g. don't care +/// about implementation or private members. But at least it guarantees recompilation on change. But only for that +/// file, and not all files with that CodeGen Attribute as before! So while we do some expensive comparisons, we +/// avoid arguably more expensive IO of writing to a file. +/// +/// The cls symbol +/// The cls syntax +public sealed class EquatableModel(ITypeSymbol typeSymbol, SyntaxNode classSyntax) + : IEquatable +{ + public ITypeSymbol TypeSymbol { get; } = typeSymbol; + + public SyntaxNode ClassSyntax { get; } = classSyntax; + + public bool Equals(EquatableModel? other) + { + if (other is null) + return false; + + if (ReferenceEquals(this, other)) + return true; + + // AutomaticInterfaceGenerator.Log(ClassSyntax.ToString()); + return ClassSyntax.IsEquivalentTo(other.ClassSyntax); + } + + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || obj is EquatableModel other && Equals(other); + } + + public override int GetHashCode() + { + return ClassSyntax.GetHashCode(); + } +} diff --git a/AutomaticInterface/Tests/Infrastructure.cs b/AutomaticInterface/Tests/Infrastructure.cs index 2fd51f7..93b3512 100644 --- a/AutomaticInterface/Tests/Infrastructure.cs +++ b/AutomaticInterface/Tests/Infrastructure.cs @@ -26,9 +26,7 @@ public static string GenerateCode(string code) var sourceDiagnostics = compilation.GetDiagnostics(); var sourceErrors = sourceDiagnostics .Where(d => d.Severity == DiagnosticSeverity.Error) - .Where(x => x.Id != "CS0246") // missing references are ok - .ToList(); - + .Where(x => x.Id != "CS0246"); // missing references are ok; Assert.Empty(sourceErrors); var generator = new AutomaticInterfaceGenerator(); @@ -41,8 +39,7 @@ public static string GenerateCode(string code) out var diagnostics ); - var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); - + var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error); Assert.Empty(errors); return outputCompilation.SyntaxTrees.Skip(1).LastOrDefault()?.ToString();