From fbb41fe25fc2afa6b5bfa77ccd8b704df7c03881 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 29 Jan 2026 17:11:27 +0100 Subject: [PATCH] Add "OptionType should be Enum" to PlatfomCop --- .../HasDiagnostic/OptionField.al | 13 ++ .../HasDiagnostic/OptionParameterGlobalVar.al | 19 ++ .../HasDiagnostic/OptionParameterLocalVar.al | 17 ++ .../HasDiagnostic/OptionReturnValue.al | 7 + .../HasDiagnostic/OptionVariable.al | 14 ++ .../NoDiagnostic/CDSDocument.al | 37 +++ .../NoDiagnostic/EventSubscriberOption.al | 23 ++ .../NoDiagnostic/FlowField.al | 27 +++ .../NoDiagnostic/ObsoleteFieldOption.al | 18 ++ .../NoDiagnostic/OptionParameter.al | 15 ++ .../OptionTypeShouldBeEnum.cs | 49 ++++ .../ALCops.LinterCopAnalyzers.cs | 27 +++ .../ALCops.LinterCopAnalyzers.resx | 9 + .../Analyzers/OptionTypeShouldBeEnum.cs | 214 ++++++++++++++++++ src/ALCops.LinterCop/DiagnosticDescriptors.cs | 10 + src/ALCops.LinterCop/DiagnosticIds.cs | 1 + 16 files changed, 500 insertions(+) create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionField.al create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionParameterGlobalVar.al create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionParameterLocalVar.al create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionReturnValue.al create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionVariable.al create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/CDSDocument.al create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/EventSubscriberOption.al create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/FlowField.al create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/ObsoleteFieldOption.al create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/OptionParameter.al create mode 100644 src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/OptionTypeShouldBeEnum.cs create mode 100644 src/ALCops.LinterCop/Analyzers/OptionTypeShouldBeEnum.cs diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionField.al b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionField.al new file mode 100644 index 0000000..62b5a7f --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionField.al @@ -0,0 +1,13 @@ +table 50100 "My Table" +{ + fields + { + field(1; "Test Option"; [|Option|]) + { + Caption = 'Test Option'; + DataClassification = CustomerContent; + OptionCaption = '0,1,2,3,4,5'; + OptionMembers = "0","1","2","3","4","5"; + } + } +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionParameterGlobalVar.al b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionParameterGlobalVar.al new file mode 100644 index 0000000..c29d73f --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionParameterGlobalVar.al @@ -0,0 +1,19 @@ +codeunit 50100 MyCodeunit +{ + var + [|Mode|]: Option "None","Allow deletion",Match; + + procedure MyProcedure() + var + ReservationManagement: Codeunit "Reservation Management PTE"; + begin + ReservationManagement.SetItemTrackingHandling(Mode); + end; +} + +codeunit 50101 "Reservation Management PTE" +{ + procedure SetItemTrackingHandling(Mode: Option "None","Allow deletion",Match) + begin + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionParameterLocalVar.al b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionParameterLocalVar.al new file mode 100644 index 0000000..ec21e0b --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionParameterLocalVar.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + ReservationManagement: Codeunit "Reservation Management PTE"; + [|Mode|]: Option "None","Allow deletion",Match; + begin + ReservationManagement.SetItemTrackingHandling(Mode); + end; +} + +codeunit 50101 "Reservation Management PTE" +{ + procedure SetItemTrackingHandling(Mode: Option "None","Allow deletion",Match) + begin + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionReturnValue.al b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionReturnValue.al new file mode 100644 index 0000000..2ee0208 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionReturnValue.al @@ -0,0 +1,7 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() Mode: [|Option "None","Allow deletion",Match|] + begin + + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionVariable.al b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionVariable.al new file mode 100644 index 0000000..073057e --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/HasDiagnostic/OptionVariable.al @@ -0,0 +1,14 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure([|TestOption: Option " ","Test1","Test2"|]) + begin + case TestOption of + TestOption::" ": + exit; + TestOption::"Test1": + Message('test 1'); + TestOption::"Test2": + Message('test 2'); + end; + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/CDSDocument.al b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/CDSDocument.al new file mode 100644 index 0000000..01d0f90 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/CDSDocument.al @@ -0,0 +1,37 @@ +table 50100 "Dataverse Project PTE" +{ + ExternalName = 'prefix_project'; + TableType = CDS; + Description = ''; + Caption = 'Project'; + + fields + { + field(1; prefix_projectId; Guid) + { + ExternalName = 'prefix_projectid'; + ExternalType = 'Uniqueidentifier'; + ExternalAccess = Insert; + Description = 'Unique identifier for entity instances'; + Caption = 'Project'; + } + field(2; prefix_name; Text[100]) + { + ExternalName = 'prefix_name'; + ExternalType = 'String'; + Description = 'The name of the custom entity.'; + Caption = 'Name'; + } + field(25; statecode; [|Option|]) + { + ExternalName = 'statecode'; + ExternalType = 'State'; + ExternalAccess = Modify; + Description = 'Status of the Project'; + Caption = 'Status'; + InitValue = " "; + OptionMembers = " ",Active,Inactive; + OptionOrdinalValues = -1, 0, 1; + } + } +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/EventSubscriberOption.al b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/EventSubscriberOption.al new file mode 100644 index 0000000..0bf08be --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/EventSubscriberOption.al @@ -0,0 +1,23 @@ +codeunit 50100 MyCodeunit +{ + trigger OnRun() + var + OptionVariable: Option "", "test1"; + begin + OnCodeunitRun(OptionVariable); + end; + + [IntegrationEvent(false, false)] + local procedure OnCodeunitRun(var OptionVariable: Option "", "test1") + begin + end; +} + +codeunit 50101 MySubscriber +{ + [EventSubscriber(ObjectType::Codeunit, Codeunit::MyCodeunit, OnCodeunitRun, '', false, false)] + local procedure OnCodeunitRun_MyCodeunit(var [|OptionVariable: Option "", "test1"|]) + begin + + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/FlowField.al b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/FlowField.al new file mode 100644 index 0000000..538b714 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/FlowField.al @@ -0,0 +1,27 @@ +table 50100 MyTable +{ + fields + { + field(1; "No."; Code[20]) { } + field(2; "Warehouse Status"; [|Option|]) + { + CalcFormula = min("My Warehouse Shipment Header"."Document Status" where("No." = field("No."))); + FieldClass = FlowField; + OptionMembers = " ","Partially Picked","Partially Shipped","Completely Picked","Completely Shipped"; + } + } +} + +table 50101 "My Warehouse Shipment Header" +{ + fields + { + field(1; "No."; Code[20]) { } + field(2; "Document Status"; Option) + { + OptionMembers = " ","Partially Picked","Partially Shipped","Completely Picked","Completely Shipped"; + } + } +} + +enum 50100 "Warehouse Status" { } \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/ObsoleteFieldOption.al b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/ObsoleteFieldOption.al new file mode 100644 index 0000000..c82d9a1 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/ObsoleteFieldOption.al @@ -0,0 +1,18 @@ +table 50100 "My Table" +{ + fields + { + field(1; MyField; Integer) + { + DataClassification = ToBeClassified; + } + field(2; "Test Option"; [|Option|]) + { + Caption = 'Test Option'; + ObsoleteState = Pending; + DataClassification = CustomerContent; + OptionCaption = '0,1,2,3,4,5'; + OptionMembers = "0","1","2","3","4","5"; + } + } +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/OptionParameter.al b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/OptionParameter.al new file mode 100644 index 0000000..6f6ce66 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/NoDiagnostic/OptionParameter.al @@ -0,0 +1,15 @@ +//TODO: Test with an codeunit from a dependency extension so the ."GetLocation().IsInSource" returns false +codeunit 50100 MyCodeunit +{ + var + ModeGlobal: Option "None","Allow deletion",Match; // Should not raise a diagnostic + + procedure MyProcedure() + var + ReservationManagement: Codeunit "Reservation Management"; + ModeLocal: Option "None","Allow deletion",Match; // Should not raise a diagnostic + begin + ReservationManagement.SetItemTrackingHandling(ModeLocal); + ReservationManagement.SetItemTrackingHandling(ModeGlobal); + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/OptionTypeShouldBeEnum.cs b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/OptionTypeShouldBeEnum.cs new file mode 100644 index 0000000..932c8a9 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/OptionTypeShouldBeEnum/OptionTypeShouldBeEnum.cs @@ -0,0 +1,49 @@ +using RoslynTestKit; + +namespace ALCops.LinterCop.Test +{ + public class OptionTypeShouldBeEnum : NavCodeAnalysisBase + { + private AnalyzerTestFixture _fixture; + private string _testCasePath; + + [SetUp] + public void Setup() + { + _fixture = RoslynFixtureFactory.Create(); + + _testCasePath = Path.Combine( + Directory.GetParent( + Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + Path.Combine("Rules", nameof(OptionTypeShouldBeEnum))); + } + + [Test] + [TestCase("OptionField")] + [TestCase("OptionParameterGlobalVar")] + [TestCase("OptionParameterLocalVar")] + [TestCase("OptionReturnValue")] + [TestCase("OptionVariable")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.OptionTypeShouldBeEnum); + } + + [Test] + [TestCase("CDSDocument")] + [TestCase("EventSubscriberOption")] + [TestCase("FlowField")] + [TestCase("ObsoleteFieldOption")] + // [TestCase("OptionParameter")] //TODO: See remarks in the test file + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(NoDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.NoDiagnosticAtAllMarkers(code, DiagnosticIds.OptionTypeShouldBeEnum); + } + } +} \ No newline at end of file diff --git a/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.cs b/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.cs index fd015d4..c392218 100644 --- a/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.cs +++ b/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.cs @@ -615,6 +615,33 @@ internal static string ObjectIdInDeclarationTitle { } } + /// + /// Looks up a localized string similar to Enums are preferred over Option types because they provide stronger typing, better extensibility, and clearer intent. Using Enums reduces error-prone patterns commonly associated with Option fields and improves maintainability and versioning of AL code.. + /// + internal static string OptionTypeShouldBeEnumDescription { + get { + return ResourceManager.GetString("OptionTypeShouldBeEnumDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefer using an Enum instead of an Option type.. + /// + internal static string OptionTypeShouldBeEnumMessageFormat { + get { + return ResourceManager.GetString("OptionTypeShouldBeEnumMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefer Enum over Option type. + /// + internal static string OptionTypeShouldBeEnumTitle { + get { + return ResourceManager.GetString("OptionTypeShouldBeEnumTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Adopting the use of the new PageStyle datatype allows to more easily get the supported pagestyles via IntelliSense and avoids incorrect behaviour when a typo is made in hardcoded strings or label variables.. /// diff --git a/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.resx b/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.resx index c0d50ae..c623866 100644 --- a/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.resx +++ b/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.resx @@ -306,6 +306,15 @@ ALCops: Replace id with name of object reference + + Prefer Enum over Option type + + + Prefer using an Enum instead of an Option type. + + + Enums are preferred over Option types because they provide stronger typing, better extensibility, and clearer intent. Using Enums reduces error-prone patterns commonly associated with Option fields and improves maintainability and versioning of AL code. + Use the new PageStyle datatype instead string literals diff --git a/src/ALCops.LinterCop/Analyzers/OptionTypeShouldBeEnum.cs b/src/ALCops.LinterCop/Analyzers/OptionTypeShouldBeEnum.cs new file mode 100644 index 0000000..7b68ebb --- /dev/null +++ b/src/ALCops.LinterCop/Analyzers/OptionTypeShouldBeEnum.cs @@ -0,0 +1,214 @@ +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.Symbols; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; + +namespace ALCops.LinterCop.Analyzers; + +[DiagnosticAnalyzer] +public sealed class OptionTypeShouldBeEnum : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( + DiagnosticDescriptors.OptionTypeShouldBeEnum); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction( + new Action(this.AnalyzeSyntaxNodes), + EnumProvider.SyntaxKind.OptionDataType); + + context.RegisterSymbolAction( + new Action(this.AnalyzeVariables), + EnumProvider.SymbolKind.GlobalVariable, + EnumProvider.SymbolKind.LocalVariable); + } + + #region SyntaxNodeAnalysis + private void AnalyzeSyntaxNodes(SyntaxNodeAnalysisContext ctx) + { + if (ctx.IsObsolete() || ctx.Node is not OptionDataTypeSyntax optionDataType) + return; + + bool skipDueToLocalOrGlobalVariable = optionDataType.Parent is SimpleTypeReferenceSyntax && !IsParameterOrReturnValue(optionDataType); + if (skipDueToLocalOrGlobalVariable) + return; + + bool skipDueToIsEventSubscriber = ctx.ContainingSymbol is IMethodSymbol method && method.IsEventSubscriber(); + if (skipDueToIsEventSubscriber) + return; + + bool skipDueToTableIsOfTypeCDS = ctx.ContainingSymbol.GetContainingApplicationObjectTypeSymbol() is ITableTypeSymbol table && table.TableType == TableTypeKind.CDS; + if (skipDueToTableIsOfTypeCDS) + return; + + if (optionDataType.Parent is FieldSyntax fieldSyntax && + IsFlowFieldWithOptionCalculation(fieldSyntax, ctx)) + { + return; + } + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.OptionTypeShouldBeEnum, + optionDataType.GetLocation() + )); + } + + private static bool IsParameterOrReturnValue(OptionDataTypeSyntax optionDataType) + { + var parent = optionDataType.Parent?.Parent; + return parent is not null && (parent.Kind == EnumProvider.SyntaxKind.Parameter || parent.Kind == EnumProvider.SyntaxKind.ReturnValue); + } + + private static bool IsFlowFieldWithOptionCalculation(FieldSyntax fieldSyntax, SyntaxNodeAnalysisContext ctx) + { + var propertyList = fieldSyntax.PropertyList?.Properties; + if (propertyList is null) + return false; + + foreach (var property in propertyList) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + if (property is PropertySyntax propertySyntax && + propertySyntax.Value is FieldCalculationFormulaSyntax fieldCalculation && + fieldCalculation.Field is QualifiedNameSyntax qualifiedNameSyntax) + { + var fieldSymbol = ctx.SemanticModel.GetSymbolInfo(qualifiedNameSyntax, ctx.CancellationToken).Symbol; + if (fieldSymbol?.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Option) + { + return true; + } + } + } + return false; + } + #endregion + + #region VariableAnalysis + private void AnalyzeVariables(SymbolAnalysisContext ctx) + { + if (ctx.IsObsolete() || ctx.Symbol is not IVariableSymbol variable) + return; + + if (variable.Type.GetNavTypeKindSafe() != NavTypeKind.Option) + return; + + bool? HasVariablesNotInSource = null; + switch (variable.Kind) + { + case var _ when variable.Kind == EnumProvider.SymbolKind.LocalVariable: + ISymbol? containingSymbol = variable.ContainingSymbol; + if (containingSymbol is null) + return; + + var localVariablesName = GetReferencedVariableNames(containingSymbol, variable); + var localVariables = ((IMethodSymbol)containingSymbol.OriginalDefinition).LocalVariables; + HasVariablesNotInSource = HasReferencedVariablesNotInSource(localVariables, localVariablesName); + break; + + case var _ when variable.Kind == EnumProvider.SymbolKind.GlobalVariable: + IApplicationObjectTypeSymbol? applicationObjectTypeSymbol = variable.GetContainingApplicationObjectTypeSymbol(); + if (applicationObjectTypeSymbol is null) + return; + + var globalVariablesName = GetReferencedVariableNames(applicationObjectTypeSymbol, variable); + var globalVariables = GetGlobalVariablesFromApplicationObject(applicationObjectTypeSymbol); + HasVariablesNotInSource = HasReferencedVariablesNotInSource(globalVariables, globalVariablesName); + break; + } + + if (HasVariablesNotInSource is false) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.OptionTypeShouldBeEnum, + variable.GetLocation() + )); + } + } + + private static bool HasReferencedVariablesNotInSource( + IEnumerable variables, + HashSet? referencedNames) + { + if (referencedNames is null) + return false; + + // Filter by name first (cheaper operation), then check location + return variables + .Where(var => referencedNames.Contains(var.OriginalDefinition.Name)) + .Any(var => !var.Type.GetLocation().IsInSource); + } + + private static List GetGlobalVariablesFromApplicationObject(IApplicationObjectTypeSymbol applicationObjectTypeSymbol) + { + var variables = new List(); + + foreach (var member in applicationObjectTypeSymbol.GetMembers()) + { + if (member.Kind == EnumProvider.SymbolKind.GlobalVariable && member is IVariableSymbol globalVar) + { + variables.Add(globalVar); + } + else if (member.Kind == EnumProvider.SymbolKind.Method && member is IMethodSymbol method) + { + variables.AddRange(method.LocalVariables); + } + } + + return variables; + } + + private static HashSet? GetReferencedVariableNames(ISymbol? containingSymbol, IVariableSymbol variable) + { + SyntaxNode? syntaxNode = containingSymbol?.DeclaringSyntaxReference?.GetSyntax(); + if (syntaxNode is null) + return null; + + var variableName = variable.Name; + var referencedNames = new HashSet(); + + // Find all argument lists that reference the variable + var argumentLists = syntaxNode.DescendantNodes() + .OfType(); + + foreach (var argList in argumentLists) + { + // Check if any argument in this list references our variable + bool hasVariableReference = false; + foreach (var argument in argList.Arguments) + { + if ((argument is OptionAccessExpressionSyntax optionAccess && + optionAccess.Expression.GetIdentifierOrLiteralValue() == variableName) || + argument.GetIdentifierOrLiteralValue() == variableName) + { + hasVariableReference = true; + break; + } + } + + if (hasVariableReference) + { + // Get the member access expressions from the containing expression statement + foreach (var exprStmt in argList.AncestorsAndSelf().OfType()) + { + foreach (var memberAccess in exprStmt.DescendantNodes().OfType()) + { + var expressionName = memberAccess.Expression.ToString().UnquoteIdentifier(); + if (!string.IsNullOrEmpty(expressionName)) + { + referencedNames.Add(expressionName); + } + } + } + } + } + + return referencedNames; + } + #endregion +} \ No newline at end of file diff --git a/src/ALCops.LinterCop/DiagnosticDescriptors.cs b/src/ALCops.LinterCop/DiagnosticDescriptors.cs index acdf79e..f67ace2 100644 --- a/src/ALCops.LinterCop/DiagnosticDescriptors.cs +++ b/src/ALCops.LinterCop/DiagnosticDescriptors.cs @@ -204,6 +204,16 @@ public static class DiagnosticDescriptors description: LinterCopAnalyzers.ObjectIdInDeclarationDescription, helpLinkUri: GetHelpUri(DiagnosticIds.ObjectIdInDeclaration)); + public static readonly DiagnosticDescriptor OptionTypeShouldBeEnum = new( + id: DiagnosticIds.OptionTypeShouldBeEnum, + title: LinterCopAnalyzers.OptionTypeShouldBeEnumTitle, + messageFormat: LinterCopAnalyzers.OptionTypeShouldBeEnumMessageFormat, + category: Category.Design, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.OptionTypeShouldBeEnumDescription, + helpLinkUri: GetHelpUri(DiagnosticIds.OptionTypeShouldBeEnum)); + public static readonly DiagnosticDescriptor PageStyleStringLiteral = new( id: DiagnosticIds.PageStyleStringLiteral, title: LinterCopAnalyzers.PageStyleStringLiteralTitle, diff --git a/src/ALCops.LinterCop/DiagnosticIds.cs b/src/ALCops.LinterCop/DiagnosticIds.cs index 274ad24..9bd9bc8 100644 --- a/src/ALCops.LinterCop/DiagnosticIds.cs +++ b/src/ALCops.LinterCop/DiagnosticIds.cs @@ -23,6 +23,7 @@ public static class DiagnosticIds public static readonly string UseQueryOrFindWithNextInsteadOfCount = "LC0082"; public static readonly string BuiltInDateTimeMethod = "LC0083"; public static readonly string PageStyleStringLiteral = "LC0086"; + public static readonly string OptionTypeShouldBeEnum = "LC0088"; public static readonly string CognitiveComplexityMetric = "LC0089"; public static readonly string CognitiveComplexityIncrement = "LC0089i"; public static readonly string CognitiveComplexityThresholdExceeded = "LC0090";