diff --git a/src/Analyzers/CSharp/Tests/UsePatternMatching/CSharpUseNotPatternTests.cs b/src/Analyzers/CSharp/Tests/UsePatternMatching/CSharpUseNotPatternTests.cs index 5a97b2bf607b8..97c6ac0dd572c 100644 --- a/src/Analyzers/CSharp/Tests/UsePatternMatching/CSharpUseNotPatternTests.cs +++ b/src/Analyzers/CSharp/Tests/UsePatternMatching/CSharpUseNotPatternTests.cs @@ -50,6 +50,37 @@ void M(object x) }.RunAsync(); } + [Fact] + public async Task BinaryIsExpression2() + { + await new VerifyCS.Test + { + TestCode = """ + class C + { + void M(object x) + { + if (!(x [|is|] string /*trailing*/)) + { + } + } + } + """, + FixedCode = """ + class C + { + void M(object x) + { + if (x is not string /*trailing*/) + { + } + } + } + """, + LanguageVersion = LanguageVersion.CSharp9, + }.RunAsync(); + } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/50690")] public async Task ConstantPattern() { @@ -174,6 +205,86 @@ void M(object x) }.RunAsync(); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72370")] + public async Task UseNotPattern2() + { + await new VerifyCS.Test + { + TestCode = """ + public class Program + { + public class C + { + } + public static void Main() + { + C C = new(); + object O = C; + + if (!(O [|is|] C)) + { + } + } + } + """, + FixedCode = """ + public class Program + { + public class C + { + } + public static void Main() + { + C C = new(); + object O = C; + + if (O is not Program.C) + { + } + } + } + """, + LanguageVersion = LanguageVersion.CSharp9, + }.RunAsync(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72370")] + public async Task UseNotPattern3() + { + await new VerifyCS.Test + { + TestCode = """ + public class Program + { + public class C + { + } + public static void Main(object O) + { + if (!(O [|is|] C)) + { + } + } + } + """, + FixedCode = """ + public class Program + { + public class C + { + } + public static void Main(object O) + { + if (O is not C) + { + } + } + } + """, + LanguageVersion = LanguageVersion.CSharp9, + }.RunAsync(); + } + [Fact] public async Task UnavailableInCSharp8() { @@ -225,6 +336,72 @@ void M(object x) }.RunAsync(); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/50690")] + public async Task BinaryIsObject2() + { + await new VerifyCS.Test + { + TestCode = """ + class C + { + void M(object x) + { + if (!(x [|is|] object /*trailing*/)) + { + } + } + } + """, + FixedCode = """ + class C + { + void M(object x) + { + if (x is null /*trailing*/) + { + } + } + } + """, + LanguageVersion = LanguageVersion.CSharp9, + }.RunAsync(); + } + + [Fact] + public async Task BinaryIsObject3() + { + await new VerifyCS.Test + { + TestCode = """ + using System; + + class C + { + void M(object x) + { + if (!(x [|is|] Object)) + { + } + } + } + """, + FixedCode = """ + using System; + + class C + { + void M(object x) + { + if (x is null) + { + } + } + } + """, + LanguageVersion = LanguageVersion.CSharp9, + }.RunAsync(); + } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/68784")] public async Task NotInExpressionTree() { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs index 05f48a4c31a89..c755f94409b07 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs @@ -1552,6 +1552,12 @@ public bool IsMethodDeclaration([NotNullWhen(true)] SyntaxNode? node) public bool IsSimpleName([NotNullWhen(true)] SyntaxNode? node) => node is SimpleNameSyntax; + public bool IsAnyName([NotNullWhen(true)] SyntaxNode? node) + => node is NameSyntax; + + public bool IsAnyType([NotNullWhen(true)] SyntaxNode? node) + => node is TypeSyntax; + public bool IsNamedMemberInitializer([NotNullWhen(true)] SyntaxNode? node) => node is AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) { Left: IdentifierNameSyntax }; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs index 9527a41fdad19..d894dd0069a2a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs @@ -499,6 +499,8 @@ void GetPartsOfTupleExpression(SyntaxNode node, bool IsMemberAccessExpression([NotNullWhen(true)] SyntaxNode? node); bool IsMethodDeclaration([NotNullWhen(true)] SyntaxNode? node); bool IsSimpleName([NotNullWhen(true)] SyntaxNode? node); + bool IsAnyName([NotNullWhen(true)] SyntaxNode? node); + bool IsAnyType([NotNullWhen(true)] SyntaxNode? node); bool IsNamedMemberInitializer([NotNullWhen(true)] SyntaxNode? node); bool IsElementAccessInitializer([NotNullWhen(true)] SyntaxNode? node); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb index 0dd63172c64a2..d0a7eaa806e90 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb @@ -1756,6 +1756,14 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageService Return TypeOf node Is SimpleNameSyntax End Function + Public Function IsAnyName(node As SyntaxNode) As Boolean Implements ISyntaxFacts.IsAnyName + Return TypeOf node Is NameSyntax + End Function + + Public Function IsAnyType(node As SyntaxNode) As Boolean Implements ISyntaxFacts.IsAnyType + Return TypeOf node Is TypeSyntax + End Function + Public Function IsNamedMemberInitializer(node As SyntaxNode) As Boolean Implements ISyntaxFacts.IsNamedMemberInitializer Return TypeOf node Is NamedFieldInitializerSyntax End Function diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/SyntaxGeneratorExtensions_Negate.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/SyntaxGeneratorExtensions_Negate.cs index 5e19a079b55e8..3e342e41ce85f 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/SyntaxGeneratorExtensions_Negate.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/SyntaxGeneratorExtensions_Negate.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Threading; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.LanguageService; @@ -147,58 +148,85 @@ private static SyntaxNode GetNegationOfBinaryExpression( syntaxFacts.GetPartsOfBinaryExpression(expressionNode, out var leftOperand, out var operatorToken, out var rightOperand); var operation = semanticModel.GetOperation(expressionNode, cancellationToken); - if (operation is not IBinaryOperation binaryOperation) + if (operation is IBinaryOperation binaryOperation) { - if (syntaxFacts.IsIsTypeExpression(expressionNode)) + if (!s_negatedBinaryMap.TryGetValue(binaryOperation.OperatorKind, out var negatedKind)) + return generator.LogicalNotExpression(expressionNode); + + // Lifted relational operators return false if either operand is null. + // Inverting the operator fails to invert the behavior when an operand is null. + if (binaryOperation.IsLifted + && binaryOperation.OperatorKind is BinaryOperatorKind.LessThan or + BinaryOperatorKind.LessThanOrEqual or + BinaryOperatorKind.GreaterThan or + BinaryOperatorKind.GreaterThanOrEqual) { - // `is object` -> `is null` - if (syntaxFacts.IsPredefinedType(rightOperand, PredefinedType.Object) && - generatorInternal.SupportsPatterns(semanticModel.SyntaxTree.Options)) - { - return generatorInternal.IsPatternExpression(leftOperand, operatorToken, generatorInternal.ConstantPattern(generator.NullLiteralExpression())); - } + return generator.LogicalNotExpression(expressionNode); + } - // `is y` -> `is not y` - if (syntaxFacts.SupportsNotPattern(semanticModel.SyntaxTree.Options)) - return generatorInternal.IsPatternExpression(leftOperand, operatorToken, generatorInternal.NotPattern(generatorInternal.TypePattern(rightOperand))); + if (binaryOperation.OperatorKind is BinaryOperatorKind.Or or + BinaryOperatorKind.And or + BinaryOperatorKind.ConditionalAnd or + BinaryOperatorKind.ConditionalOr) + { + leftOperand = generator.Negate(generatorInternal, leftOperand, semanticModel, cancellationToken); + rightOperand = generator.Negate(generatorInternal, rightOperand, semanticModel, cancellationToken); } - // Apply the logical not operator if it is not a binary operation. - return generator.LogicalNotExpression(expressionNode); - } + var newBinaryExpressionSyntax = negatedKind is BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals + ? generatorInternal.NegateEquality(generator, expressionNode, leftOperand, negatedKind, rightOperand) + : NegateRelational(generator, binaryOperation, leftOperand, negatedKind, rightOperand); + newBinaryExpressionSyntax = newBinaryExpressionSyntax.WithTriviaFrom(expressionNode); - if (!s_negatedBinaryMap.TryGetValue(binaryOperation.OperatorKind, out var negatedKind)) - return generator.LogicalNotExpression(expressionNode); - - // Lifted relational operators return false if either operand is null. - // Inverting the operator fails to invert the behavior when an operand is null. - if (binaryOperation.IsLifted - && binaryOperation.OperatorKind is BinaryOperatorKind.LessThan or - BinaryOperatorKind.LessThanOrEqual or - BinaryOperatorKind.GreaterThan or - BinaryOperatorKind.GreaterThanOrEqual) + var newToken = syntaxFacts.GetOperatorTokenOfBinaryExpression(newBinaryExpressionSyntax); + return newBinaryExpressionSyntax.ReplaceToken( + newToken, + newToken.WithTriviaFrom(operatorToken)); + } + else if (operation is IIsTypeOperation { TypeOperand.SpecialType: SpecialType.System_Object } && generatorInternal.SupportsPatterns(semanticModel.SyntaxTree.Options)) { - return generator.LogicalNotExpression(expressionNode); + // `is object` -> `is null` + return generatorInternal.IsPatternExpression(leftOperand, operatorToken, generatorInternal.ConstantPattern(generator.NullLiteralExpression().WithTriviaFrom(rightOperand))); } + else if (syntaxFacts.IsIsTypeExpression(expressionNode) && syntaxFacts.SupportsNotPattern(semanticModel.SyntaxTree.Options)) + { + // `is y` -> `is not y` + SyntaxNode innerPattern; + if (operation is IIsTypeOperation isTypeOperation) + { + if (syntaxFacts.IsAnyName(rightOperand)) + { + // For named types, we need to convert to a constant expression (where the named type becomes a member + // access expression). For other types (predefined, arrays, etc) we can keep this as a type pattern. + // For example: `x is int` can just become `x is not int` where that's a type pattern. But `x is Y` + // will need to become `x is not Y` where that's a constant pattern instead. + var typeExpression = generatorInternal.Type(isTypeOperation.TypeOperand, typeContext: false); + innerPattern = syntaxFacts.IsAnyType(typeExpression) && !syntaxFacts.IsAnyName(typeExpression) + ? generatorInternal.TypePattern(typeExpression) + : generatorInternal.ConstantPattern(typeExpression); + } + else + { + // original form was already not a name (like a predefined type, or array type, etc.). Can just + // use as is as a type pattern. + innerPattern = generatorInternal.TypePattern(rightOperand); + } + } + else + { + innerPattern = generatorInternal.ConstantPattern(rightOperand); + } - if (binaryOperation.OperatorKind is BinaryOperatorKind.Or or - BinaryOperatorKind.And or - BinaryOperatorKind.ConditionalAnd or - BinaryOperatorKind.ConditionalOr) + return generatorInternal.IsPatternExpression( + leftOperand, + operatorToken, + generatorInternal.NotPattern(innerPattern.WithTriviaFrom(rightOperand))); + } + else { - leftOperand = generator.Negate(generatorInternal, leftOperand, semanticModel, cancellationToken); - rightOperand = generator.Negate(generatorInternal, rightOperand, semanticModel, cancellationToken); + // Apply the logical not operator if it is not a binary operation and also doesn't support patterns. + return generator.LogicalNotExpression(expressionNode); } - - var newBinaryExpressionSyntax = negatedKind is BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals - ? generatorInternal.NegateEquality(generator, expressionNode, leftOperand, negatedKind, rightOperand) - : NegateRelational(generator, binaryOperation, leftOperand, negatedKind, rightOperand); - newBinaryExpressionSyntax = newBinaryExpressionSyntax.WithTriviaFrom(expressionNode); - - var newToken = syntaxFacts.GetOperatorTokenOfBinaryExpression(newBinaryExpressionSyntax); - return newBinaryExpressionSyntax.ReplaceToken( - newToken, - newToken.WithTriviaFrom(operatorToken)); } private static SyntaxNode GetNegationOfBinaryPattern(