Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement else and else-if for Rule0089 #934

Merged
merged 4 commits into from
Feb 12, 2025
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
1 change: 1 addition & 0 deletions BusinessCentral.LinterCop.Test/Rule0089.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public async Task HasDiagnostic(string testCase)
[Test]
[TestCase("IfStatement")]
[TestCase("DiscountConsecutiveAndOperator")]
[TestCase("IfStatementElseIf")]
public async Task NoDiagnostic(string testCase)
{
var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
codeunit 50100 MyCodeunit
{
procedure [|MyProcedure|]() // Cognitive Complexity: 14 (threshold >=15)
var
myBoolean: Boolean;
begin
if myBoolean then // +1 (nesting = 0)
if myBoolean then begin // +2 (nesting = 1)
if true then; // +3 (nesting = 2)
if true then; // +3 (nesting = 2)
end else // +0 (else-if)
if true then // +2 (nesting = 1)
if true then; // +3 (nesting = 2)
end;
}
72 changes: 65 additions & 7 deletions BusinessCentral.LinterCop/Design/Rule0089CognitiveComplexity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Immutable;
using BusinessCentral.LinterCop.Helpers;
using System.Collections.Concurrent;
using Microsoft.Dynamics.Nav.CodeAnalysis.Text;

namespace BusinessCentral.LinterCop.Design;

Expand All @@ -15,9 +16,10 @@ public class Rule0089CognitiveComplexity : DiagnosticAnalyzer
private static readonly HashSet<SyntaxKind> flowBreakingKinds = new()
{
SyntaxKind.IfStatement,
SyntaxKind.CaseStatement,
SyntaxKind.ForStatement,
SyntaxKind.ForEachStatement,
SyntaxKind.WhileStatement,
SyntaxKind.CaseStatement,
SyntaxKind.RepeatStatement,
#if !LessThenFall2024
SyntaxKind.ConditionalExpression
Expand All @@ -27,9 +29,10 @@ public class Rule0089CognitiveComplexity : DiagnosticAnalyzer
private static readonly HashSet<SyntaxKind> nestedStructures = new()
{
SyntaxKind.IfStatement,
SyntaxKind.CaseStatement,
SyntaxKind.ForStatement,
SyntaxKind.ForEachStatement,
SyntaxKind.WhileStatement,
SyntaxKind.CaseStatement,
SyntaxKind.RepeatStatement,
#if !LessThenFall2024
SyntaxKind.ConditionalExpression
Expand All @@ -39,6 +42,7 @@ public class Rule0089CognitiveComplexity : DiagnosticAnalyzer
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
DiagnosticDescriptors.Rule0089CognitiveComplexity,
DiagnosticDescriptors.Rule0089DEBUGCognitiveComplexity,
DiagnosticDescriptors.Rule0090CognitiveComplexity);

public override void Initialize(AnalysisContext context) =>
Expand Down Expand Up @@ -77,10 +81,10 @@ private void AnalyzeCognitiveComplexity(CodeBlockAnalysisContext context)
}

context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.Rule0089CognitiveComplexity,
context.OwningSymbol.GetLocation(),
complexity,
cognitiveComplexityThreshold));
DiagnosticDescriptors.Rule0089CognitiveComplexity,
context.OwningSymbol.GetLocation(),
complexity,
cognitiveComplexityThreshold));
}

