File: UseRecursivePatterns\UseRecursivePatternsRefactoringTests.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.CSharp;
using Microsoft.CodeAnalysis.CSharp.CodeRefactorings.UseRecursivePatterns;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Testing;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeRefactorings.UseRecursivePatterns;
 
using VerifyCS = CSharpCodeRefactoringVerifier<UseRecursivePatternsCodeRefactoringProvider>;
 
[UseExportProvider]
[Trait(Traits.Feature, Traits.Features.CodeActionsUseRecursivePatterns)]
public class UseRecursivePatternsRefactoringTests
{
    private static Task VerifyAsync(
        string initialMarkup,
        string expectedMarkup,
        bool skipCodeActionValidation = false,
        LanguageVersion languageVersion = LanguageVersion.CSharp9)
    {
        return new VerifyCS.Test
        {
            LanguageVersion = languageVersion,
            TestCode = initialMarkup,
            FixedCode = expectedMarkup,
            CodeActionValidationMode = skipCodeActionValidation
                ? CodeActionValidationMode.None
                : CodeActionValidationMode.SemanticStructure,
        }.RunAsync();
    }
 
    private static Task VerifyMissingAsync(string initialMarkup)
    {
        return VerifyAsync(initialMarkup, initialMarkup);
    }
 
    [Theory]
    [InlineData("a.b.c.d && a.b.c.a", "a.b.c is { d: n, a: n }")]
    [InlineData("a?.b.c.d && a.b.c.a", "a?.b.c is { d: n, a: n }")]
    [InlineData("a.b?.c.d && a.b.c.a", "a.b?.c is { d: n, a: n }")]
    [InlineData("a.b.c?.d && a.b.c.a", "a.b.c is { d: n, a: n }")]
    [InlineData("a.b?.c?.d && a.b.c.a", "a.b?.c is { d: n, a: n }")]
    [InlineData("a?.b.c?.d && a.b.c.a", "a?.b.c is { d: n, a: n }")]
    [InlineData("a?.b?.c.d && a.b.c.a", "a?.b?.c is { d: n, a: n }")]
    [InlineData("a?.b?.c?.d && a.b.c.a", "a?.b?.c is { d: n, a: n }")]
 
    [InlineData("a.b.c.d && a.b.a", "a.b is { c: { d: n }, a: n }")]
    [InlineData("a?.b.c.d && a.b.a", "a?.b is { c: { d: n }, a: n }")]
    [InlineData("a.b?.c.d && a.b.a", "a.b is { c: { d: n }, a: n }")]
    [InlineData("a.b.c?.d && a.b.a", "a.b is { c: { d: n }, a: n }")]
    [InlineData("a.b?.c?.d && a.b.a", "a.b is { c: { d: n }, a: n }")]
    [InlineData("a?.b.c?.d && a.b.a", "a?.b is { c: { d: n }, a: n }")]
    [InlineData("a?.b?.c.d && a.b.a", "a?.b is { c: { d: n }, a: n }")]
    [InlineData("a?.b?.c?.d && a.b.a", "a?.b is { c: { d: n }, a: n }")]
 
    [InlineData("a.b.c.d && a.a", "a is { b: { c: { d: n } }, a: n }")]
    [InlineData("a?.b.c.d && a.a", "a is { b: { c: { d: n } }, a: n }")]
    [InlineData("a.b?.c.d && a.a", "a is { b: { c: { d: n } }, a: n }")]
    [InlineData("a.b.c?.d && a.a", "a is { b: { c: { d: n } }, a: n }")]
    [InlineData("a.b?.c?.d && a.a", "a is { b: { c: { d: n } }, a: n }")]
    [InlineData("a?.b.c?.d && a.a", "a is { b: { c: { d: n } }, a: n }")]
    [InlineData("a?.b?.c.d && a.a", "a is { b: { c: { d: n } }, a: n }")]
    [InlineData("a?.b?.c?.d && a.a", "a is { b: { c: { d: n } }, a: n }")]
 
