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
6 changes: 6 additions & 0 deletions src/ALCops.Common/Reflection/EnumProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1606,6 +1606,10 @@ public static class SyntaxKind
new(() => ParseEnum<NavCodeAnalysis.SyntaxKind>(nameof(NavCodeAnalysis.SyntaxKind.WhileKeyword)));
private static readonly Lazy<NavCodeAnalysis.SyntaxKind> _whileStatement =
new(() => ParseEnum<NavCodeAnalysis.SyntaxKind>(nameof(NavCodeAnalysis.SyntaxKind.WhileStatement)));
private static readonly Lazy<NavCodeAnalysis.SyntaxKind> _xmlElement =
new(() => ParseEnum<NavCodeAnalysis.SyntaxKind>(nameof(NavCodeAnalysis.SyntaxKind.XmlElement)));
private static readonly Lazy<NavCodeAnalysis.SyntaxKind> _xmlNameAttribute =
new(() => ParseEnum<NavCodeAnalysis.SyntaxKind>(nameof(NavCodeAnalysis.SyntaxKind.XmlNameAttribute)));
private static readonly Lazy<NavCodeAnalysis.SyntaxKind> _xmlPortKeyword =
new(() => ParseEnum<NavCodeAnalysis.SyntaxKind>(nameof(NavCodeAnalysis.SyntaxKind.XmlPortKeyword)));
private static readonly Lazy<NavCodeAnalysis.SyntaxKind> _xmlPortObject =
Expand Down Expand Up @@ -1749,7 +1753,9 @@ public static class SyntaxKind
public static NavCodeAnalysis.SyntaxKind VarSection => _varSection.Value;
public static NavCodeAnalysis.SyntaxKind WhileKeyword => _whileKeyword.Value;
public static NavCodeAnalysis.SyntaxKind WhileStatement => _whileStatement.Value;
public static NavCodeAnalysis.SyntaxKind XmlElement => _xmlElement.Value;
public static NavCodeAnalysis.SyntaxKind XmlPortKeyword => _xmlPortKeyword.Value;
public static NavCodeAnalysis.SyntaxKind XmlNameAttribute => _xmlNameAttribute.Value;
public static NavCodeAnalysis.SyntaxKind XmlPortObject => _xmlPortObject.Value;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
codeunit 50100 MyCodeunit
{
/// <summary>
/// Duplicate documentation parameter.
/// </summary>
/// <param name="Value">The parameter documentation.</param>
/// [|<param name="Value">The duplicate parameter documentation.</param>|]
procedure MyProcedure(Value: Boolean)
begin

end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
codeunit 50100 MyCodeunit
{
/// <summary>
/// Duplicate documentation returns.
/// </summary>
/// <returns>A Value.</returns>
/// [|<returns>A Value (duplicate).</returns>|]
procedure MyProcedure() Value: Boolean
begin

end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
codeunit 50100 MyCodeunit
{
/// <summary>
/// Documentation comment parameter but no procedure parameter.
/// </summary>
/// [|<param name="Value">The value.</param>|]
procedure NoParameter()
begin

end;

/// <summary>
/// Procedure parameter but no documentation comment parameter.
/// </summary>
procedure ParameterButNoComment([|Value: Boolean|])
begin

end;

/// <summary>
/// Parameter name mismatch.
/// </summary>
/// [|<param name="NotMyValue">The value.</param>|]
procedure NameMissmatch([|Value: Boolean|])
begin

end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
codeunit 50100 MyCodeunit
{
/// <summary>
/// Documentation comment with returns but no return value.
/// </summary>
/// [|<returns>Some value.</returns>|]
procedure DoesNotReturn()
begin

end;

/// <summary>
/// Return value but no documentation comment returns.
/// </summary>
procedure DoesReturnButNoComment() [|ReturnValue: Boolean|]
begin

end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
codeunit 50100 MyCodeunit
{
/// <summary>
/// A method with TryFunction attribute has a implicit (boolean) return value.
/// </summary>
[[|TryFunction|]]
procedure MyTryFunction()
begin

end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
codeunit 50100 MyCodeunit
{
[|// just a normal procedure without a (structured) documentation comment
procedure NoDocumentationComment(Param: Boolean) ReturnValue: Boolean
begin

end;|]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
codeunit 50100 MyCodeunit
{
[|/// <summary>
/// Has a valid documentation comment.
/// </summary>
/// <param name="Param">The parameter.</param>
procedure ValidWithParameter(Param: Boolean)
begin

end;|]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
codeunit 50100 MyCodeunit
{
[|/// <summary>
/// Has a valid documentation comment.
/// </summary>
/// <returns>The return value.</returns>
procedure ValidWithReturn() ReturnValue: Boolean
begin

end;|]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
codeunit 50100 MyCodeunit
{
/// <summary>
/// A method with TryFunction attribute has a implicit (boolean) return value.
/// </summary>
/// <returns>Returns success (true/false)</returns>
[[|TryFunction|]]
procedure MyTryFunction()
begin

end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using RoslynTestKit;

namespace ALCops.DocumentationCop.Test
{
public class XmlDocumentationProcedureConsistency : NavCodeAnalysisBase
{
private AnalyzerTestFixture _fixture;
private string _testCasePath;

[SetUp]
public void Setup()
{
_fixture = RoslynFixtureFactory.Create<Analyzers.XmlDocumentationProcedureConsistency>();

_testCasePath = Path.Combine(
Directory.GetParent(
Environment.CurrentDirectory)!.Parent!.Parent!.FullName,
Path.Combine("Rules", nameof(XmlDocumentationProcedureConsistency)));
}

[Test]
[TestCase("Return")]
[TestCase("Parameter")]
[TestCase("DuplicateParameter")]
[TestCase("DuplicateReturns")]
[TestCase("TryFunction")]
public async Task HasDiagnostic(string testCase)
{
var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasDiagnostic), $"{testCase}.al"))
.ConfigureAwait(false);

_fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.XmlDocumentationProcedureConsistency);
}

[Test]
[TestCase("Return")]
[TestCase("Parameter")]
[TestCase("NoDocumentationComment")]
[TestCase("TryFunction")]
public async Task NoDiagnostic(string testCase)
{
var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(NoDiagnostic), $"{testCase}.al"))
.ConfigureAwait(false);

_fixture.NoDiagnosticAtAllMarkers(code, DiagnosticIds.XmlDocumentationProcedureConsistency);
}
}
}
27 changes: 27 additions & 0 deletions src/ALCops.DocumentationCop/ALCops.DocumentationCopAnalyzers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,5 +164,32 @@ internal static string WriteToFlowFieldRequiresCommentTitle {
return ResourceManager.GetString("WriteToFlowFieldRequiresCommentTitle", resourceCulture);
}
}

/// <summary>
/// Looks up a localized string similar to The XML documentation for a procedure must accurately reflect its signature..
/// </summary>
internal static string XmlDocumentationProcedureConsistencyDescription {
get {
return ResourceManager.GetString("XmlDocumentationProcedureConsistencyDescription", resourceCulture);
}
}

/// <summary>
/// Looks up a localized string similar to The XML documentation does not match the procedure signature..
/// </summary>
internal static string XmlDocumentationProcedureConsistencyMessageFormat {
get {
return ResourceManager.GetString("XmlDocumentationProcedureConsistencyMessageFormat", resourceCulture);
}
}

/// <summary>
/// Looks up a localized string similar to XML documentation must match the procedure signature.
/// </summary>
internal static string XmlDocumentationProcedureConsistencyTitle {
get {
return ResourceManager.GetString("XmlDocumentationProcedureConsistencyTitle", resourceCulture);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,13 @@
<data name="WriteToFlowFieldRequiresCommentDescription" xml:space="preserve">
<value>FlowFields are calculated fields and are not intended to be written to. Writing to a FlowField, whether accidental or intentional, can lead to runtime errors.</value>
</data>
<data name="XmlDocumentationProcedureConsistencyTitle" xml:space="preserve">
<value>XML documentation must match the procedure signature</value>
</data>
<data name="XmlDocumentationProcedureConsistencyMessageFormat" xml:space="preserve">
<value>The XML documentation does not match the procedure signature.</value>
</data>
<data name="XmlDocumentationProcedureConsistencyDescription" xml:space="preserve">
<value>The XML documentation for a procedure must accurately reflect its signature.</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.Collections.Immutable;
using ALCops.Common.Extensions;
using ALCops.Common.Reflection;
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax;
using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities;

namespace ALCops.DocumentationCop.Analyzers;

[DiagnosticAnalyzer]
public sealed class XmlDocumentationProcedureConsistency : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
DiagnosticDescriptors.XmlDocumentationProcedureConsistency);

public override void Initialize(AnalysisContext context) =>
context.RegisterSyntaxNodeAction(
AnalyzeDocumentationComments,
EnumProvider.SyntaxKind.MethodDeclaration);

private void AnalyzeDocumentationComments(SyntaxNodeAnalysisContext ctx)
{
if (ctx.IsObsolete() || ctx.Node is not MethodDeclarationSyntax methodDeclarationSyntax)
return;

var docCommentTrivia = methodDeclarationSyntax.GetLeadingTrivia().FirstOrDefault(trivia => trivia.Kind == EnumProvider.SyntaxKind.SingleLineDocumentationCommentTrivia);
if (docCommentTrivia.IsKind(EnumProvider.SyntaxKind.None))
return; // no documentation comment exists

Dictionary<string, XmlElementSyntax> docCommentParameters = new Dictionary<string, XmlElementSyntax>(StringComparer.OrdinalIgnoreCase);
XmlElementSyntax? docCommentReturns = null;

var docCommentStructure = (DocumentationCommentTriviaSyntax)docCommentTrivia.GetStructure();
var docCommentElements = docCommentStructure.Content.Where(xmlNode => xmlNode.Kind == EnumProvider.SyntaxKind.XmlElement);

// evaluate documentation comment syntax
foreach (XmlElementSyntax element in docCommentElements.Cast<XmlElementSyntax>())
{
switch (element.StartTag.Name.LocalName.Text.ToLowerInvariant())
{
case "param":
var nameAttribute = (XmlNameAttributeSyntax)element.StartTag.Attributes.First(att => att.IsKind(EnumProvider.SyntaxKind.XmlNameAttribute));
var parameterName = nameAttribute.Identifier.GetText().ToString();
if (!docCommentParameters.ContainsKey(parameterName))
docCommentParameters.Add(parameterName, element);
else
// report diagnostic for duplicate parameter documentation
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, element.GetLocation()));
break;
case "returns":
if (docCommentReturns is not null)
// report diagnostic for duplicate returns documentation
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, element.GetLocation()));
docCommentReturns = element;
break;
}
}

