Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ dotnet_diagnostic.RCS1255.severity = none
dotnet_diagnostic.RCS1260.severity = suggestion
dotnet_diagnostic.RCS1264.severity = suggestion
dotnet_diagnostic.RCS9001.severity = suggestion
dotnet_diagnostic.RCS9012.severity = warning
Comment thread
josefpihrt marked this conversation as resolved.

dotnet_diagnostic.IDE0007.severity = none
dotnet_diagnostic.IDE0007WithoutSuggestion.severity = none
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add `roslyn5.0` NuGet package flavor (`analyzers/dotnet/roslyn5.0/cs`) ([PR](https://github.com/dotnet/roslynator/pull/1787))
- Add analyzer "Call 'IsKind' instead of comparing 'Kind'" ([RCS9012](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS9012)) ([PR](https://github.com/dotnet/roslynator/pull/1820))

### Breaking

Expand Down
41 changes: 41 additions & 0 deletions src/Analyzers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7545,6 +7545,47 @@ class FooCodeFixProvider : CodeFixProvider
<After><![CDATA[[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FooCodeFixProvider))]
class FooCodeFixProvider : CodeFixProvider
{
}]]></After>
</Sample>
</Samples>
</Analyzer>
<Analyzer>
<Id>RCS9012</Id>
<Identifier>UseIsKindInsteadOfKindComparison</Identifier>
<Title>Call 'IsKind' instead of comparing 'Kind'</Title>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>true</IsEnabledByDefault>
<Samples>
<Sample>
<Before><![CDATA[if (node.Kind() == SyntaxKind.IdentifierName)
{
}]]></Before>
<After><![CDATA[if (node.IsKind(SyntaxKind.IdentifierName))
{
}]]></After>
</Sample>
<Sample>
<Before><![CDATA[if (node.Kind() != SyntaxKind.IdentifierName)
{
}]]></Before>
<After><![CDATA[if (!node.IsKind(SyntaxKind.IdentifierName))
{
}]]></After>
</Sample>
<Sample>
<Before><![CDATA[if (node.Kind() == SyntaxKind.IfStatement || node.Kind() == SyntaxKind.WhileStatement)
{
}]]></Before>
<After><![CDATA[if (node.IsKind(SyntaxKind.IfStatement) || node.IsKind(SyntaxKind.WhileStatement))
{
}]]></After>
</Sample>
<Sample>
<Before><![CDATA[if (token.Kind() == SyntaxKind.SemicolonToken)
{
}]]></Before>
<After><![CDATA[if (token.IsKind(SyntaxKind.SemicolonToken))
{
}]]></After>
</Sample>
</Samples>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Roslynator.CSharp;
using Roslynator.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Roslynator.CSharp.CSharpFactory;

namespace Roslynator.CodeAnalysis.CSharp;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseIsKindInsteadOfKindComparisonCodeFixProvider))]
[Shared]
public sealed class UseIsKindInsteadOfKindComparisonCodeFixProvider : BaseCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(CodeAnalysisDiagnosticIdentifiers.UseIsKindInsteadOfKindComparison); }
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode root = await context.GetSyntaxRootAsync().ConfigureAwait(false);

if (!TryFindFirstAncestorOrSelf(root, context.Span, out ExpressionSyntax expression))
return;

Document document = context.Document;
Diagnostic diagnostic = context.Diagnostics[0];

