Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/DataverseAnalyzer/EntityContainsAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,4 @@ private static string GetAttributeNameFromArgument(InvocationExpressionSyntax in

return "attribute";
}
}
}
23 changes: 7 additions & 16 deletions src/DataverseAnalyzer/PluginDocumentationAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,4 +449,4 @@ private static async Task<Diagnostic[]> GetDiagnosticsAsync(string source)
var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
return diagnostics.Where(d => d.Id == "CT0007").ToArray();
}
}
}
146 changes: 107 additions & 39 deletions tests/DataverseAnalyzer.Tests/PluginDocumentationAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """

/// <summary>Base plugin class.</summary>
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 { }
""";
Expand All @@ -24,8 +50,7 @@ class MyPlugin : Plugin { }
[Fact]
public async Task PluginSubclassWithEmptySummaryShouldTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

/// <summary></summary>
class MyPlugin : Plugin { }
Expand All @@ -39,8 +64,7 @@ class MyPlugin : Plugin { }
[Fact]
public async Task PluginSubclassWithWhitespaceSummaryShouldTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

/// <summary> </summary>
class MyPlugin : Plugin { }
Expand All @@ -54,8 +78,7 @@ class MyPlugin : Plugin { }
[Fact]
public async Task PluginSubclassWithValidSummaryShouldNotTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

/// <summary>Handles account creation.</summary>
class CreateAccountPlugin : Plugin { }
Expand All @@ -68,8 +91,7 @@ class CreateAccountPlugin : Plugin { }
[Fact]
public async Task PluginSubclassWithMultiLineSummaryShouldNotTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

/// <summary>
/// Handles account creation and validation.
Expand Down Expand Up @@ -108,8 +130,7 @@ class MyClass : BaseClass { }
[Fact]
public async Task PluginSubclassWithOnlyRemarksShouldTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

/// <remarks>Some remarks here.</remarks>
class MyPlugin : Plugin { }
Expand All @@ -123,8 +144,7 @@ class MyPlugin : Plugin { }
[Fact]
public async Task PluginSubclassWithInheritdocShouldNotTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

/// <inheritdoc/>
class MyPlugin : Plugin { }
Expand All @@ -137,8 +157,7 @@ class MyPlugin : Plugin { }
[Fact]
public async Task PluginSubclassWithInheritdocCrefShouldNotTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

/// <inheritdoc cref="Plugin"/>
class MyPlugin : Plugin { }
Expand All @@ -151,8 +170,7 @@ class MyPlugin : Plugin { }
[Fact]
public async Task MultiplePluginSubclassesWithMixedDocsShouldTriggerOnlyForMissing()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

class UndocumentedPlugin : Plugin { }

Expand All @@ -170,10 +188,15 @@ class AnotherUndocumentedPlugin : Plugin { }
[Fact]
public async Task PluginSubclassWithQualifiedBaseTypeShouldTrigger()
{
var source = """
var source = IPluginDefinition + """

namespace MyNamespace
{
class Plugin { }
/// <summary>Base plugin.</summary>
class Plugin : Microsoft.Xrm.Sdk.IPlugin
{
public void Execute(System.IServiceProvider serviceProvider) { }
}
}

class MyPlugin : MyNamespace.Plugin { }
Expand All @@ -187,10 +210,15 @@ class MyPlugin : MyNamespace.Plugin { }
[Fact]
public async Task PluginSubclassWithQualifiedBaseTypeAndSummaryShouldNotTrigger()
{
var source = """
var source = IPluginDefinition + """

namespace MyNamespace
{
class Plugin { }
/// <summary>Base plugin.</summary>
class Plugin : Microsoft.Xrm.Sdk.IPlugin
{
public void Execute(System.IServiceProvider serviceProvider) { }
}
}

/// <summary>My plugin.</summary>
Expand All @@ -204,8 +232,7 @@ class MyPlugin : MyNamespace.Plugin { }
[Fact]
public async Task NestedPluginSubclassShouldTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

class OuterClass
{
Expand All @@ -221,8 +248,7 @@ class NestedPlugin : Plugin { }
[Fact]
public async Task NestedPluginSubclassWithSummaryShouldNotTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

class OuterClass
{
Expand All @@ -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]
Expand All @@ -260,8 +285,7 @@ class MyPluginHelper { }
[Fact]
public async Task PluginSubclassWithSummaryAndOtherTagsShouldNotTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

/// <summary>Handles account creation.</summary>
/// <remarks>Additional details here.</remarks>
Expand All @@ -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 { }
Expand All @@ -290,8 +314,8 @@ class MyPlugin : Plugin, IMyInterface { }
[Fact]
public async Task PluginSubclassImplementingInterfaceWithSummaryShouldNotTrigger()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

interface IMyInterface { }

/// <summary>My plugin.</summary>
Expand All @@ -305,8 +329,7 @@ class MyPlugin : Plugin, IMyInterface { }
[Fact]
public async Task DiagnosticContainsClassName()
{
var source = """
class Plugin { }
var source = IPluginDefinition + DocumentedPluginBase + """

class TestPluginName : Plugin { }
""";
Expand All @@ -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 + """

/// <summary>Direct IPlugin implementation.</summary>
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<Diagnostic[]> GetDiagnosticsAsync(string source)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Latest));
Expand Down