Skip to content

Commit

Permalink
Merge pull request #499 from StefanMaron/development
Browse files Browse the repository at this point in the history
New rule 0051 SetFilter Possible Overflow
  • Loading branch information
Arthurvdv authored Jan 10, 2024
2 parents 1959cb8 + cfc815f commit ab026ae
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 1 deletion.
252 changes: 252 additions & 0 deletions Design/Rule0051SetFilterPossibleOverflow.cs
Original file line number Diff line number Diff line change
@@ -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<Regex> strSubstNoPatternLazy = new Lazy<Regex>((Func<Regex>)(() => new Regex("[#%](\\d+)", RegexOptions.Compiled)));

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

private Regex StrSubstNoPattern => this.strSubstNoPatternLazy.Value;

public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action<OperationAnalysisContext>(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<object> 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<IArgument> 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<object> 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<int> GetArgumentIndexes(IOperation operand)
{
List<int> results = new List<int>();

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;
}
}
}
5 changes: 5 additions & 0 deletions LinterCop.ruleset.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
]
}
1 change: 1 addition & 0 deletions LinterCopAnalyzers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
9 changes: 9 additions & 0 deletions LinterCopAnalyzers.resx
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,13 @@
<data name="Rule0050SetFilterOperatorCharInFilterExpressionDescription" xml:space="preserve">
<value>Operator '{0}' found in filter expression, currently treated as a literal character. Implement StrSubstNo() to apply as operator.</value>
</data>
<data name="Rule0051SetFilterPossibleOverflowTitle" xml:space="preserve">
<value>Do not assign a text to a target with smaller size.</value>
</data>
<data name="Rule0051SetFilterPossibleOverflowFormat" xml:space="preserve">
<value>Possible overflow assigning '{0}' to '{1}'.</value>
</data>
<data name="Rule0051SetFilterPossibleOverflowDescription" xml:space="preserve">
<value>Do not assign a text to a target with smaller size.</value>
</data>
</root>
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit ab026ae

Please sign in to comment.