Skip to content

Add null-ckeck analyzer and code fix #43

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions Source/CSharpEssentials.Tests/CSharpEssentials.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
<Compile Include="ConvertToInterpolatedString\ConvertToInterpolatedStringRefactoringTests.cs" />
<Compile Include="DocumentExtensions.cs" />
<Compile Include="ExpandExpressionBodiedMember\ExpandExpressionBodiedMemberRefactoringTests.cs" />
<Compile Include="NullCheckToNullConditional\NullCheckToNullConditionalAnalyzerTests.cs" />
<Compile Include="NullCheckToNullConditional\NullCheckToNullConditionalCodeFixTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UseExpressionBodiedMember\UseExpressionBodiedMemberAnalyzerTests.cs" />
<Compile Include="UseExpressionBodiedMember\UseExpressionBodiedMemberCodeFixTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using RoslynNUnitLight;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis;
using CSharpEssentials.NullCheckToNullConditional;
using NUnit.Framework;

namespace CSharpEssentials.Tests.NullCheckToNullConditional
{
class NullCheckToNullConditionalAnalyzerTests : AnalyzerTestFixture
{
protected override string LanguageName => LanguageNames.CSharp;

protected override DiagnosticAnalyzer CreateAnalyzer() => new NullCheckToNullConditionalAnalyzer();

[Test]
public void TestNoFixOnComipleError()
{
const string markup = @"
class C
{
void M(object o)
{
if(o.GetType != null){
o.GetType.ToString()
}
}
}
";
NoDiagnostic(markup, DiagnosticIds.UseNullConditional);
}

[Test]
public void TestNoFixOnNoneInvocationBody()
{
const string markup = @"
class C
{
object M(object o)
{
if(o != null){
return o;
}
}
}
";
NoDiagnostic(markup, DiagnosticIds.UseNullConditional);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using RoslynNUnitLight;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis;
using CSharpEssentials.NullCheckToNullConditional;
using NUnit.Framework;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace CSharpEssentials.Tests.NullCheckToNullConditional
{
class NullCheckToNullConditionalCodeFixTests : CodeFixTestFixture
{
protected override string LanguageName => LanguageNames.CSharp;
protected override CodeFixProvider CreateProvider() => new NullCheckToNullConditionalCodeFix();

const string codeBase = @"
class SuperAwesomeCode
{
interface A { B b(); }
interface B { C c { get; } }
interface C { DHolder this[int i] { get; } }
interface DHolder { D d { get; } }
interface D { void m(object o1, object o2); MyStruct? myStruct{ get; } }
struct MyStruct { int this[int i] => i; }
void M(A a, B b, C c, DHolder dHolder, D d, object blah, dynamic dyn)
{
<<<<<code>>>>>
}
}
";
string InsertCode(string s) => codeBase.Replace("<<<<<code>>>>>", s);

[Test]
public void SimpleTest()
{
var markupCode = InsertCode("[|if (null != d ) d.m(blah, blah);|]");
var expected = InsertCode("d?.m(blah, blah);");
TestCodeFix(markupCode, expected, DiagnosticDescriptors.UseNullConditionalMemberAccess);
}

[Test]
public void TestPropertyAccessor()
{
var markupCode = InsertCode("[|if (b != null) b.c.ToString();|]");
var expected = InsertCode("b?.c.ToString();");
TestCodeFix(markupCode, expected, DiagnosticDescriptors.UseNullConditionalMemberAccess);
}

[Test]
public void TestIndexer()
{
var markupCode = InsertCode("[|if (b.c != null) b.c[0].ToString();|]");
var expected = InsertCode("b.c?[0].ToString();");
TestCodeFix(markupCode, expected, DiagnosticDescriptors.UseNullConditionalMemberAccess);
}

[Test]
public void TestNullableValueType()
{
var markupCode = InsertCode("[|if (d.myStruct != null) d.myStruct.Value[0].CompareTo(42).ToString();|]");
var expected = InsertCode("d.myStruct?[0].CompareTo(42).ToString();");
TestCodeFix(markupCode, expected, DiagnosticDescriptors.UseNullConditionalMemberAccess);
}

[Test]
public void TestDynamicExpression()
{
var markupCode = InsertCode("[|if (dyn.x.y.z != null) dyn.x.y.z.m();|]");
var expected = InsertCode("dyn.x.y.z?.m();");
TestCodeFix(markupCode, expected, DiagnosticDescriptors.UseNullConditionalMemberAccess);
}

[Test]
public void TestBlockStatement()
{
var markupCode = InsertCode("[|if (a.b() != null) { a.b().c[0].ToString(); }|]");
var expected = InsertCode("a.b()?.c[0].ToString();");
TestCodeFix(markupCode, expected, DiagnosticDescriptors.UseNullConditionalMemberAccess);
}

[Test]
public void TestInvocationStartsWith()
{
var code = InsertCode("[|if (a != null) a.b().c[1].d.m(blah, blah);|]");
var expeced = InsertCode("a?.b().c[1].d.m(blah, blah);");

Document doc;
TextSpan span;
TestHelpers.TryGetDocumentAndSpanFromMarkup(code, LanguageNames.CSharp, out doc, out span);
var root = doc.GetSyntaxRootAsync().Result;
var ifStatement = root.FindNode(span) as IfStatementSyntax;
var exp = (ifStatement.Condition as BinaryExpressionSyntax).Left;
var chain = (ifStatement.Statement as ExpressionStatementSyntax).Expression;
ExpressionSyntax _;
Assert.True(NullCheckToNullConditionalCodeFix.MemberAccessChainExpressionStartsWith(chain, exp, out _));

}
}
}
2 changes: 2 additions & 0 deletions Source/CSharpEssentials/CSharpEssentials.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="ConvertToInterpolatedString\ConvertToInterpolatedStringRefactoring.cs" />
<Compile Include="NullCheckToNullConditional\NullCheckToNullConditionalCodeFix.cs" />
<Compile Include="NullCheckToNullConditional\NullCheckToNullConditionalAnalyzer.cs" />
<Compile Include="DiagnosticCategories.cs" />
<Compile Include="DiagnosticDescriptors.cs" />
<Compile Include="DiagnosticIds.cs" />
Expand Down
17 changes: 17 additions & 0 deletions Source/CSharpEssentials/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,22 @@ public static class DiagnosticDescriptors
category: DiagnosticCategories.Language,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor UseNullConditionalMemberAccess = new DiagnosticDescriptor(
id: DiagnosticIds.UseNullConditional,
title: "Replace null-check if statement with null-conditional member access",
messageFormat: "Consider replacing the null-check if statement with null-conditional member access",
category: DiagnosticCategories.Language,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor UseNullConditionalMemberAccessFadedToken = new DiagnosticDescriptor(
id: "UseNullConditionalMemberAccessFadedToken",
title: UseNullConditionalMemberAccess.Title,
messageFormat: UseNullConditionalMemberAccess.MessageFormat,
category: DiagnosticCategories.Language,
defaultSeverity: DiagnosticSeverity.Hidden,
isEnabledByDefault: true,
customTags: new[] { WellKnownDiagnosticTags.Unnecessary });
}
}
1 change: 1 addition & 0 deletions Source/CSharpEssentials/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ internal static class DiagnosticIds
public const string UseNameOf = "CSE0001";
public const string UseGetterOnlyAutoProperty = "CSE0002";
public const string UseExpressionBodiedMember = "CSE0003";
public const string UseNullConditional = "CSE0004";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;

namespace CSharpEssentials.NullCheckToNullConditional
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class NullCheckToNullConditionalAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UseNullConditionalMemberAccessFadedToken, DiagnosticDescriptors.UseNullConditionalMemberAccess);

private static async void AnalyzeThat(SyntaxNodeAnalysisContext context)
{
var ifStatement = context.Node.FindNode(context.Node.Span, getInnermostNodeForTie: true)?.FirstAncestorOrSelf<IfStatementSyntax>();
try
{
if (await NullCheckToNullConditionalCodeFix.GetCodeFixAsync(() => Task.FromResult(context.SemanticModel), ifStatement) != null)
{
if (ifStatement.SyntaxTree.IsGeneratedCode(context.CancellationToken))
return;
var fadeoutLocations = ImmutableArray.CreateBuilder<Location>();
fadeoutLocations.Add(Location.Create(context.Node.SyntaxTree, TextSpan.FromBounds(ifStatement.IfKeyword.SpanStart, ifStatement.Statement.SpanStart)));

var statementBlock = ifStatement.Statement as BlockSyntax;
if (statementBlock != null)
{
fadeoutLocations.Add(Location.Create(context.Node.SyntaxTree, (statementBlock.OpenBraceToken.Span)));
fadeoutLocations.Add(Location.Create(context.Node.SyntaxTree, (statementBlock.CloseBraceToken.Span)));
}
foreach (var location in fadeoutLocations)
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseNullConditionalMemberAccessFadedToken, location));
}
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseNullConditionalMemberAccess,
Location.Create(context.Node.SyntaxTree, ifStatement.Span)));
}
}
catch (OperationCanceledException ex) when (ex.CancellationToken == context.CancellationToken)
{
// we should ignore cancellation exceptions, instead of blowing up the universe!
}
}

public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeThat, ImmutableArray.Create(SyntaxKind.IfStatement));
}
}
}
Loading