Skip to content

Commit

Permalink
Merge pull request #46800 from CyrusNajmabadi/useNotPattern
Browse files Browse the repository at this point in the history
Add a simple feature to recommend people use the new C# 9 'not' pattern.
  • Loading branch information
CyrusNajmabadi authored Aug 17, 2020
2 parents 1919454 + 996ebb5 commit 163404b
Show file tree
Hide file tree
Showing 63 changed files with 863 additions and 6 deletions.
3 changes: 2 additions & 1 deletion src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,12 @@
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpAsAndNullCheckDiagnosticAnalyzer.Analyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpAsAndNullCheckDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpIsAndCastCheckDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpUseNotPatternDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseSimpleUsingStatement\UseSimpleUsingStatementDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseThrowExpression\CSharpUseThrowExpressionDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ValidateFormatString\CSharpValidateFormatStringDiagnosticAnalyzer.cs" />
</ItemGroup>
<ItemGroup Condition="'$(DefaultLanguageSourceExtension)' != '' AND '$(BuildingInsideVisualStudio)' != 'true'">
<ExpectedCompile Include="$(MSBuildThisFileDirectory)**\*$(DefaultLanguageSourceExtension)" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Shared.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching
{
/// <summary>
/// Looks for code of the forms:
///
/// var x = o as Type;
/// if (!(x is Y y)) ...
///
/// and converts it to:
///
/// if (x is not Y y) ...
///
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal partial class CSharpUseNotPatternDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
public CSharpUseNotPatternDiagnosticAnalyzer()
: base(IDEDiagnosticIds.UseNotPatternDiagnosticId,
CSharpCodeStyleOptions.PreferNotPattern,
LanguageNames.CSharp,
new LocalizableResourceString(
nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
{
}

public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SemanticSpanAnalysis;

protected override void InitializeWorker(AnalysisContext context)
{
#if !CODE_STYLE // CODE_STYLE layer doesn't currently support generating 'not patterns'. Do not bother analyzing.
context.RegisterSyntaxNodeAction(SyntaxNodeAction, SyntaxKind.LogicalNotExpression);
#endif
}

private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext)
{
var node = syntaxContext.Node;
var syntaxTree = node.SyntaxTree;

// "x is not Type y" is only available in C# 9.0 and above. Don't offer this refactoring
// in projects targeting a lesser version.
if (!((CSharpParseOptions)syntaxTree.Options).LanguageVersion.IsCSharp9OrAbove())
return;

var options = syntaxContext.Options;
var cancellationToken = syntaxContext.CancellationToken;

// Bail immediately if the user has disabled this feature.
var styleOption = options.GetOption(CSharpCodeStyleOptions.PreferNotPattern, syntaxTree, cancellationToken);
if (!styleOption.Value)
return;

// Look for the form: !(x is Y y)
if (!(node is PrefixUnaryExpressionSyntax
{
Operand: ParenthesizedExpressionSyntax
{
Expression: IsPatternExpressionSyntax
{
Pattern: DeclarationPatternSyntax,
} isPattern,
},
} notExpression))
{
return;
}

// Put a diagnostic with the appropriate severity on `is` keyword.
syntaxContext.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
isPattern.IsKeyword.GetLocation(),
styleOption.Notification.Severity,
ImmutableArray.Create(notExpression.GetLocation()),
properties: null));
}
}
}
1 change: 1 addition & 0 deletions src/Analyzers/CSharp/CodeFixes/CSharpCodeFixes.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<Compile Include="$(MSBuildThisFileDirectory)UseNullPropagation\CSharpUseNullPropagationCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseObjectInitializer\CSharpUseObjectInitializerCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseObjectInitializer\UseInitializerHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpUseNotPatternCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpAsAndNullCheckCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpIsAndCastCheckCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseSimpleUsingStatement\UseSimpleUsingStatementCodeFixProvider.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching
{
using static SyntaxFactory;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
internal partial class CSharpUseNotPatternCodeFixProvider : SyntaxEditorBasedCodeFixProvider
{
[ImportingConstructor]
[SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
public CSharpUseNotPatternCodeFixProvider()
{
}

public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(IDEDiagnosticIds.UseNotPatternDiagnosticId);

internal sealed override CodeFixCategory CodeFixCategory => CodeFixCategory.CodeStyle;

public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
context.RegisterCodeFix(new MyCodeAction(
c => FixAsync(context.Document, context.Diagnostics.First(), c)),
context.Diagnostics);
return Task.CompletedTask;
}

protected override Task FixAllAsync(
Document document, ImmutableArray<Diagnostic> diagnostics,
SyntaxEditor editor, CancellationToken cancellationToken)
{
foreach (var diagnostic in diagnostics)
{
cancellationToken.ThrowIfCancellationRequested();
ProcessDiagnostic(editor, diagnostic, cancellationToken);
}

return Task.CompletedTask;
}

private static void ProcessDiagnostic(
SyntaxEditor editor,
Diagnostic diagnostic,
CancellationToken cancellationToken)
{
#if CODE_STYLE
Contract.Fail("We should have never gotten here as CODE_STYLE doesn't support C# 9 yet.");
#else

var notExpressionLocation = diagnostic.AdditionalLocations[0];

var notExpression = (PrefixUnaryExpressionSyntax)notExpressionLocation.FindNode(getInnermostNodeForTie: true, cancellationToken);
var parenthesizedExpression = (ParenthesizedExpressionSyntax)notExpression.Operand;
var isPattern = (IsPatternExpressionSyntax)parenthesizedExpression.Expression;

var updatedPattern = isPattern.WithPattern(UnaryPattern(Token(SyntaxKind.NotKeyword), isPattern.Pattern));
editor.ReplaceNode(
notExpression,
updatedPattern.WithPrependedLeadingTrivia(notExpression.GetLeadingTrivia())
.WithAppendedTrailingTrivia(notExpression.GetTrailingTrivia()));

#endif

}

private class MyCodeAction : CustomCodeActions.DocumentChangeAction
{
public MyCodeAction(Func<CancellationToken, Task<Document>> createChangedDocument)
: base(CSharpAnalyzersResources.Use_pattern_matching, createChangedDocument, CSharpAnalyzersResources.Use_pattern_matching)
{
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
<Compile Include="$(MSBuildThisFileDirectory)UseObjectInitializer\UseObjectInitializerTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpAsAndNullCheckTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpAsAndNullCheckTests_FixAllTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpUseNotPatternTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpIsAndCastCheckDiagnosticAnalyzerTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePatternMatching\CSharpIsAndCastCheckDiagnosticAnalyzerTests_FixAllTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseSimpleUsingStatement\UseSimpleUsingStatementTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.UsePatternMatching;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.UsePatternMatching
{
using VerifyCS = CSharpCodeFixVerifier<
CSharpUseNotPatternDiagnosticAnalyzer,
CSharpUseNotPatternCodeFixProvider>;

public partial class CSharpUseNotPatternTests
{
#if !CODE_STYLE

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseNotPattern)]
[WorkItem(46699, "https://github.com/dotnet/roslyn/issues/46699")]
public async Task UseNotPattern()
{
await new VerifyCS.Test
{
TestCode =
@"class C
{
void M(object x)
{
if (!(x [|is|] string s))
{
}
}
}",
FixedCode =
@"class C
{
void M(object x)
{
if (x is not string s)
{
}
}
}",
LanguageVersion = LanguageVersion.CSharp9,
}.RunAsync();
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseNotPattern)]
public async Task UnavailableInCSharp8()
{
await new VerifyCS.Test
{
TestCode =
@"class C
{
void M(object x)
{
if (!(x is string s))
{
}
}
}",
LanguageVersion = LanguageVersion.CSharp8,
}.RunAsync();
}

#endif
}
}
3 changes: 3 additions & 0 deletions src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ internal static class IDEDiagnosticIds

public const string ConvertTypeOfToNameOfDiagnosticId = "IDE0082";

public const string UseNotPatternDiagnosticId = "IDE0083";
public const string UseIsNotExpressionDiagnosticId = "IDE0084";

// Analyzer error Ids
public const string AnalyzerChangedId = "IDE1001";
public const string AnalyzerDependencyConflictId = "IDE1002";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
' Licensed to the .NET Foundation under one or more agreements.
' The .NET Foundation licenses this file to you under the MIT license.
' See the LICENSE file in the project root for more information.

Imports System.Collections.Immutable
Imports Microsoft.CodeAnalysis.CodeStyle
Imports Microsoft.CodeAnalysis.Diagnostics
Imports Microsoft.CodeAnalysis.VisualBasic.CodeStyle
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax

Namespace Microsoft.CodeAnalysis.VisualBasic.UseIsNotExpression
''' <summary>
''' Looks for code of the forms:
'''
''' if not x is ...
'''
''' and converts it to:
'''
''' if x isnot ...
'''
''' </summary>
<DiagnosticAnalyzer(LanguageNames.VisualBasic)>
Partial Friend Class VisualBasicUseIsNotExpressionDiagnosticAnalyzer
Inherits AbstractBuiltInCodeStyleDiagnosticAnalyzer

Public Sub New()
MyBase.New(IDEDiagnosticIds.UseIsNotExpressionDiagnosticId,
VisualBasicCodeStyleOptions.PreferIsNotExpression,
LanguageNames.VisualBasic,
New LocalizableResourceString(
NameOf(VisualBasicAnalyzersResources.Use_IsNot_expression), VisualBasicAnalyzersResources.ResourceManager, GetType(VisualBasicAnalyzersResources)))
End Sub

Public Overrides Function GetAnalyzerCategory() As DiagnosticAnalyzerCategory
Return DiagnosticAnalyzerCategory.SemanticSpanAnalysis
End Function

Protected Overrides Sub InitializeWorker(context As AnalysisContext)
context.RegisterSyntaxNodeAction(AddressOf SyntaxNodeAction, SyntaxKind.NotExpression)
End Sub

Private Sub SyntaxNodeAction(syntaxContext As SyntaxNodeAnalysisContext)
Dim node = syntaxContext.Node
Dim syntaxTree = node.SyntaxTree

' "x is not Type y" is only available in C# 9.0 and above. Don't offer this refactoring
' in projects targeting a lesser version.
If DirectCast(syntaxTree.Options, VisualBasicParseOptions).LanguageVersion < LanguageVersion.VisualBasic14 Then
Return
End If

Dim options = syntaxContext.Options
Dim cancellationToken = syntaxContext.CancellationToken

' Bail immediately if the user has disabled this feature.
Dim styleOption = options.GetOption(VisualBasicCodeStyleOptions.PreferIsNotExpression, syntaxTree, cancellationToken)
If Not styleOption.Value Then
Return
End If

Dim notExpression = DirectCast(node, UnaryExpressionSyntax)
Dim operand = notExpression.Operand

' Look for the form: not x is y, or not typeof x is y
If Not operand.IsKind(SyntaxKind.IsExpression) AndAlso Not operand.IsKind(SyntaxKind.TypeOfIsExpression) Then
Return
End If

Dim isKeyword = If(operand.IsKind(SyntaxKind.IsExpression),
DirectCast(operand, BinaryExpressionSyntax).OperatorToken,
DirectCast(operand, TypeOfExpressionSyntax).OperatorToken)

' Put a diagnostic with the appropriate severity on `is` keyword.
syntaxContext.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
isKeyword.GetLocation(),
styleOption.Notification.Severity,
ImmutableArray.Create(notExpression.GetLocation()),
properties:=Nothing))
End Sub
End Class
End Namespace
Loading

0 comments on commit 163404b

Please sign in to comment.