From 8456dd0077c91a3d42e0863fb6cb876d2e39aaa0 Mon Sep 17 00:00:00 2001 From: Collin Alpert Date: Fri, 24 May 2024 15:09:21 +0200 Subject: [PATCH] Add Roslyn Analyzer project (#349) --- ...ModuleShouldNotHaveDependenciesAnalyzer.cs | 71 ++++ src/Carter/Analyzers/DiagnosticDescriptors.cs | 16 + src/Carter/Carter.csproj | 8 +- .../DependencyContextAssemblyCatalog.cs | 2 + src/Carter/ModelBinding/BindExtensions.cs | 2 + ...terModuleShouldNotHaveDependenciesTests.cs | 327 ++++++++++++++++++ test/Carter.Tests/Carter.Tests.csproj | 1 + 7 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 src/Carter/Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs create mode 100644 src/Carter/Analyzers/DiagnosticDescriptors.cs create mode 100644 test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs diff --git a/src/Carter/Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs b/src/Carter/Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs new file mode 100644 index 0000000..70ac8ad --- /dev/null +++ b/src/Carter/Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs @@ -0,0 +1,71 @@ +namespace Carter.Analyzers; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CarterModuleShouldNotHaveDependenciesAnalyzer : DiagnosticAnalyzer +{ + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + var carterModuleType = context.Compilation.GetTypeByMetadataName("Carter.ICarterModule"); + if (carterModuleType is null) + { + return; + } + + context.RegisterSymbolAction(ctx => OnTypeAnalysis(ctx, carterModuleType), SymbolKind.NamedType); + } + + private static void OnTypeAnalysis(SymbolAnalysisContext context, INamedTypeSymbol carterModuleType) + { + var typeSymbol = (INamedTypeSymbol)context.Symbol; + if (!typeSymbol.Interfaces.Contains(carterModuleType, SymbolEqualityComparer.Default)) + { + return; + } + + foreach (var constructor in typeSymbol.Constructors) + { + if (constructor.DeclaredAccessibility == Accessibility.Private || constructor.Parameters.Length == 0) + { + continue; + } + + foreach (var syntaxReference in constructor.DeclaringSyntaxReferences) + { + var node = syntaxReference.GetSyntax(); + SyntaxToken identifier; + if (node is ConstructorDeclarationSyntax constructorDeclaration) + { + identifier = constructorDeclaration.Identifier; + } else if (node is RecordDeclarationSyntax recordDeclaration) + { + identifier = recordDeclaration.Identifier; + } + else + { + continue; + } + + var diagnostic = Diagnostic.Create( + DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies, + identifier.GetLocation(), + identifier.Text + ); + context.ReportDiagnostic(diagnostic); + } + } + } + + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies]; +} diff --git a/src/Carter/Analyzers/DiagnosticDescriptors.cs b/src/Carter/Analyzers/DiagnosticDescriptors.cs new file mode 100644 index 0000000..975ddb5 --- /dev/null +++ b/src/Carter/Analyzers/DiagnosticDescriptors.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; + +namespace Carter.Analyzers; + +internal static class DiagnosticDescriptors +{ + public static readonly DiagnosticDescriptor CarterModuleShouldNotHaveDependencies = new( + "CARTER1", + "Carter module should not have dependencies", + "'{0}' should not have dependencies", + "Usage", + DiagnosticSeverity.Warning, + true, + "When a class implements ICarterModule, it should not have any dependencies. This is because Carter uses minimal APIs and dependencies should be declared in the request delegate." + ); +} \ No newline at end of file diff --git a/src/Carter/Carter.csproj b/src/Carter/Carter.csproj index 369fd95..2aa310f 100644 --- a/src/Carter/Carter.csproj +++ b/src/Carter/Carter.csproj @@ -13,21 +13,27 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb true README.md + true + RS2008 + + all - + + + diff --git a/src/Carter/DependencyContextAssemblyCatalog.cs b/src/Carter/DependencyContextAssemblyCatalog.cs index 300ae56..610f2d8 100644 --- a/src/Carter/DependencyContextAssemblyCatalog.cs +++ b/src/Carter/DependencyContextAssemblyCatalog.cs @@ -89,7 +89,9 @@ private static Assembly SafeLoadAssembly(AssemblyName assemblyName) { try { +#pragma warning disable RS1035 return Assembly.Load(assemblyName); +#pragma warning restore RS1035 } catch (Exception) { diff --git a/src/Carter/ModelBinding/BindExtensions.cs b/src/Carter/ModelBinding/BindExtensions.cs index 54b6590..7e4fa52 100644 --- a/src/Carter/ModelBinding/BindExtensions.cs +++ b/src/Carter/ModelBinding/BindExtensions.cs @@ -88,6 +88,7 @@ public static async Task BindAndSaveFile(this HttpRequest request, string saveLo private static async Task SaveFileInternal(IFormFile file, string saveLocation, string fileName = "") { +#pragma warning disable RS1035 if (!Directory.Exists(saveLocation)) Directory.CreateDirectory(saveLocation); @@ -95,5 +96,6 @@ private static async Task SaveFileInternal(IFormFile file, string saveLocation, using (var fileToSave = File.Create(Path.Combine(saveLocation, fileName))) await file.CopyToAsync(fileToSave); +#pragma warning restore RS1035 } } \ No newline at end of file diff --git a/test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs b/test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs new file mode 100644 index 0000000..2d58df5 --- /dev/null +++ b/test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs @@ -0,0 +1,327 @@ +namespace Carter.Tests.Analyzers; + +using System.IO; +using System.Threading.Tasks; +using Carter.Analyzers; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Xunit; + +public sealed class CarterModuleShouldNotHaveDependenciesTests +{ + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithDependency_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + internal {|#0:MyCarterModule|}(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public Task RecordWithDependency_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} {|#0:MyCarterModule|}(string S) : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithMultipleDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + internal {|#0:MyCarterModule|}(string s, int i) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public Task RecordWithMultipleDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} {|#0:MyCarterModule|}(string S, int I) : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithDefaultDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + internal {|#0:MyCarterModule|}(string s = "", char c = 'c') {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public Task RecordWithDefaultDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} {|#0:MyCarterModule|}(string S = "", char C = 'c') : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithDependencies_WhenConstructorIsPrivate_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + private MyCarterModule(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithDependencies_WhenConstructorIsImplicitlyPrivate_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + MyCarterModule(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithoutConstructor_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + void M() {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithZeroParameterConstructor_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + using System; + + {{type}} MyCarterModule : ICarterModule + { + public MyCarterModule() + { + Console.WriteLine("Hello World."); + } + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task NonCarterModuleWithConstructorDependencies_NoDiagnostic(string type) + { + var code = $$""" + using System; + + {{type}} MyCarterModule + { + internal MyCarterModule(string s, int i) {} + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public Task RecordNonCarterModuleWithConstructorDependencies_NoDiagnostic(string type) + { + var code = $$""" + using System; + + {{type}} MyCarterModule(string S, int I) + { + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public Task CarterSubModuleWithConstructorDependencies_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + + {{type}} MySubCarterModule : MyCarterModule + { + public MySubCarterModule(string s) {} + } + """; + + return this.VerifyAsync(code); + } + + private Task VerifyAsync(string code, DiagnosticResult? diagnosticResult = null) + { + var carterPackage = new PackageIdentity("Carter", "8.1.0"); + var aspNetPackage = new PackageIdentity("Microsoft.AspNetCore.App.Ref", "8.0.0"); + var bclPackage = new PackageIdentity("Microsoft.NETCore.App.Ref", "8.0.0"); + var referenceAssemblies = new ReferenceAssemblies("net8.0", bclPackage, Path.Combine("ref", "net8.0")) + .AddPackages([carterPackage, aspNetPackage]); + AnalyzerTest test = new CSharpAnalyzerTest + { + TestCode = code, + ReferenceAssemblies = referenceAssemblies + }; + + if (diagnosticResult.HasValue) + { + test.ExpectedDiagnostics.Add(diagnosticResult.Value); + } + + return test.RunAsync(); + } +} diff --git a/test/Carter.Tests/Carter.Tests.csproj b/test/Carter.Tests/Carter.Tests.csproj index a926504..98b491d 100644 --- a/test/Carter.Tests/Carter.Tests.csproj +++ b/test/Carter.Tests/Carter.Tests.csproj @@ -7,5 +7,6 @@ +