diff --git a/BusinessCentral.LinterCop.Test/Rule0095.cs b/BusinessCentral.LinterCop.Test/Rule0095.cs new file mode 100644 index 00000000..1b740bf0 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0095.cs @@ -0,0 +1,37 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0095 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0095"); + } + + [Test] + [TestCase("ProcedureWithUnusedParameters")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnosticAtAllMarkers(code, DiagnosticDescriptors.Rule0095UnusedProcedureParameter.Id); + } + + [Test] + [TestCase("InternalProcedureWithUsedParameters")] + [TestCase("LocalProcedureWithUnusedParameters")] + [TestCase("GlobalProcedureWithUnusedParameters")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtAllMarkers(code, DiagnosticDescriptors.Rule0095UnusedProcedureParameter.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0095/HasDiagnostic/ProcedureWithUnusedParameters.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0095/HasDiagnostic/ProcedureWithUnusedParameters.al new file mode 100644 index 00000000..dc626187 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0095/HasDiagnostic/ProcedureWithUnusedParameters.al @@ -0,0 +1,8 @@ +codeunit 70020 MyCodeunit +{ + internal procedure TestProcedure([|NotUsed|]: Text; Used: Text; [|NotUsed2|]: Text; Used2: Text) + begin + Used := '42'; + Used2 := '42'; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0095/NoDiagnostic/GlobalProcedureWithUnusedParameters.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0095/NoDiagnostic/GlobalProcedureWithUnusedParameters.al new file mode 100644 index 00000000..074b7cea --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0095/NoDiagnostic/GlobalProcedureWithUnusedParameters.al @@ -0,0 +1,8 @@ +codeunit 70020 MyCodeunit +{ + procedure TestProcedure([|NotUsed|]: Text; Used: Text; [|NotUsed2|]: Text; Used2: Text) + begin + Used := '42'; + Used2 := '42'; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0095/NoDiagnostic/InternalProcedureWithUsedParameters.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0095/NoDiagnostic/InternalProcedureWithUsedParameters.al new file mode 100644 index 00000000..3ff676f3 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0095/NoDiagnostic/InternalProcedureWithUsedParameters.al @@ -0,0 +1,10 @@ +codeunit 70020 MyCodeunit +{ + internal procedure TestProcedure([|Used1|]: Text; [|Used2|]: Text; [|Used3|]: Text; [|Used4|]: Text) + begin + Used1 := '42'; + Used2 := '42'; + Used3 := '42'; + Used4 := '42'; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0095/NoDiagnostic/LocalProcedureWithUnusedParameters.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0095/NoDiagnostic/LocalProcedureWithUnusedParameters.al new file mode 100644 index 00000000..a049454c --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0095/NoDiagnostic/LocalProcedureWithUnusedParameters.al @@ -0,0 +1,8 @@ +codeunit 70020 MyCodeunit +{ + local procedure TestProcedure([|NotUsed|]: Text; Used: Text; [|NotUsed2|]: Text; Used2: Text) + begin + Used := '42'; + Used2 := '42'; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0095UnusedParameter.cs b/BusinessCentral.LinterCop/Design/Rule0095UnusedParameter.cs new file mode 100644 index 00000000..3559a06a --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0095UnusedParameter.cs @@ -0,0 +1,93 @@ +using System.Collections.Immutable; +using BusinessCentral.LinterCop.Helpers; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; + +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0095UnusedParameter : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0095UnusedProcedureParameter); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSymbolAction( + new Action(this.AnalyzeMethod), + SymbolKind.Method + ); + } + + private void AnalyzeMethod(SymbolAnalysisContext context) + { + if (context.IsObsoletePendingOrRemoved() || context.Symbol is not IMethodSymbol methodSymbol) + return; + + // If containing object is not an internal object, we do not need to check + if (methodSymbol.DeclaredAccessibility != Accessibility.Internal) + return; + + // Skip event publishers and event subscribers + if (methodSymbol.IsEvent) + return; + + // Skip if method has no parameters + if (methodSymbol.Parameters.IsEmpty) + return; + + // Get method body for analysis + var syntaxReference = methodSymbol.DeclaringSyntaxReference; + if (syntaxReference == null) + return; + + var methodSyntax = syntaxReference.GetSyntax(); + if (methodSyntax == null) + return; + + foreach (var parameter in methodSymbol.Parameters) + { + // Check if parameter is used in method body + if (!IsParameterUsed(parameter, methodSyntax, context.Compilation)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0095UnusedProcedureParameter, + parameter.GetLocation(), + parameter.Name + )); + } + } + } + + private static bool IsParameterUsed(IParameterSymbol parameter, SyntaxNode methodSyntax, Compilation compilation) + { + var semanticModel = compilation.GetSemanticModel(methodSyntax.SyntaxTree); + + // Find all identifier nodes in the method body + foreach (var node in methodSyntax.DescendantNodes()) + { + if (!node.IsKind(SyntaxKind.IdentifierName)) + continue; + + // Quick name check first (avoid expensive GetSymbolInfo call) + var nodeText = node.ToString(); + if (!string.Equals(nodeText, parameter.Name, StringComparison.OrdinalIgnoreCase)) + continue; + + // Skip parameter declarations (parent is Parameter node) + if (node.Parent?.IsKind(SyntaxKind.Parameter) == true) + continue; + + // Now check if identifier actually references parameter + var symbolInfo = semanticModel.GetSymbolInfo(node); + if (symbolInfo.Symbol is IParameterSymbol paramSymbol && + string.Equals(paramSymbol.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/DiagnosticDescriptors.cs b/BusinessCentral.LinterCop/DiagnosticDescriptors.cs index 32f38983..1a841455 100644 --- a/BusinessCentral.LinterCop/DiagnosticDescriptors.cs +++ b/BusinessCentral.LinterCop/DiagnosticDescriptors.cs @@ -973,6 +973,16 @@ public static class DiagnosticDescriptors description: LinterCopAnalyzers.GetLocalizableString("Rule0094UnnecessaryParameterInMethodCallDescription"), helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0094"); + public static readonly DiagnosticDescriptor Rule0095UnusedProcedureParameter = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0095", + title: LinterCopAnalyzers.GetLocalizableString("Rule0095UnusedParameterInProcedureTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0095UnusedParameterInProcedureFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0095UnusedParameterInProcedureDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0095"); + public static readonly DiagnosticDescriptor Rule9999AssemblyVersionCompatibilityAnalyzer = new( id: LinterCopAnalyzers.AnalyzerPrefix + "9999", title: LinterCopAnalyzers.GetLocalizableString("Rule9999AssemblyVersionCompatibilityAnalyzerTitle"), diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index 09db353a..f6b92e45 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -462,6 +462,16 @@ "action": "Info", "justification": "Global test method requires test attribute. Use e.g. library codeunits for public procedures." }, + { + "id": "LC0094", + "action": "Info", + "justification": "Unnecessary parameters in method calls should be avoided." + }, + { + "id": "LC0095", + "action": "Info", + "justification": "Unused internal procedure parameters should be removed to improve code readability." + }, { "id": "LC9999", "action": "Error", diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 9e532e4c..a43d6caf 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -981,6 +981,15 @@ A method invoked on a record must not contain same variable in parameter list as the one on which the call was made. + + Unused parameter. + + + Parameter '{0}' is not used in the method body. + + + Unused parameters should be avoided. + Analyzer and AL Language version mismatch diff --git a/README.md b/README.md index b4c83dc9..15bbe6ea 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0092](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0092)|Names must match the allowed pattern and must not match the disallowed pattern|Info| |[LC0093](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0093)|Global procedure in test codeunit requires test attribute.|Info|| |[LC0094](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0094)|A method invoked on a record must not contain same variable in parameter list as the one on which the call was made.|Info|| +|[LC0095](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0095)|Internal procedure parameter is unused.|Info|| |[LC9999](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0099)|The version of LinterCop does not match the version of the AL Language compiler it is running on.|Error|| ## Codespace