// excess documentation comment return value
if (docCommentReturns is not null && methodDeclarationSyntax.ReturnValue is null)
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, docCommentReturns.GetLocation()));

// return value without documentation comment
if (docCommentReturns is null && (methodDeclarationSyntax.ReturnValue is not null))
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, methodDeclarationSyntax.ReturnValue.GetLocation()));

// method with TryFunction decorator without return in documentation comment
if (docCommentReturns is null)
{
var tryFunctionAttribute =
methodDeclarationSyntax.Attributes
.FirstOrDefault(attr =>
string.Equals(
attr.Name.Identifier.ValueText?.UnquoteIdentifier(),
"TryFunction",
StringComparison.OrdinalIgnoreCase));

if (tryFunctionAttribute is not null)
{
ctx.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.XmlDocumentationProcedureConsistency,
tryFunctionAttribute.Name.GetLocation()));
}
}

// check documentation comment parameters against method syntax
foreach (var docCommentParameter in docCommentParameters)
{
if (!methodDeclarationSyntax.ParameterList.Parameters.Any(param => (param.Name.Identifier.ValueText?.UnquoteIdentifier() ?? string.Empty).Equals(docCommentParameter.Key, StringComparison.OrdinalIgnoreCase)))
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, docCommentParameter.Value.GetLocation()));
}

