From 5be304ed3bbf804566c264d6d604f480be375d7f Mon Sep 17 00:00:00 2001 From: Thad House Date: Mon, 5 Aug 2024 01:14:54 -0700 Subject: [PATCH] Big start on the epilogue source generator --- .../LogGenerator/EpilogueLogGeneratorTest.cs | 65 +++ .../EpilogueGenerator/CustomLoggerType.cs | 43 ++ .../EpilogueGenerator/FailureMode.cs | 15 + .../EpilogueGenerator/LogAttributeInfo.cs | 229 +++++++++ .../EpilogueGenerator/LoggableMember.cs | 451 ++++++++++++++++++ .../EpilogueGenerator/LoggableType.cs | 201 ++++++++ .../SourceGenerator/EpilogueGeneratorSharp.cs | 56 +++ .../CodeHelpers/EpilogueGenerator/Strings.cs | 14 + .../EpilogueGenerator/SymbolExtensions.cs | 105 ++++ .../CodeHelpers/LogGenerator/LoggableType.cs | 8 +- codehelp/CodeHelpers/TypeDeclarationModel.cs | 9 +- .../CodeHelpers/WPILib.CodeHelpers.csproj | 2 + src/epilogue/LogImportance.cs | 5 + src/epilogue/LogStrategy.cs | 5 + test/epilogue.test/ClassSpecificLoggerTest.cs | 3 + 15 files changed, 1205 insertions(+), 6 deletions(-) create mode 100644 codehelp/CodeHelpers.Test/LogGenerator/EpilogueLogGeneratorTest.cs create mode 100644 codehelp/CodeHelpers/EpilogueGenerator/CustomLoggerType.cs create mode 100644 codehelp/CodeHelpers/EpilogueGenerator/FailureMode.cs create mode 100644 codehelp/CodeHelpers/EpilogueGenerator/LogAttributeInfo.cs create mode 100644 codehelp/CodeHelpers/EpilogueGenerator/LoggableMember.cs create mode 100644 codehelp/CodeHelpers/EpilogueGenerator/LoggableType.cs create mode 100644 codehelp/CodeHelpers/EpilogueGenerator/SourceGenerator/EpilogueGeneratorSharp.cs create mode 100644 codehelp/CodeHelpers/EpilogueGenerator/Strings.cs create mode 100644 codehelp/CodeHelpers/EpilogueGenerator/SymbolExtensions.cs diff --git a/codehelp/CodeHelpers.Test/LogGenerator/EpilogueLogGeneratorTest.cs b/codehelp/CodeHelpers.Test/LogGenerator/EpilogueLogGeneratorTest.cs new file mode 100644 index 00000000..4f27a92f --- /dev/null +++ b/codehelp/CodeHelpers.Test/LogGenerator/EpilogueLogGeneratorTest.cs @@ -0,0 +1,65 @@ +namespace CodeHelpers.Test.LogGenerator; + +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using Stereologue; +using WPILib.CodeHelpers.LogGenerator.SourceGenerator; + +public class EpilogueGeneratorTest +{ + [Theory] + [InlineData("bool", "LogBoolean")] + [InlineData("int", "LogInteger")] + [InlineData("long", "LogInteger")] + public async Task TestPrimitives(string type, string output) + { + string testString = @" +using Epilogue; + +[Logged] +public partial class MyNewClass +{ + [Logged] + public REPLACEME Variable() { return default; } +} +"; + + string expected = @"partial class MyNewClass + : global::Stereologue.ILogged +{ + public void UpdateStereologue(string path, global::Stereologue.Stereologuer logger) + { + logger.REPLACEME($""{path}/Variable"", global::Stereologue.LogType.File | global::Stereologue.LogType.Nt, Variable(), global::Stereologue.LogLevel.Default); + } +} +"; + testString = testString.Replace("REPLACEME", type); + + // Due to StringBuilder in the source generator, + // We must normalize the output line endings + expected = expected.NormalizeLineEndings(); + expected = expected.Replace("REPLACEME", output); + + await new CSharpSourceGeneratorTest() + { + TestState = { + AdditionalReferences = { + typeof(LogAttribute).Assembly + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + Sources = { + testString, + }, + AnalyzerConfigFiles = { + ("/.editorconfig", SourceText.From(TestHelpers.EditorConfig, Encoding.UTF8)) + }, + GeneratedSources = { + ($"WPILib.CodeHelpers{Path.DirectorySeparatorChar}WPILib.CodeHelpers.EpilogueGenerator.SourceGenerator.EpilogueGeneratorSharp{Path.DirectorySeparatorChar}MyNewClass.g.cs", SourceText.From(expected, Encoding.UTF8)) + }, + }, + }.RunAsync(); + } +} diff --git a/codehelp/CodeHelpers/EpilogueGenerator/CustomLoggerType.cs b/codehelp/CodeHelpers/EpilogueGenerator/CustomLoggerType.cs new file mode 100644 index 00000000..cfbdfbe8 --- /dev/null +++ b/codehelp/CodeHelpers/EpilogueGenerator/CustomLoggerType.cs @@ -0,0 +1,43 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace WPILib.CodeHelpers.EpilogueGenerator; + +public record CustomLoggerType(TypeDeclarationModel TypeDeclarations, EquatableArray SupportedTypes); + +internal static class CustomLoggerTypeExtensions +{ + public static CustomLoggerType GetCustomLoggerType(this ImmutableArray attributes, INamedTypeSymbol symbol, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + var loggableTypes = ImmutableArray.CreateBuilder(1); + + foreach (var attribute in attributes) + { + token.ThrowIfCancellationRequested(); + foreach (var named in attribute.NamedArguments) + { + token.ThrowIfCancellationRequested(); + if (named.Key == "Types") + { + if (!named.Value.IsNull && named.Value.Kind is TypedConstantKind.Array) + { + foreach (var value in named.Value.Values) + { + token.ThrowIfCancellationRequested(); + if (!value.IsNull && value.Kind is TypedConstantKind.Type && value.Value is INamedTypeSymbol typeFor) + { + loggableTypes.Add(typeFor.GetTypeDeclarationModel()); + } + } + } + } + } + } + + token.ThrowIfCancellationRequested(); + + return new(symbol.GetTypeDeclarationModel(), loggableTypes.ToImmutable()); + } +} diff --git a/codehelp/CodeHelpers/EpilogueGenerator/FailureMode.cs b/codehelp/CodeHelpers/EpilogueGenerator/FailureMode.cs new file mode 100644 index 00000000..0d83c580 --- /dev/null +++ b/codehelp/CodeHelpers/EpilogueGenerator/FailureMode.cs @@ -0,0 +1,15 @@ +namespace WPILib.CodeHelpers.EpilogueGenerator; + +public enum FailureMode +{ + None, + AttributeUnknownMemberType, + ProtobufArray, + UnknownTypeNonArray, + UnknownTypeArray, + MethodReturnsVoid, + MethodHasParameters, + UnknownTypeToLog, + NullableStructArray, + MissingGenerateLog, +} diff --git a/codehelp/CodeHelpers/EpilogueGenerator/LogAttributeInfo.cs b/codehelp/CodeHelpers/EpilogueGenerator/LogAttributeInfo.cs new file mode 100644 index 00000000..8ae3ffb6 --- /dev/null +++ b/codehelp/CodeHelpers/EpilogueGenerator/LogAttributeInfo.cs @@ -0,0 +1,229 @@ +using System.Collections.Immutable; +using System.Text; +using Epilogue; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace WPILib.CodeHelpers.EpilogueGenerator; + +// Contains all information about a [Logged] attribute +public record LogAttributeInfo(string? Name, LogStrategy LogStrategy, LogImportance LogImportance) +{ + // public string GetLogStrageyString(LanguageKind language) + // { + // if (language == LanguageKind.CSharp) + // { + // return AllLevelValues[(int)LogLevel]; + // } + // else if (language == LanguageKind.VisualBasic) + // { + // return AllLevelValuesVb[(int)LogLevel]; + // } + // return ""; + // } + + // public string GetLogTypeString(LanguageKind language) + // { + // if (language == LanguageKind.CSharp) + // { + // return AllTypeValues[(int)LogType]; + // } + // else if (language == LanguageKind.VisualBasic) + // { + // return AllTypeValuesVb[(int)LogType]; + // } + // return ""; + // } + + // private static ImmutableList GetAllLevelValues() + // { + // LogLevel[] allLogLevels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); + // var builder = ImmutableList.CreateBuilder(); + // string fullName = typeof(LogLevel).FullName; + // string rootName = $"global::{fullName}."; + // foreach (var i in allLogLevels) + // { + // builder.Add($"{rootName}{i}"); + // } + // return builder.ToImmutable(); + // } + + // private static ImmutableList GetAllTypeValues() + // { + // LogType[] allLogTypes = (LogType[])Enum.GetValues(typeof(LogType)); + // var builder = ImmutableList.CreateBuilder(); + // LogType baseLog = LogType.None; + // foreach (var i in allLogTypes) + // { + // baseLog |= i; + // } + // int permutations = (int)baseLog; + // permutations += 1; + // string fullName = typeof(LogType).FullName; + // string rootName = $"global::{fullName}."; + // builder.Add($"{rootName}{nameof(LogType.None)}"); + // StringBuilder stringBuilder = new(); + // for (int i = 1; i < permutations; i++) + // { + // LogType type = (LogType)i; + + // if ((type & LogType.File) != 0) + // { + // if (stringBuilder.Length != 0) + // { + // stringBuilder.Append(" | "); + // } + // stringBuilder.Append($"{rootName}{nameof(LogType.File)}"); + // } + // if ((type & LogType.Nt) != 0) + // { + // if (stringBuilder.Length != 0) + // { + // stringBuilder.Append(" | "); + // } + // stringBuilder.Append($"{rootName}{nameof(LogType.Nt)}"); + // } + // if ((type & LogType.Once) != 0) + // { + // if (stringBuilder.Length != 0) + // { + // stringBuilder.Append(" | "); + // } + // stringBuilder.Append($"{rootName}{nameof(LogType.Once)}"); + // } + // builder.Add(stringBuilder.ToString()); + // stringBuilder.Clear(); + // } + + // return builder.ToImmutable(); + // } + + // private static ImmutableList GetAllLevelValuesVb() + // { + // LogLevel[] allLogLevels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); + // var builder = ImmutableList.CreateBuilder(); + // string fullName = typeof(LogLevel).FullName; + // string rootName = $"Global.{fullName}."; + // foreach (var i in allLogLevels) + // { + // builder.Add($"{rootName}{i}"); + // } + // return builder.ToImmutable(); + // } + + // private static ImmutableList GetAllTypeValuesVb() + // { + // LogType[] allLogTypes = (LogType[])Enum.GetValues(typeof(LogType)); + // var builder = ImmutableList.CreateBuilder(); + // LogType baseLog = LogType.None; + // foreach (var i in allLogTypes) + // { + // baseLog |= i; + // } + // int permutations = (int)baseLog; + // permutations += 1; + // string fullName = typeof(LogType).FullName; + // string rootName = $"Global.{fullName}."; + // builder.Add($"{rootName}{nameof(LogType.None)}"); + // StringBuilder stringBuilder = new(); + // for (int i = 1; i < permutations; i++) + // { + // LogType type = (LogType)i; + + // if ((type & LogType.File) != 0) + // { + // if (stringBuilder.Length != 0) + // { + // stringBuilder.Append(" Or "); + // } + // stringBuilder.Append($"{rootName}{nameof(LogType.File)}"); + // } + // if ((type & LogType.Nt) != 0) + // { + // if (stringBuilder.Length != 0) + // { + // stringBuilder.Append(" Or "); + // } + // stringBuilder.Append($"{rootName}{nameof(LogType.Nt)}"); + // } + // if ((type & LogType.Once) != 0) + // { + // if (stringBuilder.Length != 0) + // { + // stringBuilder.Append(" Or "); + // } + // stringBuilder.Append($"{rootName}{nameof(LogType.Once)}"); + // } + // builder.Add(stringBuilder.ToString()); + // stringBuilder.Clear(); + // } + + // return builder.ToImmutable(); + // } + + // public static ImmutableList AllTypeValues { get; } = GetAllTypeValues(); + + // public static ImmutableList AllLevelValues { get; } = GetAllLevelValues(); + + // public static ImmutableList AllTypeValuesVb { get; } = GetAllTypeValuesVb(); + + // public static ImmutableList AllLevelValuesVb { get; } = GetAllLevelValuesVb(); +} + +internal static class LogAttributeInfoExtensions +{ + public static LogAttributeInfo? ToAttributeInfo(this AttributeData attributeData, INamedTypeSymbol? attributeClass, CancellationToken token, out bool notLogged) + { + if (attributeClass is null) + { + notLogged = false; + return null; + } + + if (attributeClass.IsNotLoggedAttributeClass()) + { + notLogged = true; + return null; + } + notLogged = false; + if (attributeClass.IsLoggedAttributeClass()) + { + token.ThrowIfCancellationRequested(); + + string? path = null; + LogStrategy logStrategyEnum = LogStrategyExtensions.DefaultLogStrategy; + LogImportance logImportanceEnum = LogImportanceExtensions.DefaultLogImportance; + + // Get the log attribute + foreach (var named in attributeData.NamedArguments) + { + if (named.Key == "Name") + { + if (!named.Value.IsNull) + { + path = SymbolDisplay.FormatPrimitive(named.Value.Value!, false, false); + } + token.ThrowIfCancellationRequested(); + } + else if (named.Key == "Strategy") + { + // A boxed primitive can be unboxed to an enum with the same underlying type. + logStrategyEnum = (LogStrategy)named.Value.Value!; + token.ThrowIfCancellationRequested(); + } + else if (named.Key == "Importance") + { + // A boxed primitive can be unboxed to an enum with the same underlying type. + logImportanceEnum = (LogImportance)named.Value.Value!; + token.ThrowIfCancellationRequested(); + } + } + + return new LogAttributeInfo(path, logStrategyEnum, logImportanceEnum); + } + return null; + } + + +} diff --git a/codehelp/CodeHelpers/EpilogueGenerator/LoggableMember.cs b/codehelp/CodeHelpers/EpilogueGenerator/LoggableMember.cs new file mode 100644 index 00000000..b6015393 --- /dev/null +++ b/codehelp/CodeHelpers/EpilogueGenerator/LoggableMember.cs @@ -0,0 +1,451 @@ +using Microsoft.CodeAnalysis; + +namespace WPILib.CodeHelpers.EpilogueGenerator; + +public enum MemberType +{ + Field, + Property, + Method +} + +public enum DeclarationType +{ + None, + Logged, + Struct, + SpecialType, + PotentialCustomLogger, +} + +public enum DeclarationKind +{ + None, + ReadOnlySpan, + Span, + ReadOnlyMemory, + Memory, + Array, + NullableValueType, + NullableReferenceType +} + +public record MemberDeclaration(DeclarationType LoggedType, SpecialType SpecialType, DeclarationKind LoggedKind); + +// Contains all information about a loggable member +public record LoggableMember(string Name, MemberType MemberType, MemberDeclaration MemberDeclaration, LogAttributeInfo AttributeInfo) +{ + public FailureMode WriteLogCall(IndentedStringBuilder? builder) + { + return FailureMode.None; + // var getOperation = MemberType switch + // { + // MemberType.Field => Name, + // MemberType.Property => Name, + // MemberType.Method => $"{Name}()", + // _ => null + // }; + + // if (getOperation is null) + // { + // // Attribute applied to unknown type + // return FailureMode.AttributeUnknownMemberType; + // } + + // var path = string.IsNullOrWhiteSpace(AttributeInfo.Path) ? Name : AttributeInfo.Path; + + // if (MemberDeclaration.LoggedType == DeclarationType.Logged) + // { + // if (builder is null) + // { + // // At this point, we cannot error anymore. Just bail early if the string builder is null + // return FailureMode.None; + // } + // if (MemberDeclaration.LoggedKind == DeclarationKind.None || MemberDeclaration.LoggedKind == DeclarationKind.NullableValueType || MemberDeclaration.LoggedKind == DeclarationKind.NullableReferenceType) + // { + // string nullCheck = ""; + // if (MemberDeclaration.LoggedKind != DeclarationKind.None) + // { + // nullCheck = "?"; + // } + // string semi = builder.Language == LanguageKind.VisualBasic ? "" : ";"; + // builder.AppendFullLine($"{getOperation}{nullCheck}.{Strings.UpdateStereologueName}($\"{{path}}/{path}\", logger){semi}"); + // } + // else + // { + // // We're an array, loop + // if (builder.Language == LanguageKind.CSharp) + // { + // builder.AppendFullLine($"foreach (var __tmpValue in {getOperation})"); + // } + // else if (builder.Language == LanguageKind.VisualBasic) + // { + // builder.AppendFullLine($"For Each __tmpValue in {getOperation}"); + // } + // builder.EnterScope(ScopeType.ForEach); + // string nullCheck = ""; + // if (MemberDeclaration.LoggedKind != DeclarationKind.None) + // { + // nullCheck = "?"; + // } + // string semi = builder.Language == LanguageKind.VisualBasic ? "" : ";"; + // builder.AppendFullLine($"__tmpValue{nullCheck}.{Strings.UpdateStereologueName}($\"{{path}}/{path}\", logger){semi}"); + // builder.ExitScope(); + // } + // return FailureMode.None; + // } + + // string? logMethod; + + // if (MemberDeclaration.LoggedType == DeclarationType.Struct) + // { + // if (MemberDeclaration.LoggedKind != DeclarationKind.None && MemberDeclaration.LoggedKind != DeclarationKind.NullableValueType && MemberDeclaration.LoggedKind != DeclarationKind.NullableReferenceType) + // { + // // We're an array + // logMethod = "LogStructArray"; + // } + // else + // { + // logMethod = "LogStruct"; + // } + // } + + // else if (MemberDeclaration.LoggedType == DeclarationType.Protobuf) + // { + + // if (MemberDeclaration.LoggedKind != DeclarationKind.None && MemberDeclaration.LoggedKind != DeclarationKind.NullableValueType && MemberDeclaration.LoggedKind != DeclarationKind.NullableReferenceType) + // { + // // Protobuf is array + // return FailureMode.ProtobufArray; + // } + // else + // { + // logMethod = "LogProto"; + // } + // } + // else if (MemberDeclaration.LoggedKind == DeclarationKind.None || MemberDeclaration.LoggedKind == DeclarationKind.NullableReferenceType || MemberDeclaration.LoggedKind == DeclarationKind.NullableValueType) + // { + // // We're not an array. We're either Nullable or a plain type + // if (MemberDeclaration.SpecialType == SpecialType.System_UInt64 || MemberDeclaration.SpecialType == SpecialType.System_IntPtr || MemberDeclaration.SpecialType == SpecialType.System_UIntPtr) + // { + // getOperation = $"(long){getOperation}"; + // } + + // logMethod = MemberDeclaration.SpecialType switch + // { + // SpecialType.System_Char => "LogChar", + // SpecialType.System_String => "LogString", + // SpecialType.System_Boolean => "LogBoolean", + // SpecialType.System_Single => "LogFloat", + // SpecialType.System_Double => "LogDouble", + // SpecialType.System_Byte => "LogInteger", + // SpecialType.System_SByte => "LogInteger", + // SpecialType.System_Int16 => "LogInteger", + // SpecialType.System_UInt16 => "LogInteger", + // SpecialType.System_Int32 => "LogInteger", + // SpecialType.System_UInt32 => "LogInteger", + // SpecialType.System_Int64 => "LogInteger", + // SpecialType.System_UInt64 => "LogInteger", + // SpecialType.System_IntPtr => "LogInteger", + // SpecialType.System_UIntPtr => "LogInteger", + // _ => null + // }; + + // if (logMethod is null) + // { + // // SpecialType is unknown, for non array + // return FailureMode.UnknownTypeNonArray; + // } + // } + // else + // { + // // We're array of a basic type + + // logMethod = MemberDeclaration.SpecialType switch + // { + // SpecialType.System_String => "LogStringArray", + // SpecialType.System_Boolean => "LogBooleanArray", + // SpecialType.System_Single => "LogFloatArray", + // SpecialType.System_Double => "LogDoubleArray", + // SpecialType.System_Byte => "LogRaw", + // SpecialType.System_Int64 => "LogIntegerArray", + // _ => null + // }; + + // if (logMethod is null) + // { + // // SpecialType is unknown for array + // return FailureMode.UnknownTypeArray; + // } + // } + + // if (builder is null) + // { + // // At this point, we cannot error anymore. Just bail early if the string builder is null + // return FailureMode.None; + // } + + // if (MemberDeclaration.LoggedKind == DeclarationKind.Array || MemberDeclaration.LoggedKind == DeclarationKind.NullableReferenceType || MemberDeclaration.LoggedKind == DeclarationKind.NullableValueType) + // { + // builder.EnterScope(ScopeType.Empty); + // if (builder.Language == LanguageKind.CSharp) + // { + // builder.AppendFullLine($"var __tmpValue = {getOperation};"); + // builder.AppendFullLine($"if (__tmpValue is not null)"); + // } + // else if (builder.Language == LanguageKind.VisualBasic) + // { + // builder.AppendFullLine($"Dim __tmpValue = {getOperation}"); + // builder.AppendFullLine($"If __tmpValue IsNot Nothing"); + // } + // builder.EnterScope(ScopeType.If); + // getOperation = "__tmpValue"; + // if (MemberDeclaration.SpecialType == SpecialType.System_String || MemberDeclaration.LoggedKind == DeclarationKind.Array) + // { + // getOperation = $"{getOperation}.AsSpan()"; + // } + // if (MemberDeclaration.LoggedKind == DeclarationKind.NullableValueType) + // { + // getOperation = $"{getOperation}.Value"; + // } + // string semi = builder.Language == LanguageKind.VisualBasic ? "" : ";"; + // builder.AppendFullLine($"logger.{logMethod}($\"{{path}}/{path}\", {AttributeInfo.GetLogTypeString(builder.Language)}, {getOperation}, {AttributeInfo.GetLogLevelString(builder.Language)}){semi}"); + // builder.ExitScope(); // If + // builder.ExitScope(); // Empty + // } + // else if (MemberDeclaration.LoggedKind == DeclarationKind.ReadOnlyMemory || MemberDeclaration.LoggedKind == DeclarationKind.Memory) + // { + // string semi = builder.Language == LanguageKind.VisualBasic ? "" : ";"; + // builder.AppendFullLine($"logger.{logMethod}($\"{{path}}/{path}\", {AttributeInfo.GetLogTypeString(builder.Language)}, {getOperation}.Span, {AttributeInfo.GetLogLevelString(builder.Language)}){semi}"); + // } + // else + // { + // string semi = builder.Language == LanguageKind.VisualBasic ? "" : ";"; + // builder.AppendFullLine($"logger.{logMethod}($\"{{path}}/{path}\", {AttributeInfo.GetLogTypeString(builder.Language)}, {getOperation}, {AttributeInfo.GetLogLevelString(builder.Language)}){semi}"); + // } + + // return FailureMode.None; + } +} + +internal static class LoggableMemberExtensions +{ + public static DeclarationKind GetInnerType(this ITypeSymbol typeSymbol, out ITypeSymbol innerType) + { + // Check if we're an array + if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) + { + innerType = arrayTypeSymbol.ElementType; + return DeclarationKind.Array; + } + + INamedTypeSymbol namedTypeSymbol; + + // Check if we're a nullable + if (typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + namedTypeSymbol = (INamedTypeSymbol)typeSymbol; + innerType = namedTypeSymbol.TypeArguments[0]; + return DeclarationKind.NullableValueType; + } + + if (typeSymbol.IsReadOnlySpan()) + { + namedTypeSymbol = (INamedTypeSymbol)typeSymbol; + innerType = namedTypeSymbol.TypeArguments[0]; + return DeclarationKind.ReadOnlySpan; + } + else if (typeSymbol.IsSpan()) + { + namedTypeSymbol = (INamedTypeSymbol)typeSymbol; + innerType = namedTypeSymbol.TypeArguments[0]; + return DeclarationKind.Span; + } + else if (typeSymbol.IsReadOnlyMemory()) + { + namedTypeSymbol = (INamedTypeSymbol)typeSymbol; + innerType = namedTypeSymbol.TypeArguments[0]; + return DeclarationKind.ReadOnlyMemory; + } + else if (typeSymbol.IsMemory()) + { + namedTypeSymbol = (INamedTypeSymbol)typeSymbol; + innerType = namedTypeSymbol.TypeArguments[0]; + return DeclarationKind.Memory; + } + + innerType = typeSymbol; + return innerType.IsReferenceType ? DeclarationKind.NullableReferenceType : DeclarationKind.None; + } + + private static MemberDeclaration? GetDeclarationType(this ITypeSymbol typeSymbol, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + var nestedKind = typeSymbol.GetInnerType(out typeSymbol); + + token.ThrowIfCancellationRequested(); + + if (typeSymbol.SpecialType != SpecialType.None) + { + // We're a built in special type, no need to check for anything else + return new(DeclarationType.SpecialType, typeSymbol.SpecialType, nestedKind); + } + + // See if we need to unwrap a nullable array + bool innerNullable = false; + if (typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + var namedTypeSymbol = (INamedTypeSymbol)typeSymbol; + typeSymbol = namedTypeSymbol.TypeArguments[0]; + innerNullable = true; + } + + // If we know we're generating a loggable implementation + if (typeSymbol.HasLoggedAttribute()) + { + return new(DeclarationType.Logged, (innerNullable | typeSymbol.IsReferenceType) ? SpecialType.System_Nullable_T : SpecialType.None, nestedKind); + } + // token.ThrowIfCancellationRequested(); + // // If we know we already implement ILogged + // if (typeSymbol.HasILoggedInterface()) + // { + // return new(DeclarationType.Logged, (innerNullable | typeSymbol.IsReferenceType) ? SpecialType.System_Nullable_T : SpecialType.None, nestedKind); + // } + // token.ThrowIfCancellationRequested(); + // // If we have an UpdateMonologue function + // var members = typeSymbol.GetMembers(Strings.UpdateStereologueName); + // foreach (var member in members) + // { + // token.ThrowIfCancellationRequested(); + // // Must be a method + // if (member is IMethodSymbol method) + // { + // // Must return void + // if (!method.ReturnsVoid) + // { + // continue; + // } + // // Must have a string first parameter, and a Stereologue.Stereologuer second paramter + // var parameters = method.Parameters; + // if (parameters.Length != 2) + // { + // continue; + // } + // if (parameters[0].Type.SpecialType == SpecialType.System_String && parameters[1].Type.IsLoggerType()) + // { + // return new(DeclarationType.Logged, (innerNullable | typeSymbol.IsReferenceType) ? SpecialType.System_Nullable_T : SpecialType.None, nestedKind); + // } + // } + // } + + token.ThrowIfCancellationRequested(); + + foreach (var inf in typeSymbol.AllInterfaces) + { + token.ThrowIfCancellationRequested(); + if (inf.IsStructSerializable()) + { + // If we're an array, make sure we're not a nullable + if (nestedKind == DeclarationKind.ReadOnlySpan || nestedKind == DeclarationKind.ReadOnlyMemory || nestedKind == DeclarationKind.Array || nestedKind == DeclarationKind.Memory || nestedKind == DeclarationKind.Span) + { + if (innerNullable) + { + return new(DeclarationType.Struct, SpecialType.System_Nullable_T, nestedKind); + } + } + return new(DeclarationType.Struct, SpecialType.None, nestedKind); + } + } + + // We get here by attempting to log a type we have no clue about. It could be a custom logger + // For now, only custom loggers are supported on bare symbols + if (nestedKind is DeclarationKind.None || nestedKind is DeclarationKind.NullableReferenceType) + { + return new(DeclarationType.PotentialCustomLogger, SpecialType.None, nestedKind); + } + // Otherwise, we can't log it + return null; + } + + public static FailureMode ToLoggableMember(this ISymbol member, CancellationToken token, out LoggableMember? loggableMember) + { + loggableMember = null; + + var attributes = member.GetAttributes(); + LogAttributeInfo? attributeInfo = null; + // Search for either NotLogged or Logged. + // If neither or NotLogged is found, return no failure mode + // Otherwise logged will have been found. + foreach (AttributeData attribute in attributes) + { + token.ThrowIfCancellationRequested(); + var attributeClass = attribute.AttributeClass; + if (attributeClass is null) + { + continue; + } + attributeInfo = attribute.ToAttributeInfo(attributeClass, token, out var notLogged); + if (notLogged) + { + return FailureMode.None; + } + token.ThrowIfCancellationRequested(); + if (attributeInfo is not null) + { + break; + } + + } + if (attributeInfo is null) + { + return FailureMode.None; + } + + ITypeSymbol logType; + MemberType memberType; + if (member is IFieldSymbol field) + { + logType = field.Type; + memberType = MemberType.Field; + } + else if (member is IPropertySymbol property) + { + logType = property.Type; + memberType = MemberType.Property; + } + else if (member is IMethodSymbol method) + { + if (method.ReturnsVoid) + { + return FailureMode.MethodReturnsVoid; + } + if (!method.Parameters.IsEmpty) + { + return FailureMode.MethodHasParameters; + } + logType = method.ReturnType; + memberType = MemberType.Method; + } + else + { + return FailureMode.AttributeUnknownMemberType; + } + token.ThrowIfCancellationRequested(); + + var declType = logType.GetDeclarationType(token); + token.ThrowIfCancellationRequested(); + if (declType is null) + { + return FailureMode.UnknownTypeToLog; + } + else if (declType.LoggedType == DeclarationType.Struct && declType.SpecialType == SpecialType.System_Nullable_T) + { + // Special case of logging an array of Nullable which is not supported + return FailureMode.NullableStructArray; + } + + loggableMember = new LoggableMember(member.Name, memberType, declType, attributeInfo); + return FailureMode.None; + } +} diff --git a/codehelp/CodeHelpers/EpilogueGenerator/LoggableType.cs b/codehelp/CodeHelpers/EpilogueGenerator/LoggableType.cs new file mode 100644 index 00000000..834aa626 --- /dev/null +++ b/codehelp/CodeHelpers/EpilogueGenerator/LoggableType.cs @@ -0,0 +1,201 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using WPILib.CodeHelpers.LogGenerator.Analyzer; +using static WPILib.CodeHelpers.IndentedStringBuilder; + +namespace WPILib.CodeHelpers.EpilogueGenerator; + +// Contains all information on a loggable type +public record LoggableType(TypeDeclarationModel TypeDeclaration, EquatableArray LoggableMembers) +{ + private static void WriteTypeName(TypeDeclarationModel type, IndentedStringBuilder builder, bool leaf) + { + if (type.Parent is not null) + { + WriteTypeName(type.Parent, builder, false); + } + builder.Append($"{type.TypeName}{(leaf ? "" : "_")}"); + } + + private void WriteFQN(IndentedStringBuilder builder) + { + builder.Append("global::"); + TypeDeclaration.WriteFileName(builder, true); + TypeDeclaration.GetGenericParameters(builder); + } + + private int AddClassDeclaration(IndentedStringBuilder builder) + { + // Find our namespace and write it + TypeDeclarationModel root = TypeDeclaration; + TypeDeclarationModel? parent = TypeDeclaration.Parent; + while (parent is not null) + { + root = parent; + parent = parent.Parent; + } + + int scopeCount = root.Namespace!.WriteNamespaceDeclaration(builder); + + // Write out type name + builder.StartLine(); + builder.Append("public sealed class "); + WriteTypeName(TypeDeclaration, builder, true); + builder.Append("Logger"); + builder.EndLine(); + + // Add inheritance + if (builder.Language == LanguageKind.CSharp) + { + builder.StartLine(); + builder.Append($" : {Strings.ClassSpecificLoggerFullyQualifiedTypeName}<"); + WriteFQN(builder); + builder.Append(">"); + builder.EndLine(); + } + else if (builder.Language == LanguageKind.VisualBasic) + { + throw new NotImplementedException(); + // builder.StartLine(); + // builder.Append(" Implements "); + // bool first = false; + // foreach (var item in inheritanceSpan) + // { + // if (!first) + // { + // first = true; + // } + // else + // { + // builder.Append(", "); + // } + // builder.Append(item); + // } + // builder.EndLine(); + } + + builder.EnterScope(ScopeType.Class); + + return scopeCount + 1; + } + + private void WriteMethodDeclaration(IndentedStringBuilder builder) + { + if (builder.Language == LanguageKind.CSharp) + { + builder.StartLine(); + builder.Append(Strings.UpdateFunctionStart); + WriteFQN(builder); + builder.Append(" value)"); + builder.EndLine(); + } + else if (builder.Language == LanguageKind.VisualBasic) + { + throw new NotImplementedException(); + } + } + + public void WriteMethod(IndentedStringBuilder builder) + { + var classScopes = AddClassDeclaration(builder); + WriteMethodDeclaration(builder); + builder.EnterScope(ScopeType.NonReturningMethod); + foreach (var call in LoggableMembers) + { + call.WriteLogCall(builder); + } + builder.ExitScope(); // Method scope + for (int i = 0; i < classScopes; i++) + { + builder.ExitScope(); // Class scopes + } + } +} + +internal static class LoggableTypeExtensions +{ + public static LoggableType GetLoggableType(this INamedTypeSymbol classSymbol, CancellationToken token, List<(FailureMode, ISymbol)>? failures, Dictionary? symbolMap) + { + // When we get here, the classSymbol is guaranteeds to have "LoggedAttribute" + token.ThrowIfCancellationRequested(); + + var typeDeclType = classSymbol.GetTypeDeclarationModel(); + token.ThrowIfCancellationRequested(); + + var classMembers = classSymbol.GetMembers(); + token.ThrowIfCancellationRequested(); + + var loggableMembers = ImmutableArray.CreateBuilder(classMembers.Length); + + // Find all loggable memeber + foreach (var member in classMembers) + { + token.ThrowIfCancellationRequested(); + + var loggableMemberFailure = member.ToLoggableMember(token, out var loggableMember); + token.ThrowIfCancellationRequested(); + if (loggableMemberFailure == FailureMode.None) + { + // This is either a valid log, or has no attribute (or NotLogged) + if (loggableMember is not null) + { + loggableMembers.Add(loggableMember); + symbolMap?.Add(loggableMember, member); + } + continue; + } + // Getting here means we errored + failures?.Add((loggableMemberFailure, member)); + } + + var loggableType = new LoggableType(typeDeclType, loggableMembers.ToImmutable()); + + return loggableType; + } + + public static void ExecuteAnalysis(this LoggableType? maybeType, SymbolAnalysisContext context, Dictionary symbolMap) + { + if (maybeType is { } loggableType) + { + foreach (var call in loggableType.LoggableMembers) + { + var failureMode = call.WriteLogCall(null); + if (failureMode == FailureMode.None) + { + continue; + } + // We're errored + throw new NotImplementedException(); + //context.ReportDiagnostic(failureMode, symbolMap[call]); + } + + if (!loggableType.LoggableMembers.IsEmpty && !context.Symbol.HasLoggedAttribute()) + { + foreach (var location in context.Symbol.Locations) + { + context.ReportDiagnostic(Diagnostic.Create(LoggerDiagnostics.MissingGenerateLog, location, context.Symbol.Name)); + } + } + } + } + + public static void ExecuteSourceGeneration(this LoggableType? maybeType, SourceProductionContext context, ImmutableArray customLoggers, LanguageKind language) + { + if (maybeType is { } loggableType) + { + IndentedStringBuilder builder = new IndentedStringBuilder(language); + + loggableType.TypeDeclaration.WriteFileName(builder, true); + builder.Append(".g"); + string fileName = builder.ToString(); + builder.Clear(); + + loggableType.WriteMethod(builder); + + context.AddSource(fileName, SourceText.From(builder.ToString(), Encoding.UTF8)); + } + } +} diff --git a/codehelp/CodeHelpers/EpilogueGenerator/SourceGenerator/EpilogueGeneratorSharp.cs b/codehelp/CodeHelpers/EpilogueGenerator/SourceGenerator/EpilogueGeneratorSharp.cs new file mode 100644 index 00000000..b3ebdfbe --- /dev/null +++ b/codehelp/CodeHelpers/EpilogueGenerator/SourceGenerator/EpilogueGeneratorSharp.cs @@ -0,0 +1,56 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace WPILib.CodeHelpers.EpilogueGenerator.SourceGenerator; + +[Generator(LanguageNames.CSharp)] +public class EpilogueGeneratorSharp : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var attributedTypes = context.SyntaxProvider + .ForAttributeWithMetadataName( + Strings.LoggedAttributeNameWithoutGlobal, + predicate: static (s, _) => s is TypeDeclarationSyntax, + transform: static (ctx, token) => + { + if (ctx.TargetSymbol is not INamedTypeSymbol classSymbol) + { + return null; + } + return classSymbol.GetLoggableType(token, null, null); + }) + .Where(static m => m is not null); + + var customLoggers = context.SyntaxProvider + .ForAttributeWithMetadataName( + Strings.CustomLoggerForAttributeNameWithoutGlobal, + predicate: static (s, _) => s is TypeDeclarationSyntax, + transform: static (ctx, token) => + { + if (ctx.TargetSymbol is not INamedTypeSymbol classSymbol) + { + return null; + } + return ctx.Attributes.GetCustomLoggerType(classSymbol, token); + }) + .Where(static m => m is not null); + + var value = attributedTypes.Combine(customLoggers.Collect()); + + context.RegisterSourceOutput(value, + static (spc, source) => source.Left.ExecuteSourceGeneration(spc, source.Right, LanguageKind.CSharp)); + } +} + +// Notes on what analyzer needs to block +// * Type marked [GenerateLog] is not partial +// * Type marked [GenerateLog] is interface (Might be fixed) +// * Array Like types of Nullable for [Log] marked members +// * Pointer types for [Log] marked members +// * Only Allow Span, ROS and [] for array types +// * If array, only allow Long for integers +// * Types must be either primitives, ILogged, Array of ILogged, IStructSerializable, Array of IStructSerializable, or IProtobufSerializable + +// What analyzer needs to warn +// * Class contains [Log] annotations, but is not marked [GenerateLog] diff --git a/codehelp/CodeHelpers/EpilogueGenerator/Strings.cs b/codehelp/CodeHelpers/EpilogueGenerator/Strings.cs new file mode 100644 index 00000000..6f0903c5 --- /dev/null +++ b/codehelp/CodeHelpers/EpilogueGenerator/Strings.cs @@ -0,0 +1,14 @@ +namespace WPILib.CodeHelpers.EpilogueGenerator; + +public static class Strings +{ + public const string LogNamespace = "Epilogue"; + public const string LoggedAttributeTypeName = "LoggedAttribute"; + public const string NotLoggedAttributeTypeName = "NotLoggedAttribute"; + public const string CustomLoggerForAttributeName = "CustomLoggerForAttribute"; + public const string LoggedAttributeNameWithoutGlobal = $"{LogNamespace}.{LoggedAttributeTypeName}"; + public const string CustomLoggerForAttributeNameWithoutGlobal = $"{LogNamespace}.{CustomLoggerForAttributeName}"; + public const string ClassSpecificLoggerFullyQualifiedTypeName = "global::Epilogue.Logging.ClassSpecificLogger"; + public const string UpdateFunctionStart = "protected override void Update(global::Epilogue.Logging.IDataLogger dataLogger, "; + public const string IStructSerializableName = "IStructSerializable"; +} diff --git a/codehelp/CodeHelpers/EpilogueGenerator/SymbolExtensions.cs b/codehelp/CodeHelpers/EpilogueGenerator/SymbolExtensions.cs new file mode 100644 index 00000000..982338bc --- /dev/null +++ b/codehelp/CodeHelpers/EpilogueGenerator/SymbolExtensions.cs @@ -0,0 +1,105 @@ +using Microsoft.CodeAnalysis; + +namespace WPILib.CodeHelpers.EpilogueGenerator; + +public static class SymbolExtensions +{ + public static bool IsReadOnlySpan(this ITypeSymbol symbol) + { + return symbol is + { + Name: "ReadOnlySpan", + ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } + }; + } + + public static bool IsSpan(this ITypeSymbol symbol) + { + return symbol is + { + Name: "Span", + ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } + }; + } + + public static bool IsReadOnlyMemory(this ITypeSymbol symbol) + { + return symbol is + { + Name: "ReadOnlyMemory", + ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } + }; + } + + public static bool IsMemory(this ITypeSymbol symbol) + { + return symbol is + { + Name: "Memory", + ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } + }; + } + + public static bool IsStructSerializable(this ITypeSymbol symbol) + { + return symbol is + { + Name: Strings.IStructSerializableName, + ContainingNamespace: + { + Name: "Struct", + ContainingNamespace: + { + Name: "Serialization", + ContainingNamespace: + { + Name: "WPIUtil", + ContainingNamespace.IsGlobalNamespace: true + } + } + } + }; + } + + public static bool IsLoggedAttributeClass(this ITypeSymbol symbol) + { + return symbol is + { + Name: Strings.LoggedAttributeTypeName, + ContainingNamespace: { Name: Strings.LogNamespace, ContainingNamespace.IsGlobalNamespace: true } + }; + } + + public static bool IsNotLoggedAttributeClass(this ITypeSymbol symbol) + { + return symbol is + { + Name: Strings.NotLoggedAttributeTypeName, + ContainingNamespace: { Name: Strings.LogNamespace, ContainingNamespace.IsGlobalNamespace: true } + }; + } + + public static bool IsCustomLoggerForAttributeClass(this ITypeSymbol symbol) + { + return symbol is + { + Name: Strings.CustomLoggerForAttributeName, + ContainingNamespace: { Name: Strings.LogNamespace, ContainingNamespace.IsGlobalNamespace: true } + }; + } + + + public static bool HasLoggedAttribute(this ISymbol symbol) + { + return symbol.GetAttributes() + .Where(x => x.AttributeClass?.IsLoggedAttributeClass() ?? false) + .Any(); + } + + public static bool HasNotLoggedAttribute(this ISymbol symbol) + { + return symbol.GetAttributes() + .Where(x => x.AttributeClass?.IsNotLoggedAttributeClass() ?? false) + .Any(); + } +} diff --git a/codehelp/CodeHelpers/LogGenerator/LoggableType.cs b/codehelp/CodeHelpers/LogGenerator/LoggableType.cs index 3736c61a..8ead20b5 100644 --- a/codehelp/CodeHelpers/LogGenerator/LoggableType.cs +++ b/codehelp/CodeHelpers/LogGenerator/LoggableType.cs @@ -47,6 +47,10 @@ public void WriteMethod(IndentedStringBuilder builder) var classScopes = AddClassDeclaration(builder); WriteMethodDeclaration(builder); builder.EnterScope(ScopeType.NonReturningMethod); + + //builder.AppendFullLine("if (global::Epilogue.)") + + foreach (var call in LoggableMembers) { call.WriteLogCall(builder); @@ -131,8 +135,8 @@ public static void ExecuteSourceGeneration(this LoggableType? maybeType, SourceP { IndentedStringBuilder builder = new IndentedStringBuilder(language); - loggableType.TypeDeclaration.WriteFileName(builder); - builder.Append("g"); + loggableType.TypeDeclaration.WriteFileName(builder, true); + builder.Append(".g"); string fileName = builder.ToString(); builder.Clear(); diff --git a/codehelp/CodeHelpers/TypeDeclarationModel.cs b/codehelp/CodeHelpers/TypeDeclarationModel.cs index 9cff712c..d82cc1d9 100644 --- a/codehelp/CodeHelpers/TypeDeclarationModel.cs +++ b/codehelp/CodeHelpers/TypeDeclarationModel.cs @@ -15,17 +15,18 @@ public enum TypeModifiers public record TypeDeclarationModel(TypeKind Kind, TypeModifiers Modifiers, string TypeName, EquatableArray TypeParameters, NamespaceModel? Namespace, TypeDeclarationModel? Parent) { - public void WriteFileName(IndentedStringBuilder builder) + + public void WriteFileName(IndentedStringBuilder builder, bool leaf) { if (Parent is not null) { - Parent.WriteFileName(builder); + Parent.WriteFileName(builder, false); } else { Namespace!.WriteFileName(builder); } - builder.Append($"{TypeName}."); + builder.Append($"{TypeName}{(leaf ? "" : "_")}"); } private static string GetClassNameForLanguage(LanguageKind language) @@ -96,7 +97,7 @@ private string GetClassDeclaration(bool addUnsafe, LanguageKind language) return $"{unsafeString}{GetPartialNameForLanguage(language)} {recordString}{kindString}"; } - private void GetGenericParameters(IndentedStringBuilder builder) + public void GetGenericParameters(IndentedStringBuilder builder) { bool first = true; foreach (var typeParamter in TypeParameters.AsSpan()) diff --git a/codehelp/CodeHelpers/WPILib.CodeHelpers.csproj b/codehelp/CodeHelpers/WPILib.CodeHelpers.csproj index b219acf4..244af171 100644 --- a/codehelp/CodeHelpers/WPILib.CodeHelpers.csproj +++ b/codehelp/CodeHelpers/WPILib.CodeHelpers.csproj @@ -16,6 +16,8 @@ + + diff --git a/src/epilogue/LogImportance.cs b/src/epilogue/LogImportance.cs index f3bd7fe6..50d34302 100644 --- a/src/epilogue/LogImportance.cs +++ b/src/epilogue/LogImportance.cs @@ -6,3 +6,8 @@ public enum LogImportance Info, Critical, } + +public static class LogImportanceExtensions +{ + public const LogImportance DefaultLogImportance = LogImportance.Debug; +} diff --git a/src/epilogue/LogStrategy.cs b/src/epilogue/LogStrategy.cs index bcef29ed..9571a6bb 100644 --- a/src/epilogue/LogStrategy.cs +++ b/src/epilogue/LogStrategy.cs @@ -5,3 +5,8 @@ public enum LogStrategy OptIn, OptOut, } + +public static class LogStrategyExtensions +{ + public const LogStrategy DefaultLogStrategy = LogStrategy.OptOut; +} diff --git a/test/epilogue.test/ClassSpecificLoggerTest.cs b/test/epilogue.test/ClassSpecificLoggerTest.cs index e794e6b6..498e0a31 100644 --- a/test/epilogue.test/ClassSpecificLoggerTest.cs +++ b/test/epilogue.test/ClassSpecificLoggerTest.cs @@ -1,3 +1,6 @@ using Xunit; namespace Epilogue; + +[Logged(Importance = LogImportance.Info)] +public record Point2d(double x, double y, int dim);