Skip to content

Commit

Permalink
Add IsNull analyzer and CodeFix provider. (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
maroontress-tomohisa authored Mar 14, 2019
1 parent 5c27163 commit 3b28bc7
Show file tree
Hide file tree
Showing 12 changed files with 699 additions and 5 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ It is intended to be used together with StyleCop Analyzers.
- [IneffectiveReadByte](doc/rules/IneffectiveReadByte.md) —
Avoid invoking `ReadByte()` method of `System.IO.BinaryReader` class
in incremental `for` loops.
- [IsNull](doc/rules/IsNull.md) —
Use `==` operator with `null` instead of `is null` pattern matching.
- [NotDesignedForExtension](doc/rules/NotDesignedForExtension.md) —
Must design a class for inheritance, or else prohibit it.
- [StaticGenericClass](doc/rules/StaticGenericClass.md) —
Expand Down
41 changes: 41 additions & 0 deletions StyleChecker/StyleChecker.Test/Refactoring/IsNull/AnalyzerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace StyleChecker.Test.Refactoring.IsNull
{
using System.IO;
using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using StyleChecker.Refactoring.IsNull;
using StyleChecker.Test.Framework;

[TestClass]
public sealed class AnalyzerTest : CodeFixVerifier
{
public AnalyzerTest()
: base(
Path.Combine(Categories.Refactoring, "IsNull"),
new Analyzer(),
new CodeFixer())
{
}

[TestMethod]
public void Okay()
=> VerifyDiagnostic(ReadText("Okay"), Atmosphere.Default);

[TestMethod]
public void Code()
{
var code = ReadText("Code");
var fix = ReadText("CodeFix");
Result Expected(Belief b)
{
var token = b.Message;
return b.ToResult(
Analyzer.DiagnosticId,
$"Use '{token}' operator instead of 'is' pattern matching.",
DiagnosticSeverity.Info);
}

VerifyDiagnosticAndFix(code, Atmosphere.Default, Expected, fix);
}
}
}
41 changes: 41 additions & 0 deletions StyleChecker/StyleChecker.Test/Refactoring/IsNull/Code.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace StyleChecker.Test.Refactoring.IsNull
{
public sealed class Code
{
public void EqualToNull(string value)
{
if (value is null)
//@ ^==
{
return;
}
}

public void NotEqualToNull(string value)
{
if (!(value is null))
//@ ^!=
{
return;
}
}

public void NullableValueType(int? value)
{
if (value is null)
//@ ^==
{
return;
}
}

public void KeepTrivia(string value)
{
if ( /*A*/ ! /*B*/ ( /*C*/ value /*D*/ is /*E*/ null /*F*/ ) /*G*/ )
//@ ^!=
{
return;
}
}
}
}
37 changes: 37 additions & 0 deletions StyleChecker/StyleChecker.Test/Refactoring/IsNull/CodeFix.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace StyleChecker.Test.Refactoring.IsNull
{
public sealed class Code
{
public void EqualToNull(string value)
{
if (value == null)
{
return;
}
}

public void NotEqualToNull(string value)
{
if (value != null)
{
return;
}
}

public void NullableValueType(int? value)
{
if (value == null)
{
return;
}
}

public void KeepTrivia(string value)
{
if ( /*A*/ /*B*/ /*C*/ value /*D*/ != /*E*/ null /*F*/ /*G*/ )
{
return;
}
}
}
}
6 changes: 6 additions & 0 deletions StyleChecker/StyleChecker.Test/Refactoring/IsNull/Okay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace StyleChecker.Test.Refactoring.IsNull
{
public sealed class Okay
{
}
}
12 changes: 12 additions & 0 deletions StyleChecker/StyleChecker.Test/StyleChecker.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
<Compile Remove="Ordering\PostIncrement\CodeFix.cs" />
<Compile Remove="Refactoring\DiscardingReturnValue\Methods.cs" />
<Compile Remove="Refactoring\DiscardingReturnValue\Okay.cs" />
<Compile Remove="Refactoring\IsNull\Code.cs" />
<Compile Remove="Refactoring\IsNull\CodeFix.cs" />
<Compile Remove="Refactoring\IsNull\Okay.cs" />
<Compile Remove="Refactoring\EqualsNull\Code.cs" />
<Compile Remove="Refactoring\EqualsNull\CodeFix.cs" />
<Compile Remove="Refactoring\EqualsNull\Okay.cs" />
Expand Down Expand Up @@ -156,6 +159,15 @@
<Content Include="Refactoring\DiscardingReturnValue\MethodsConfig.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Refactoring\IsNull\Code.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Refactoring\IsNull\CodeFix.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Refactoring\IsNull\Okay.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Refactoring\EqualsNull\CodeFix.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
Expand Down
105 changes: 105 additions & 0 deletions StyleChecker/StyleChecker/Refactoring/IsNull/Analyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
namespace StyleChecker.Refactoring.IsNull
{
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using StyleChecker.Refactoring;
using R = Resources;

/// <summary>
/// IsNull analyzer.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class Analyzer : DiagnosticAnalyzer
{
/// <summary>
/// The ID of this analyzer.
/// </summary>
public const string DiagnosticId = "IsNull";

private const string Category = Categories.Cleaning;
private static readonly DiagnosticDescriptor Rule = NewRule();

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor>
SupportedDiagnostics => ImmutableArray.Create(Rule);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(
GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSemanticModelAction(AnalyzeModel);
}

private static DiagnosticDescriptor NewRule()
{
var localize = Localizers.Of<R>(R.ResourceManager);
return new DiagnosticDescriptor(
DiagnosticId,
localize(nameof(R.Title)),
localize(nameof(R.MessageFormat)),
Category,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: localize(nameof(R.Description)),
helpLinkUri: HelpLink.ToUri(DiagnosticId));
}

private static void AnalyzeModel(
SemanticModelAnalysisContext context)
{
bool IsNullConstant(Optional<object> v)
=> v.HasValue && v.Value == null;

bool IsNullLiteral(IOperation o)
=> (o.IsImplicit && o is IConversionOperation conversion)
? IsNullLiteral(conversion.Operand)
: (o is ILiteralOperation literal
&& IsNullConstant(literal.ConstantValue));

bool IsNull(IPatternOperation o)
=> o is IConstantPatternOperation constantPattern
&& IsNullLiteral(constantPattern.Value);

bool Matches(IIsPatternOperation o)
=> IsNull(o.Pattern);

var root = context.GetCompilationUnitRoot();
var model = context.SemanticModel;
var all = root.DescendantNodes()
.OfType<IsPatternExpressionSyntax>()
.Select(n => model.GetOperation(n))
.OfType<IIsPatternOperation>()
.Where(Matches);

foreach (var o in all)
{
if (!(o.Syntax is IsPatternExpressionSyntax node))
{
continue;
}

Diagnostic Of(SyntaxNode n, SyntaxKind k)
=> Diagnostic.Create(
Rule,
n.GetLocation(),
SyntaxFactory.Token(k));

var diagnostic
= (node.Parent is ParenthesizedExpressionSyntax paren
&& paren.Parent is PrefixUnaryExpressionSyntax prefix
&& prefix.OperatorToken.Kind()
== SyntaxKind.ExclamationToken)
? Of(prefix, SyntaxKind.ExclamationEqualsToken)
: Of(node, SyntaxKind.EqualsEqualsToken);
context.ReportDiagnostic(diagnostic);
}
}
}
}
Loading

0 comments on commit 3b28bc7

Please sign in to comment.