diff --git a/VContainerAnalyzer.Test/PreserveAttributeAnalyzerTest.cs b/VContainerAnalyzer.Test/PreserveAttributeAnalyzerTest.cs index ce0767f..4bce26e 100644 --- a/VContainerAnalyzer.Test/PreserveAttributeAnalyzerTest.cs +++ b/VContainerAnalyzer.Test/PreserveAttributeAnalyzerTest.cs @@ -9,6 +9,7 @@ using Dena.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Text; using NUnit.Framework; +using VContainerAnalyzer.Analyzers; using Assert = NUnit.Framework.Assert; namespace VContainerAnalyzer.Test; @@ -230,4 +231,42 @@ public async ValueTask AnalyzeRegisterMethod_ConstructorHasInjectAttribute_Repor Assert.That(actual.Length, Is.EqualTo(0)); } + + [Test] + public async ValueTask AnalyzeAddMethod_ConstructorDoesNotHaveInjectAttribute_ReportDiagnostics() + { + var source = ReadCodes("ConstructorWithoutInjectAttributeClass.cs", + "EmptyClassStub.cs", + "Interfaces.cs", + "AddConstructorWithoutInjectAttributeClassLifetimeScope.cs"); + + var analyzer = new PreserveAttributeAnalyzer(); + var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); + + var actual = diagnostics + .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" + .Where(x => x.Id != "CS8019") // Ignore "Unnecessary using directive" + .ToArray(); + + Assert.Multiple(() => + { + Assert.That(actual.First().Id, Is.EqualTo("VContainer0001")); + Assert.That(actual.First().GetMessage(), + Is.EqualTo( + "The constructor of 'ConstructorWithoutInjectAttributeClass' have no attribute that extends PreserveAttribute, such as InjectAttribute.")); + }); + + var expectedPositions = new[] { new { Start = new LinePosition(15, 32), End = new LinePosition(15, 70) }, }; + + Assert.That(actual, Has.Length.EqualTo(expectedPositions.Length)); + + for (var i = 0; i < expectedPositions.Length; i++) + { + LocationAssert.HaveTheSpan( + expectedPositions[i].Start, + expectedPositions[i].End, + actual[i].Location + ); + } + } } diff --git a/VContainerAnalyzer.Test/TestData/AddConstructorWithoutInjectAttributeClassLifetimeScope.cs b/VContainerAnalyzer.Test/TestData/AddConstructorWithoutInjectAttributeClassLifetimeScope.cs new file mode 100644 index 0000000..c5dd0bd --- /dev/null +++ b/VContainerAnalyzer.Test/TestData/AddConstructorWithoutInjectAttributeClassLifetimeScope.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2020-2024 VeyronSakai. +// This software is released under the MIT License. + +using VContainer; +using VContainer.Unity; + +namespace VContainerAnalyzer.Test.TestData +{ + public class AddConstructorWithoutInjectAttributeClassLifetimeScope + { + // ReSharper disable once UnusedMember.Global + public void Configure(IContainerBuilder builder) + { + builder.UseEntryPoints(Lifetime.Singleton, entryPoints => + { + entryPoints.Add(); + }); + } + } +} diff --git a/VContainerAnalyzer.Test/TestData/VContainer/ContainerBuilderUnityExtensions.cs b/VContainerAnalyzer.Test/TestData/VContainer/ContainerBuilderUnityExtensions.cs index 2813f38..b3fcebc 100644 --- a/VContainerAnalyzer.Test/TestData/VContainer/ContainerBuilderUnityExtensions.cs +++ b/VContainerAnalyzer.Test/TestData/VContainer/ContainerBuilderUnityExtensions.cs @@ -1,10 +1,26 @@ // Copyright (c) 2020-2024 VeyronSakai. // This software is released under the MIT License. -// ReSharper disable once CheckNamespace +using System; +// ReSharper disable once CheckNamespace namespace VContainer.Unity { + public readonly struct EntryPointsBuilder + { + private readonly IContainerBuilder _containerBuilder; + private readonly Lifetime _lifetime; + + // ReSharper disable once ConvertToPrimaryConstructor + public EntryPointsBuilder(IContainerBuilder containerBuilder, Lifetime lifetime) + { + this._containerBuilder = containerBuilder; + this._lifetime = lifetime; + } + + public RegistrationBuilder Add() => _containerBuilder.Register(_lifetime).AsImplementedInterfaces(); + } + public static class ContainerBuilderUnityExtensions { public static RegistrationBuilder RegisterEntryPoint(this IContainerBuilder builder, @@ -12,5 +28,13 @@ public static RegistrationBuilder RegisterEntryPoint(this IContainerBuilder b { return new RegistrationBuilder(); } + + public static void UseEntryPoints( + this IContainerBuilder builder, + Lifetime lifetime, + Action configuration) + { + configuration(new EntryPointsBuilder(builder, lifetime)); + } } } diff --git a/VContainerAnalyzer.Test/TestData/VContainer/RegistrationBuilder.cs b/VContainerAnalyzer.Test/TestData/VContainer/RegistrationBuilder.cs index 87dc119..78d4219 100644 --- a/VContainerAnalyzer.Test/TestData/VContainer/RegistrationBuilder.cs +++ b/VContainerAnalyzer.Test/TestData/VContainer/RegistrationBuilder.cs @@ -12,5 +12,10 @@ public RegistrationBuilder As() { return this; } + + public virtual RegistrationBuilder AsImplementedInterfaces() + { + return this; + } } } diff --git a/VContainerAnalyzer/PreserveAttributeAnalyzer.cs b/VContainerAnalyzer/Analyzers/PreserveAttributeAnalyzer.cs similarity index 81% rename from VContainerAnalyzer/PreserveAttributeAnalyzer.cs rename to VContainerAnalyzer/Analyzers/PreserveAttributeAnalyzer.cs index 2246a00..ad8621d 100644 --- a/VContainerAnalyzer/PreserveAttributeAnalyzer.cs +++ b/VContainerAnalyzer/Analyzers/PreserveAttributeAnalyzer.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; -namespace VContainerAnalyzer; +namespace VContainerAnalyzer.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class PreserveAttributeAnalyzer : DiagnosticAnalyzer @@ -38,22 +38,34 @@ private static void AnalyzeAttributes(OperationAnalysisContext context) var invocation = (IInvocationOperation)context.Operation; var methodSymbol = invocation.TargetMethod; var namespaceSymbol = methodSymbol.ContainingNamespace; + if (IsContainerBuilderUnityExtensions(namespaceSymbol, methodSymbol)) { switch (invocation.TargetMethod.Name) { case "RegisterEntryPoint": AnalyzeRegisterEntryPointMethod(ref context, invocation); - break; + return; } } - else if (IsContainerBuilderExtensions(namespaceSymbol, methodSymbol)) + + if (IsContainerBuilderExtensions(namespaceSymbol, methodSymbol)) { switch (invocation.TargetMethod.Name) { case "Register": AnalyzeRegisterMethod(ref context, invocation); - break; + return; + } + } + + if (IsEntryPointBuilder(namespaceSymbol, methodSymbol)) + { + switch (invocation.TargetMethod.Name) + { + case "Add": + AnalyzeAddMethod(ref context, invocation); + return; } } } @@ -80,6 +92,28 @@ private static bool IsContainerBuilderUnityExtensions(INamespaceSymbol namespace return methodSymbol.ContainingType.Name == "ContainerBuilderUnityExtensions"; } + private static bool IsEntryPointBuilder(INamespaceSymbol namespaceSymbol, IMethodSymbol methodSymbol) + { + if (namespaceSymbol is not { Name: "Unity" }) + { + return false; + } + + namespaceSymbol = namespaceSymbol.ContainingNamespace; + if (namespaceSymbol is not { Name: "VContainer" }) + { + return false; + } + + namespaceSymbol = namespaceSymbol.ContainingNamespace; + if (namespaceSymbol is not { Name: "" }) + { + return false; + } + + return methodSymbol.ContainingType.Name == "EntryPointsBuilder"; + } + private static bool IsContainerBuilderExtensions(INamespaceSymbol namespaceSymbol, IMethodSymbol methodSymbol) { if (namespaceSymbol is not { Name: "VContainer" }) @@ -119,6 +153,29 @@ private static void AnalyzeRegisterMethod(ref OperationAnalysisContext context, context.ReportDiagnostic(Diagnostic.Create(s_rule, targetLocation, concreteType.Name)); } + private static void AnalyzeAddMethod(ref OperationAnalysisContext context, IInvocationOperation invocation) + { + var typeArgument = invocation.TargetMethod.TypeArguments.SingleOrDefault(); + if (typeArgument is not INamedTypeSymbol concreteType) + { + return; + } + + if (concreteType.TypeKind != TypeKind.Class) + { + return; + } + + if (HasConstructorWithPreserveAttribute(concreteType) || !HasCustomConstructor(concreteType)) + { + return; + } + + var typeArgumentLocation = GetTypeArgumentLocation(invocation); + var targetLocation = typeArgumentLocation == default ? invocation.Syntax.GetLocation() : typeArgumentLocation; + context.ReportDiagnostic(Diagnostic.Create(s_rule, targetLocation, concreteType.Name)); + } + private static void AnalyzeRegisterInstanceMethod(ref OperationAnalysisContext context, IInvocationOperation invocation) { diff --git a/VContainerAnalyzer/VContainerAnalyzer.csproj b/VContainerAnalyzer/VContainerAnalyzer.csproj index 6b33a8b..db3e0ee 100644 --- a/VContainerAnalyzer/VContainerAnalyzer.csproj +++ b/VContainerAnalyzer/VContainerAnalyzer.csproj @@ -19,9 +19,6 @@ - - -