diff --git a/src/Carter.Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs b/src/Carter.Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs index 3219595..2d4e096 100644 --- a/src/Carter.Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs +++ b/src/Carter.Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs @@ -29,11 +29,18 @@ private static void OnCompilationStart(CompilationStartAnalysisContext context) private static void OnTypeAnalysis(SymbolAnalysisContext context, INamedTypeSymbol carterModuleType) { var typeSymbol = (INamedTypeSymbol)context.Symbol; - if (!typeSymbol.Interfaces.Contains(carterModuleType, SymbolEqualityComparer.Default)) + if (typeSymbol.IsAbstract + || !typeSymbol.AllInterfaces.Contains(carterModuleType, SymbolEqualityComparer.Default) + || typeSymbol.DeclaredAccessibility is not (Accessibility.Public or Accessibility.Internal)) { return; } + var isDirectImplementation = typeSymbol.Interfaces.Contains(carterModuleType, SymbolEqualityComparer.Default); + var descriptor = isDirectImplementation + ? DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies + : DiagnosticDescriptors.CarterDerivedModuleShouldNotHaveDependencies; + foreach (var constructor in typeSymbol.Constructors) { if (constructor.DeclaredAccessibility == Accessibility.Private || constructor.Parameters.Length == 0) @@ -58,7 +65,7 @@ private static void OnTypeAnalysis(SymbolAnalysisContext context, INamedTypeSymb } var diagnostic = Diagnostic.Create( - DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies, + descriptor, identifier.Value.GetLocation(), identifier.Value.Text ); @@ -67,5 +74,9 @@ private static void OnTypeAnalysis(SymbolAnalysisContext context, INamedTypeSymb } } - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies); + public override ImmutableArray SupportedDiagnostics { get; } = + [ + DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies, + DiagnosticDescriptors.CarterDerivedModuleShouldNotHaveDependencies, + ]; } diff --git a/src/Carter.Analyzers/DiagnosticDescriptors.cs b/src/Carter.Analyzers/DiagnosticDescriptors.cs index 975ddb5..f3d4694 100644 --- a/src/Carter.Analyzers/DiagnosticDescriptors.cs +++ b/src/Carter.Analyzers/DiagnosticDescriptors.cs @@ -13,4 +13,14 @@ internal static class DiagnosticDescriptors 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." ); + + public static readonly DiagnosticDescriptor CarterDerivedModuleShouldNotHaveDependencies = new( + "CARTER2", + "Derived Carter module should not have dependencies", + "'{0}' inherits ICarterModule and should not have dependencies", + "Usage", + DiagnosticSeverity.Warning, + true, + "Carter registers all non-abstract types assignable to ICarterModule as singletons, including types that inherit ICarterModule through a base class. Dependencies should be declared in the request delegate." + ); } \ No newline at end of file diff --git a/test/Carter.Tests/Analyzers/CarterDerivedModuleShouldNotHaveDependenciesTests.cs b/test/Carter.Tests/Analyzers/CarterDerivedModuleShouldNotHaveDependenciesTests.cs new file mode 100644 index 0000000..306afe6 --- /dev/null +++ b/test/Carter.Tests/Analyzers/CarterDerivedModuleShouldNotHaveDependenciesTests.cs @@ -0,0 +1,173 @@ +namespace Carter.Tests.Analyzers; + +using System.Threading.Tasks; +using Carter.Analyzers; +using Microsoft.CodeAnalysis.Testing; +using Xunit; + +public sealed class CarterDerivedModuleShouldNotHaveDependenciesTests +{ + [Theory] + [InlineData("class")] + [InlineData("record")] + public Task CarterSubModuleWithConstructorDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + + {{type}} MySubCarterModule : MyCarterModule + { + public {|#0:MySubCarterModule|}(string s) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterDerivedModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MySubCarterModule"); + + return VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public Task ConcreteTypeDerivedFromAbstractModuleWithDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + abstract {{type}} MyAbstractCarterModule : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + + {{type}} MyCarterModule : MyAbstractCarterModule + { + public {|#0:MyCarterModule|}(string s) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterDerivedModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public Task DerivedModuleWithPrivateConstructor_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + + {{type}} MySubCarterModule : MyCarterModule + { + private MySubCarterModule(string s) {} + } + """; + + return VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public Task AbstractDerivedModuleWithDependencies_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + + abstract {{type}} MyAbstractSubCarterModule : MyCarterModule + { + public MyAbstractSubCarterModule(string s) {} + } + """; + + return VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public Task DerivedModuleWithoutConstructor_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + + {{type}} MySubCarterModule : MyCarterModule + { + } + """; + + return VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public Task ThreeLevelInheritanceWithDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + + {{type}} MySubCarterModule : MyCarterModule + { + } + + {{type}} MySubSubCarterModule : MySubCarterModule + { + public {|#0:MySubSubCarterModule|}(string s) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterDerivedModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MySubSubCarterModule"); + + return VerifyAsync(code, diagnosticResult); + } + + private static Task VerifyAsync(string code, DiagnosticResult? diagnosticResult = null) + { + AnalyzerTest test = new CSharpPreviewAnalyzerTest(code); + if (diagnosticResult.HasValue) + { + test.ExpectedDiagnostics.Add(diagnosticResult.Value); + } + + return test.RunAsync(); + } +} diff --git a/test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs b/test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs index a758b89..87c8669 100644 --- a/test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs +++ b/test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs @@ -21,11 +21,11 @@ public Task TypeWithDependency_Diagnostic(string type) {{type}} MyCarterModule : ICarterModule { internal {|#0:MyCarterModule|}(string s) {} - + public void AddRoutes(IEndpointRouteBuilder app) {} } """; - + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) .WithLocation(0) .WithArguments("MyCarterModule"); @@ -47,14 +47,14 @@ public Task RecordWithDependency_Diagnostic(string type) public void AddRoutes(IEndpointRouteBuilder app) {} } """; - + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) .WithLocation(0) .WithArguments("MyCarterModule"); return VerifyAsync(code, diagnosticResult); } - + [Theory] [InlineData("class")] [InlineData("struct")] @@ -65,11 +65,11 @@ 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) {} } """; @@ -80,7 +80,7 @@ public void AddRoutes(IEndpointRouteBuilder app) {} return VerifyAsync(code, diagnosticResult); } - + [Theory] [InlineData("record")] [InlineData("record struct")] @@ -89,7 +89,7 @@ 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) {} @@ -102,7 +102,7 @@ public void AddRoutes(IEndpointRouteBuilder app) {} return VerifyAsync(code, diagnosticResult); } - + [Theory] [InlineData("class")] [InlineData("struct")] @@ -113,11 +113,11 @@ 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) {} } """; @@ -128,7 +128,7 @@ public void AddRoutes(IEndpointRouteBuilder app) {} return VerifyAsync(code, diagnosticResult); } - + [Theory] [InlineData("record")] [InlineData("record struct")] @@ -137,7 +137,7 @@ 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) {} @@ -150,7 +150,7 @@ public void AddRoutes(IEndpointRouteBuilder app) {} return VerifyAsync(code, diagnosticResult); } - + [Theory] [InlineData("class")] [InlineData("struct")] @@ -161,18 +161,18 @@ public Task TypeWithDependencies_WhenConstructorIsPrivate_NoDiagnostic(string ty var code = $$""" using Carter; using Microsoft.AspNetCore.Routing; - + {{type}} MyCarterModule : ICarterModule { private MyCarterModule(string s) {} - + public void AddRoutes(IEndpointRouteBuilder app) {} } """; - + return VerifyAsync(code); } - + [Theory] [InlineData("class")] [InlineData("struct")] @@ -183,18 +183,18 @@ public Task TypeWithDependencies_WhenConstructorIsImplicitlyPrivate_NoDiagnostic var code = $$""" using Carter; using Microsoft.AspNetCore.Routing; - + {{type}} MyCarterModule : ICarterModule { MyCarterModule(string s) {} - + public void AddRoutes(IEndpointRouteBuilder app) {} } """; - + return VerifyAsync(code); } - + [Theory] [InlineData("class")] [InlineData("struct")] @@ -205,18 +205,18 @@ 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 VerifyAsync(code); } - + [Theory] [InlineData("class")] [InlineData("struct")] @@ -228,21 +228,21 @@ public Task TypeWithZeroParameterConstructor_NoDiagnostic(string type) using Carter; using Microsoft.AspNetCore.Routing; using System; - + {{type}} MyCarterModule : ICarterModule { public MyCarterModule() { Console.WriteLine("Hello World."); } - + public void AddRoutes(IEndpointRouteBuilder app) {} } """; - + return VerifyAsync(code); } - + [Theory] [InlineData("class")] [InlineData("struct")] @@ -252,16 +252,16 @@ public Task NonCarterModuleWithConstructorDependencies_NoDiagnostic(string type) { var code = $$""" using System; - + {{type}} MyCarterModule { internal MyCarterModule(string s, int i) {} } """; - + return VerifyAsync(code); } - + [Theory] [InlineData("record")] [InlineData("record struct")] @@ -269,35 +269,74 @@ public Task RecordNonCarterModuleWithConstructorDependencies_NoDiagnostic(string { var code = $$""" using System; - + {{type}} MyCarterModule(string S, int I) { } """; - + + return VerifyAsync(code); + } + + [Fact] + public Task PrivateNestedTypeWithDependencies_NoDiagnostic() + { + var code = """ + using Carter; + using Microsoft.AspNetCore.Routing; + + class Outer + { + private class MyCarterModule : ICarterModule + { + public MyCarterModule(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + } + """; + return VerifyAsync(code); } - + + [Fact] + public Task ImplicitlyPrivateNestedTypeWithDependencies_NoDiagnostic() + { + var code = """ + using Carter; + using Microsoft.AspNetCore.Routing; + + class Outer + { + class MyCarterModule : ICarterModule + { + public MyCarterModule(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + } + """; + + return VerifyAsync(code); + } + [Theory] [InlineData("class")] [InlineData("record")] - public Task CarterSubModuleWithConstructorDependencies_NoDiagnostic(string type) + public Task AbstractTypeWithDependencies_NoDiagnostic(string type) { var code = $$""" using Carter; using Microsoft.AspNetCore.Routing; - - {{type}} MyCarterModule : ICarterModule + + abstract {{type}} MyCarterModule : ICarterModule { + internal MyCarterModule(string s) {} + public void AddRoutes(IEndpointRouteBuilder app) {} } - - {{type}} MySubCarterModule : MyCarterModule - { - public MySubCarterModule(string s) {} - } """; - + return VerifyAsync(code); } @@ -315,7 +354,7 @@ public Task EmptyPrimaryConstructor_NoDiagnostic(string type) public void AddRoutes(IEndpointRouteBuilder app) {} } """; - + return VerifyAsync(code); } @@ -335,11 +374,111 @@ public Task PrimaryConstructor_Diagnostic(string type, string parameters) public void AddRoutes(IEndpointRouteBuilder app) {} } """; - + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return VerifyAsync(code, diagnosticResult); + } + + [Fact] + public Task PublicNestedTypeWithDependencies_Diagnostic() + { + var code = """ + using Carter; + using Microsoft.AspNetCore.Routing; + + class Outer + { + public class MyCarterModule : ICarterModule + { + public {|#0:MyCarterModule|}(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return VerifyAsync(code, diagnosticResult); + } + + [Fact] + public Task InternalNestedTypeWithDependencies_Diagnostic() + { + var code = """ + using Carter; + using Microsoft.AspNetCore.Routing; + + class Outer + { + internal class MyCarterModule : ICarterModule + { + public {|#0:MyCarterModule|}(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public Task ProtectedConstructorWithDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + protected {|#0:MyCarterModule|}(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) .WithLocation(0) .WithArguments("MyCarterModule"); - + + return VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + public Task TypeWithMixedConstructors_OnlyParameterizedFlagged(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + public MyCarterModule() {} + + public {|#0:MyCarterModule|}(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + return VerifyAsync(code, diagnosticResult); }