Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Expand All @@ -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<ITypeSymbol> 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);
}

/// <summary>
/// Couldnt get it to disable when running tests so currently just disabled manually...
/// </summary>
/// <param name="message"></param>
/// <param name="member"></param>
/// <param name="file"></param>
/// <param name="line"></param>
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
}
}
134 changes: 72 additions & 62 deletions AutomaticInterface/DotnetAutomaticInterface/Builder.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
)
{
Expand Down Expand Up @@ -95,16 +96,19 @@ ClassDeclarationSyntax classSyntax

private static void AddMethodsToInterface(List<ISymbol> members, InterfaceBuilder codeGenerator)
{
members
.Where(x => x.Kind == SymbolKind.Method)
.OfType<IMethodSymbol>()
.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<IMethodSymbol>()
.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)
Expand All @@ -115,10 +119,15 @@ private static void AddMethod(InterfaceBuilder codeGenerator, IMethodSymbol meth
ActivateNullableIfNeeded(codeGenerator, method);

var paramResult = new HashSet<string>();
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 =>
Expand Down Expand Up @@ -206,64 +215,65 @@ bool nullableContextEnabled
.Visit(param.DeclaringSyntaxReferences.First().GetSyntax())
.ToFullString();
}

return param.ToDisplayString(FullyQualifiedDisplayFormat);
}

private static void AddEventsToInterface(List<ISymbol> members, InterfaceBuilder codeGenerator)
{
members
.Where(x => x.Kind == SymbolKind.Event)
.OfType<IEventSymbol>()
.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<IEventSymbol>()
.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(
List<ISymbol> members,
InterfaceBuilder interfaceGenerator
)
{
members
.Where(x => x.Kind == SymbolKind.Property)
.OfType<IPropertySymbol>()
.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<IPropertySymbol>()
.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)
Expand Down
43 changes: 43 additions & 0 deletions AutomaticInterface/DotnetAutomaticInterface/EquatableModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using Microsoft.CodeAnalysis;

namespace DotnetAutomaticInterface;

/// <summary>
/// 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.
/// </summary>
/// <param name="typeSymbol">The cls symbol</param>
/// <param name="classSyntax">The cls syntax</param>
public sealed class EquatableModel(ITypeSymbol typeSymbol, SyntaxNode classSyntax)
: IEquatable<EquatableModel>
{
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();
}
}
7 changes: 2 additions & 5 deletions AutomaticInterface/Tests/Infrastructure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down