    [InlineData("a.b.c.d && b", "this is { a: { b: { c: { d: n } } }, b: n }")]
    [InlineData("a?.b.c.d && b", "this is { a: { b: { c: { d: n } } }, b: n }")]
    [InlineData("a.b?.c.d && b", "this is { a: { b: { c: { d: n } } }, b: n }")]
    [InlineData("a.b.c?.d && b", "this is { a: { b: { c: { d: n } } }, b: n }")]
    [InlineData("a.b?.c?.d && b", "this is { a: { b: { c: { d: n } } }, b: n }")]
    [InlineData("a?.b.c?.d && b", "this is { a: { b: { c: { d: n } } }, b: n }")]
    [InlineData("a?.b?.c.d && b", "this is { a: { b: { c: { d: n } } }, b: n }")]
    [InlineData("a?.b?.c?.d && b", "this is { a: { b: { c: { d: n } } }, b: n }")]
 
    [InlineData("a.b.c.d && b", "this is { a.b.c.d: n, b: n }", LanguageVersion.CSharp10)]
    [InlineData("a?.b.c.d && b", "this is { a.b.c.d: n, b: n }", LanguageVersion.CSharp10)]
    [InlineData("a.b?.c.d && b", "this is { a.b.c.d: n, b: n }", LanguageVersion.CSharp10)]
    [InlineData("a.b.c?.d && b", "this is { a.b.c.d: n, b: n }", LanguageVersion.CSharp10)]
    [InlineData("a.b?.c?.d && b", "this is { a.b.c.d: n, b: n }", LanguageVersion.CSharp10)]
    [InlineData("a?.b.c?.d && b", "this is { a.b.c.d: n, b: n }", LanguageVersion.CSharp10)]
    [InlineData("a?.b?.c.d && b", "this is { a.b.c.d: n, b: n }", LanguageVersion.CSharp10)]
    [InlineData("a?.b?.c?.d && b", "this is { a.b.c.d: n, b: n }", LanguageVersion.CSharp10)]
 
    [InlineData("a.b.m().d && a.b.m().a", "a.b.m() is { d: n, a: n }")]
    [InlineData("a.m().c.d && a.m().a", "a.m() is { c: { d: n }, a: n }")]
    [InlineData("a?.m().c.d && a?.m().a", "a?.m() is { c: { d: n }, a: n }")]
    [InlineData("a.m()?.c.d && a.m().a", "a.m() is { c: { d: n }, a: n }")]
    [InlineData("a.m().c?.d && a.m().a", "a.m() is { c: { d: n }, a: n }")]
    [InlineData("a.m()?.c?.d && a.m().a", "a.m() is { c: { d: n }, a: n }")]
    [InlineData("a?.m().c?.d && a?.m().a", "a?.m() is { c: { d: n }, a: n }")]
    [InlineData("a?.m()?.c.d && a?.m().a", "a?.m() is { c: { d: n }, a: n }")]
    [InlineData("a?.m()?.c?.d && a?.m().a", "a?.m() is { c: { d: n }, a: n }")]
    public async Task TestLogicalAndExpression_Receiver(string actual, string expected, LanguageVersion languageVersion = LanguageVersion.CSharp9)
    {
        await VerifyAsync(WrapInIfStatement("n == " + actual + " == n", "&&"), WrapInIfStatement(expected), languageVersion: languageVersion);
    }
 
    [Theory]
    [InlineData("this.P1 < 1 && 2 >= this.P2", "this is { P1: < 1, P2: <= 2 }")]
    [InlineData("this.P1 > 1 && 2 <= this.P2", "this is { P1: > 1, P2: >= 2 }")]
    [InlineData("this.P1 <= 1 && 2 > this.P2", "this is { P1: <= 1, P2: < 2 }")]
    [InlineData("this.P1 >= 1 && 2 < this.P2", "this is { P1: >= 1, P2: > 2 }")]
    // Nested
    [InlineData("this.CP1?.P1 < 1 && 2 >= this.CP2.P2", "this is { CP1: { P1: < 1 }, CP2: { P2: <= 2 } }")]
    [InlineData("this.CP1?.P1 > 1 && 2 <= this.CP2.P2", "this is { CP1: { P1: > 1 }, CP2: { P2: >= 2 } }")]
    [InlineData("this.CP1.P1 <= 1 && 2 > this.CP2?.P2", "this is { CP1: { P1: <= 1 }, CP2: { P2: < 2 } }")]
    [InlineData("this.CP1.P1 >= 1 && 2 < this.CP2?.P2", "this is { CP1: { P1: >= 1 }, CP2: { P2: > 2 } }")]
    public async Task TestLogicalAndExpression_Relational(string actual, string expected)
    {
        await VerifyAsync(WrapInIfStatement(actual, "&&"), WrapInIfStatement(expected));
    }
 
