File: UsePatternCombinators\CSharpUsePatternCombinatorsDiagnosticAnalyzerTests.cs
Web Access
Project: src\src\Features\CSharpTest\Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.Features.UnitTests)
// 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.CodeStyle;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Microsoft.CodeAnalysis.CSharp.UsePatternCombinators;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.UsePatternCombinators;
 
[Trait(Traits.Feature, Traits.Features.CodeActionsUsePatternCombinators)]
public sealed class CSharpUsePatternCombinatorsDiagnosticAnalyzerTests(ITestOutputHelper logger)
    : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest_NoEditor(logger)
{
    private static readonly ParseOptions CSharp9 = TestOptions.RegularPreview.WithLanguageVersion(LanguageVersion.CSharp9);
 
    private static readonly OptionsCollection s_disabled = new(LanguageNames.CSharp)
    {
        { CSharpCodeStyleOptions.PreferPatternMatching, new CodeStyleOption2<bool>(false, NotificationOption2.None) }
    };
 
    internal override (DiagnosticAnalyzer, CodeFixProvider) CreateDiagnosticProviderAndFixer(Workspace workspace)
        => (new CSharpUsePatternCombinatorsDiagnosticAnalyzer(), new CSharpUsePatternCombinatorsCodeFixProvider());
 
    private Task TestAllMissingOnExpressionAsync(string expression, ParseOptions? parseOptions = null, bool enabled = true)
        => TestMissingAsync(FromExpression(expression), parseOptions, enabled);
 
    private Task TestMissingAsync(string initialMarkup, ParseOptions? parseOptions = null, bool enabled = true)
        => TestMissingAsync(initialMarkup, new TestParameters(
            parseOptions: parseOptions ?? CSharp9, options: enabled ? null : s_disabled));
 
    private Task TestAllAsync(string initialMarkup, string expectedMarkup)
        => TestInRegularAndScriptAsync(initialMarkup, expectedMarkup,
            parseOptions: CSharp9, options: null);
 
    private Task TestAllOnExpressionAsync(string expression, string expected)
        => TestAllAsync(FromExpression(expression), FromExpression(expected));
 
    private static string FromExpression(string expression)
    {
        const string initialMarkup = """
            using System;
            using System.Collections.Generic;
            class C
            {
                static bool field = {|FixAllInDocument:EXPRESSION|};
                static bool Method() => EXPRESSION;
                static bool Prop1 => EXPRESSION;
                static bool Prop2 { get; } = EXPRESSION;
                static void If() { if (EXPRESSION) ; }
                static void Argument1() => Test(EXPRESSION);
                static void Argument2() => Test(() => EXPRESSION);
                static void Argument3() => Test(_ => EXPRESSION);
                static void Test(bool b) {}
                static void Test(Func<bool> b) {}
                static void Test(Func<object, bool> b) {}
                static void For() { for (; EXPRESSION; ); }
                static void Local() { var local = EXPRESSION; }
                static void Conditional() { _ = EXPRESSION ? EXPRESSION : EXPRESSION; }
                static void Assignment() { _ = EXPRESSION; }
                static void Do() { do ; while (EXPRESSION); }
                static void While() { while (EXPRESSION) ; }
                static bool When() => o switch { _ when EXPRESSION => EXPRESSION };
                static bool Return() { return EXPRESSION; }
                static IEnumerable<bool> YieldReturn() { yield return EXPRESSION; }
                static Func<object, bool> SimpleLambda() => o => EXPRESSION;
                static Func<bool> ParenthesizedLambda() => () => EXPRESSION;
                static void LocalFunc() { bool LocalFunction() => EXPRESSION; }
                static int i;
                static int? nullable;
                static object o;
                static char ch;
            }
            """;
        return initialMarkup.Replace("EXPRESSION", expression);
    }
 
    [InlineData("i == 0")]
    [InlineData("i > 0")]
    [InlineData("o is C")]
    [InlineData("o is C c")]
    [InlineData("o != null")]
    [InlineData("!(o is null)")]
    [InlineData("o is int ii || o is long jj")]
    [Theory]
    public async Task TestMissingOnExpression(string expression)
    {
        await TestAllMissingOnExpressionAsync(expression);
    }
 
    [InlineData("i == default || i > default(int)", "i is default(int) or > default(int)")]
    [InlineData("!(o is C c)", "o is not C c")]
    [InlineData("o is int ii && o is long jj", "o is int ii and long jj")]
    [InlineData("o is string || o is Exception", "o is string or Exception")]
    [InlineData("o is System.String || o is System.Exception", "o is System.String or System.Exception")]
    [InlineData("!(o is C)", "o is not C")]
    [InlineData("!(o is C _)", "o is not C _")]
    [InlineData("i == (0x02 | 0x04) || i != 0", "i is (0x02 | 0x04) or not 0")]
    [InlineData("i == 1 || 2 == i", "i is 1 or 2")]
    [InlineData("i == (short)1 || (short)2 == i", "i is ((short)1) or ((short)2)")]
    [InlineData("i != 1 || 2 != i", "i is not 1 or not 2")]
    [InlineData("i != 1 && 2 != i", "i is not 1 and not 2")]
    [InlineData("!(i != 1 && 2 != i)", "i is 1 or 2")]
    [InlineData("i < 1 && 2 <= i", "i is < 1 and >= 2")]
    [InlineData("i < 1 && 2 <= i && i is not 0", "i is < 1 and >= 2 and not 0")]
    [InlineData("(int.MaxValue - 1D) < i && i > 0", "i is > (int)(int.MaxValue - 1D) and > 0")]
    [InlineData("ch < ' ' || ch >= 0x100 || 'a' == ch", "ch is < ' ' or >= (char)0x100 or 'a'")]
    [InlineData("ch == 'a' || 'b' == ch", "ch is 'a' or 'b'")]
    [Theory]
    public async Task TestOnExpression(string expression, string expected)
    {
        await TestAllOnExpressionAsync(expression, expected);
    }
 
    [InlineData("nullable == 1 || 2 == nullable", "nullable is 1 or 2")]
    [Theory]
    public async Task TestOnNullableExpression(string expression, string expected)
    {
        await TestAllOnExpressionAsync(expression, expected);
    }
 
    [Fact]
    public async Task TestMissingIfDisabled()
    {
        await TestAllMissingOnExpressionAsync("o == 1 || o == 2", enabled: false);
    }
 
    [Fact]
    public async Task TestMissingOnCSharp8()
    {
        await TestAllMissingOnExpressionAsync("o == 1 || o == 2", parseOptions: TestOptions.Regular8);
    }
 
    [Fact]
    public async Task TestMultilineTrivia_01()
    {
        await TestAllAsync(
            """
            class C
            {
                bool M0(int variable)
                {
                    return {|FixAllInDocument:variable == 0 || /*1*/
                           variable == 1 || /*2*/
                           variable == 2|}; /*3*/
                }
                bool M1(int variable)
                {
                    return variable != 0 && /*1*/
                           variable != 1 && /*2*/
                           variable != 2; /*3*/
                }
            }
            """,
            """
            class C
            {
                bool M0(int variable)
                {
                    return variable is 0 or /*1*/
                           1 or /*2*/
                           2; /*3*/
                }
                bool M1(int variable)
                {
                    return variable is not 0 and /*1*/
                           not 1 and /*2*/
                           not 2; /*3*/
                }
            }
            """);
    }
 
    [Fact]
    public async Task TestMultilineTrivia_02()
    {
        await TestAllAsync(
            """
            class C
            {
                bool M0(int variable)
                {
                    return {|FixAllInDocument:variable == 0 /*1*/
                        || variable == 1 /*2*/
                        || variable == 2|}; /*3*/
                }
                bool M1(int variable)
                {
                    return variable != 0 /*1*/
                        && variable != 1 /*2*/
                        && variable != 2; /*3*/
                }
            }
            """,
            """
            class C
            {
                bool M0(int variable)
                {
                    return variable is 0 /*1*/
                        or 1 /*2*/
                        or 2; /*3*/
                }
                bool M1(int variable)
                {
                    return variable is not 0 /*1*/
                        and not 1 /*2*/
                        and not 2; /*3*/
                }
            }
            """);
    }
 
    [Fact]
    public async Task TestParenthesized()
    {
        await TestAllAsync(
            """
            class C
            {
                bool M0(int v)
                {
                    return {|FixAllInDocument:(v == 0 || v == 1 || v == 2)|};
                }
                bool M1(int v)
                {
                    return (v == 0) || (v == 1) || (v == 2);
                }
            }
            """,
            """
            class C
            {
                bool M0(int v)
                {
                    return (v is 0 or 1 or 2);
                }
                bool M1(int v)
                {
                    return v is 0 or 1 or 2;
                }
            }
            """);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/66787")]
    public async Task TestConvertedConstants()
    {
        await TestAllAsync(
            """
            class C
            {
                bool M(long l)
                {
                    return {|FixAllInDocument:(l > int.MaxValue || l < int.MinValue)|};
                }
            }
            """,
            """
            class C
            {
                bool M(long l)
                {
                    return (l is > int.MaxValue or < int.MinValue);
                }
            }
            """);
    }
 
    [Fact]
    public async Task TestMissingInExpressionTree()
    {
        await TestMissingAsync(
            """
            using System.Linq;
            class C
            {
                void M0(IQueryable<int> q)
                {
                    q.Where(item => item == 1 [||]|| item == 2);
                }
            }
            """);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52397")]
    public async Task TestMissingInPropertyAccess_NullCheckOnLeftSide()
    {
        await TestMissingAsync(
            """
            using System;
 
            public class C
            {
                public int I { get; }
 
                public EventArgs Property { get; } 
 
                public void M()
                {
                    if (Property != null [|&&|] I == 1)
                    {
                    }
                }
            }
            """);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52397")]
    public async Task TestMissingInPropertyAccess_NullCheckOnRightSide()
    {
        await TestMissingAsync(
            """
            using System;
 
            public class C
            {
                public int I { get; }
 
                public EventArgs Property { get; } 
 
                public void M()
                {
                    if (I == 1 [|&&|] Property != null)
                    {
                    }
                }
            }
            """);
    }
 
    [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/51691")]
    [InlineData("&&")]
    [InlineData("||")]
    public async Task TestMissingInPropertyAccess_EnumCheckAndNullCheck(string logicalOperator)
    {
        await TestMissingAsync(
$@"using System.Diagnostics;
 
public class C
{{
    public void M()
    {{
            var p = default(Process);
            if (p.StartInfo.WindowStyle == ProcessWindowStyle.Hidden [|{logicalOperator}|] p.StartInfo != null)
            {{
            }}
    }}
}}");
    }
 
    [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/51691")]
    [InlineData("&&")]
    [InlineData("||")]
    public async Task TestMissingInPropertyAccess_EnumCheckAndNullCheckOnOtherType(string logicalOperator)
    {
        await TestMissingAsync(
$@"using System.Diagnostics;
 
public class C
{{
    public void M()
    {{
            var p = default(Process);
            if (p.StartInfo.WindowStyle == ProcessWindowStyle.Hidden [|{logicalOperator}|] this != null)
            {{
            }}
    }}
}}");
    }
 
    [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/51693")]
    [InlineData("&&")]
    [InlineData("||")]
    public async Task TestMissingInPropertyAccess_IsCheckAndNullCheck(string logicalOperator)
    {
        await TestMissingAsync(
$@"using System;
 
public class C
{{
    public void M()
    {{
            var o1 = new object();
            if (o1 is IAsyncResult ar [|{logicalOperator}|] ar.AsyncWaitHandle != null)
            {{
            }}
    }}
}}");
    }
 
    [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/52573")]
    [InlineData("&&")]
    [InlineData("||")]
    public async Task TestMissingIntegerAndStringIndex(string logicalOperator)
    {
        await TestMissingAsync(
$@"using System;
 
public class C
{{
    private static bool IsS(char[] ch, int count)
    {{
        return count == 1 [|{logicalOperator}|] ch[0] == 'S';
    }}
}}");
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/66787")]
    public async Task TestMissingForImplicitUserDefinedCasts1()
    {
        await TestMissingAsync(
            """
            using System;
            class C
            {
                void M0(Int128 i)
                {
                    if (i == int.MaxValue [||] i == int.MinValue)
                    {
                    }
                }
            }
            """);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/66787")]
    public async Task TestMissingForImplicitUserDefinedCasts2()
    {
        await TestMissingAsync(
            """
            using System;
            class C
            {
                void M0(Int128 i)
                {
                    if (i > int.MaxValue [||] i < int.MinValue)
                    {
                    }
                }
            }
            """);
    }
 
    [Fact]
    public async Task TestOnSideEffects1()
    {
        await TestInRegularAndScriptAsync(
            """
            class C
            {
                char ReadChar() => default;
 
                void M(char c)
                {
                    if ({|FixAllInDocument:c == 'x' && c == 'y'|})
                    {
                    }
 
                    if (c == 'x' && c == 'y')
                    {
                    }
 
                    if (ReadChar() == 'x' && ReadChar() == 'y')
                    {
                    }
                }
            }
            """,
 
            """
            class C
            {
                char ReadChar() => default;
 
                void M(char c)
                {
                    if (c is 'x' and 'y')
                    {
                    }
 
                    if (c is 'x' and 'y')
                    {
                    }
 
                    if (ReadChar() == 'x' && ReadChar() == 'y')
                    {
                    }
                }
            }
            """);
    }
 
    [Fact]
    public async Task TestOnSideEffects2()
    {
        await TestInRegularAndScriptAsync(
            """
            class C
            {
                char ReadChar() => default;
 
                void M(char c)
                {
                    if ({|FixAllInDocument:ReadChar() == 'x' && ReadChar() == 'y'|})
                    {
                    }
 
                    if (ReadChar() == 'x' && ReadChar() == 'y')
                    {
                    }
 
                    if (c == 'x' && c == 'y')
                    {
                    }
                }
            }
            """,
 
            """
            class C
            {
                char ReadChar() => default;
 
                void M(char c)
                {
                    if (ReadChar() is 'x' and 'y')
                    {
                    }
 
                    if (ReadChar() is 'x' and 'y')
                    {
                    }
 
                    if (c == 'x' && c == 'y')
                    {
                    }
                }
            }
            """);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/57199")]
    public async Task TestMissingInNonConvertibleTypePattern1()
    {
        await TestMissingAsync(
            """
            static class C
            {
                public struct S1 : I { }
                public struct S2 : I { }
                public interface I { }
            }
 
            class Test<T>
            {
                public readonly T C;
                bool P => [|C is C.S1 || C is C.S2|];
            }
            """);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/57199")]
    public async Task TestMissingInNonConvertibleTypePattern2()
    {
        await TestMissingAsync(
            """
            class Goo
            {
                private class X { }
                private class Y { }
 
                private void M(object o)
                {
                    var X = 1;
                    var Y = 2;
 
                    if [|(o is X || o is Y)|]
                    {
                    }
                }
            }
            """);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/57199")]
    public async Task TestMissingInNonConvertibleTypePattern3()
    {
        await TestMissingAsync(
            """
            class Goo
            {
                private class X { }
                private class Y { }
                private void M(object o)
                {
                    var X = 1;
                    var Y = 2;
                    if [|(o is global::Goo.X || o is Y)|]
                    {
                    }
                }
            }
            """);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/57199")]
    public async Task TestInConvertibleTypePattern()
    {
        await TestInRegularAndScriptAsync(
            """
            static class C
            {
                public struct S1 : I { }
                public struct S2 : I { }
                public interface I { }
            }
 
            class Test<T>
            {
                bool P => [|C is C.S1 || C is C.S2|];
            }
            """,
 
            """
            static class C
            {
                public struct S1 : I { }
                public struct S2 : I { }
                public interface I { }
            }
 
            class Test<T>
            {
                bool P => C is C.S1 or C.S2;
            }
            """);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/57199")]
    public async Task TestInConvertibleTypePattern2()
    {
        await TestInRegularAndScriptAsync(
            """
            public class Goo
            {
                private class X { }
                private class Y { }
 
                private void M(object o)
                {
                    var X = 1;
                    var Y = 2;
 
                    var @int = 1;
                    var @long = 2;
                    if [|(o is int || o is long)|]
                    {
                    }
                }
            }
            """, """
            public class Goo
            {
                private class X { }
                private class Y { }
 
                private void M(object o)
                {
                    var X = 1;
                    var Y = 2;
 
                    var @int = 1;
                    var @long = 2;
                    if (o is int or long)
                    {
                    }
                }
            }
            """);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/75122")]
    public async Task TestNotWithMultipleCallsToInvocationWithRefArgument()
    {
        await TestMissingAsync(
            """
            using System;
 
            static class DataUtils
            {
                internal static string ReadLine(byte[] bytes, ref int index)
                {
                    throw new NotImplementedException();
                }
            }
 
            class C
            {
                public void Main(byte[] bytes)
                {
                    int index = 0;
 
                    if ([|DataUtils.ReadLine(bytes, ref index) != "YAFC" || DataUtils.ReadLine(bytes, ref index) != "ProjectPage"|])
                    {
                        throw new InvalidDataException();
                    }
                }
            }
            """);
    }
}