Skip to content

Commit

Permalink
Merge pull request #514 from rvanbekkum/feature/0052-internal-procedu…
Browse files Browse the repository at this point in the history
…res-never-referenced

Added rule LC0052 and LC0053 Internal Procedures Not Referenced & Internal procedure only referenced from the defining object
  • Loading branch information
Arthurvdv authored Jan 20, 2024
2 parents fe8dc2c + b458e12 commit a625188
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 3 deletions.
226 changes: 226 additions & 0 deletions Design/Rule0052and0053InternalProceduresNotReferenced.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
using BusinessCentral.LinterCop.Helpers;
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using Microsoft.Dynamics.Nav.CodeAnalysis.InternalSyntax;
using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols;
using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace BusinessCentral.LinterCop.Design {
[DiagnosticAnalyzer]
public class Rule0052InternalProceduresNotReferencedAnalyzer : DiagnosticAnalyzer {

private class MethodSymbolAnalyzer : IDisposable {
private readonly PooledDictionary<IMethodSymbol, string> methodSymbols = PooledDictionary<IMethodSymbol, string>.GetInstance();

private readonly PooledDictionary<IMethodSymbol, string> internalMethodsUnused = PooledDictionary<IMethodSymbol, string>.GetInstance();
private readonly PooledDictionary<IMethodSymbol, string> internalMethodsUsedInCurrentObject = PooledDictionary<IMethodSymbol, string>.GetInstance();
private readonly PooledDictionary<IMethodSymbol, string> internalMethodsUsedInOtherObjects = PooledDictionary<IMethodSymbol, string>.GetInstance();

private readonly AttributeKind[] attributeKindsOfMethodsToSkip = new AttributeKind[] { AttributeKind.ConfirmHandler, AttributeKind.FilterPageHandler, AttributeKind.HyperlinkHandler, AttributeKind.MessageHandler, AttributeKind.ModalPageHandler, AttributeKind.PageHandler, AttributeKind.RecallNotificationHandler, AttributeKind.ReportHandler, AttributeKind.RequestPageHandler, AttributeKind.SendNotificationHandler, AttributeKind.SessionSettingsHandler, AttributeKind.StrMenuHandler, AttributeKind.Test };

public MethodSymbolAnalyzer(CompilationAnalysisContext compilationAnalysisContext)
{
ImmutableArray<IApplicationObjectTypeSymbol>.Enumerator objectEnumerator = compilationAnalysisContext.Compilation.GetDeclaredApplicationObjectSymbols().GetEnumerator();
while (objectEnumerator.MoveNext())
{
IApplicationObjectTypeSymbol applicationSymbol = objectEnumerator.Current;
ImmutableArray<ISymbol>.Enumerator objectMemberEnumerator = applicationSymbol.GetMembers().GetEnumerator();
while (objectMemberEnumerator.MoveNext())
{
ISymbol objectMember = objectMemberEnumerator.Current;
if (objectMember.Kind == SymbolKind.Method)
{
IMethodSymbol methodSymbol = objectMember as IMethodSymbol;
if (MethodNeedsReferenceCheck(methodSymbol))
{
methodSymbols.Add(methodSymbol, methodSymbol.Name.ToLowerInvariant());
internalMethodsUnused.Add(methodSymbol, methodSymbol.Name.ToLowerInvariant());
}
}
}
}
}

private bool MethodNeedsReferenceCheck(IMethodSymbol methodSymbol)
{
if (methodSymbol.MethodKind != MethodKind.Method)
{
return false;
}
if (methodSymbol.IsObsoletePending)
{
return false;
}
if (methodSymbol.Attributes.Any(attr => attributeKindsOfMethodsToSkip.Contains(attr.AttributeKind)))
{
return false;
}
if (!methodSymbol.IsInternal)
{
// Check if public procedure in internal object
if (methodSymbol.DeclaredAccessibility == Accessibility.Public && methodSymbol.ContainingSymbol is IApplicationObjectTypeSymbol)
{
var objectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol();

// If the containing object is not an internal object, then we do not need to check for references for this public procedure.
if (objectSymbol.DeclaredAccessibility != Accessibility.Internal)
{
return false;
}

if (HelperFunctions.MethodImplementsInterfaceMethod(objectSymbol, methodSymbol))
{
return false;
}
}
else
{
return false;
}
}

// If the procedure has signature ProcedureName(HostNotification: Notification) or ProcedureName(ErrorInfo: ErrorInfo), then the procedure does not need a reference check
if (methodSymbol.Parameters.Length == 1)
{
ITypeSymbol firstParameterTypeSymbol = methodSymbol.Parameters[0].ParameterType;
if (firstParameterTypeSymbol.GetNavTypeKindSafe() == NavTypeKind.Notification || firstParameterTypeSymbol.GetNavTypeKindSafe() == NavTypeKind.ErrorInfo)
{
return false;
}
}

return true;
}

public void AnalyzeObjectSyntax(CompilationAnalysisContext compilationAnalysisContext)
{
if (methodSymbols.Count == 0)
{
return;
}

Compilation compilation = compilationAnalysisContext.Compilation;
ImmutableArray<SyntaxTree>.Enumerator enumerator = compilation.SyntaxTrees.GetEnumerator();
while (enumerator.MoveNext())
{
if (methodSymbols.Count == 0)
{
break;
}

SyntaxTree syntaxTree = enumerator.Current;
SemanticModel semanticModel = compilation.GetSemanticModel(syntaxTree);
syntaxTree.GetRoot().WalkDescendantsAndPerformAction(delegate (SyntaxNode syntaxNode)
{
if (methodSymbols.Count == 0)
{
return;
}
if (syntaxNode.Parent.IsKind(SyntaxKind.MethodDeclaration) || !syntaxNode.IsKind(SyntaxKind.IdentifierName))
{
return;
}
IdentifierNameSyntax identifierNameSyntax = (IdentifierNameSyntax)syntaxNode;
if (methodSymbols.ContainsValue(identifierNameSyntax.Identifier.ValueText.ToLowerInvariant()) && TryGetSymbolFromIdentifier(semanticModel, (IdentifierNameSyntax)syntaxNode, SymbolKind.Method, out var methodSymbol))
{
if (methodSymbol.IsInternal)
{
var objectSyntax = syntaxNode.GetContainingObjectSyntax();
var objectSyntaxName = objectSyntax.Name.Identifier.ValueText.ToLowerInvariant();

var methodObjectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol();
var methodObjectSymbolName = methodObjectSymbol.Name.ToLowerInvariant();

if (
(methodObjectSymbolName == objectSyntaxName) &&
(objectSyntax.Kind.ToString().Replace("Object", "").ToLowerInvariant() == methodObjectSymbol.Kind.ToString().ToLowerInvariant())
)
{
internalMethodsUsedInCurrentObject[methodSymbol] = methodSymbol.Name.ToLowerInvariant();
}
else
{
internalMethodsUsedInOtherObjects[methodSymbol] = methodSymbol.Name.ToLowerInvariant();
}
}

internalMethodsUnused.Remove(methodSymbol);
}
});
}
}

internal static bool TryGetSymbolFromIdentifier(SemanticModel semanticModel, IdentifierNameSyntax identifierName, SymbolKind symbolKind, out IMethodSymbol methodSymbol)
{
methodSymbol = null;
SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(identifierName);
ISymbol symbol = symbolInfo.Symbol;
if (symbol == null || symbol.Kind != symbolKind)
{
return false;
}
methodSymbol = symbolInfo.Symbol as IMethodSymbol;
if (methodSymbol == null)
{
return false;
}
return true;
}

public void ReportUnchangedReferencePassedParameters(Action<Diagnostic> action)
{
if (internalMethodsUnused.Count == 0)
{
return;
}
foreach (KeyValuePair<IMethodSymbol, string> unusedInternalMethod in internalMethodsUnused)
{
IMethodSymbol methodSymbol = unusedInternalMethod.Key;
IApplicationObjectTypeSymbol objectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol();

Diagnostic diagnostic = Diagnostic.Create(DiagnosticDescriptors.Rule0052InternalProceduresNotReferencedAnalyzerDescriptor, methodSymbol.OriginalDefinition.GetLocation(), methodSymbol.DeclaredAccessibility.ToString().ToLowerInvariant(), methodSymbol.Name, objectSymbol.NavTypeKind, objectSymbol.Name, objectSymbol.DeclaredAccessibility);
action(diagnostic);
}
}

public void ReportInternalMethodOnlyReferencedInCurrentObject(Action<Diagnostic> action)
{
var internalMethodsUsedOnlyInCurrentObject = internalMethodsUsedInCurrentObject.Except(internalMethodsUsedInOtherObjects);

foreach (KeyValuePair<IMethodSymbol, string> internalMethodPair in internalMethodsUsedOnlyInCurrentObject)
{
IMethodSymbol methodSymbol = internalMethodPair.Key;
IApplicationObjectTypeSymbol objectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol();

Diagnostic diagnostic = Diagnostic.Create(DiagnosticDescriptors.Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescriptor, methodSymbol.OriginalDefinition.GetLocation(), methodSymbol.DeclaredAccessibility.ToString().ToLowerInvariant(), methodSymbol.Name, objectSymbol.NavTypeKind, objectSymbol.Name, objectSymbol.DeclaredAccessibility);
action(diagnostic);
}
}

public void Dispose()
{
methodSymbols.Free();
}
}

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0052InternalProceduresNotReferencedAnalyzerDescriptor, DiagnosticDescriptors.Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescriptor);


public override void Initialize(AnalysisContext context)
{
context.RegisterCompilationAction(CheckApplicationObjects);
}

private static void CheckApplicationObjects(CompilationAnalysisContext compilationAnalysisContext)
{
MethodSymbolAnalyzer methodSymbolAnalyzer = new MethodSymbolAnalyzer(compilationAnalysisContext);
methodSymbolAnalyzer.AnalyzeObjectSyntax(compilationAnalysisContext);
methodSymbolAnalyzer.ReportUnchangedReferencePassedParameters(compilationAnalysisContext.ReportDiagnostic);
methodSymbolAnalyzer.ReportInternalMethodOnlyReferencedInCurrentObject(compilationAnalysisContext.ReportDiagnostic);
}
}
}
62 changes: 62 additions & 0 deletions Helpers/HelperFunctions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Microsoft.Dynamics.Nav.CodeAnalysis;

