Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new rule MiKo_5018 to call value comparisons before reference comparisons #1104

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Rules\Performance\MiKo_5014_CodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Performance\MiKo_5015_CodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Performance\MiKo_5017_CodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Performance\MiKo_5018_CodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Performance\PerformanceCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Spacing\MiKo_6001_CodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Spacing\MiKo_6002_CodeFixProvider.cs" />
Expand Down
1 change: 1 addition & 0 deletions MiKo.Analyzer.Shared/MiKo.Analyzer.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Rules\Performance\MiKo_5015_StringLiteralGetsInternedAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Performance\MiKo_5016_RemoveAllUsesContainsOfHashSetAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Performance\MiKo_5017_StringLiteralVariableAssignmentIsConstantAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Performance\MiKo_5018_LogicalValueComparisonsComeFirstAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Performance\PerformanceAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Spacing\CallSurroundedByBlankLinesAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rules\Spacing\MiKo_6001_LogStatementSurroundedByBlankLinesAnalyzer.cs" />
Expand Down
36 changes: 36 additions & 0 deletions MiKo.Analyzer.Shared/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions MiKo.Analyzer.Shared/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -4928,6 +4928,18 @@ This organization ensures clarity and makes the code easier to find, understand
<data name="MiKo_5017_Title" xml:space="preserve">
<value>Fields or variables assigned with string literals should be constant</value>
</data>
<data name="MiKo_5018_CodeFixTitle" xml:space="preserve">
<value>Move value comparison before reference comparison</value>
</data>
<data name="MiKo_5018_Description" xml:space="preserve">
<value>Place value comparisons before reference comparisons. Simple value comparisons are faster than complex reference comparisons, like those involving collections. This approach improves performance.</value>
</data>
<data name="MiKo_5018_MessageFormat" xml:space="preserve">
<value>Place value comparison before reference comparison</value>
</data>
<data name="MiKo_5018_Title" xml:space="preserve">
<value>Value comparisons should be performed before reference comparisons</value>
</data>
<data name="MiKo_6001_CodeFixTitle" xml:space="preserve">
<value>Surround with blank lines</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Composition;
using System.Linq;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace MiKoSolutions.Analyzers.Rules.Performance
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MiKo_5018_CodeFixProvider)), Shared]
public sealed class MiKo_5018_CodeFixProvider : PerformanceCodeFixProvider
{
private static readonly SyntaxKind[] LogicalConditions = { SyntaxKind.LogicalAndExpression, SyntaxKind.LogicalOrExpression };

private static readonly SyntaxKind[] SpecialParents = { SyntaxKind.ArrowExpressionClause, SyntaxKind.ReturnStatement, SyntaxKind.IfStatement };

public override string FixableDiagnosticId => "MiKo_5018";

protected override SyntaxNode GetSyntax(IEnumerable<SyntaxNode> syntaxNodes) => syntaxNodes.OfType<BinaryExpressionSyntax>().FirstOrDefault();

protected override SyntaxNode GetUpdatedSyntax(Document document, SyntaxNode syntax, Diagnostic issue)
{
if (syntax is BinaryExpressionSyntax binary)
{
var left = FindProblematicNode(binary.Left); // TODO RKN: left will be "values.Length == 8"
var right = FindProblematicNode(binary.Right);

var updatedSyntax = binary.ReplaceNodes(new[] { left, right }, (original, rewritten) => ReferenceEquals(original, left) ? right.WithTriviaFrom(original) : left.WithTriviaFrom(original));

return binary.Parent.IsAnyKind(SpecialParents)
? updatedSyntax.WithoutTrailingTrivia() // only remove trailing trivia if condition is direct child of 'if/return/arrow clause' so that semicolon fits
: updatedSyntax;
}

return syntax;
}

private static ExpressionSyntax FindProblematicNode(ExpressionSyntax expression)
{
var node = expression;

while (true)
{
if (node is BinaryExpressionSyntax binary && binary.IsAnyKind(LogicalConditions))
{
var next = binary.Right;

if (next is BinaryExpressionSyntax nested)
{
if (nested.Left is ElementAccessExpressionSyntax || nested.Right is ElementAccessExpressionSyntax)
{
// we have some null checks or some element access, so we need to replace the complete node
return expression;
}
}

node = next;
}
else
{
return node;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace MiKoSolutions.Analyzers.Rules.Performance
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MiKo_5018_LogicalValueComparisonsComeFirstAnalyzer : PerformanceAnalyzer
{
public const string Id = "MiKo_5018";

private static readonly SyntaxKind[] LogicalConditions = { SyntaxKind.LogicalAndExpression, SyntaxKind.LogicalOrExpression };

public MiKo_5018_LogicalValueComparisonsComeFirstAnalyzer() : base(Id)
{
}

protected override void InitializeCore(CompilationStartAnalysisContext context) => context.RegisterSyntaxNodeAction(AnalyzeLogicalCondition, LogicalConditions);

private static bool IsNullCheck(ExpressionSyntax expression)
{
switch (expression.WithoutParenthesis())
{
case BinaryExpressionSyntax binary when IsNullCheck(binary):
return true;

case IsPatternExpressionSyntax pattern when pattern.Pattern is UnaryPatternSyntax unary && unary.Pattern is ConstantPatternSyntax constant && constant.Expression.IsKind(SyntaxKind.NullLiteralExpression):
return true;

default:
return false;
}
}

private static bool IsNullCheck(BinaryExpressionSyntax expression) => expression.Left.IsKind(SyntaxKind.NullLiteralExpression) || expression.Right.IsKind(SyntaxKind.NullLiteralExpression);

private static bool IsNumberCheck(ExpressionSyntax expression)
{
switch (expression.WithoutParenthesis())
{
case BinaryExpressionSyntax binary when IsNumberCheck(binary):
return true;

default:
return false;
}
}

private static bool IsNumberCheck(BinaryExpressionSyntax expression)
{
var left = expression.Left;
var right = expression.Right;

if (left.IsKind(SyntaxKind.NumericLiteralExpression) || right.IsKind(SyntaxKind.NumericLiteralExpression))
{
return true;
}

if (IsNumberCheck(left) && IsNumberCheck(right))
{
return true;
}

return false;
}

private static bool HasIssue(BinaryExpressionSyntax binary, SyntaxNodeAnalysisContext context)
{
if (binary.Right.WithoutParenthesis() is BinaryExpressionSyntax right && right.IsKind(SyntaxKind.EqualsExpression))
{
var semanticModel = context.SemanticModel;

if (binary.Left.WithoutParenthesis() is ExpressionSyntax left)
{
if (IsNullCheck(left))
{
// do not report null checks
return false;
}

if (IsNumberCheck(left))
{
// do not report nested number checks
return false;
}

switch (left)
{
case IdentifierNameSyntax _: return false; // do not report checks on boolean members
case IsPatternExpressionSyntax e when e.Pattern is DeclarationPatternSyntax: return false; // do not report pattern checks
case MemberAccessExpressionSyntax m when m.GetTypeSymbol(semanticModel)?.IsValueType is true: return false; // do not report checks on value type members
case BinaryExpressionSyntax nested:
{
if (nested.IsAnyKind(LogicalConditions))
{
if (IsNullCheck(nested.Right) && IsNullCheck(nested.Left))
{
return false; // do not report nested null checks
}

if (IsNumberCheck(nested.Right) && IsNumberCheck(nested.Left))
{
return false; // do not report nested number checks
}
}
else
{
// no logical condition
var nestedLeft = nested.Left;
var nestedRight = nested.Right;

if (nestedLeft is InvocationExpressionSyntax || nestedRight is InvocationExpressionSyntax)
{
// report on method calls
return true;
}

if (nestedLeft.IsKind(SyntaxKind.StringLiteralExpression) || nestedRight.IsKind(SyntaxKind.StringLiteralExpression))
{
// do not report checks on strings and value types
return false;
}

if (IsNumberCheck(nested))
{
// do not report checks on value types
return false;
}

if (nestedLeft is MemberAccessExpressionSyntax || nestedRight is MemberAccessExpressionSyntax)
{
// do not report on members
return false;
}

if (nestedLeft is IdentifierNameSyntax && nestedRight is IdentifierNameSyntax)
{
if (nestedLeft.GetTypeSymbol(semanticModel)?.IsValueType is true && nestedRight.GetTypeSymbol(semanticModel)?.IsValueType is true)
{
// do not report checks on value types
return false;
}
}
}

break;
}
}
}

if (right.Left is ElementAccessExpressionSyntax && right.Right is LiteralExpressionSyntax)
{
// do not report on element access
return false;
}

if (right.Left.GetTypeSymbol(semanticModel)?.IsValueType is true && right.Right.GetTypeSymbol(semanticModel)?.IsValueType is true)
{
return true;
}
}

return false;
}

private void AnalyzeLogicalCondition(SyntaxNodeAnalysisContext context)
{
if (context.Node is BinaryExpressionSyntax expression)
{
if (HasIssue(expression, context))
{
context.ReportDiagnostic(Issue(expression));
}
}
}
}
}
Loading