Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions Source/CSharpEssentials.Tests/CSharpEssentials.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Reference Include="Microsoft.CodeAnalysis.Workspaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.CodeAnalysis.Workspaces.Common.1.0.0\lib\net45\Microsoft.CodeAnalysis.Workspaces.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. Fixed it.

<Reference Include="nunit.framework, Version=2.6.4.14350, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL">
<HintPath>..\..\packages\NUnit.2.6.4\lib\nunit.framework.dll</HintPath>
<Private>True</Private>
Expand Down Expand Up @@ -59,6 +60,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