    [Theory]
    [InlineData("!B1 && B2", "this is { B1: false, B2: true }")]
    [InlineData("CP1.B1 && !CP2.B2", "this is { CP1: { B1: true }, CP2: { B2: false } }")]
    public async Task TestLogicalAndExpression_Boolean(string actual, string expected)
    {
        await VerifyAsync(WrapInIfStatement(actual, "&&"), WrapInIfStatement(expected, "&&"));
    }
 
    [Theory]
    [InlineData("this.P1 == 1 && 2 == this.P2", "this is { P1: 1, P2: 2 }")]
    [InlineData("this.cf != null && this.cf.C != 0", "this.cf is { C: not 0 }")]
    [InlineData("cf != null && cf.C != 0", "cf is { C: not 0 }")]
    [InlineData("this.P1 != 1 && 2 != this.P2", "this is { P1: not 1, P2: not 2 }")]
    [InlineData("this.CP1.P1 == 1 && 2 == this.CP2.P2", "this is { CP1: { P1: 1 }, CP2: { P2: 2 } }")]
    [InlineData("this.CP1.P1 != 1 && 2 != this.CP2.P2", "this is { CP1: { P1: not 1 }, CP2: { P2: not 2 } }")]
    public async Task TestLogicalAndExpression_Equality(string actual, string expected)
    {
        await VerifyAsync(WrapInIfStatement(actual, "&&"), WrapInIfStatement(expected, "&&"));
    }
 
    [Theory]
    [InlineData("NS.C.SCP1.P1 == 1 && NS.C.SCP1.P2 == 2", "NS.C.SCP1 is { P1: 1, P2: 2 }")]
    [InlineData("NS.C.SCP1.CP1.P1 == 1 && NS.C.SCP1.CP2.P2 == 2", "NS.C.SCP1 is { CP1: { P1: 1 }, CP2: { P2: 2 } }")]
    public async Task TestLogicalAndExpression_StaticMembers(string actual, string expected)
    {
        await VerifyAsync(WrapInIfStatement(actual, "&&"), WrapInIfStatement(expected, "&&"));
    }
 
    [Theory]
    [InlineData("this.B1 && this.CP1.P1 == 1 [||]&& this.CP1.CP2.P3 == 3 && B2", "this.B1 && this.CP1 is { P1: 1, CP2: { P3: 3 } } && B2")]
    [InlineData("this.B1 || this.CP1.P1 == 1 [||]&& this.CP1.CP2.P3 == 3 || B2", "this.B1 || this.CP1 is { P1: 1, CP2: { P3: 3 } } || B2")]
    public async Task TestLogicalAndExpression_Chain(string actual, string expected)
    {
        await VerifyAsync(WrapInIfStatement(actual, entry: null), WrapInIfStatement(expected, entry: null));
    }
 
    [Theory]
    [InlineData("NS.C.SCP1 == null && NS.C.SCP2 == null")]
    [InlineData("NS.C.SCP1.P1 == 1 && NS.C.SCP2.P1 == 2")]
    [InlineData("base.P1 == 1 && base.P2 == 2")]
    [InlineData("base.B1 && base.B2")]
    public async Task TestLogicalAndExpression_Invalid(string actual)
    {
        await VerifyMissingAsync(WrapInIfStatement(actual, "&&"));
    }
 
