diff --git a/src/DataverseAnalyzer/EntityContainsAnalyzer.cs b/src/DataverseAnalyzer/EntityContainsAnalyzer.cs index fc2d0da..724ebd0 100644 --- a/src/DataverseAnalyzer/EntityContainsAnalyzer.cs +++ b/src/DataverseAnalyzer/EntityContainsAnalyzer.cs @@ -102,4 +102,4 @@ private static string GetAttributeNameFromArgument(InvocationExpressionSyntax in return "attribute"; } -} +} \ No newline at end of file diff --git a/src/DataverseAnalyzer/PluginDocumentationAnalyzer.cs b/src/DataverseAnalyzer/PluginDocumentationAnalyzer.cs index c5e7224..58bc031 100644 --- a/src/DataverseAnalyzer/PluginDocumentationAnalyzer.cs +++ b/src/DataverseAnalyzer/PluginDocumentationAnalyzer.cs @@ -39,7 +39,7 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) { var classDeclaration = (ClassDeclarationSyntax)context.Node; - if (!InheritsFromPlugin(classDeclaration)) + if (!ImplementsIPlugin(context, classDeclaration)) return; if (HasValidDocumentation(classDeclaration)) @@ -53,31 +53,22 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) context.ReportDiagnostic(diagnostic); } - private static bool InheritsFromPlugin(ClassDeclarationSyntax classDeclaration) + private static bool ImplementsIPlugin(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclaration) { - if (classDeclaration.BaseList is null) + var symbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration); + if (symbol is null) return false; - foreach (var baseType in classDeclaration.BaseList.Types) + foreach (var implementedInterface in symbol.AllInterfaces) { - var typeName = GetBaseTypeName(baseType.Type); - if (typeName == "Plugin") + var namespaceName = implementedInterface.ContainingNamespace?.ToDisplayString(); + if (namespaceName == "Microsoft.Xrm.Sdk" && implementedInterface.Name == "IPlugin") return true; } return false; } - private static string? GetBaseTypeName(TypeSyntax type) - { - return type switch - { - IdentifierNameSyntax identifier => identifier.Identifier.ValueText, - QualifiedNameSyntax qualified => qualified.Right.Identifier.ValueText, - _ => null, - }; - } - private static bool HasValidDocumentation(ClassDeclarationSyntax classDeclaration) { var leadingTrivia = classDeclaration.GetLeadingTrivia(); diff --git a/tests/DataverseAnalyzer.Tests/EntityContainsAnalyzerTests.cs b/tests/DataverseAnalyzer.Tests/EntityContainsAnalyzerTests.cs index dc917f0..00ca0bf 100644 --- a/tests/DataverseAnalyzer.Tests/EntityContainsAnalyzerTests.cs +++ b/tests/DataverseAnalyzer.Tests/EntityContainsAnalyzerTests.cs @@ -449,4 +449,4 @@ private static async Task GetDiagnosticsAsync(string source) var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); return diagnostics.Where(d => d.Id == "CT0007").ToArray(); } -} +} \ No newline at end of file diff --git a/tests/DataverseAnalyzer.Tests/PluginDocumentationAnalyzerTests.cs b/tests/DataverseAnalyzer.Tests/PluginDocumentationAnalyzerTests.cs index 16a6746..572cfaa 100644 --- a/tests/DataverseAnalyzer.Tests/PluginDocumentationAnalyzerTests.cs +++ b/tests/DataverseAnalyzer.Tests/PluginDocumentationAnalyzerTests.cs @@ -7,11 +7,37 @@ namespace DataverseAnalyzer.Tests; public sealed class PluginDocumentationAnalyzerTests { + private const string IPluginDefinition = """ + namespace Microsoft.Xrm.Sdk + { + public interface IPlugin + { + void Execute(System.IServiceProvider serviceProvider); + } + } + """; + + private const string DocumentedPluginBase = """ + + /// Base plugin class. + class Plugin : Microsoft.Xrm.Sdk.IPlugin + { + public void Execute(System.IServiceProvider serviceProvider) { } + } + """; + + private const string UndocumentedPluginBase = """ + + class Plugin : Microsoft.Xrm.Sdk.IPlugin + { + public void Execute(System.IServiceProvider serviceProvider) { } + } + """; + [Fact] public async Task PluginSubclassWithoutXmlCommentShouldTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ class MyPlugin : Plugin { } """; @@ -24,8 +50,7 @@ class MyPlugin : Plugin { } [Fact] public async Task PluginSubclassWithEmptySummaryShouldTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ /// class MyPlugin : Plugin { } @@ -39,8 +64,7 @@ class MyPlugin : Plugin { } [Fact] public async Task PluginSubclassWithWhitespaceSummaryShouldTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ /// class MyPlugin : Plugin { } @@ -54,8 +78,7 @@ class MyPlugin : Plugin { } [Fact] public async Task PluginSubclassWithValidSummaryShouldNotTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ /// Handles account creation. class CreateAccountPlugin : Plugin { } @@ -68,8 +91,7 @@ class CreateAccountPlugin : Plugin { } [Fact] public async Task PluginSubclassWithMultiLineSummaryShouldNotTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ /// /// Handles account creation and validation. @@ -108,8 +130,7 @@ class MyClass : BaseClass { } [Fact] public async Task PluginSubclassWithOnlyRemarksShouldTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ /// Some remarks here. class MyPlugin : Plugin { } @@ -123,8 +144,7 @@ class MyPlugin : Plugin { } [Fact] public async Task PluginSubclassWithInheritdocShouldNotTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ /// class MyPlugin : Plugin { } @@ -137,8 +157,7 @@ class MyPlugin : Plugin { } [Fact] public async Task PluginSubclassWithInheritdocCrefShouldNotTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ /// class MyPlugin : Plugin { } @@ -151,8 +170,7 @@ class MyPlugin : Plugin { } [Fact] public async Task MultiplePluginSubclassesWithMixedDocsShouldTriggerOnlyForMissing() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ class UndocumentedPlugin : Plugin { } @@ -170,10 +188,15 @@ class AnotherUndocumentedPlugin : Plugin { } [Fact] public async Task PluginSubclassWithQualifiedBaseTypeShouldTrigger() { - var source = """ + var source = IPluginDefinition + """ + namespace MyNamespace { - class Plugin { } + /// Base plugin. + class Plugin : Microsoft.Xrm.Sdk.IPlugin + { + public void Execute(System.IServiceProvider serviceProvider) { } + } } class MyPlugin : MyNamespace.Plugin { } @@ -187,10 +210,15 @@ class MyPlugin : MyNamespace.Plugin { } [Fact] public async Task PluginSubclassWithQualifiedBaseTypeAndSummaryShouldNotTrigger() { - var source = """ + var source = IPluginDefinition + """ + namespace MyNamespace { - class Plugin { } + /// Base plugin. + class Plugin : Microsoft.Xrm.Sdk.IPlugin + { + public void Execute(System.IServiceProvider serviceProvider) { } + } } /// My plugin. @@ -204,8 +232,7 @@ class MyPlugin : MyNamespace.Plugin { } [Fact] public async Task NestedPluginSubclassShouldTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ class OuterClass { @@ -221,8 +248,7 @@ class NestedPlugin : Plugin { } [Fact] public async Task NestedPluginSubclassWithSummaryShouldNotTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ class OuterClass { @@ -236,14 +262,13 @@ class NestedPlugin : Plugin { } } [Fact] - public async Task PluginBaseClassItselfShouldNotTrigger() + public async Task PluginBaseClassItselfShouldTrigger() { - var source = """ - class Plugin { } - """; + var source = IPluginDefinition + UndocumentedPluginBase; var diagnostics = await GetDiagnosticsAsync(source); - Assert.Empty(diagnostics); + Assert.Single(diagnostics); + Assert.Equal("CT0006", diagnostics[0].Id); } [Fact] @@ -260,8 +285,7 @@ class MyPluginHelper { } [Fact] public async Task PluginSubclassWithSummaryAndOtherTagsShouldNotTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ /// Handles account creation. /// Additional details here. @@ -275,8 +299,8 @@ class CreateAccountPlugin : Plugin { } [Fact] public async Task PluginSubclassImplementingInterfaceShouldTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ + interface IMyInterface { } class MyPlugin : Plugin, IMyInterface { } @@ -290,8 +314,8 @@ class MyPlugin : Plugin, IMyInterface { } [Fact] public async Task PluginSubclassImplementingInterfaceWithSummaryShouldNotTrigger() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ + interface IMyInterface { } /// My plugin. @@ -305,8 +329,7 @@ class MyPlugin : Plugin, IMyInterface { } [Fact] public async Task DiagnosticContainsClassName() { - var source = """ - class Plugin { } + var source = IPluginDefinition + DocumentedPluginBase + """ class TestPluginName : Plugin { } """; @@ -316,6 +339,51 @@ class TestPluginName : Plugin { } Assert.Contains("TestPluginName", diagnostics[0].GetMessage(System.Globalization.CultureInfo.InvariantCulture), StringComparison.Ordinal); } + [Fact] + public async Task ClassDirectlyImplementingIPluginShouldTrigger() + { + var source = IPluginDefinition + """ + + class MyDirectPlugin : Microsoft.Xrm.Sdk.IPlugin + { + public void Execute(System.IServiceProvider serviceProvider) { } + } + """; + + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Single(diagnostics); + Assert.Equal("CT0006", diagnostics[0].Id); + } + + [Fact] + public async Task ClassDirectlyImplementingIPluginWithSummaryShouldNotTrigger() + { + var source = IPluginDefinition + """ + + /// Direct IPlugin implementation. + class MyDirectPlugin : Microsoft.Xrm.Sdk.IPlugin + { + public void Execute(System.IServiceProvider serviceProvider) { } + } + """; + + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Empty(diagnostics); + } + + [Fact] + public async Task ClassInheritingFromNonIPluginPluginClassShouldNotTrigger() + { + var source = """ + class Plugin { } + + class MyPlugin : Plugin { } + """; + + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Empty(diagnostics); + } + private static async Task GetDiagnosticsAsync(string source) { var syntaxTree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Latest));