private int CalculateCognitiveComplexity(CodeBlockAnalysisContext context, SyntaxNode root)
Expand All @@ -93,15 +97,57 @@ private int CalculateCognitiveComplexity(CodeBlockAnalysisContext context, Synta
{
var (node, nestingLevel) = stack.Pop();

// The 'else if' increment causes a problem
// In the AL Language 'else if' is an 'else" keyword followed by an 'if' node (not a single 'elsif' node).
// If we increment for both 'else' and 'if' kinds the number will be too high.
// So we'll increment for 'else' nodes not followed by an 'if' and rely on the 'if' to increment 'else if' statements.
if (node is IfStatementSyntax ifStatement)
{
// Increment for the 'if' condition (+1 + nesting level)
complexity += 1 + nestingLevel;
RaiseDEBUGDiagnostic(context, node, node.SpanStart, node.Kind, nestingLevel);

// Increase nesting level for the 'if' body
int newNestingLevel = nestingLevel + 1;

// Push the 'then' block with increased nesting
if (ifStatement.Statement != null)
stack.Push((ifStatement.Statement, newNestingLevel));

// Handle the 'else' ElseStatement
if (ifStatement.ElseStatement is not null)
{
if (ifStatement.ElseStatement is IfStatementSyntax)
{
// ELSE IF: Do not increment and no nesting penalty
// Rely on the 'if' to increment
stack.Push((ifStatement.ElseStatement, nestingLevel));
}
else
{
// ELSE (not followed by IF): Increment by 1, No(!) nesting penalty
complexity += 1;
RaiseDEBUGDiagnostic(context, node, ifStatement.ElseKeywordToken.SpanStart, SyntaxKind.ElseKeyword, nestingLevel);
stack.Push((ifStatement.ElseStatement, nestingLevel));
}
}

continue; // Skip further processing for this IF node
}

if (IsFlowBreakingStructure(node) && !IsGuardClause(node))
{
complexity += 1 + nestingLevel;
RaiseDEBUGDiagnostic(context, node, node.SpanStart, node.Kind, nestingLevel);

if (IsNestedStructure(node))
nestingLevel++; // Only increment for true nested structures
nestingLevel++;
}

foreach (var child in node.ChildNodes())
{
stack.Push((child, nestingLevel));
}
}

return complexity;
Expand Down Expand Up @@ -149,4 +195,16 @@ private int GetCognitiveComplexityThreshold(CodeBlockAnalysisContext context)

return threshold;
}

private static void RaiseDEBUGDiagnostic(CodeBlockAnalysisContext context, SyntaxNode node, int SpanStart, SyntaxKind nodeKind, int nestingLevel)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.Rule0089DEBUGCognitiveComplexity,
Location.Create(
node.GetLocation().SourceTree!,
new TextSpan(SpanStart, 1)),
nodeKind,
1 + nestingLevel,
nestingLevel));
}
}
8 changes: 8 additions & 0 deletions BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,14 @@ public static class DiagnosticDescriptors
description: LinterCopAnalyzers.GetLocalizableString("Rule0089CognitiveComplexityDescription"),
helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0089");

public static readonly DiagnosticDescriptor Rule0089DEBUGCognitiveComplexity = new(
id: LinterCopAnalyzers.AnalyzerPrefix + "0089DEBUG",
title: LinterCopAnalyzers.GetLocalizableString("Rule0089CognitiveComplexityTitle"),
messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0089DEBUGCognitiveComplexityFormat"),
category: "Debug",
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: false);

public static readonly DiagnosticDescriptor Rule0090CognitiveComplexity = new(
id: LinterCopAnalyzers.AnalyzerPrefix + "0090",
title: LinterCopAnalyzers.GetLocalizableString("Rule0089CognitiveComplexityTitle"),
Expand Down
3 changes: 3 additions & 0 deletions BusinessCentral.LinterCop/LinterCopAnalyzers.resx
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,9 @@
<data name="Rule0089CognitiveComplexityFormat" xml:space="preserve">
<value>Cognitive Complexity: {0} (threshold &gt;={1})</value>
</data>
<data name="Rule0089DEBUGCognitiveComplexityFormat" xml:space="preserve">
<value>{0}: +{1} (nesting = {2})</value>
</data>
<data name="Rule0089CognitiveComplexityDescription" xml:space="preserve">
<value>This rule analyzes the cognitive complexity of methods to measure how difficult the code is to understand.</value>
</data>
Expand Down