CodeAction codeAction = CodeAction.Create(
"Call 'IsKind' instead of comparing 'Kind'",
ct => RefactorAsync(document, expression, ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
}

private static async Task<Document> RefactorAsync(
Document document,
ExpressionSyntax expression,
CancellationToken cancellationToken)
{
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

expression = expression.WalkUpParentheses();

ExpressionSyntax newExpression;

if (expression.IsKind(SyntaxKind.LogicalOrExpression))
{
var logicalOr = (BinaryExpressionSyntax)expression;

ImmutableArray<IsKindExpressionInfo>.Builder builder = ImmutableArray.CreateBuilder<IsKindExpressionInfo>();

foreach (ExpressionSyntax operand in SyntaxInfo.BinaryExpressionInfo(logicalOr).AsChain())
{
IsKindExpressionInfo info = IsKindExpressionInfo.Create(
operand,
semanticModel,
cancellationToken: cancellationToken);

if (!info.Success)
return document;

builder.Add(info);
}

if (builder.Count == 0)
return document;

newExpression = CreateIsKindExpression(builder[0]);
Comment thread
josefpihrt marked this conversation as resolved.

for (int i = 1; i < builder.Count; i++)
newExpression = LogicalOrExpression(newExpression, CreateIsKindExpression(builder[i]));
}
else
{
IsKindExpressionInfo info = IsKindExpressionInfo.Create(
expression,
semanticModel,
cancellationToken: cancellationToken);

if (!info.Success)
return document;

newExpression = CreateIsKindExpression(info);
}

newExpression = newExpression
.WithTriviaFrom(expression)
.WithFormatterAnnotation();

return await document.ReplaceNodeAsync(expression, newExpression, cancellationToken).ConfigureAwait(false);
}

private static ExpressionSyntax CreateIsKindExpression(IsKindExpressionInfo info)
{
InvocationExpressionSyntax invocation = SimpleMemberInvocationExpression(
info.Expression.WithoutTrivia(),
IdentifierName("IsKind"),
Argument(info.KindExpression.WithoutTrivia()));

return info.Style switch
{
IsKindExpressionStyle.NotKind or IsKindExpressionStyle.NotKindConditional => LogicalNotExpression(invocation),
_ => invocation,
};
}
}
19 changes: 13 additions & 6 deletions src/CodeAnalysis.Analyzers/CSharp/CSharpSymbolUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,7 @@ public static bool IsKindExtensionMethod(
if (!methodSymbol.ReturnType.HasMetadataName(RoslynMetadataNames.Microsoft_CodeAnalysis_CSharp_SyntaxKind))
return false;

return methodSymbol
.Parameters
.SingleOrDefault(shouldThrow: false)?
.Type
.HasMetadataName(MetadataNames.Microsoft_CodeAnalysis_SyntaxNode) == true;
return IsSyntaxKindExtensionReceiver(methodSymbol.Parameters.SingleOrDefault(shouldThrow: false)?.Type);
}

