Skip to content

Commit

Permalink
Add diagnostic info
Browse files Browse the repository at this point in the history
  • Loading branch information
ThadHouse committed Feb 17, 2024
1 parent 20ba35f commit d0db90a
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 15 deletions.
54 changes: 54 additions & 0 deletions sourcegeneration/StereologueSourceGenerator/DiagnosticInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Microsoft.CodeAnalysis;

namespace Stereologue.SourceGenerator;

/// <summary>
/// Descriptor for diagnostic instances using structural equality comparison.
/// Provides a work-around for https://github.com/dotnet/roslyn/issues/68291.
/// </summary>
internal readonly struct DiagnosticInfo : IEquatable<DiagnosticInfo>
{
public DiagnosticDescriptor Descriptor { get; private init; }
public object?[] MessageArgs { get; private init; }
public Location? Location { get; private init; }

public static DiagnosticInfo Create(DiagnosticDescriptor descriptor, Location? location, object?[]? messageArgs)
{
Location? trimmedLocation = location is null ? null : GetTrimmedLocation(location);

return new DiagnosticInfo
{
Descriptor = descriptor,
Location = trimmedLocation,
MessageArgs = messageArgs ?? []
};

// Creates a copy of the Location instance that does not capture a reference to Compilation.
static Location GetTrimmedLocation(Location location)
=> Location.Create(location.SourceTree?.FilePath ?? "", location.SourceSpan, location.GetLineSpan().Span);
}

public Diagnostic CreateDiagnostic()
=> Diagnostic.Create(Descriptor, Location, MessageArgs);

public override readonly bool Equals(object? obj) => obj is DiagnosticInfo info && Equals(info);

public readonly bool Equals(DiagnosticInfo other)
{
return Descriptor.Equals(other.Descriptor) &&
MessageArgs.SequenceEqual(other.MessageArgs) &&
Location == other.Location;
}

public override readonly int GetHashCode()
{
int hashCode = Descriptor.GetHashCode();
foreach (object? messageArg in MessageArgs)
{
hashCode = HashCode.Combine(hashCode, messageArg?.GetHashCode() ?? 0);
}

hashCode = HashCode.Combine(hashCode, Location?.GetHashCode() ?? 0);
return hashCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace Stereologue.SourceGenerator;

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable RS2008 // Enable analyzer release tracking

using System;
using Microsoft.CodeAnalysis;

public static class GeneratorDiagnostics
{
public class Ids
{
public const string Prefix = "WPILIB";
public const string GeneratedTypeNotPartial = Prefix + "1000";
public const string GeneratedTypeImplementsILogged = Prefix + "1001";
public const string LoggableTypeNotSupported = Prefix + "1002";
public const string GeneratedTypeIsInterface = Prefix + "1003";
public const string GeneratedTypeIsRefStruct = Prefix + "1004";
public const string LoggedMethodDoesntReturnVoid = Prefix + "1005";
public const string LoggedMethodTakesArguments = Prefix + "1006";
public const string LoggedMemberTypeNotSupported = Prefix + "1007";
}

private const string Category = "StereologueSourceGenerator";

public static readonly DiagnosticDescriptor GeneratedTypeNotPartial = new(
Ids.GeneratedTypeNotPartial, "", "", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");

public static readonly DiagnosticDescriptor GeneratedTypeImplementsILogged = new(
Ids.GeneratedTypeImplementsILogged, "", "", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");

public static readonly DiagnosticDescriptor LoggableTypeNotSupported = new(
Ids.LoggableTypeNotSupported, "", "", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");

public static readonly DiagnosticDescriptor GeneratedTypeIsInterface = new(
Ids.LoggableTypeNotSupported, "", "", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");

public static readonly DiagnosticDescriptor GeneratedTypeIsRefStruct = new(
Ids.GeneratedTypeIsRefStruct, "", "", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");

public static readonly DiagnosticDescriptor LoggedMethodDoesntReturnVoid = new(
Ids.LoggedMethodDoesntReturnVoid, "", "", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");

public static readonly DiagnosticDescriptor LoggedMethodTakesArguments = new(
Ids.LoggedMethodTakesArguments, "", "", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
public static readonly DiagnosticDescriptor LoggedMemberTypeNotSupported = new(
Ids.LoggedMemberTypeNotSupported, "", "", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
}
78 changes: 63 additions & 15 deletions sourcegeneration/StereologueSourceGenerator/LogGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,30 @@ internal record LogAttributeInfo(string Path, string LogLevel, string LogType, b

internal record LogData(string GetOperation, string? Type, DeclarationType DecelType, LogAttributeInfo AttributeInfo);

internal record ClassData(ImmutableArray<LogData> LoggedItems, string Name, string ClassDeclaration, string? Namespace);
internal record ClassData(EquatableArray<LogData> LoggedItems, string Name, string ClassDeclaration, string? Namespace);

internal record ClassOrDiagnostic(ClassData? ValidClassData, EquatableArray<DiagnosticInfo> Diagnostic);

[Generator]
public class LogGenerator : IIncrementalGenerator
{
static ClassData? GetClassData(SemanticModel semanticModel, SyntaxNode classDeclarationSyntax, CancellationToken token)
static ClassOrDiagnostic? GetClassData(GeneratorAttributeSyntaxContext context, CancellationToken token)
{
if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol)
if (context.SemanticModel.GetDeclaredSymbol(context.TargetNode) is not INamedTypeSymbol classSymbol)
{
return null;
}
token.ThrowIfCancellationRequested();

var diagnosticList = new ImmutableArray<DiagnosticInfo>();

var diagnostic = GetDiagnosticIfInvalidClassForGeneration((TypeDeclarationSyntax)context.TargetNode, classSymbol);
if (diagnostic is { } ds)
{
diagnosticList.Add(ds);
return new(null, diagnosticList);
}

var ns = classSymbol.ContainingNamespace?.ToDisplayString();
token.ThrowIfCancellationRequested();
StringBuilder typeBuilder = new StringBuilder();
Expand Down Expand Up @@ -109,19 +120,22 @@ public class LogGenerator : IIncrementalGenerator
{
if (method.ReturnsVoid)
{
throw new InvalidOperationException("Cannot have a void returning method");
diagnosticList.Add(DiagnosticInfo.Create(GeneratorDiagnostics.LoggedMethodDoesntReturnVoid, null, [method.Name]));
continue;
}
if (!method.Parameters.IsEmpty)
{
throw new InvalidOperationException("Cannot take a parameter");
diagnosticList.Add(DiagnosticInfo.Create(GeneratorDiagnostics.LoggedMethodTakesArguments, null, [method.Name]));
continue;
}

getOperation = $"{method.Name}()";
logType = method.ReturnType;
}
else
{
throw new InvalidOperationException("Field is not loggable");
diagnosticList.Add(DiagnosticInfo.Create(GeneratorDiagnostics.LoggedMemberTypeNotSupported, null, [member.Name]));
continue;
}

var fullOperation = ComputeOperation(logType, getOperation, attributeInfo);
Expand All @@ -135,7 +149,7 @@ public class LogGenerator : IIncrementalGenerator
var fmt = new SymbolDisplayFormat(genericsOptions: SymbolDisplayGenericsOptions.None);
var fileName = $"{classSymbol.ContainingNamespace}{classSymbol.ToDisplayString(fmt)}{classSymbol.MetadataName}";

return new ClassData(loggableMembers.ToImmutable(), $"{classSymbol.ContainingNamespace}{classSymbol.ToDisplayString(fmt)}{classSymbol.MetadataName}", typeBuilder.ToString(), ns);
return new ClassOrDiagnostic(new ClassData(loggableMembers.ToImmutable(), $"{classSymbol.ContainingNamespace}{classSymbol.ToDisplayString(fmt)}{classSymbol.MetadataName}", typeBuilder.ToString(), ns), diagnosticList);
}

private static LogData ComputeOperation(ITypeSymbol logType, string getOp, LogAttributeInfo attributeInfo)
Expand Down Expand Up @@ -181,14 +195,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.ForAttributeWithMetadataName(
"Stereologue.GenerateLogAttribute",
predicate: static (s, _) => s is TypeDeclarationSyntax,
transform: static (ctx, token) => GetClassData(ctx.SemanticModel, ctx.TargetNode, token))
transform: static (ctx, token) => GetClassData(ctx, token))
.Where(static m => m is not null);

context.RegisterSourceOutput(attributedTypes,
static (spc, source) => Execute(source, spc));
}

static void ConstructCall(LogData data, StringBuilder builder)
static void ConstructCall(LogData data, StringBuilder builder, SourceProductionContext context)
{
builder.Append(" ");

Expand Down Expand Up @@ -239,15 +253,30 @@ static void ConstructCall(LogData data, StringBuilder builder)
"System.Span<System.String>" => ("LogStringArray", "", ""),
"System.Span<System.Byte>" => ("LogRaw", "", ""),
"System.Span<System.Boolean>" => ("LogBooleanArray", "", ""),
_ => (data.Type, "", "")
_ => (null, "", "")
};

if (ret.LogMethod is null)
{
//context.ReportDiagnostic(DiagnosticInfo.Create(GeneratorDiagnostics.LoggableTypeNotSupported, null, [data.Type]).CreateDiagnostic());
builder.AppendLine();
return;
}

builder.AppendLine($"logger.{ret.LogMethod}($\"{{path}}/{data.AttributeInfo.Path}\", {data.AttributeInfo.LogType}, {ret.Cast}{data.GetOperation}{ret.Conversion}, {data.AttributeInfo.LogLevel});");
}

static void Execute(ClassData? classData, SourceProductionContext context)
static void Execute(ClassOrDiagnostic? classData, SourceProductionContext context)
{
if (classData is { } value)
if (classData?.Diagnostic is { } diagnostic)
{
foreach (var d in diagnostic)
{
context.ReportDiagnostic(d.CreateDiagnostic());
}
}

if (classData?.ValidClassData is { } value)
{
StringBuilder builder = new StringBuilder();
if (value.Namespace is not null)
Expand All @@ -261,7 +290,7 @@ static void Execute(ClassData? classData, SourceProductionContext context)
builder.AppendLine(" {");
foreach (var call in value.LoggedItems)
{
ConstructCall(call, builder);
ConstructCall(call, builder, context);
}
builder.AppendLine(" }");
builder.AppendLine("}");
Expand All @@ -270,12 +299,31 @@ static void Execute(ClassData? classData, SourceProductionContext context)
}
}

private static object? GetDiagnosticIfInvalidClassForGeneration(TypeDeclarationSyntax syntax, ITypeSymbol symbol)
private static DiagnosticInfo? GetDiagnosticIfInvalidClassForGeneration(TypeDeclarationSyntax syntax, ITypeSymbol symbol)
{
// Ensure class is partial
if (!syntax.IsInPartialContext(out var nonPartialIdentifier))
{
return new object();
return DiagnosticInfo.Create(GeneratorDiagnostics.GeneratedTypeNotPartial, syntax.Identifier.GetLocation(), [symbol.Name, nonPartialIdentifier]);
;
}

// Ensure class doesn't implement ILogged
if (symbol.AllInterfaces.Where(x => x.ToDisplayString() == "Stereologue.ILogged").Any())
{
return DiagnosticInfo.Create(GeneratorDiagnostics.GeneratedTypeImplementsILogged, syntax.Identifier.GetLocation(), [symbol.Name]);
}

// Ensure implementation isn't ref struct
if (symbol.IsRefLikeType)
{
return DiagnosticInfo.Create(GeneratorDiagnostics.GeneratedTypeIsRefStruct, syntax.Identifier.GetLocation(), [symbol.Name]);
}

// Ensure implementation isn't interface
if (symbol.TypeKind == TypeKind.Interface)
{
return DiagnosticInfo.Create(GeneratorDiagnostics.GeneratedTypeIsInterface, syntax.Identifier.GetLocation(), [symbol.Name]);
}

return null;
Expand Down

0 comments on commit d0db90a

Please sign in to comment.