diff --git a/Design/Rule0052and0053InternalProceduresNotReferenced.cs b/Design/Rule0052and0053InternalProceduresNotReferenced.cs new file mode 100644 index 00000000..f8e50ab6 --- /dev/null +++ b/Design/Rule0052and0053InternalProceduresNotReferenced.cs @@ -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 methodSymbols = PooledDictionary.GetInstance(); + + private readonly PooledDictionary internalMethodsUnused = PooledDictionary.GetInstance(); + private readonly PooledDictionary internalMethodsUsedInCurrentObject = PooledDictionary.GetInstance(); + private readonly PooledDictionary internalMethodsUsedInOtherObjects = PooledDictionary.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.Enumerator objectEnumerator = compilationAnalysisContext.Compilation.GetDeclaredApplicationObjectSymbols().GetEnumerator(); + while (objectEnumerator.MoveNext()) + { + IApplicationObjectTypeSymbol applicationSymbol = objectEnumerator.Current; + ImmutableArray.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.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 action) + { + if (internalMethodsUnused.Count == 0) + { + return; + } + foreach (KeyValuePair 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 action) + { + var internalMethodsUsedOnlyInCurrentObject = internalMethodsUsedInCurrentObject.Except(internalMethodsUsedInOtherObjects); + + foreach (KeyValuePair 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 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); + } + } +} diff --git a/Helpers/HelperFunctions.cs b/Helpers/HelperFunctions.cs new file mode 100644 index 00000000..de288494 --- /dev/null +++ b/Helpers/HelperFunctions.cs @@ -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().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; + } + } +} diff --git a/LinterCopAnalyzers.Generated.cs b/LinterCopAnalyzers.Generated.cs index 0d9dbbb0..d653fd74 100644 --- a/LinterCopAnalyzers.Generated.cs +++ b/LinterCopAnalyzers.Generated.cs @@ -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"); } } \ No newline at end of file diff --git a/LinterCopAnalyzers.resx b/LinterCopAnalyzers.resx index 45dd33ea..7085f87f 100644 --- a/LinterCopAnalyzers.resx +++ b/LinterCopAnalyzers.resx @@ -518,7 +518,7 @@ Zero (0) Enum value should be reserved for Empty Value. - + Label with suffix Tok must be locked. @@ -527,7 +527,7 @@ Label with suffix Tok must be locked. - + Locked Label must have a suffix Tok. @@ -573,4 +573,22 @@ Do not assign a text to a target with smaller size. + + The internal method is declared but never used. + + + The {0} method {1} in {2} {3} (Access = {4}) is declared but never used. + + + The internal method is declared but never used. + + + The internal method is only used in the object in which it is declared. + + + The {0} method {1} is only used in the object {2} {3} (Access = {4}) in which it is declared. Consider making the procedure local. + + + The internal method is only used in the object in which it is declared. + \ No newline at end of file diff --git a/README.md b/README.md index bbb51aab..e3bda5c9 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,8 @@ Further note that you should have BcContainerHelper version 2.0.16 (or newer) in |[LC0049](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0049)|`SourceTable` property not defined on Page.|Info| |[LC0050](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0050)|`SetFilter` with unsupported operator in filter expression.|Info| |[LC0051](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0051)|Do not assign a text to a target with smaller size.|Warning| - +|[LC0052](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0052)|The internal procedure is declared but never used.|Info| +|[LC0053](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0053)|The internal procedure is only used in the object in which it is declared. Consider making the procedure local.|Info| ## Configuration