// check method parameters against documentation comment syntax
foreach (var methodParameter in methodDeclarationSyntax.ParameterList.Parameters)
{
if (!docCommentParameters.Any(docParam => docParam.Key.Equals(methodParameter.Name.Identifier.ValueText?.UnquoteIdentifier(), StringComparison.OrdinalIgnoreCase)))
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, methodParameter.GetLocation()));
}
}
}
10 changes: 10 additions & 0 deletions src/ALCops.DocumentationCop/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ public static class DiagnosticDescriptors
description: DocumentationCopAnalyzers.WriteToFlowFieldRequiresCommentDescription,
helpLinkUri: GetHelpUri(DiagnosticIds.WriteToFlowFieldRequiresComment));

public static readonly DiagnosticDescriptor XmlDocumentationProcedureConsistency = new(
id: DiagnosticIds.XmlDocumentationProcedureConsistency,
title: DocumentationCopAnalyzers.XmlDocumentationProcedureConsistencyTitle,
messageFormat: DocumentationCopAnalyzers.XmlDocumentationProcedureConsistencyMessageFormat,
category: Category.Design,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: DocumentationCopAnalyzers.XmlDocumentationProcedureConsistencyDescription,
helpLinkUri: GetHelpUri(DiagnosticIds.XmlDocumentationProcedureConsistency));

public static string GetHelpUri(string identifier)
{
return string.Format(CultureInfo.InvariantCulture, "https://alcops.dev/docs/analyzers/documentationcop/{0}/", identifier.ToLower());
Expand Down
1 change: 1 addition & 0 deletions src/ALCops.DocumentationCop/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public static class DiagnosticIds
public static readonly string WriteToFlowFieldRequiresComment = "DC0002";
public static readonly string EmptyStatementRequiresComment = "DC0003";
public static readonly string PublicProcedureRequiresDocumentation = "DC0004";
public static readonly string XmlDocumentationProcedureConsistency = "DC0005";
}
Loading