提交 20452ab2 编写于 作者: A Alireza Habibi

Start work on 'use pattern combinators' feature

上级 5ee3f8c6
......@@ -130,6 +130,8 @@ internal static class IDEDiagnosticIds
public const string SimplifyConditionalExpressionDiagnosticId = "IDE0075";
public const string UsePatternCombinatorsDiagnosticId = "IDE0076";
// Analyzer error Ids
public const string AnalyzerChangedId = "IDE1001";
public const string AnalyzerDependencyConflictId = "IDE1002";
......
// 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.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.UsePatternCombinators;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics;
using Microsoft.CodeAnalysis.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.UsePatternCombinators
{
public class CSharpUsePatternMatchingDiagnosticAnalyzerTests : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest
{
internal override (DiagnosticAnalyzer, CodeFixProvider) CreateDiagnosticProviderAndFixer(Workspace workspace)
=> (new CSharpUsePatternCombinatorsDiagnosticAnalyzer(), new CSharpUsePatternCombinatorsCodeFixProvider());
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUsePatternCombinators)]
public async Task Test00()
{
await TestInRegularAndScript1Async(
@"class C
{
void Missing(int i, object o)
{
if (i == 0) { }
if (i > 0) { }
if (i is C) { }
if (i is C c) { }
if (!(i > 0)) { }
if (p != null) { }
}
void Good(int i, object o)
{
if (!(o is C c)) { }
if (!(o is C)) { }
if ({|FixAllInDocument:i == 1 || 2 == i|}) { }
if (i != 1 || 2 != i) { }
if (!(i != 1 || 2 != i)) { }
if (i < 1 && 2 <= i) { }
if (i < 1 && 2 <= i && i is not 0) { }
}
}",
@"class C
{
void Missing(int i, object o)
{
if (i == 0) { }
if (i > 0) { }
if (i is C) { }
if (i is C c) { }
if (!(i > 0)) { }
if (p != null) { }
}
void Good(int i, object o)
{
if (o is not C c) { }
if (o is not C) { }
if (i is 1 or 2) { }
if (i is not (1 and 2)) { }
if (i is 1 and 2) { }
if (i is < 1 and >= 2) { }
if (i is < 1 and >= 2 and not 0) { }
}
}");
}
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUsePatternCombinators)]
public async Task TestReturn()
{
await TestInRegularAndScript1Async(
@"class C
{
bool M(int variable)
{
return [|variable == 0 ||
variable == 1 ||
variable == 2|];
}
}",
@"class C
{
bool M(int variable)
{
return variable is 0 or
1 or
2;
}
}");
}
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUsePatternCombinators)]
public async Task TestReturn_Not()
{
await TestInRegularAndScript1Async(
@"class C
{
bool M(int variable)
{
return [|variable != 0 &&
variable != 1 &&
variable != 2|];
}
}",
@"class C
{
bool M(int variable)
{
return variable is not (0 or
1 or
2);
}
}");
}
}
}
// 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 Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators
{
/// <summary>
/// Base class to represent a pattern constructed from various checks
/// </summary>
internal abstract class AnalyzedPattern
{
private AnalyzedPattern()
{
}
/// <summary>
/// Represents a type-pattern, constructed from is-expression
/// </summary>
internal sealed class Type : AnalyzedPattern
{
public readonly TypeSyntax TypeSyntax;
public Type(TypeSyntax expression)
=> TypeSyntax = expression;
}
/// <summary>
/// Represents a source-pattern, constructed from C# patterns
/// </summary>
internal sealed class Source : AnalyzedPattern
{
public readonly PatternSyntax PatternSyntax;
public Source(PatternSyntax patternSyntax)
=> PatternSyntax = patternSyntax;
}
/// <summary>
/// Represents a constant-pattern, constructed from an equality check
/// </summary>
internal sealed class Constant : AnalyzedPattern
{
public readonly ExpressionSyntax ExpressionSyntax;
public Constant(ExpressionSyntax expression)
=> ExpressionSyntax = expression;
}
/// <summary>
/// Represents a relational-pattern, constructed from relational operators
/// </summary>
internal sealed class Relational : AnalyzedPattern
{
public readonly BinaryOperatorKind OperatorKind;
public readonly ExpressionSyntax Value;
public Relational(BinaryOperatorKind operatorKind, ExpressionSyntax value)
{
OperatorKind = operatorKind;
Value = value;
}
}
/// <summary>
/// Represents an and/or pattern, constructed from a logical and/or expression.
/// </summary>
internal sealed class Binary : AnalyzedPattern
{
public readonly AnalyzedPattern Left;
public readonly AnalyzedPattern Right;
public readonly bool IsDisjunctive;
public readonly SyntaxToken Token;
private Binary(AnalyzedPattern leftPattern, AnalyzedPattern rightPattern, bool isDisjunctive, SyntaxToken token)
{
Left = leftPattern;
Right = rightPattern;
IsDisjunctive = isDisjunctive;
Token = token;
}
public static AnalyzedPattern Create(AnalyzedPattern leftPattern, AnalyzedPattern rightPattern, bool isDisjunctive, SyntaxToken token)
{
return (leftPattern, rightPattern) switch
{
(Not left, Not right) => Not.Create(new Binary(left.Pattern, right.Pattern, !isDisjunctive, token)),
_ => new Binary(leftPattern, rightPattern, isDisjunctive, token)
};
}
}
/// <summary>
/// Represents a not-pattern, constructed from inequality check or a logical-not expression.
/// </summary>
internal sealed class Not : AnalyzedPattern
{
public readonly AnalyzedPattern Pattern;
private Not(AnalyzedPattern pattern) => Pattern = pattern;
private static BinaryOperatorKind Negate(BinaryOperatorKind kind) => kind switch
{
BinaryOperatorKind.LessThan => BinaryOperatorKind.GreaterThanOrEqual,
BinaryOperatorKind.GreaterThan => BinaryOperatorKind.LessThanOrEqual,
BinaryOperatorKind.LessThanOrEqual => BinaryOperatorKind.GreaterThan,
BinaryOperatorKind.GreaterThanOrEqual => BinaryOperatorKind.LessThan,
var v => throw ExceptionUtilities.UnexpectedValue(v)
};
public static AnalyzedPattern Create(AnalyzedPattern pattern) => pattern switch
{
Not p => p.Pattern,
Relational p => new Relational(Negate(p.OperatorKind), p.Value),
_ => new Not(pattern)
};
}
}
}
// 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 Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators
{
using static BinaryOperatorKind;
using static AnalyzedPattern;
internal sealed class CSharpUsePatternCombinatorsAnalyzer
{
private ExpressionSyntax _targetExpression = null!;
public static AnalyzedPattern? Analyze(IOperation operation, out ExpressionSyntax targetExpression)
{
var patternAnalyzer = new CSharpUsePatternCombinatorsAnalyzer();
var analyzedPattern = patternAnalyzer.ParsePattern(operation);
targetExpression = patternAnalyzer._targetExpression;
return analyzedPattern;
}
private enum ConstantResult
{
/// <summary>
/// None of operands were constant.
/// </summary>
None,
/// <summary>
/// Signifies that the left operand is the constant.
/// </summary>
Left,
/// <summary>
/// Signifies that the right operand is the constant.
/// </summary>
Right,
}
private AnalyzedPattern? ParsePattern(IOperation operation)
{
switch (operation)
{
case IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals } op:
return ParseConstantPattern(op);
case IBinaryOperation { OperatorKind: NotEquals } op:
{
var pattern = ParseConstantPattern(op);
if (pattern == null)
break;
return Not.Create(pattern);
}
case IBinaryOperation { OperatorKind: ConditionalOr, Syntax: BinaryExpressionSyntax syntax } op:
return ParseBinaryPattern(op, isDisjunctive: true, syntax.OperatorToken);
case IBinaryOperation { OperatorKind: ConditionalAnd, Syntax: BinaryExpressionSyntax syntax } op:
return ParseBinaryPattern(op, isDisjunctive: false, syntax.OperatorToken);
case IBinaryOperation op when IsRelationalOperator(op.OperatorKind):
return ParseRelationalPattern(op);
case IUnaryOperation { OperatorKind: UnaryOperatorKind.Not } op:
{
var pattern = ParsePattern(op.Operand);
if (pattern == null)
break;
return Not.Create(pattern);
}
case IIsTypeOperation op when CheckTargetExpression(op.ValueOperand) &&
op.Syntax is BinaryExpressionSyntax { Right: TypeSyntax type }:
return new Type(type);
case IIsPatternOperation op when CheckTargetExpression(op.Value) &&
op.Pattern.Syntax is PatternSyntax pattern:
return new Source(pattern);
case IParenthesizedOperation op:
return ParsePattern(op.Operand);
}
return null;
}
private AnalyzedPattern? ParseBinaryPattern(IBinaryOperation op, bool isDisjunctive, SyntaxToken token)
{
var leftPattern = ParsePattern(op.LeftOperand);
if (leftPattern == null)
return null;
var rightPattern = ParsePattern(op.RightOperand);
if (rightPattern == null)
return null;
return Binary.Create(leftPattern, rightPattern, isDisjunctive, token);
}
private ConstantResult DetermineConstant(IBinaryOperation op)
{
return (op.LeftOperand, op.RightOperand) switch
{
var (e, v) when IsConstant(v) && CheckTargetExpression(e) => ConstantResult.Right,
var (v, e) when IsConstant(v) && CheckTargetExpression(e) => ConstantResult.Left,
_ => ConstantResult.None,
};
}
private AnalyzedPattern? ParseRelationalPattern(IBinaryOperation op)
{
return DetermineConstant(op) switch
{
ConstantResult.Left when op.LeftOperand.Syntax is ExpressionSyntax left
=> new Relational(Flip(op.OperatorKind), left),
ConstantResult.Right when op.RightOperand.Syntax is ExpressionSyntax right
=> new Relational(op.OperatorKind, right),
_ => null
};
}
private AnalyzedPattern? ParseConstantPattern(IBinaryOperation op)
{
return DetermineConstant(op) switch
{
ConstantResult.Left when op.LeftOperand.Syntax is ExpressionSyntax left
=> new Constant(left),
ConstantResult.Right when op.RightOperand.Syntax is ExpressionSyntax right
=> new Constant(right),
_ => null
};
}
private static bool IsRelationalOperator(BinaryOperatorKind operatorKind)
{
switch (operatorKind)
{
case LessThan:
case LessThanOrEqual:
case GreaterThanOrEqual:
case GreaterThan:
return true;
default:
return false;
}
}
/// <summary>
/// Changes the direction the operator is pointing
/// </summary>
public static BinaryOperatorKind Flip(BinaryOperatorKind operatorKind)
{
return operatorKind switch
{
LessThan => GreaterThan,
LessThanOrEqual => GreaterThanOrEqual,
GreaterThanOrEqual => LessThanOrEqual,
GreaterThan => LessThan,
var v => throw ExceptionUtilities.UnexpectedValue(v)
};
}
private static bool IsConstant(IOperation operation)
{
// Constants do not propagate to conversions
return operation is IConversionOperation op
? IsConstant(op.Operand)
: operation.ConstantValue.HasValue;
}
private bool CheckTargetExpression(IOperation operation)
{
if (operation is IConversionOperation { IsImplicit: false } op)
{
// Unwrap explicit casts because the pattern will emit those anyways
operation = op.Operand;
}
if (!(operation.Syntax is ExpressionSyntax expression))
{
return false;
}
// If we have not figured the target expression yet,
// we will assume that the first expression is the one.
if (_targetExpression is null)
{
_targetExpression = expression;
return true;
}
return SyntaxFactory.AreEquivalent(expression, _targetExpression);
}
}
}
// 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.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators
{
using static SyntaxFactory;
using static AnalyzedPattern;
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
internal class CSharpUsePatternCombinatorsCodeFixProvider : SyntaxEditorBasedCodeFixProvider
{
[ImportingConstructor]
[SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
public CSharpUsePatternCombinatorsCodeFixProvider()
: base(supportsFixAll: true)
{
}
private static SyntaxKind MapToSyntaxKind(BinaryOperatorKind kind) => kind switch
{
BinaryOperatorKind.LessThan => SyntaxKind.LessThanToken,
BinaryOperatorKind.GreaterThan => SyntaxKind.GreaterThanToken,
BinaryOperatorKind.LessThanOrEqual => SyntaxKind.LessThanEqualsToken,
BinaryOperatorKind.GreaterThanOrEqual => SyntaxKind.GreaterThanEqualsToken,
_ => throw ExceptionUtilities.UnexpectedValue(kind)
};
public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(IDEDiagnosticIds.UsePatternCombinatorsDiagnosticId);
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 async Task FixAllAsync(
Document document, ImmutableArray<Diagnostic> diagnostics,
SyntaxEditor editor, CancellationToken cancellationToken)
{
var semanticModel = await document.RequireSemanticModelAsync(cancellationToken).ConfigureAwait(false);
foreach (var diagnostic in diagnostics)
{
var location = diagnostic.AdditionalLocations[0];
var node = editor.OriginalRoot.FindNode(location.SourceSpan);
var expression = FixNode(CSharpUsePatternCombinatorsHelpers.GetExpression(node));
var operation = semanticModel.GetOperation(expression!);
var pattern = CSharpUsePatternCombinatorsAnalyzer.Analyze(operation!, out var targetExpression);
var patternSyntax = AsPatternSyntax(pattern!).WithAdditionalAnnotations(Formatter.Annotation);
editor.ReplaceNode(expression, IsPatternExpression(targetExpression, patternSyntax));
}
}
private static ExpressionSyntax? FixNode(ExpressionSyntax? e)
{
return e switch
{
AssignmentExpressionSyntax n => n.Right,
LambdaExpressionSyntax n => n.ExpressionBody,
var n => n,
};
}
private static PatternSyntax AsPatternSyntax(AnalyzedPattern pattern) => pattern switch
{
Binary p => BinaryPattern(
p.IsDisjunctive ? SyntaxKind.OrPattern : SyntaxKind.AndPattern,
AsPatternSyntax(p.Left).Parenthesize(),
Token(p.Token.LeadingTrivia, p.IsDisjunctive ? SyntaxKind.OrKeyword : SyntaxKind.AndKeyword, TriviaList(p.Token.GetAllTrailingTrivia())),
AsPatternSyntax(p.Right).Parenthesize()),
Constant p => ConstantPattern(p.ExpressionSyntax),
Source p => p.PatternSyntax,
Type p => TypePattern(p.TypeSyntax),
Relational p => RelationalPattern(Token(MapToSyntaxKind(p.OperatorKind)), p.Value),
Not p => UnaryPattern(AsPatternSyntax(p.Pattern).Parenthesize()),
var p => throw ExceptionUtilities.UnexpectedValue(p)
};
private class MyCodeAction : CodeAction.DocumentChangeAction
{
public MyCodeAction(Func<CancellationToken, Task<Document>> createChangedDocument)
: base(CSharpAnalyzersResources.Use_pattern_matching, createChangedDocument)
{
}
internal override CodeActionPriority Priority => CodeActionPriority.Low;
}
}
}
// 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 Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators
{
using static AnalyzedPattern;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpUsePatternCombinatorsDiagnosticAnalyzer :
AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
public CSharpUsePatternCombinatorsDiagnosticAnalyzer()
: base(IDEDiagnosticIds.UsePatternCombinatorsDiagnosticId,
option: null,
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_pattern_matching),
CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
{
}
protected override void InitializeWorker(AnalysisContext context)
=> context.RegisterSyntaxNodeAction(AnalyzeNode, CSharpUsePatternCombinatorsHelpers.SyntaxKinds);
public void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
// TODO need an option for user to disable the feature
// TODO need to check language version >= C# 9.0
var parentNode = context.Node;
var expression = CSharpUsePatternCombinatorsHelpers.GetExpression(parentNode);
if (expression is null)
return;
var operation = context.SemanticModel.GetOperation(expression);
if (operation is null)
return;
var pattern = CSharpUsePatternCombinatorsAnalyzer.Analyze(operation, out _);
if (pattern is null)
return;
if (!ShouldReportDiagnostic(pattern))
return;
context.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
location: expression.GetLocation(),
effectiveSeverity: ReportDiagnostic.Warn,
additionalLocations: new[] { parentNode.GetLocation() },
properties: null,
messageArgs: null));
}
private static bool ShouldReportDiagnostic(AnalyzedPattern pattern)
{
switch (pattern)
{
case Not { Pattern: Constant _ }:
break;
case Not _:
case Binary _:
return true;
}
return false;
}
public override DiagnosticAnalyzerCategory GetAnalyzerCategory() =>
DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
}
}
// 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 Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators
{
internal static class CSharpUsePatternCombinatorsHelpers
{
public static SyntaxKind[] SyntaxKinds => new[]
{
SyntaxKind.ForStatement,
SyntaxKind.EqualsValueClause,
SyntaxKind.IfStatement,
SyntaxKind.WhenClause,
SyntaxKind.WhileStatement,
SyntaxKind.DoStatement,
SyntaxKind.ReturnStatement,
SyntaxKind.SimpleAssignmentExpression,
SyntaxKind.ArrowExpressionClause,
SyntaxKind.SimpleLambdaExpression,
SyntaxKind.ParenthesizedLambdaExpression,
SyntaxKind.Argument,
};
public static ExpressionSyntax? GetExpression(SyntaxNode node)
{
return node switch
{
ForStatementSyntax n => n.Condition,
EqualsValueClauseSyntax n => n.Value,
IfStatementSyntax n => n.Condition,
WhenClauseSyntax n => n.Condition,
WhileStatementSyntax n => n.Condition,
DoStatementSyntax n => n.Condition,
ReturnStatementSyntax n => n.Expression,
YieldStatementSyntax n => n.Expression,
ArrowExpressionClauseSyntax n => n.Expression,
AssignmentExpressionSyntax n => n.Right,
LambdaExpressionSyntax n => n.ExpressionBody,
ArgumentSyntax { RefKindKeyword: { RawKind: 0 } } n => n.Expression,
_ => null,
};
}
}
}
......@@ -179,6 +179,7 @@ public static class Features
public const string CodeActionsUseIsNullCheck = "CodeActions.UseIsNullCheck";
public const string CodeActionsUseLocalFunction = "CodeActions.UseLocalFunction";
public const string CodeActionsUseNamedArguments = "CodeActions.UseNamedArguments";
public const string CodeActionsUsePatternCombinators = "CodeActions.UsePatternCombinators";
public const string CodeActionsUseNullPropagation = "CodeActions.UseNullPropagation";
public const string CodeActionsUseObjectInitializer = "CodeActions.UseObjectInitializer";
public const string CodeActionsUseRangeOperator = "CodeActions.UseRangeOperator";
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册