public static bool IsIsKindExtensionMethod(
Expand Down Expand Up @@ -73,12 +69,23 @@ public static bool IsIsKindExtensionMethod(
if (parameters.Length != 2)
return false;

if (!parameters[0].Type.HasMetadataName(MetadataNames.Microsoft_CodeAnalysis_SyntaxNode))
if (!IsSyntaxKindExtensionReceiver(parameters[0].Type))
return false;

if (!parameters[1].Type.HasMetadataName(RoslynMetadataNames.Microsoft_CodeAnalysis_CSharp_SyntaxKind))
return false;

return true;
}

private static bool IsSyntaxKindExtensionReceiver(ITypeSymbol type)
{
if (type is null)
return false;

return type.HasMetadataName(MetadataNames.Microsoft_CodeAnalysis_SyntaxNode)
|| type.HasMetadataName(RoslynMetadataNames.Microsoft_CodeAnalysis_SyntaxToken)
|| type.HasMetadataName(RoslynMetadataNames.Microsoft_CodeAnalysis_SyntaxTrivia)
|| type.HasMetadataName(RoslynMetadataNames.Microsoft_CodeAnalysis_SyntaxNodeOrToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Roslynator.CSharp;
using Roslynator.CSharp.Syntax;

namespace Roslynator.CodeAnalysis.CSharp;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UseIsKindInsteadOfKindComparisonAnalyzer : BaseDiagnosticAnalyzer
{
private static ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics;

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
if (_supportedDiagnostics.IsDefault)
Immutable.InterlockedInitialize(ref _supportedDiagnostics, CodeAnalysisDiagnosticRules.UseIsKindInsteadOfKindComparison);

return _supportedDiagnostics;
}
}

public override void Initialize(AnalysisContext context)
{
base.Initialize(context);

context.RegisterSyntaxNodeAction(f => AnalyzeBinaryExpression(f), SyntaxKind.EqualsExpression, SyntaxKind.NotEqualsExpression);
Comment thread
josefpihrt marked this conversation as resolved.
}

private static void AnalyzeBinaryExpression(SyntaxNodeAnalysisContext context)
{
var binaryExpression = (BinaryExpressionSyntax)context.Node;

if (binaryExpression.ContainsDiagnostics)
return;

ExpressionSyntax outermost = GetOutermostLogicalOrOrSelf(binaryExpression);

if (outermost.IsKind(SyntaxKind.LogicalOrExpression))
{
var logicalOr = (BinaryExpressionSyntax)outermost;

if (!TryGetKindComparisonChain(
logicalOr,
context.SemanticModel,
context.CancellationToken,
out ImmutableArray<IsKindExpressionInfo> infos))
{
AnalyzeSingleComparison(context, binaryExpression);
return;
}

if (!binaryExpression.Equals(infos[0].IsKindExpression))
return;

DiagnosticHelpers.ReportDiagnostic(
context,
CodeAnalysisDiagnosticRules.UseIsKindInsteadOfKindComparison,
logicalOr);
}
else
{
AnalyzeSingleComparison(context, binaryExpression);
}
}

private static void AnalyzeSingleComparison(SyntaxNodeAnalysisContext context, BinaryExpressionSyntax binaryExpression)
{
IsKindExpressionInfo info = IsKindExpressionInfo.Create(
binaryExpression,
context.SemanticModel,
cancellationToken: context.CancellationToken);

if (!info.Success)
return;

if (!IsKindComparisonStyle(info.Style))
return;

if (!IsConstantSyntaxKind(info.KindExpression, context.SemanticModel, context.CancellationToken))
return;

DiagnosticHelpers.ReportDiagnostic(
context,
CodeAnalysisDiagnosticRules.UseIsKindInsteadOfKindComparison,
info.IsKindExpression);
}

private static bool TryGetKindComparisonChain(
BinaryExpressionSyntax logicalOr,
SemanticModel semanticModel,
CancellationToken cancellationToken,
out ImmutableArray<IsKindExpressionInfo> infos)
{
infos = default;

BinaryExpressionInfo binaryExpressionInfo = SyntaxInfo.BinaryExpressionInfo(logicalOr);

if (!binaryExpressionInfo.Success)
return false;

ImmutableArray<IsKindExpressionInfo>.Builder builder = ImmutableArray.CreateBuilder<IsKindExpressionInfo>();
ExpressionSyntax commonExpression = null;

foreach (ExpressionSyntax expression in binaryExpressionInfo.AsChain())
{
IsKindExpressionInfo info = IsKindExpressionInfo.Create(
expression,
semanticModel,
cancellationToken: cancellationToken);

if (!info.Success)
return false;

if (info.Style is not (IsKindExpressionStyle.Kind or IsKindExpressionStyle.KindConditional))
return false;

if (!IsConstantSyntaxKind(info.KindExpression, semanticModel, cancellationToken))
return false;

if (commonExpression is null)
{
commonExpression = info.Expression;
}
else if (!CSharpFactory.AreEquivalent(commonExpression, info.Expression))
{
return false;
}

builder.Add(info);
}

if (builder.Count < 2)
return false;

infos = builder.ToImmutable();
return true;
}

private static ExpressionSyntax GetOutermostLogicalOrOrSelf(ExpressionSyntax expression)
{
expression = expression.WalkUpParentheses();

while (expression.Parent.IsKind(SyntaxKind.LogicalOrExpression))
expression = ((ExpressionSyntax)expression.Parent).WalkUpParentheses();

return expression;
}

private static bool IsKindComparisonStyle(IsKindExpressionStyle style)
{
return style is IsKindExpressionStyle.Kind
or IsKindExpressionStyle.KindConditional
or IsKindExpressionStyle.NotKind
or IsKindExpressionStyle.NotKindConditional;
}

private static bool IsConstantSyntaxKind(
ExpressionSyntax expression,
SemanticModel semanticModel,
CancellationToken cancellationToken)
{
Optional<object> optionalConstantValue = semanticModel.GetConstantValue(expression, cancellationToken);

return optionalConstantValue.HasValue
&& optionalConstantValue.Value is ushort;
}
}
Loading
Loading