    [Theory]
    [InlineData("{ a: var x }", "x is { b: n }", "{ a: { b: n } x }")]
    [InlineData("{ a: C x }", "x is { b: n }", "{ a: C { b: n } x }")]
    [InlineData("{ a: { b: n } x }", "x is C", "{ a: C { b: n } x }")]
    [InlineData("{ a: { b: n } x }", "x is { a: n }", "{ a: { b: n, a: n } x }")]
    [InlineData("{ a: var x }", "x.c is { b: n }", "{ a: { c: { b: n } } x }")]
    [InlineData("{ a: C x }", "x.c is { b: n }", "{ a: C { c: { b: n } } x }")]
    [InlineData("{ a: { b: n } x }", "x.c is C", "{ a: { b: n, c: C } x }", true)]
    [InlineData("{ a: { b: n } x }", "x.c is { a: n }", "{ a: { b: n, c: { a: n } } x }")]
    [InlineData("{ a: var x }", "x == null", "{ a: var x and null }")]
    public async Task TestVariableDesignation(string pattern, string expression, string expected, bool skipCodeActionValidation = false)
    {
        await ValidateAsync(WrapInSwitchArm($"{pattern} when {expression}", "when"), WrapInSwitchArm($"{expected}"));
        await ValidateAsync(WrapInSwitchArm($"{pattern} when {expression}", "=>"), WrapInSwitchArm($"{expected}"));
        await ValidateAsync(WrapInSwitchLabel($"{pattern} when {expression}", "when"), WrapInSwitchLabel($"{expected}"));
        await ValidateAsync(WrapInSwitchLabel($"{pattern} when {expression}", "case"), WrapInSwitchLabel($"{expected}"));
        await ValidateAsync(WrapInIfStatement($"this is {pattern} && {expression}", "&&"), WrapInIfStatement($"this is {expected}"));
 
        await ValidateAsync(WrapInSwitchArm($"{pattern} when {expression} && B1 && B2", "when"), WrapInSwitchArm($"{expected} when B1 && B2"));
        await ValidateAsync(WrapInSwitchArm($"{pattern} when {expression} && B1 && B2", "=>"), WrapInSwitchArm($"{expected} when B1 && B2"));
        await ValidateAsync(WrapInSwitchLabel($"{pattern} when {expression} && B1 && B2", "when"), WrapInSwitchLabel($"{expected} when B1 && B2"));
        await ValidateAsync(WrapInSwitchLabel($"{pattern} when {expression} && B1 && B2", "case"), WrapInSwitchLabel($"{expected} when B1 && B2"));
        await ValidateAsync(WrapInIfStatement($"B1 && this is {pattern} [||]&& {expression} && B2"), WrapInIfStatement($"B1 && this is {expected} && B2"));
 
        Task ValidateAsync(string initialMarkup, string expectedMarkup)
            => VerifyAsync(initialMarkup, expectedMarkup, skipCodeActionValidation);
    }
 
    private static string WrapInIfStatement(string actual, string? entry = null)
    {
        var markup =
@"
            if (" + actual + @") {}
";
        return CreateMarkup(markup, entry);
    }
 
    private static string WrapInSwitchArm(string actual, string? entry = null)
    {
        var markup =
@"
            _ = this switch
            {
                " + actual + @" => 0
            };
";
        return CreateMarkup(markup, entry);
    }
 
    private static string WrapInSwitchLabel(string actual, string? entry = null)
    {
        var markup =
@"
            switch (this)
            {
                case " + actual + @":
                    break;
            };
";
        return CreateMarkup(markup, entry);
    }
 
    private static string CreateMarkup(string actual, string? entry = null)
    {
        var markup = @"
namespace NS
{
    class C : B
    {
        void Test()
        {
            " + actual + @"
        }
    }
    class B
    {
        public const C n = null;
        public C a, b, c, d;
        public int P1, P2, P3;
        public bool B1, B2;
        public C CP1, CP2;
        public static C SCP1, SCP2;
        public static int SP1, SP2;
        public C m() { return null; }
        public D cf = null;
    }
 
    class D
    {
        public int C = 0;
    }
}";
        return entry is null ? markup : markup.Replace(entry, "[||]" + entry);
    }
}