Skip to content

Commit

Permalink
Move CodeHelpers to their own thing, add unit test for fixup
Browse files Browse the repository at this point in the history
  • Loading branch information
ThadHouse committed Feb 19, 2024
1 parent ad32e9d commit ad431ae
Show file tree
Hide file tree
Showing 20 changed files with 219 additions and 28 deletions.
11 changes: 9 additions & 2 deletions WPILib.sln
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stereologue", "src\thirdpar
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "stereologue.test", "test\stereologue.test\stereologue.test.csproj", "{630D08FD-CD06-4674-BC5A-F1F211619E83}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sourcegeneration", "sourcegeneration", "{909FC1DB-3083-4F01-8496-B8C9DD4FEA13}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "codehelp", "codehelp", "{909FC1DB-3083-4F01-8496-B8C9DD4FEA13}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StereologueSourceGenerator", "sourcegeneration\StereologueSourceGenerator\StereologueSourceGenerator.csproj", "{76F4D0AE-2123-493B-B721-4118330C52BB}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WPILib.CodeHelpers", "codehelp\CodeHelpers\WPILib.CodeHelpers.csproj", "{76F4D0AE-2123-493B-B721-4118330C52BB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeHelpers.Test", "codehelp\CodeHelpers.Test\CodeHelpers.Test.csproj", "{42E0EFC6-4990-4395-A9D1-8683778751E7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -106,6 +108,10 @@ Global
{76F4D0AE-2123-493B-B721-4118330C52BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76F4D0AE-2123-493B-B721-4118330C52BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76F4D0AE-2123-493B-B721-4118330C52BB}.Release|Any CPU.Build.0 = Release|Any CPU
{42E0EFC6-4990-4395-A9D1-8683778751E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42E0EFC6-4990-4395-A9D1-8683778751E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42E0EFC6-4990-4395-A9D1-8683778751E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42E0EFC6-4990-4395-A9D1-8683778751E7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8F38C25E-641E-47FC-AC0A-0717F2159E8F} = {DB664556-4BF0-4874-8CB6-DC24E60A67AF}
Expand All @@ -123,5 +129,6 @@ Global
{2124D403-17C4-4116-932D-74933812ECE6} = {822627EF-820D-488B-BC14-BDC4BA88454B}
{630D08FD-CD06-4674-BC5A-F1F211619E83} = {AD95ECD8-E708-4FB4-9B7E-A8A8EF3FCB3E}
{76F4D0AE-2123-493B-B721-4118330C52BB} = {909FC1DB-3083-4F01-8496-B8C9DD4FEA13}
{42E0EFC6-4990-4395-A9D1-8683778751E7} = {909FC1DB-3083-4F01-8496-B8C9DD4FEA13}
EndGlobalSection
EndGlobal
29 changes: 29 additions & 0 deletions codehelp/CodeHelpers.Test/CodeHelpers.Test.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2" />
<ProjectReference Include="..\CodeHelpers\WPILib.CodeHelpers.csproj"/>
<ProjectReference Include="..\..\src\thirdparty\Stereologue\Stereologue.csproj" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.8.0" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
49 changes: 49 additions & 0 deletions codehelp/CodeHelpers.Test/LogGeneratorFixerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using WPILib.CodeHelpers.LogGenerator.Analyzer;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.CodeFixVerifier<WPILib.CodeHelpers.LogGenerator.Analyzer.LogGeneratorAnalyzer, WPILib.CodeHelpers.LogGenerator.CodeFixer.LogGeneratorFixer>;

namespace CodeHelpers.Test;

public class LogGeneratorFixerTest
{
const string InternalTypes = @"
namespace Stereologue
{
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class LogAttribute : System.Attribute
{
}
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)]
public sealed class GenerateLogAttribute : System.Attribute
{
}
}
";
[Fact]
public async void Test1()
{
string testString = @"
using Stereologue;
public partial class MyNewClass
{
[Log]
public int Variable { get; }
}
";
string fixedCode = @"
using Stereologue;
[GenerateLog]
public partial class MyNewClass
{
[Log]
public int Variable { get; }
}";
testString += InternalTypes;
fixedCode += InternalTypes;
var expected = Verify.Diagnostic(LoggerDiagnostics.MissingGenerateLog).WithLocation(3, 22).WithArguments(["Variable", "MyNewClass"]);
await Verify.VerifyCodeFixAsync(testString, expected, fixedCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers;

/// <summary>
/// Extensions for <see cref="EquatableArray{T}"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

#pragma warning disable CS0809, IDE0009, IDE1006, IDE0048

namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers;

/// <summary>
/// A polyfill type that mirrors some methods from <see cref="HashCode"/> on .7.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers.LogGenerator.Analyzer;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class GenerateLogAnalyzer : DiagnosticAnalyzer
public sealed class LogGeneratorAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create([
LoggerDiagnostics.UnknownMemberType,
Expand All @@ -16,14 +16,34 @@ public sealed class GenerateLogAnalyzer : DiagnosticAnalyzer
LoggerDiagnostics.LoggedHasUnknownType,
LoggerDiagnostics.UnknownFailureMode,
LoggerDiagnostics.NullableStructArray,
LoggerDiagnostics.UnknownSpecialTypeIntArray
LoggerDiagnostics.UnknownSpecialTypeIntArray,
LoggerDiagnostics.MissingGenerateLog,
]);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
context.RegisterSymbolAction(AnalyzeMembers, SymbolKind.Method | SymbolKind.Field | SymbolKind.Property);
}

private static void AnalyzeMembers(SymbolAnalysisContext context)
{
if (!context.Symbol.HasLogAttribute()) {
return;
}

var containingType = context.Symbol.ContainingType;
if (containingType is null) {
return;
}

if (!containingType.HasGenerateLogAttribute()) {
foreach (var location in containingType.Locations) {
context.ReportDiagnostic(Diagnostic.Create(LoggerDiagnostics.MissingGenerateLog, location, context.Symbol.Name, containingType.ToDisplayString()));
}
}
}

private static void AnalyzeSymbol(SymbolAnalysisContext context)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers.LogGenerator.Analyzer;

#pragma warning disable RS2008 // Enable analyzer release tracking

using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using WPILib.CodeHelpers;

public static class LoggerDiagnostics
{
Expand All @@ -20,28 +21,31 @@ public class Ids
public const string UnknownFailureMode = Prefix + "1006";
public const string NullableStructArray = Prefix + "1007";
public const string UnknownSpecialTypeIntArray = Prefix + "1008";
public const string MissingGenerateLog = Prefix + "1008";
}

private const string Category = "StereologueSourceGenerator";
private const string Category = "SourceGeneration";

public static readonly DiagnosticDescriptor UnknownMemberType = new(
Ids.UnknownMemberType, "Loggable member call type is unknown", "[Log] attribute cannot be applied to member {0}. Member type is unknown.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
Ids.UnknownMemberType, "Loggable member call type is unknown", "[Log] attribute cannot be applied to member '{0}'. Member type is unknown.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
public static readonly DiagnosticDescriptor ProtobufIsArray = new(
Ids.ProtobufIsArray, "Loggable member is array of protobufs", "[Log] attribute cannot be applied to member {0}. Cannot log array of Protobufs.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
Ids.ProtobufIsArray, "Loggable member is array of protobufs", "[Log] attribute cannot be applied to member '{0}'. Cannot log array of Protobufs.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
public static readonly DiagnosticDescriptor UnknownSpecialTypeArray = new(
Ids.UnknownSpecialTypeArray, "Loggable member has invalid type for array use", "[Log] attribute cannot be applied to member {0}. Cannot log arrays of type '{1}'.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
Ids.UnknownSpecialTypeArray, "Loggable member has invalid type for array use", "[Log] attribute cannot be applied to member '{0}'. Cannot log arrays of type '{1}'.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
public static readonly DiagnosticDescriptor LoggedMethodReturnsVoid = new(
Ids.LoggedMethodReturnsVoid, "Loggable method returns void", "[Log] attribute cannot be applied to member {0}. Cannot log from a void returning method.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
Ids.LoggedMethodReturnsVoid, "Loggable method returns void", "[Log] attribute cannot be applied to member '{0}'. Cannot log from a void returning method.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
public static readonly DiagnosticDescriptor LoggedMethodTakeParameters = new(
Ids.LoggedMethodTakeParameters, "Loggable method takes parameters", "[Log] attribute cannot be applied to member {0}. Cannot log from a method taking parameters.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
Ids.LoggedMethodTakeParameters, "Loggable method takes parameters", "[Log] attribute cannot be applied to member '{0}'. Cannot log from a method taking parameters.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
public static readonly DiagnosticDescriptor LoggedHasUnknownType = new(
Ids.LoggedHasUnknownType, "Loggable member type is not loggable", "[Log] attribute cannot be applied to member {0}; cannot log type '{1}'", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
Ids.LoggedHasUnknownType, "Loggable member type is not loggable", "[Log] attribute cannot be applied to member '{0}'; cannot log type '{1}'", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
public static readonly DiagnosticDescriptor UnknownFailureMode = new(
Ids.UnknownFailureMode, "Unknown Failure Mode", "Failure mode has no diagnostic. Report to RobotDotNet.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
public static readonly DiagnosticDescriptor NullableStructArray = new(
Ids.NullableStructArray, "Loggable member is array of Nullable<Struct>", "[Log] attribute cannot be applied to member {0}. Cannot log arrays of Nullable Structs.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
Ids.NullableStructArray, "Loggable member is array of Nullable<Struct>", "[Log] attribute cannot be applied to member '{0}'. Cannot log arrays of Nullable Structs.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
public static readonly DiagnosticDescriptor UnknownSpecialTypeIntArray = new(
Ids.UnknownSpecialTypeIntArray, "Loggable member has invalid integer type for array use", "[Log] attribute cannot be applied to member {0}. Cannot log arrays of type '{1}'. Can only log arrays of 'long'.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
Ids.UnknownSpecialTypeIntArray, "Loggable member has invalid integer type for array use", "[Log] attribute cannot be applied to member '{0}'. Cannot log arrays of type '{1}'. Can only log arrays of 'long'.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");
public static readonly DiagnosticDescriptor MissingGenerateLog = new(
Ids.MissingGenerateLog, "Type has Log member but is not GenerateLog", "Member '{0}' has [Log] attribute, however containing type {1} is not attributed with [GenerateLog]. No logging will be generated for this member.", Category, DiagnosticSeverity.Error, isEnabledByDefault: true, "");

public static void ReportDiagnostic(this SymbolAnalysisContext context, FailureMode failureMode, ISymbol symbol)
{
Expand Down Expand Up @@ -89,6 +93,9 @@ public static void ReportDiagnostic(this SymbolAnalysisContext context, FailureM
case FailureMode.NullableStructArray:
context.ReportDiagnostic(Diagnostic.Create(NullableStructArray, location, symbol.Name));
break;
case FailureMode.MissingGenerateLog:
context.ReportDiagnostic(Diagnostic.Create(MissingGenerateLog, location, symbol.Name, symbol.ContainingType?.ToDisplayString()));
break;
default:
context.ReportDiagnostic(Diagnostic.Create(UnknownFailureMode, location));
break;
Expand Down
58 changes: 58 additions & 0 deletions codehelp/CodeHelpers/LogGenerator/CodeFixer/LogGeneratorFixer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using WPILib.CodeHelpers.LogGenerator.Analyzer;

namespace WPILib.CodeHelpers.LogGenerator.CodeFixer;

[ExportCodeFixProvider(LanguageNames.CSharp)]
public class LogGeneratorFixer : CodeFixProvider
{
private const string title = "Make constant";

public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create([
LoggerDiagnostics.Ids.MissingGenerateLog,
]);

public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;

// Find the type declaration identified by the diagnostic.
var declaration = root!.FindToken(diagnosticSpan.Start).Parent!.AncestorsAndSelf().OfType<TypeDeclarationSyntax>().First();

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedDocument: c => AddGenerateLogAttribute(context.Document, declaration!, c),
equivalenceKey: title),
diagnostic);
}

private async Task<Document> AddGenerateLogAttribute(Document document, TypeDeclarationSyntax typeSyntax, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken)!;
var attributes = typeSyntax.AttributeLists.Add(
SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("GenerateLog"))
)));

return document.WithSyntaxRoot(
root!.ReplaceNode(
typeSyntax,
typeSyntax.WithAttributeLists(attributes).NormalizeWhitespace()
));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers.LogGenerator;

public enum FailureMode
{
Expand All @@ -11,4 +11,5 @@ public enum FailureMode
MethodHasParameters,
UnknownTypeToLog,
NullableStructArray,
MissingGenerateLog,
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers.LogGenerator;

// Contains all information about a [Log] attribute
internal record LogAttributeInfo(string? Path, string LogLevel, string LogType, bool UseProtobuf);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers.LogGenerator;

internal enum MemberType
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using WPILib.CodeHelpers;
using WPILib.CodeHelpers.LogGenerator.Analyzer;

namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers.LogGenerator;


internal record TypeDeclType(TypeKind TypeKind, bool IsReadOnly, bool IsRefLikeType, bool IsRecord);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using WPILib.CodeHelpers.LogGenerator;

namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers.LogGenerator.SourceGenerator;

[Generator]
public class LogGenerator : IIncrementalGenerator
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers.LogGenerator;

public static class Strings
{
public const string GenerateLogAttributeName = "Stereologue.GenerateLogAttribute";
public const string LogAttributeName = "Stereologue.LogAttribute";
public const string ILoggedName = "Stereologue.ILogged";
public const string UpdateStereologueName = "UpdateStereologue";
}
16 changes: 16 additions & 0 deletions codehelp/CodeHelpers/LogGenerator/SymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.CodeAnalysis;

namespace WPILib.CodeHelpers.LogGenerator;

public static class SymbolExtensions
{
public static bool HasLogAttribute(this ISymbol symbol)
{
return symbol.GetAttributes().Where(x => x.AttributeClass?.ToDisplayString() == Strings.LogAttributeName).Any();
}

public static bool HasGenerateLogAttribute(this ISymbol symbol)
{
return symbol.GetAttributes().Where(x => x.AttributeClass?.ToDisplayString() == Strings.GenerateLogAttributeName).Any();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;

namespace Stereologue;
namespace WPILib.CodeHelpers;

public static class SpecialTypeExtensions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Stereologue.SourceGenerator;
namespace WPILib.CodeHelpers;

public static class SyntaxExtensions
{
Expand Down
Loading

0 comments on commit ad431ae

Please sign in to comment.