From 9f70ad33fa96d07194826d97e64dc41e2c7c11b9 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Thu, 16 May 2024 02:26:58 +0200 Subject: [PATCH] Application metadata validation in analyzer --- AppLibDotnet.sln | 7 -- .../Altinn.App.Analyzers.csproj | 11 +++ .../AnalyzerReleases.Unshipped.md | 6 +- src/Altinn.App.Analyzers/Diagnostics.cs | 70 ++++++++++++++ src/Altinn.App.Analyzers/MetadataAnalyzer.cs | 93 ++++++++++++++++++ src/Altinn.App.Analyzers/TestAnalyzer.cs | 84 ---------------- .../Altinn.App.Analyzers.Tests.csproj | 28 ------ test/Altinn.App.Analyzers.Tests/UnitTest1.cs | 96 ------------------- 8 files changed, 179 insertions(+), 216 deletions(-) create mode 100644 src/Altinn.App.Analyzers/Diagnostics.cs create mode 100644 src/Altinn.App.Analyzers/MetadataAnalyzer.cs delete mode 100644 src/Altinn.App.Analyzers/TestAnalyzer.cs delete mode 100644 test/Altinn.App.Analyzers.Tests/Altinn.App.Analyzers.Tests.csproj delete mode 100644 test/Altinn.App.Analyzers.Tests/UnitTest1.cs diff --git a/AppLibDotnet.sln b/AppLibDotnet.sln index e5d00f268..4e51c4539 100644 --- a/AppLibDotnet.sln +++ b/AppLibDotnet.sln @@ -20,8 +20,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.App.Core", "src\Alti EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.App.Analyzers", "src\Altinn.App.Analyzers\Altinn.App.Analyzers.csproj", "{9F956488-F123-48A2-B21A-2C2918E8B416}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.App.Analyzers.Tests", "test\Altinn.App.Analyzers.Tests\Altinn.App.Analyzers.Tests.csproj", "{34E40EB8-DFEA-432C-9F53-371932E21D8A}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,10 +46,6 @@ Global {9F956488-F123-48A2-B21A-2C2918E8B416}.Debug|Any CPU.Build.0 = Debug|Any CPU {9F956488-F123-48A2-B21A-2C2918E8B416}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F956488-F123-48A2-B21A-2C2918E8B416}.Release|Any CPU.Build.0 = Release|Any CPU - {34E40EB8-DFEA-432C-9F53-371932E21D8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {34E40EB8-DFEA-432C-9F53-371932E21D8A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {34E40EB8-DFEA-432C-9F53-371932E21D8A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {34E40EB8-DFEA-432C-9F53-371932E21D8A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -62,7 +56,6 @@ Global {2FD56505-1DB2-4AE1-8911-E076E535EAC6} = {6C8EB054-1747-4BAC-A637-754F304BCAFA} {1745B251-BD5C-43B7-BA7D-9C4BFAB37535} = {7AD5FADE-607F-4D5F-8511-6647D0C1AA1C} {9F956488-F123-48A2-B21A-2C2918E8B416} = {7AD5FADE-607F-4D5F-8511-6647D0C1AA1C} - {34E40EB8-DFEA-432C-9F53-371932E21D8A} = {6C8EB054-1747-4BAC-A637-754F304BCAFA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4584C6E1-D5B4-40B1-A8C4-CF4620EB0896} diff --git a/src/Altinn.App.Analyzers/Altinn.App.Analyzers.csproj b/src/Altinn.App.Analyzers/Altinn.App.Analyzers.csproj index b607cda2a..cf989419b 100644 --- a/src/Altinn.App.Analyzers/Altinn.App.Analyzers.csproj +++ b/src/Altinn.App.Analyzers/Altinn.App.Analyzers.csproj @@ -23,6 +23,17 @@ + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + diff --git a/src/Altinn.App.Analyzers/AnalyzerReleases.Unshipped.md b/src/Altinn.App.Analyzers/AnalyzerReleases.Unshipped.md index 6ee9f5d64..7aaa5e4d8 100644 --- a/src/Altinn.App.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Altinn.App.Analyzers/AnalyzerReleases.Unshipped.md @@ -5,4 +5,8 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- -ALTINN999 | General | Warning | TestAnalyzer +ALTINN001 | General | Error | Project not found +ALTINN002 | General | Error | App metadata file not found +ALTINN003 | General | Error | App metadata file parsing +ALTINN004 | General | Error | Classref not resolved +ALTINN999 | General | Error | Unknown error diff --git a/src/Altinn.App.Analyzers/Diagnostics.cs b/src/Altinn.App.Analyzers/Diagnostics.cs new file mode 100644 index 000000000..69d912feb --- /dev/null +++ b/src/Altinn.App.Analyzers/Diagnostics.cs @@ -0,0 +1,70 @@ +using System.Reflection; + +namespace Altinn.App.Analyzers; + +internal static class Diagnostics +{ + internal static readonly DiagnosticDescriptor UnknownError = Error( + "ALTINN999", + "Unknown analyzer error", + "Unknown error occurred during analysis: '{0}' {1}" + ); + + internal static readonly DiagnosticDescriptor ProjectNotFoundError = Error( + "ALTINN001", + "Altinn app project not found", + "While starting analysis, we couldn't find the project directory - contact support" + ); + + internal static readonly DiagnosticDescriptor ApplicationMetadataFileNotFoundError = Error( + "ALTINN002", + "Altinn app metadata file not found", + "Could not find application metadata file at 'config/applicationmetadata.json'" + ); + + internal static readonly DiagnosticDescriptor FailedToParseApplicationMetadataError = Error( + "ALTINN003", + "Altinn app metadata file couldn't be parsed", + "Could not parse application metadata file at 'config/applicationmetadata.json': '{0}' {1}" + ); + + internal static readonly DiagnosticDescriptor DataTypeClassRefInvalidError = Error( + "ALTINN004", + "Data type class reference could not be found", + "Class reference '{0}' for data type '{1}' could not be found" + ); + + internal static readonly ImmutableArray All; + + static Diagnostics() + { + All = ImmutableArray.CreateRange( + typeof(Diagnostics) + .GetFields(BindingFlags.NonPublic | BindingFlags.Static) + .Where(f => f.FieldType == typeof(DiagnosticDescriptor)) + .Select(f => (DiagnosticDescriptor)f.GetValue(null)) + ); + } + + private const string DocsRoot = "https://docs.altinn.studio/app/development/analysis/"; + private const string RulesRoot = DocsRoot + "rules/"; + + private static DiagnosticDescriptor Warning(string id, string title, string messageFormat) => + Create(id, title, messageFormat, Category.General, DiagnosticSeverity.Warning); + + private static DiagnosticDescriptor Error(string id, string title, string messageFormat) => + Create(id, title, messageFormat, Category.General, DiagnosticSeverity.Error); + + private static DiagnosticDescriptor Create( + string id, + string title, + string messageFormat, + string category, + DiagnosticSeverity severity + ) => new(id, title, messageFormat, category, severity, true, helpLinkUri: RulesRoot + id); + + private static class Category + { + public const string General = nameof(General); + } +} diff --git a/src/Altinn.App.Analyzers/MetadataAnalyzer.cs b/src/Altinn.App.Analyzers/MetadataAnalyzer.cs new file mode 100644 index 000000000..4356ff197 --- /dev/null +++ b/src/Altinn.App.Analyzers/MetadataAnalyzer.cs @@ -0,0 +1,93 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Altinn.App.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class MetadataAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => Diagnostics.All; + + public override void Initialize(AnalysisContext context) + { + // var configFlags = GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics; + var configFlags = GeneratedCodeAnalysisFlags.None; + context.ConfigureGeneratedCodeAnalysis(configFlags); + context.EnableConcurrentExecution(); + + context.RegisterCompilationAction(OnCompilation); + // System.Diagnostics.Debugger.Launch(); + } + + private void OnCompilation(CompilationAnalysisContext context) + { + if ( + !context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue( + "build_property.projectdir", + out var projectDir + ) + ) + { + context.ReportDiagnostic(Diagnostic.Create(Diagnostics.ProjectNotFoundError, Location.None)); + return; + } + +#pragma warning disable RS1035 // Do not use APIs banned for analyzers + // On compilation end, it is not totally unreasonable to read a file + var file = Path.Combine(projectDir, "config/applicationmetadata.json"); + if (!File.Exists(file)) + { + context.ReportDiagnostic( + Diagnostic.Create(Diagnostics.ApplicationMetadataFileNotFoundError, Location.None) + ); + return; + } + + try + { + var jsonText = File.ReadAllText(file); +#pragma warning restore RS1035 // Do not use APIs banned for analyzers + var metadata = JObject.Parse(jsonText) ?? throw new Exception("Deserialization returned 'null'"); + var dataTypes = + (metadata.GetValue("dataTypes") as JArray) ?? throw new JsonException("Failed to parse 'dataTypes'"); + + foreach (var dataTypeToken in dataTypes) + { + var dataType = (dataTypeToken as JObject) ?? throw new JsonException("Could not parse 'dataType'"); + var dataTypeId = + (dataType.GetValue("id") as JValue)?.Value as string + ?? throw new JsonException("Could not parse 'id' from 'dataType'"); + + var appLogic = dataType.GetValue("appLogic") as JObject; + if (appLogic is null) + continue; + + var classRefToken = appLogic.GetValue("classRef"); + var classRef = + (classRefToken as JValue)?.Value as string ?? throw new JsonException("Could not parse 'classRef'"); + var classRefSymbol = context.Compilation.GetTypeByMetadataName(classRef); + + if (classRefSymbol is null) + { + // TODO: create location + // Location.Create(file, TextSpan.FromBounds(..), LinePositionSpan); + context.ReportDiagnostic( + Diagnostic.Create(Diagnostics.DataTypeClassRefInvalidError, Location.None, classRef, dataTypeId) + ); + } + } + } + catch (Exception ex) + { + context.ReportDiagnostic( + Diagnostic.Create( + Diagnostics.FailedToParseApplicationMetadataError, + Location.None, + ex.Message, + ex.StackTrace + ) + ); + return; + } + } +} diff --git a/src/Altinn.App.Analyzers/TestAnalyzer.cs b/src/Altinn.App.Analyzers/TestAnalyzer.cs deleted file mode 100644 index db0dc041f..000000000 --- a/src/Altinn.App.Analyzers/TestAnalyzer.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace Altinn.App.Analyzers; - -internal static class Diagnostics -{ - internal static readonly DiagnosticDescriptor UnknownError = Warning( - "ALTINN999", - "Unknown analyzer error", - "Unknown error occurred during analysis: '{0}' {1}" - ); - - private const string DocsRoot = "https://docs.altinn.studio/app/development/analysis/"; - private const string RulesRoot = DocsRoot + "rules/"; - - private static DiagnosticDescriptor Warning(string id, string title, string messageFormat) => - Create(id, title, messageFormat, Category.General, DiagnosticSeverity.Warning); - - private static DiagnosticDescriptor Create( - string id, - string title, - string messageFormat, - string category, - DiagnosticSeverity severity - ) => new(id, title, messageFormat, category, severity, true, helpLinkUri: RulesRoot + id); - - private static class Category - { - public const string General = nameof(General); - } -} - -/// -/// Analyzer -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class TestAnalyzer : DiagnosticAnalyzer -{ - /// - /// Supported diagnostics - /// - public override ImmutableArray SupportedDiagnostics => - ImmutableArray.Create(Diagnostics.UnknownError); - - /// - /// Initializer - /// - /// - public override void Initialize(AnalysisContext context) - { - // var configFlags = GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics; - var configFlags = GeneratedCodeAnalysisFlags.None; - context.ConfigureGeneratedCodeAnalysis(configFlags); - context.EnableConcurrentExecution(); - - // context.RegisterCompilationStartAction(OnCompilationStart); - context.RegisterCompilationAction(OnCompilation); - context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.SimpleMemberAccessExpression); - } - - // private void OnCompilationStart(CompilationStartAnalysisContext context) - // { - // } - - private void OnCompilation(CompilationAnalysisContext context) { } - - private void AnalyzeNode(SyntaxNodeAnalysisContext context) - { - try - { - if (context.Node is not MemberAccessExpressionSyntax memberAccess) - return; - - if (memberAccess.Name.Identifier.Text != "OpenTelemetry") - return; - - var symbol = context.SemanticModel.GetSymbolInfo(memberAccess.Expression, context.CancellationToken).Symbol; - } - catch (Exception ex) - { - context.ReportDiagnostic( - Diagnostic.Create(Diagnostics.UnknownError, context.Node.GetLocation(), ex.Message, ex.StackTrace) - ); - } - } -} diff --git a/test/Altinn.App.Analyzers.Tests/Altinn.App.Analyzers.Tests.csproj b/test/Altinn.App.Analyzers.Tests/Altinn.App.Analyzers.Tests.csproj deleted file mode 100644 index 540ac461e..000000000 --- a/test/Altinn.App.Analyzers.Tests/Altinn.App.Analyzers.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - - - - - - - - - - - - diff --git a/test/Altinn.App.Analyzers.Tests/UnitTest1.cs b/test/Altinn.App.Analyzers.Tests/UnitTest1.cs deleted file mode 100644 index c9d7f3937..000000000 --- a/test/Altinn.App.Analyzers.Tests/UnitTest1.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Testing.Verifiers; -using Verifier = Altinn.App.Analyzers.Tests.CSharpAnalyzerVerifier; - -namespace Altinn.App.Analyzers.Tests; - -public static partial class CSharpAnalyzerVerifier - where TAnalyzer : DiagnosticAnalyzer, new() -{ - /// - public static DiagnosticResult Diagnostic() => CSharpAnalyzerVerifier.Diagnostic(); - - /// - public static DiagnosticResult Diagnostic(string diagnosticId) => - CSharpAnalyzerVerifier.Diagnostic(diagnosticId); - - /// - public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => - CSharpAnalyzerVerifier.Diagnostic(descriptor); - - /// - public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) - { - var test = new Test { TestCode = source, }; - - test.ExpectedDiagnostics.AddRange(expected); - await test.RunAsync(CancellationToken.None); - } - - public class Test : CSharpAnalyzerTest - { - public Test() - { - SolutionTransforms.Add( - (solution, projectId) => - { - var compilationOptions = solution.GetProject(projectId).CompilationOptions; - compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( - compilationOptions.SpecificDiagnosticOptions.SetItems(NullableWarnings) - ); - solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); - - return solution; - } - ); - } - } - - internal static ImmutableDictionary NullableWarnings { get; } = - GetNullableWarningsFromCompiler(); - - private static ImmutableDictionary GetNullableWarningsFromCompiler() - { - string[] args = { "/warnaserror:nullable" }; - var commandLineArguments = CSharpCommandLineParser.Default.Parse( - args, - baseDirectory: Environment.CurrentDirectory, - sdkDirectory: Environment.CurrentDirectory - ); - var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; - - // Workaround for https://github.com/dotnet/roslyn/issues/41610 - nullableWarnings = nullableWarnings - .SetItem("CS8632", ReportDiagnostic.Error) - .SetItem("CS8669", ReportDiagnostic.Error); - - return nullableWarnings; - } -} - -public class UnitTest1 -{ - [Fact] - public async Task Test1() - { - var code = """ - using System; - - class Program - { - static void Main() - { - int i = 0; - Console.WriteLine(i++); - } - } - """; - - await Verifier.VerifyAnalyzerAsync(code); - } -}