namespace BusinessCentral.LinterCop.Helpers {
public class HelperFunctions {
public static bool MethodImplementsInterfaceMethod(IMethodSymbol methodSymbol)
{
return MethodImplementsInterfaceMethod(methodSymbol.GetContainingApplicationObjectTypeSymbol(), methodSymbol);
}

public static bool MethodImplementsInterfaceMethod(IApplicationObjectTypeSymbol objectSymbol, IMethodSymbol methodSymbol)
{
if (!(objectSymbol is ICodeunitTypeSymbol))
{
return false;
}

var codeunitSymbol = objectSymbol as ICodeunitTypeSymbol;
foreach (var implementedInterface in codeunitSymbol.ImplementedInterfaces)
{
if (implementedInterface.GetMembers().OfType<IMethodSymbol>().Any(interfaceMethodSymbol => MethodImplementsInterfaceMethod(methodSymbol, interfaceMethodSymbol)))
{
return true;
}
}

return false;
}

public static bool MethodImplementsInterfaceMethod(IMethodSymbol methodSymbol, IMethodSymbol interfaceMethodSymbol)
{
if (methodSymbol.Name != interfaceMethodSymbol.Name)
{
return false;
}
if (methodSymbol.Parameters.Length != interfaceMethodSymbol.Parameters.Length)
{
return false;
}
var methodReturnValType = methodSymbol.ReturnValueSymbol?.ReturnType.NavTypeKind ?? NavTypeKind.None;
var interfaceMethodReturnValType = interfaceMethodSymbol.ReturnValueSymbol?.ReturnType.NavTypeKind ?? NavTypeKind.None;
if (methodReturnValType != interfaceMethodReturnValType)
{
return false;
}
for (int i = 0; i < methodSymbol.Parameters.Length; i++)
{
var methodParameter = methodSymbol.Parameters[i];
var interfaceMethodParameter = interfaceMethodSymbol.Parameters[i];

if (methodParameter.IsVar != interfaceMethodParameter.IsVar)
{
return false;
}
if (!methodParameter.ParameterType.Equals(interfaceMethodParameter.ParameterType))
{
return false;
}
}
return true;
}
}
}
2 changes: 2 additions & 0 deletions LinterCopAnalyzers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,7 @@ public static class DiagnosticDescriptors
public static readonly DiagnosticDescriptor Rule0049PageWithoutSourceTable = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0049", (LocalizableString)new LocalizableResourceString("Rule0049PageWithoutSourceTableTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0049PageWithoutSourceTableFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0049PageWithoutSourceTableDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0049");
public static readonly DiagnosticDescriptor Rule0050OperatorAndPlaceholderInFilterExpression = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0050", (LocalizableString)new LocalizableResourceString("Rule0050OperatorAndPlaceholderInFilterExpressionTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0050OperatorAndPlaceholderInFilterExpressionFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0050OperatorAndPlaceholderInFilterExpressionDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0050");
public static readonly DiagnosticDescriptor Rule0051SetFilterPossibleOverflow = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0051", (LocalizableString)new LocalizableResourceString("Rule0051SetFilterPossibleOverflowTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0051SetFilterPossibleOverflowFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0051SetFilterPossibleOverflowDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0051");
public static readonly DiagnosticDescriptor Rule0052InternalProceduresNotReferencedAnalyzerDescriptor = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0052", (LocalizableString)new LocalizableResourceString("Rule0052InternalProceduresNotReferencedAnalyzer", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0052InternalProceduresNotReferencedAnalyzerFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, isEnabledByDefault: true, (LocalizableString)new LocalizableResourceString("Rule0052InternalProceduresNotReferencedAnalyzerDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0052");
public static readonly DiagnosticDescriptor Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescriptor = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0053", (LocalizableString)new LocalizableResourceString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzer", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, isEnabledByDefault: true, (LocalizableString)new LocalizableResourceString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0053");
}
}
Loading

0 comments on commit a625188

Please sign in to comment.