From d957010f6728afd77b2b8f3706671e90f3716552 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Mon, 8 Jan 2024 13:58:25 +0100 Subject: [PATCH 01/16] Typo --- Design/Rule0050SetFilterOperatorCharInFilterExpression.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Design/Rule0050SetFilterOperatorCharInFilterExpression.cs b/Design/Rule0050SetFilterOperatorCharInFilterExpression.cs index 2159c1cb..52bcc9eb 100644 --- a/Design/Rule0050SetFilterOperatorCharInFilterExpression.cs +++ b/Design/Rule0050SetFilterOperatorCharInFilterExpression.cs @@ -41,7 +41,7 @@ private void CheckParameter(IOperation operand, ref IInvocationExpression operat string parameterString = operand.Syntax.ToFullString(); - string pattern = @"%\d+"; // Only when a %1 is used in the filter expression the unsupported operators are threaded as a literal character + string pattern = @"%\d+"; // Only when a %1 is used in the filter expression the unsupported operators are treated as a literal character Regex regex = new Regex(pattern); if (!regex.IsMatch(parameterString)) return; From a7aebe65ed378adfd7e071f4a05e18a19aba5b24 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Mon, 8 Jan 2024 17:43:54 +0100 Subject: [PATCH 02/16] Prep for SecretText on IsolatedStorage --- Design/Rule0043SecretText.cs | 51 ++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/Design/Rule0043SecretText.cs b/Design/Rule0043SecretText.cs index 104235a0..2fe2c07e 100644 --- a/Design/Rule0043SecretText.cs +++ b/Design/Rule0043SecretText.cs @@ -11,6 +11,7 @@ public class Rule0043SecretText : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0043SecretText); private static readonly string authorization = "Authorization"; + private static readonly List buildInMethodNames = new List { "add", @@ -18,7 +19,48 @@ public class Rule0043SecretText : DiagnosticAnalyzer "tryaddwithoutvalidation" }; - public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.AnalyzeHttpObjects), OperationKind.InvocationExpression); + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction(new Action(this.AnalyzeHttpObjects), OperationKind.InvocationExpression); + // TODO: enable after Spring2024OrGreater release + // context.RegisterOperationAction(new Action(this.AnalyzeIsolatedStorage), OperationKind.InvocationExpression); + } + + private void AnalyzeIsolatedStorage(OperationAnalysisContext ctx) + { + // TODO: enable after Spring2024OrGreater release + // if (!VersionChecker.IsSupported(ctx.ContainingSymbol, VersionCompatibility.Spring2024OrGreater)) return; + + if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return; + if (ctx.ContainingSymbol.IsObsoletePending || ctx.ContainingSymbol.IsObsoleteRemoved) return; + + IInvocationExpression operation = (IInvocationExpression)ctx.Operation; + if (operation.Arguments.Count() < 3) return; + + IMethodSymbol targetMethod = operation.TargetMethod; + if (targetMethod == null || targetMethod.ContainingSymbol.Kind != SymbolKind.Class) return; + if (!SemanticFacts.IsSameName(targetMethod.ContainingSymbol.Name, "IsolatedStorage")) return; + + int argumentIndex; + switch (operation.TargetMethod.Name.ToLowerInvariant()) + { + case "get": + argumentIndex = 2; + break; + case "set": + case "setencrypted": + argumentIndex = 1; + break; + default: + argumentIndex = -1; + break; + } + + if (argumentIndex == -1) return; + + if (!IsArgumentOfTypeSecretText(operation.Arguments[argumentIndex])) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0043SecretText, ctx.Operation.Syntax.GetLocation())); + } private void AnalyzeHttpObjects(OperationAnalysisContext ctx) { @@ -51,10 +93,15 @@ private void AnalyzeHttpObjects(OperationAnalysisContext ctx) if (!IsAuthorizationArgument(operation.Arguments[0])) return; - if (operation.Arguments[1].Parameter.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.SecretText) + if (!IsArgumentOfTypeSecretText(operation.Arguments[1])) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0043SecretText, ctx.Operation.Syntax.GetLocation())); } + private bool IsArgumentOfTypeSecretText(IArgument argument) + { + return argument.Parameter.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.SecretText; + } + private static bool IsAuthorizationArgument(IArgument argument) { switch (argument.Syntax.Kind) From b7b7e6bc13c5b13edd557b9dcbf3cb3289b91fb8 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Tue, 9 Jan 2024 11:42:42 +0100 Subject: [PATCH 03/16] Resolve NullReference exception --- Design/Rule0043SecretText.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Design/Rule0043SecretText.cs b/Design/Rule0043SecretText.cs index 2fe2c07e..a707b431 100644 --- a/Design/Rule0043SecretText.cs +++ b/Design/Rule0043SecretText.cs @@ -56,7 +56,7 @@ private void AnalyzeIsolatedStorage(OperationAnalysisContext ctx) break; } - if (argumentIndex == -1) return; + if (argumentIndex == -1 && operation.Arguments[argumentIndex].Parameter == null) return; if (!IsArgumentOfTypeSecretText(operation.Arguments[argumentIndex])) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0043SecretText, ctx.Operation.Syntax.GetLocation())); From 7e30b24f43bd55dbf8b8ef9a73d964749c4cd28a Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Tue, 9 Jan 2024 11:49:11 +0100 Subject: [PATCH 04/16] Fix wrong operator --- Design/Rule0043SecretText.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Design/Rule0043SecretText.cs b/Design/Rule0043SecretText.cs index a707b431..8878deab 100644 --- a/Design/Rule0043SecretText.cs +++ b/Design/Rule0043SecretText.cs @@ -56,7 +56,7 @@ private void AnalyzeIsolatedStorage(OperationAnalysisContext ctx) break; } - if (argumentIndex == -1 && operation.Arguments[argumentIndex].Parameter == null) return; + if (argumentIndex == -1 || operation.Arguments[argumentIndex].Parameter == null) return; if (!IsArgumentOfTypeSecretText(operation.Arguments[argumentIndex])) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0043SecretText, ctx.Operation.Syntax.GetLocation())); From 660a61a36ef489835340fcaca4188bcef12bb640 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Tue, 9 Jan 2024 17:09:02 +0100 Subject: [PATCH 05/16] Exclude fields with #pragma warning disable --- Design/Rule0044AnalyzeTransferField.cs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Design/Rule0044AnalyzeTransferField.cs b/Design/Rule0044AnalyzeTransferField.cs index 2e9f89d8..a6f72f2d 100644 --- a/Design/Rule0044AnalyzeTransferField.cs +++ b/Design/Rule0044AnalyzeTransferField.cs @@ -748,7 +748,7 @@ public void PopulateFields(FieldExtensionListSyntax fieldList) foreach (FieldSyntax field in fieldList.Fields.Where(fld => fld.IsKind(SyntaxKind.Field))) { - if (!FieldIsObsolete(field)) + if (!FieldIsObsolete(field) && !IsRuleDisabledWithPragma(field)) Fields.Add(new Field((int)field.No.Value, field.Name.Identifier.ValueText.UnquoteIdentifier(), field.Type.ToString(), field.GetLocation(), this, GetFieldClass(field))); } } @@ -759,7 +759,7 @@ public void PopulateFields(FieldListSyntax fieldList) foreach (FieldSyntax field in fieldList.Fields) { - if (!FieldIsObsolete(field)) + if (!FieldIsObsolete(field) && !IsRuleDisabledWithPragma(field)) Fields.Add(new Field((int)field.No.Value, field.Name.Identifier.ValueText.UnquoteIdentifier(), field.Type.ToString(), field.GetLocation(), this, GetFieldClass(field))); } } @@ -783,6 +783,24 @@ private bool FieldIsObsolete(FieldSyntax field) return false; } + private static bool IsRuleDisabledWithPragma(FieldSyntax field) + { + IList pragmaWarningDirectives = field.GetDirectives() + .Where(directive => directive.IsKind(SyntaxKind.PragmaWarningDirectiveTrivia)) + .OfType() + .ToList(); + + foreach (PragmaWarningDirectiveTriviaSyntax pragmaWarningDirective in pragmaWarningDirectives) + { + if (pragmaWarningDirective.ErrorCodes.OfType() + .Where(i => i.Identifier.Text.Contains(DiagnosticDescriptors.Rule0044AnalyzeTransferFields.Id)) + .Any()) + return true; + } + + return false; + } + private FieldClassKind GetFieldClass(FieldSyntax field) { PropertySyntax fieldClassProperty = (PropertySyntax)field.PropertyList.Properties.Where(prop => !prop.IsKind(SyntaxKind.EmptyProperty)) From cfc815fb1f4775f5edbe2bd835aa4aab12d4665f Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Wed, 10 Jan 2024 17:43:40 +0100 Subject: [PATCH 06/16] New rule 0051 SetFilter Possible Overflow --- Design/Rule0051SetFilterPossibleOverflow.cs | 252 ++++++++++++++++++++ LinterCop.ruleset.json | 5 + LinterCopAnalyzers.Generated.cs | 1 + LinterCopAnalyzers.resx | 9 + README.md | 3 +- 5 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 Design/Rule0051SetFilterPossibleOverflow.cs diff --git a/Design/Rule0051SetFilterPossibleOverflow.cs b/Design/Rule0051SetFilterPossibleOverflow.cs new file mode 100644 index 00000000..316cad07 --- /dev/null +++ b/Design/Rule0051SetFilterPossibleOverflow.cs @@ -0,0 +1,252 @@ +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; +using System.Collections.Immutable; +using System.Text.RegularExpressions; + +namespace BusinessCentral.LinterCop.Design +{ + [DiagnosticAnalyzer] + public class Rule0051SetFilterPossibleOverflow : DiagnosticAnalyzer + { + private readonly Lazy strSubstNoPatternLazy = new Lazy((Func)(() => new Regex("[#%](\\d+)", RegexOptions.Compiled))); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0051SetFilterPossibleOverflow); + + private Regex StrSubstNoPattern => this.strSubstNoPatternLazy.Value; + + public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); + + private void AnalyzeInvocation(OperationAnalysisContext ctx) + { + if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return; + if (ctx.ContainingSymbol.IsObsoletePending || ctx.ContainingSymbol.IsObsoleteRemoved) return; + + IInvocationExpression operation = (IInvocationExpression)ctx.Operation; + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; + + if (operation.TargetMethod == null || !SemanticFacts.IsSameName(operation.TargetMethod.Name, "SetFilter") || operation.Arguments.Count() < 3) + return; + + if (operation.Arguments[0].Value.Kind != OperationKind.ConversionExpression) return; + IOperation fieldOperand = ((IConversionExpression)operation.Arguments[0].Value).Operand; + ITypeSymbol fieldType = fieldOperand.Type; + bool isError = false; + int typeLength = GetTypeLength(fieldType, ref isError); + if (isError || typeLength == int.MaxValue) + return; + + foreach (int argIndex in GetArgumentIndexes(operation.Arguments[1].Value)) + { + int index = argIndex + 1; // The placeholders are defines as %1, %2, %3, where in case of %1 we need the second (zero based) index of the arguments of the SetFilter method + if (index < 2 || index >= operation.Arguments.Count()) continue; + + int expressionLength = this.CalculateMaxExpressionLength(((IConversionExpression)operation.Arguments[index].Value).Operand, ref isError); + if (!isError && expressionLength > typeLength) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0051SetFilterPossibleOverflow, operation.Syntax.GetLocation(), GetDisplayString(operation.Arguments[index], operation), GetDisplayString(operation.Arguments[0], operation))); + } + } + + private static int GetTypeLength(ITypeSymbol type, ref bool isError) + { + if (!type.IsTextType()) + { + isError = true; + return 0; + } + if (type.HasLength) + return type.Length; + return type.NavTypeKind == NavTypeKind.Label ? GetLabelTypeLength(type) : int.MaxValue; + } + + private static int GetLabelTypeLength(ITypeSymbol type) + { + ILabelTypeSymbol labelType = (ILabelTypeSymbol)type; + + if (labelType.Locked) + return labelType.GetLabelText().Length; + + return labelType.MaxLength; + } + + private int CalculateMaxExpressionLength(IOperation expression, ref bool isError) + { + if (expression.Syntax.Parent.IsKind(SyntaxKind.CaseLine)) + { + isError = true; + return 0; + } + switch (expression.Kind) + { + case OperationKind.LiteralExpression: + if (expression.Type.IsTextType()) + return expression.ConstantValue.Value.ToString().Length; + ITypeSymbol type = expression.Type; + if ((type != null ? (type.NavTypeKind == NavTypeKind.Char ? 1 : 0) : 0) != 0) + return 1; + break; + case OperationKind.ConversionExpression: + return this.CalculateMaxExpressionLength(((IConversionExpression)expression).Operand, ref isError); + case OperationKind.InvocationExpression: + IInvocationExpression invocation = (IInvocationExpression)expression; + IMethodSymbol targetMethod = invocation.TargetMethod; + if (targetMethod != null && targetMethod.ContainingSymbol?.Kind == SymbolKind.Class) + { + switch (targetMethod.Name.ToLowerInvariant()) + { + case "convertstr": + case "delchr": + case "delstr": + case "incstr": + case "lowercase": + case "uppercase": + if (invocation.Arguments.Length > 0) + return this.CalculateBuiltInMethodResultLength(invocation, 0, ref isError); + break; + case "copystr": + if (invocation.Arguments.Length == 3) + return this.CalculateBuiltInMethodResultLength(invocation, 2, ref isError); + break; + case "format": + return 0; + case "padstr": + case "substring": + if (invocation.Arguments.Length >= 2) + return this.CalculateBuiltInMethodResultLength(invocation, 1, ref isError); + break; + case "strsubstno": + if (invocation.Arguments.Length > 0) + return this.CalculateStrSubstNoMethodResultLength(invocation, ref isError); + break; + case "tolower": + case "toupper": + if (invocation.Instance.IsBoundExpression()) + return GetTypeLength(invocation.Instance.Type, ref isError); + break; + } + } + return GetTypeLength(expression.Type, ref isError); + case OperationKind.LocalReferenceExpression: + case OperationKind.GlobalReferenceExpression: + case OperationKind.ReturnValueReferenceExpression: + case OperationKind.ParameterReferenceExpression: + case OperationKind.FieldAccess: + return GetTypeLength(expression.Type, ref isError); + case OperationKind.BinaryOperatorExpression: + IBinaryOperatorExpression operatorExpression = (IBinaryOperatorExpression)expression; + return Math.Min(int.MaxValue, this.CalculateMaxExpressionLength(operatorExpression.LeftOperand, ref isError) + this.CalculateMaxExpressionLength(operatorExpression.RightOperand, ref isError)); + } + isError = true; + return 0; + } + + private static int? TryGetLength(IInvocationExpression invocation, int lengthArgPos) + { + if (!(SemanticFacts.GetBoundExpressionArgument(invocation, lengthArgPos) is IConversionExpression expressionArgument)) + return new int?(); + ITypeSymbol type = expressionArgument.Operand.Type; + return type.HasLength ? new int?(type.Length) : new int?(); + } + + private int CalculateBuiltInMethodResultLength( + IInvocationExpression invocation, + int lengthArgPos, + ref bool isError) + { + IOperation operation = invocation.Arguments[lengthArgPos].Value; + switch (operation.Kind) + { + case OperationKind.LiteralExpression: + Optional constantValue = operation.ConstantValue; + if (constantValue.HasValue) + { + if (operation.Type.IsIntegralType()) + { + constantValue = operation.ConstantValue; + return (int)constantValue.Value; + } + if (operation.Type.IsTextType()) + { + constantValue = operation.ConstantValue; + return constantValue.Value.ToString().Length; + } + break; + } + break; + case OperationKind.InvocationExpression: + invocation = (IInvocationExpression)operation; + IMethodSymbol targetMethod = invocation.TargetMethod; + if (targetMethod != null && SemanticFacts.IsSameName(targetMethod.Name, "maxstrlen") && targetMethod.ContainingSymbol?.Kind == SymbolKind.Class) + { + ImmutableArray arguments = invocation.Arguments; + if (arguments.Length == 1) + { + arguments = invocation.Arguments; + IOperation operand = arguments[0].Value; + if (operand.Kind == OperationKind.ConversionExpression) + operand = ((IConversionExpression)operand).Operand; + return GetTypeLength(operand.Type, ref isError); + } + break; + } + break; + } + return TryGetLength(invocation, lengthArgPos) ?? GetTypeLength(invocation.Type, ref isError); + } + + private int CalculateStrSubstNoMethodResultLength( + IInvocationExpression invocation, + ref bool isError) + { + IOperation operation = invocation.Arguments[0].Value; + if (!operation.Type.IsTextType()) + { + isError = true; + return -1; + } + Optional constantValue = operation.ConstantValue; + if (!constantValue.HasValue) + { + isError = true; + return -1; + } + constantValue = operation.ConstantValue; + string input = constantValue.Value.ToString(); + Match match = this.StrSubstNoPattern.Match(input); + int num; + for (num = input.Length; !isError && match.Success && num < int.MaxValue; match = match.NextMatch()) + { + string s = match.Groups[1].Value; + int result = 0; + if (int.TryParse(s, out result) && 0 < result && result < invocation.Arguments.Length) + { + int expressionLength = this.CalculateMaxExpressionLength(invocation.Arguments[result].Value, ref isError); + num = expressionLength == int.MaxValue ? expressionLength : num + expressionLength - s.Length - 1; + } + } + return !isError ? num : -1; + } + + private static string GetDisplayString(IArgument argument, IInvocationExpression operation) + { + return ((IConversionExpression)argument.Value).Operand.Type.ToDisplayString(); + } + + private List GetArgumentIndexes(IOperation operand) + { + List results = new List(); + + if (operand.Syntax.Kind != SyntaxKind.LiteralExpression) + return results; + + foreach (Match match in this.StrSubstNoPattern.Matches(operand.Syntax.ToFullString())) + { + if (int.TryParse(match.Groups[1].Value, out int number)) + if (!results.Contains(number)) + results.Add(number); + } + + return results; + } + } +} \ No newline at end of file diff --git a/LinterCop.ruleset.json b/LinterCop.ruleset.json index 9c474dd7..0c09d093 100644 --- a/LinterCop.ruleset.json +++ b/LinterCop.ruleset.json @@ -251,6 +251,11 @@ "id": "LC0050", "action": "Info", "justification": "SetFilter with unsupported operator in filter expression." + }, + { + "id": "LC0051", + "action": "Warning", + "justification": "Do not assign a text to a target with smaller size." } ] } \ No newline at end of file diff --git a/LinterCopAnalyzers.Generated.cs b/LinterCopAnalyzers.Generated.cs index 67621a84..04737689 100644 --- a/LinterCopAnalyzers.Generated.cs +++ b/LinterCopAnalyzers.Generated.cs @@ -57,5 +57,6 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor Rule0048ErrorWithTextConstant = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0048", (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0048"); 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 Rule0050SetFilterOperatorCharInFilterExpression = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0050", (LocalizableString)new LocalizableResourceString("Rule0050SetFilterOperatorCharInFilterExpressionTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0050SetFilterOperatorCharInFilterExpressionFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0050SetFilterOperatorCharInFilterExpressionDescription", 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"); } } \ No newline at end of file diff --git a/LinterCopAnalyzers.resx b/LinterCopAnalyzers.resx index 26e30c28..898161f9 100644 --- a/LinterCopAnalyzers.resx +++ b/LinterCopAnalyzers.resx @@ -564,4 +564,13 @@ Operator '{0}' found in filter expression, currently treated as a literal character. Implement StrSubstNo() to apply as operator. + + Do not assign a text to a target with smaller size. + + + Possible overflow assigning '{0}' to '{1}'. + + + Do not assign a text to a target with smaller size. + \ No newline at end of file diff --git a/README.md b/README.md index a3f1df49..7c8261cb 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,8 @@ Further note that you should have BcContainerHelper version 2.0.16 (or newer) in |[LC0047](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0047)|Locked `Label` must have a suffix Tok.|Info| |[LC0048](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0048)|Use Error with a `ErrorInfo` or `Label` variable to improve telemetry details.|Info| |[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| +|[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/LC0050)|Do not assign a text to a target with smaller size.|Warning| ## Configuration From cf78dacb3689a386e62a9521497d4170e8f065f0 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 11 Jan 2024 09:14:58 +0100 Subject: [PATCH 07/16] Resolve wrong link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c8261cb..86fedc3f 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Further note that you should have BcContainerHelper version 2.0.16 (or newer) in |[LC0048](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0048)|Use Error with a `ErrorInfo` or `Label` variable to improve telemetry details.|Info| |[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/LC0050)|Do not assign a text to a target with smaller size.|Warning| +|[LC0051](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0051)|Do not assign a text to a target with smaller size.|Warning| ## Configuration From 78a9951b2d30a8ae19479686f0d42aa94ec2e737 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 11 Jan 2024 10:53:07 +0100 Subject: [PATCH 08/16] Exclude Rule0016 for Promoted Groups --- Design/Rule0016CheckForMissingCaptions.cs | 29 ++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Design/Rule0016CheckForMissingCaptions.cs b/Design/Rule0016CheckForMissingCaptions.cs index f8ab434b..825e5627 100644 --- a/Design/Rule0016CheckForMissingCaptions.cs +++ b/Design/Rule0016CheckForMissingCaptions.cs @@ -11,6 +11,30 @@ class Rule0016CheckForMissingCaptions : DiagnosticAnalyzer { public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0016CheckForMissingCaptions); + private static readonly List PromotedGroupNames = new List + { + "category_new", + "category_process", + "category_report", + "category_category4", + "category_category5", + "category_category6", + "category_category7", + "category_category8", + "category_category9", + "category_category10", + "category_category11", + "category_category12", + "category_category13", + "category_category14", + "category_category15", + "category_category16", + "category_category17", + "category_category18", + "category_category19", + "category_category20", + }; + public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(new Action(this.CheckForMissingCaptions), SymbolKind.Page, @@ -30,7 +54,7 @@ private void CheckForMissingCaptions(SymbolAnalysisContext context) if (context.Symbol.Kind == SymbolKind.Control) { - var Control = ((IControlSymbol)context.Symbol); + var Control = (IControlSymbol)context.Symbol; switch (Control.ControlKind) { case ControlKind.Field: @@ -126,6 +150,9 @@ private bool CaptionIsMissing(ISymbol Symbol, SymbolAnalysisContext context) if (Symbol.GetEnumPropertyValue(PropertyKind.ShowAs) == ShowAsKind.SplitButton) return false; + if (SemanticFacts.IsSameName(Symbol.MostSpecificKind, "Group") && PromotedGroupNames.Contains(Symbol.Name.ToLowerInvariant())) + return false; + if (Symbol.GetBooleanPropertyValue(PropertyKind.ShowCaption) != false) if (Symbol.GetProperty(PropertyKind.Caption) == null && Symbol.GetProperty(PropertyKind.CaptionClass) == null) return true; From 4cb36ce135f0b73a7fbc6fe429c062f6bb735a68 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 11 Jan 2024 11:44:24 +0100 Subject: [PATCH 09/16] Remove try/catch --- ...oNotUseObjectIDsInVariablesOrProperties.cs | 27 +++++++------------ .../Rule0004LookupPageIdAndDrillDownPageId.cs | 22 +++++++-------- ...le0012DoNotUseObjectIdInSystemFunctions.cs | 13 +++------ Design/Rule0016CheckForMissingCaptions.cs | 15 ++++------- 4 files changed, 30 insertions(+), 47 deletions(-) diff --git a/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs b/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs index 156fc2a7..7487a59d 100644 --- a/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs +++ b/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs @@ -99,27 +99,20 @@ private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); } } - try - { - IReturnValueSymbol returnValue = method.ReturnValueSymbol; - if (returnValue.ReturnType.NavTypeKind == NavTypeKind.DotNet) - { - return; - } + IReturnValueSymbol returnValue = method.ReturnValueSymbol; + if (returnValue == null || returnValue.ReturnType.NavTypeKind == NavTypeKind.DotNet) + return; - if (ctx.Node.GetLocation().SourceSpan.End == returnValue.DeclaringSyntaxReference.GetSyntax(CancellationToken.None).Span.End) - { - correctName = returnValue.ReturnType.Name; + if (ctx.Node.GetLocation().SourceSpan.End == returnValue.DeclaringSyntaxReference.GetSyntax(CancellationToken.None).Span.End) + { + correctName = returnValue.ReturnType.Name; - if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); + if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); - if (ctx.Node.GetLastToken().ToString().Trim('"') != correctName) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); - } + if (ctx.Node.GetLastToken().ToString().Trim('"') != correctName) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); } - catch (System.NullReferenceException) - { } } } } diff --git a/Design/Rule0004LookupPageIdAndDrillDownPageId.cs b/Design/Rule0004LookupPageIdAndDrillDownPageId.cs index f7d7fbf4..21197200 100644 --- a/Design/Rule0004LookupPageIdAndDrillDownPageId.cs +++ b/Design/Rule0004LookupPageIdAndDrillDownPageId.cs @@ -7,7 +7,7 @@ namespace BusinessCentral.LinterCop.Design [DiagnosticAnalyzer] public class Rule0004LookupPageIdAndDrillDownPageId : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0004LookupPageIdAndDrillDownPageId); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0004LookupPageIdAndDrillDownPageId, DiagnosticDescriptors.Rule0000ErrorInRule); public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(new Action(this.CheckForLookupPageIdAndDrillDownPageId), SymbolKind.Page); @@ -18,13 +18,13 @@ private void CheckForLookupPageIdAndDrillDownPageId(SymbolAnalysisContext contex IPageTypeSymbol pageTypeSymbol = (IPageTypeSymbol)context.Symbol; if (pageTypeSymbol.PageType != PageTypeKind.List || pageTypeSymbol.RelatedTable == null) return; if (pageTypeSymbol.RelatedTable.ContainingModule != context.Symbol.ContainingModule) return; - CheckTable(pageTypeSymbol.RelatedTable, ref context); + CheckTable(pageTypeSymbol.RelatedTable, context); } - private void CheckTable(ITableTypeSymbol table, ref SymbolAnalysisContext context) + private void CheckTable(ITableTypeSymbol table, SymbolAnalysisContext context) { if (table.IsObsoletePending || table.IsObsoleteRemoved) return; - if (!IsSymbolAccessible(table)) return; + if (!IsSymbolAccessible(table, context)) return; if (table.TableType == TableTypeKind.Temporary) return; bool exists = table.Properties.Where(e => e.PropertyKind == PropertyKind.DrillDownPageId || e.PropertyKind == PropertyKind.LookupPageId).Count() == 2; @@ -34,24 +34,24 @@ private void CheckTable(ITableTypeSymbol table, ref SymbolAnalysisContext contex Diagnostic.Create( DiagnosticDescriptors.Rule0004LookupPageIdAndDrillDownPageId, table.GetLocation(), - new object[] { GetDeclaration(table), table.Name, context.Symbol.Name })); + new object[] { GetDeclaration(table, context), table.Name, context.Symbol.Name })); } - private static string GetDeclaration(ISymbol symbol) - => symbol.Location.SourceTree.GetText(CancellationToken.None).GetSubText(symbol.DeclaringSyntaxReference.Span).ToString(); + private static string GetDeclaration(ISymbol symbol, SymbolAnalysisContext context) + => symbol.Location.SourceTree.GetText(context.CancellationToken).GetSubText(symbol.DeclaringSyntaxReference.Span).ToString(); - private static bool IsSymbolAccessible(ISymbol symbol) + private static bool IsSymbolAccessible(ISymbol symbol, SymbolAnalysisContext context) { try { - GetDeclaration(symbol); + GetDeclaration(symbol, context); return true; } catch (Exception) { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, context.Symbol.GetLocation(), new Object[] { "Rule0004", "Exception", "at Line 47" })); return false; } } } - -} +} \ No newline at end of file diff --git a/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs b/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs index 9c0fb908..8b1fcf22 100644 --- a/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs +++ b/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs @@ -39,17 +39,12 @@ private void CheckForObjectIdsInFunctionInvocations(OperationAnalysisContext con if (context.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || context.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return; if (context.ContainingSymbol.IsObsoletePending || context.ContainingSymbol.IsObsoleteRemoved) return; IInvocationExpression operation = (IInvocationExpression)context.Operation; - RelevantFuntion CurrentFunction = null; - try - { - CurrentFunction = FunctionCallsWithIDParamaters.RelevantFunctions.First(o => (o.ObjectType.ToString().ToUpper() == operation.TargetMethod.ContainingSymbol.Name.ToUpper() && o.FunctionName == operation.TargetMethod.Name)); - } - catch (System.InvalidOperationException) - { } - SyntaxKind[] AllowedParameterKinds = { SyntaxKind.MemberAccessExpression, SyntaxKind.IdentifierName, SyntaxKind.InvocationExpression, SyntaxKind.QualifiedName }; + RelevantFuntion CurrentFunction = FunctionCallsWithIDParamaters.RelevantFunctions.FirstOrDefault(o => (o.ObjectType.ToString().ToUpper() == operation.TargetMethod.ContainingSymbol.Name.ToUpper() && o.FunctionName == operation.TargetMethod.Name)); + if (CurrentFunction == null) return; - if (CurrentFunction != null && operation.TargetMethod.Parameters.Length != 0 && !AllowedParameterKinds.Contains(operation.Arguments[0].Syntax.Kind) && (operation.Arguments[0].Syntax.ToString() != "0" || !CurrentFunction.ZeroIDAllowed)) + SyntaxKind[] AllowedParameterKinds = { SyntaxKind.MemberAccessExpression, SyntaxKind.IdentifierName, SyntaxKind.InvocationExpression, SyntaxKind.QualifiedName }; + if (operation.TargetMethod.Parameters.Length != 0 && !AllowedParameterKinds.Contains(operation.Arguments[0].Syntax.Kind) && (operation.Arguments[0].Syntax.ToString() != "0" || !CurrentFunction.ZeroIDAllowed)) { if (operation.TargetMethod.Parameters[0].ParameterType.NavTypeKind == NavTypeKind.Integer) { diff --git a/Design/Rule0016CheckForMissingCaptions.cs b/Design/Rule0016CheckForMissingCaptions.cs index 825e5627..dbf252ac 100644 --- a/Design/Rule0016CheckForMissingCaptions.cs +++ b/Design/Rule0016CheckForMissingCaptions.cs @@ -134,18 +134,13 @@ private void CheckForMissingCaptions(SymbolAnalysisContext context) private bool CaptionIsMissing(ISymbol Symbol, SymbolAnalysisContext context) { - try + if (Symbol.ContainingType?.Kind == SymbolKind.Table) { - if (Symbol.ContainingType.Kind == SymbolKind.Table) - { - if (((ITableTypeSymbol)Symbol.ContainingType).Id >= 2000000000) - return false; - if (((IFieldSymbol)Symbol).Id >= 2000000000) - return false; - } + if (((ITableTypeSymbol)Symbol.ContainingType).Id >= 2000000000) + return false; + if (((IFieldSymbol)Symbol).Id >= 2000000000) + return false; } - catch (NullReferenceException) - { } if (Symbol.GetEnumPropertyValue(PropertyKind.ShowAs) == ShowAsKind.SplitButton) return false; From b49c38d56388ebf82813bcd7ebfdd6d4bac3d09e Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 11 Jan 2024 14:29:32 +0100 Subject: [PATCH 10/16] Improve description for Rule0050 --- ...eratorAndPlaceholderInFilterExpression.cs} | 34 +++++++------------ LinterCopAnalyzers.Generated.cs | 2 +- LinterCopAnalyzers.resx | 12 +++---- 3 files changed, 20 insertions(+), 28 deletions(-) rename Design/{Rule0050SetFilterOperatorCharInFilterExpression.cs => Rule0050OperatorAndPlaceholderInFilterExpression.cs} (64%) diff --git a/Design/Rule0050SetFilterOperatorCharInFilterExpression.cs b/Design/Rule0050OperatorAndPlaceholderInFilterExpression.cs similarity index 64% rename from Design/Rule0050SetFilterOperatorCharInFilterExpression.cs rename to Design/Rule0050OperatorAndPlaceholderInFilterExpression.cs index 52bcc9eb..644c154e 100644 --- a/Design/Rule0050SetFilterOperatorCharInFilterExpression.cs +++ b/Design/Rule0050OperatorAndPlaceholderInFilterExpression.cs @@ -7,13 +7,9 @@ namespace BusinessCentral.LinterCop.Design { [DiagnosticAnalyzer] - public class Rule0050SetFilterOperatorCharInFilterExpression : DiagnosticAnalyzer + public class Rule0050OperatorAndPlaceholderInFilterExpression : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0050SetFilterOperatorCharInFilterExpression); - private static readonly List unsupportedOperators = new List - { - '*', '?', '@' - }; + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0050OperatorAndPlaceholderInFilterExpression); public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); @@ -41,22 +37,18 @@ private void CheckParameter(IOperation operand, ref IInvocationExpression operat string parameterString = operand.Syntax.ToFullString(); - string pattern = @"%\d+"; // Only when a %1 is used in the filter expression the unsupported operators are treated as a literal character + string pattern = @"%\d+"; // Only when a placeholders (%1) is used in the filter expression we need to raise the rule that the placeholders won't work as expected Regex regex = new Regex(pattern); - if (!regex.IsMatch(parameterString)) return; - - foreach (char unsupportedOperator in unsupportedOperators) - { - ctx.CancellationToken.ThrowIfCancellationRequested(); - - if (parameterString.Contains(unsupportedOperator)) - { - ctx.ReportDiagnostic( - Diagnostic.Create( - DiagnosticDescriptors.Rule0050SetFilterOperatorCharInFilterExpression, - operation.Syntax.GetLocation(), new object[] { unsupportedOperator })); - } - } + Match match = regex.Match(parameterString); + if (!match.Success) return; + + int operatorIndex = parameterString.IndexOfAny("*?@".ToCharArray()); // Only the *, ? and @ operator changes the behavior of the placeholder + if (operatorIndex == -1) return; + + ctx.ReportDiagnostic( + Diagnostic.Create( + DiagnosticDescriptors.Rule0050OperatorAndPlaceholderInFilterExpression, + operation.Syntax.GetLocation(), new object[] { parameterString.Substring(operatorIndex, 1), match.Value })); } } } \ No newline at end of file diff --git a/LinterCopAnalyzers.Generated.cs b/LinterCopAnalyzers.Generated.cs index 04737689..0d9dbbb0 100644 --- a/LinterCopAnalyzers.Generated.cs +++ b/LinterCopAnalyzers.Generated.cs @@ -56,7 +56,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor Rule0047LockedLabelsTok = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0047", (LocalizableString)new LocalizableResourceString("Rule0047LockedLabelsTokTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0047LockedLabelsTokFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0047LockedLabelsTokDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0047"); public static readonly DiagnosticDescriptor Rule0048ErrorWithTextConstant = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0048", (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0048"); 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 Rule0050SetFilterOperatorCharInFilterExpression = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0050", (LocalizableString)new LocalizableResourceString("Rule0050SetFilterOperatorCharInFilterExpressionTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0050SetFilterOperatorCharInFilterExpressionFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0050SetFilterOperatorCharInFilterExpressionDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0050"); + 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"); } } \ No newline at end of file diff --git a/LinterCopAnalyzers.resx b/LinterCopAnalyzers.resx index 898161f9..45dd33ea 100644 --- a/LinterCopAnalyzers.resx +++ b/LinterCopAnalyzers.resx @@ -555,14 +555,14 @@ SourceTable property not defined on {0} '{1}'. - - Operator '{0}' found in filter expression, currently treated as a literal character. Implement StrSubstNo() to apply as operator. + + Found operator '{0}' together with placeholder '{1}' in filter expression, which results in unexpected behavior. Use the StrSubstNo() method to circumvent this. - - Operator '{0}' found in filter expression, currently treated as a literal character. Implement StrSubstNo() to apply as operator. + + Found operator '{0}' together with placeholder '{1}' in filter expression, which results in unexpected behavior. Use the StrSubstNo() method to circumvent this. - - Operator '{0}' found in filter expression, currently treated as a literal character. Implement StrSubstNo() to apply as operator. + + Found operator '{0}' together with placeholder '{1}' in filter expression, which results in unexpected behavior. Use the StrSubstNo() method to circumvent this. Do not assign a text to a target with smaller size. From c60ec968f7bc88f653d8edcef151ab4b07b60b27 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 11 Jan 2024 14:44:57 +0100 Subject: [PATCH 11/16] Probarly need to contine running --- Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs b/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs index 8b1fcf22..850763a0 100644 --- a/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs +++ b/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs @@ -41,7 +41,6 @@ private void CheckForObjectIdsInFunctionInvocations(OperationAnalysisContext con IInvocationExpression operation = (IInvocationExpression)context.Operation; RelevantFuntion CurrentFunction = FunctionCallsWithIDParamaters.RelevantFunctions.FirstOrDefault(o => (o.ObjectType.ToString().ToUpper() == operation.TargetMethod.ContainingSymbol.Name.ToUpper() && o.FunctionName == operation.TargetMethod.Name)); - if (CurrentFunction == null) return; SyntaxKind[] AllowedParameterKinds = { SyntaxKind.MemberAccessExpression, SyntaxKind.IdentifierName, SyntaxKind.InvocationExpression, SyntaxKind.QualifiedName }; if (operation.TargetMethod.Parameters.Length != 0 && !AllowedParameterKinds.Contains(operation.Arguments[0].Syntax.Kind) && (operation.Arguments[0].Syntax.ToString() != "0" || !CurrentFunction.ZeroIDAllowed)) From 3e2afccbca29098eb16a688642ab8557f24fdcd5 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 11 Jan 2024 20:33:46 +0100 Subject: [PATCH 12/16] Resolve IndexOutOfRangeException --- Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs b/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs index 850763a0..f63a8524 100644 --- a/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs +++ b/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs @@ -38,24 +38,27 @@ private void CheckForObjectIdsInFunctionInvocations(OperationAnalysisContext con { if (context.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || context.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return; if (context.ContainingSymbol.IsObsoletePending || context.ContainingSymbol.IsObsoleteRemoved) return; + IInvocationExpression operation = (IInvocationExpression)context.Operation; + if (operation.TargetMethod.Parameters.Length == 0) return; + if (operation.Arguments.Length == 0) return; RelevantFuntion CurrentFunction = FunctionCallsWithIDParamaters.RelevantFunctions.FirstOrDefault(o => (o.ObjectType.ToString().ToUpper() == operation.TargetMethod.ContainingSymbol.Name.ToUpper() && o.FunctionName == operation.TargetMethod.Name)); + if (CurrentFunction == null) return; SyntaxKind[] AllowedParameterKinds = { SyntaxKind.MemberAccessExpression, SyntaxKind.IdentifierName, SyntaxKind.InvocationExpression, SyntaxKind.QualifiedName }; - if (operation.TargetMethod.Parameters.Length != 0 && !AllowedParameterKinds.Contains(operation.Arguments[0].Syntax.Kind) && (operation.Arguments[0].Syntax.ToString() != "0" || !CurrentFunction.ZeroIDAllowed)) + if (!AllowedParameterKinds.Contains(operation.Arguments[0].Syntax.Kind) && (operation.Arguments[0].Syntax.ToString() != "0" || !CurrentFunction.ZeroIDAllowed)) { if (operation.TargetMethod.Parameters[0].ParameterType.NavTypeKind == NavTypeKind.Integer) { int tempint = 0; if (int.TryParse(operation.Arguments[0].Syntax.ToString(), out tempint)) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, context.Operation.Syntax.GetLocation(), new object[] { CurrentFunction.CorrectAccessSymbol, "" })); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, context.Operation.Syntax.GetLocation(), new object[] { "CurrentFunction.CorrectAccessSymbol", "" })); else if (!operation.Arguments[0].Syntax.ToString().ToUpper().StartsWith(CurrentFunction.CorrectAccessSymbol.ToUpper())) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, context.Operation.Syntax.GetLocation(), new object[] { CurrentFunction.CorrectAccessSymbol, "" })); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, context.Operation.Syntax.GetLocation(), new object[] { "CurrentFunction.CorrectAccessSymbol", "" })); } } - } } From 68aeabe8c100ba8382fe127b8eea98b3817b3f73 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Fri, 12 Jan 2024 08:10:54 +0100 Subject: [PATCH 13/16] Fix typo --- Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs b/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs index f63a8524..17b39b05 100644 --- a/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs +++ b/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs @@ -51,12 +51,11 @@ private void CheckForObjectIdsInFunctionInvocations(OperationAnalysisContext con { if (operation.TargetMethod.Parameters[0].ParameterType.NavTypeKind == NavTypeKind.Integer) { - int tempint = 0; - if (int.TryParse(operation.Arguments[0].Syntax.ToString(), out tempint)) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, context.Operation.Syntax.GetLocation(), new object[] { "CurrentFunction.CorrectAccessSymbol", "" })); + if (int.TryParse(operation.Arguments[0].Syntax.ToString(), out int tempint)) + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, context.Operation.Syntax.GetLocation(), new object[] { CurrentFunction.CorrectAccessSymbol, "" })); else if (!operation.Arguments[0].Syntax.ToString().ToUpper().StartsWith(CurrentFunction.CorrectAccessSymbol.ToUpper())) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, context.Operation.Syntax.GetLocation(), new object[] { "CurrentFunction.CorrectAccessSymbol", "" })); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, context.Operation.Syntax.GetLocation(), new object[] { CurrentFunction.CorrectAccessSymbol, "" })); } } } From 1f9165a5a87c33b1705336f836eb46f08a55fee8 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Fri, 12 Jan 2024 11:41:35 +0100 Subject: [PATCH 14/16] Resolve NullReferenceException --- ...oNotUseObjectIDsInVariablesOrProperties.cs | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs b/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs index 7487a59d..ff9e499e 100644 --- a/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs +++ b/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs @@ -1,5 +1,6 @@ using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using System.Collections.Immutable; namespace BusinessCentral.LinterCop.Design @@ -7,7 +8,7 @@ namespace BusinessCentral.LinterCop.Design [DiagnosticAnalyzer] public class Rule0003DoNotUseObjectIDsInVariablesOrProperties : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, DiagnosticDescriptors.Rule0000ErrorInRule); public override void Initialize(AnalysisContext context) { @@ -15,21 +16,20 @@ public override void Initialize(AnalysisContext context) SyntaxKind.ObjectReference, SyntaxKind.PermissionValue }); - } + private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext ctx) { if (ctx.ContainingSymbol.IsObsoletePending || ctx.ContainingSymbol.IsObsoleteRemoved) return; if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return; - var correctName = ""; - + string correctName; if (ctx.ContainingSymbol.Kind == SymbolKind.LocalVariable || ctx.ContainingSymbol.Kind == SymbolKind.GlobalVariable) { IVariableSymbol variable = (IVariableSymbol)ctx.ContainingSymbol; - if (variable.Type.NavTypeKind == NavTypeKind.DotNet) return; + if (variable.Type.GetNavTypeKindSafe() == NavTypeKind.DotNet) return; - if (variable.Type.NavTypeKind == NavTypeKind.Array) + if (variable.Type.GetNavTypeKindSafe() == NavTypeKind.Array) correctName = ((IArrayTypeSymbol)variable.Type).ElementType.Name.ToString(); else correctName = variable.Type.Name; @@ -43,7 +43,6 @@ private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext if (ctx.ContainingSymbol.Kind == SymbolKind.Property) { IPropertySymbol property = (IPropertySymbol)ctx.ContainingSymbol; - if (ctx.Node.Kind == SyntaxKind.PermissionValue) { var nodes = ctx.Node.ChildNodesAndTokens().GetEnumerator(); @@ -60,7 +59,6 @@ private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, nodes.Current.GetLocation(), new object[] { "", "the object name" })); }; } - }; } @@ -80,15 +78,12 @@ private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext foreach (IParameterSymbol parameter in method.Parameters) { - if (parameter.ParameterType.NavTypeKind == NavTypeKind.DotNet) - { - continue; - } + if (parameter.ParameterType.GetNavTypeKindSafe() == NavTypeKind.DotNet) continue; if (ctx.Node.GetLocation().SourceSpan.End == parameter.DeclaringSyntaxReference.GetSyntax(CancellationToken.None).Span.End) { - if (parameter.ParameterType.NavTypeKind == NavTypeKind.Array) - correctName = ((IArrayTypeSymbol)(parameter.ParameterType)).ElementType.Name.ToString(); + if (parameter.ParameterType.GetNavTypeKindSafe() == NavTypeKind.Array) + correctName = ((IArrayTypeSymbol)parameter.ParameterType).ElementType.Name.ToString(); else correctName = parameter.ParameterType.Name; @@ -99,21 +94,27 @@ private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); } } - IReturnValueSymbol returnValue = method.ReturnValueSymbol; - if (returnValue == null || returnValue.ReturnType.NavTypeKind == NavTypeKind.DotNet) - return; - - if (ctx.Node.GetLocation().SourceSpan.End == returnValue.DeclaringSyntaxReference.GetSyntax(CancellationToken.None).Span.End) + try { - correctName = returnValue.ReturnType.Name; + IReturnValueSymbol returnValue = method.ReturnValueSymbol; + if (returnValue == null || returnValue.ReturnType.GetNavTypeKindSafe() == NavTypeKind.DotNet) return; - if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); + if (ctx.Node.GetLocation().SourceSpan.End == returnValue.DeclaringSyntaxReference.GetSyntax(CancellationToken.None).Span.End) + { + correctName = returnValue.ReturnType.Name; - if (ctx.Node.GetLastToken().ToString().Trim('"') != correctName) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); + if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); + + if (ctx.Node.GetLastToken().ToString().Trim('"') != correctName) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); + } + } + catch (NullReferenceException) + { + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, ctx.Node.GetLocation(), new Object[] { "Rule0003/Rule0005", "NullReferenceException", "" })); } } } } -} +} \ No newline at end of file From fdcc76f5cb8a3b68af95799fb3433b1a4f150224 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Fri, 12 Jan 2024 12:58:34 +0100 Subject: [PATCH 15/16] Remove need for try/catch --- ...oNotUseObjectIDsInVariablesOrProperties.cs | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs b/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs index ff9e499e..b6e8aa1d 100644 --- a/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs +++ b/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs @@ -80,7 +80,7 @@ private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext { if (parameter.ParameterType.GetNavTypeKindSafe() == NavTypeKind.DotNet) continue; - if (ctx.Node.GetLocation().SourceSpan.End == parameter.DeclaringSyntaxReference.GetSyntax(CancellationToken.None).Span.End) + if (ctx.Node.GetLocation().SourceSpan.End == parameter.DeclaringSyntaxReference.GetSyntax(ctx.CancellationToken).Span.End) { if (parameter.ParameterType.GetNavTypeKindSafe() == NavTypeKind.Array) correctName = ((IArrayTypeSymbol)parameter.ParameterType).ElementType.Name.ToString(); @@ -94,25 +94,18 @@ private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); } } - try - { - IReturnValueSymbol returnValue = method.ReturnValueSymbol; - if (returnValue == null || returnValue.ReturnType.GetNavTypeKindSafe() == NavTypeKind.DotNet) return; + IReturnValueSymbol returnValue = method.ReturnValueSymbol; + if (returnValue?.DeclaringSyntaxReference == null || returnValue.ReturnType.GetNavTypeKindSafe() == NavTypeKind.DotNet) return; - if (ctx.Node.GetLocation().SourceSpan.End == returnValue.DeclaringSyntaxReference.GetSyntax(CancellationToken.None).Span.End) - { - correctName = returnValue.ReturnType.Name; + if (ctx.Node.GetLocation().SourceSpan.End == returnValue.DeclaringSyntaxReference.GetSyntax(ctx.CancellationToken).Span.End) + { + correctName = returnValue.ReturnType.Name; - if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); + if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); - if (ctx.Node.GetLastToken().ToString().Trim('"') != correctName) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); - } - } - catch (NullReferenceException) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, ctx.Node.GetLocation(), new Object[] { "Rule0003/Rule0005", "NullReferenceException", "" })); + if (ctx.Node.GetLastToken().ToString().Trim('"') != correctName) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); } } } From 423d69b66ef9324319d5d56193a77b4a44314e8f Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Fri, 12 Jan 2024 13:00:34 +0100 Subject: [PATCH 16/16] Rule0000ErrorInRule no longer needed --- Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs b/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs index b6e8aa1d..91de31c6 100644 --- a/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs +++ b/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs @@ -8,7 +8,7 @@ namespace BusinessCentral.LinterCop.Design [DiagnosticAnalyzer] public class Rule0003DoNotUseObjectIDsInVariablesOrProperties : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, DiagnosticDescriptors.Rule0000ErrorInRule); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration); public override void Initialize(AnalysisContext context) {